Features¶
The feature core is the extracted
gitlab.com/phpboyscout/go/features module
(API on pkg.go.dev),
which props and setup build on
(spec 0199).
This page is GTB's side: how the framework uses the module and what it adds.
The module's own documentation covers the core in full; the summary below is
the map, not the territory.
The core answers three questions, each through an interface with one default implementation, and imports nothing of GTB.
| Question | Role | Default |
|---|---|---|
| What does this binary offer? | Registry (declare, contribute, snapshot) |
NewRegistry(); Default() is the init-time instance |
| What was offered when the root was built? | Snapshot (immutable, ordered) |
taken by Registry.Snapshot() |
| What has the tool chosen? | Resolver β Set |
DefaultResolver(): defaults, then the tool's states |
| Is it on for this request? | Evaluator |
a Set (static); Dynamic over a Backend in a later phase |
Declaration is init-time; everything else is a value¶
A blank import runs init, and init can only reach package state, so a
package declares its feature and its contributions on features.Default().
That is the one process-wide thing here. Everything from main on works with
values: a root takes a Snapshot, resolves a Set, and hands the set down.
Nothing seals. A snapshot is an immutable copy, so a registration made after it
is not in it and a fresh snapshot sees it; the panics the earlier seals raised
are gone with them (D1). A test that needs a feature, an initialiser or a
middleware of its own builds NewRegistry() and never writes to the default
(D5); internal/repopolicy enforces that.
Descriptors, kinds and order¶
A Descriptor is an interface (FeatureID, FeatureKind, DefaultOn,
IsDynamic) so a consumer carries its own facts beside it: GTB's
props.FeatureDescriptor adds the generated-code identifiers the generator
emits. Kind classifies so "every forge" is a query: builtin (the framework's
commands), forge (a forge integration a blank import contributes) and link
(a feature whose only effect is a blank import; its presence is its enablement,
it declares no runtime default, and the generator toggles it by writing or
removing the file that imports it). The OS keychain is one link, declared by
pkg/setup/keychain. A generated tool's adapter files declare the rest with
props.DeclareLinks: chat-claude and forge-github name the provider and
forge the author chose, so doctor reports that choice rather than every
provider the linked modules happen to register
(#81). A snapshot's order is derived
from data, never from init sequencing: descriptors that report a Rank
first (GTB's built-ins in their declared order, then any plugin that sets an
Order, which the forges do so a chooser lists GitHub first), then the rest by
kind and ID.
Contributions and slots¶
A contribution is a value registered under a feature ID and a Slot name. The
core knows a slot is a name and a value; setup defines GTB's slots
(SlotInitialiser, SlotSubcommand, SlotInitFlag, SlotCheck,
SlotAssets, SlotMiddleware, SlotRootCommand) and asserts the types on
the way out.
features.Global is the ID for a contribution that applies to every feature,
which is how global middleware is registered. A Set hands out only the
contributions of enabled features; ContributionsOf[T] does the typed read,
and that is how init, doctor and the root find their initialisers,
checks, asset bundles, middleware and contributed top-level commands (a
linked package such as pkg/mcp hands the root its command through
setup.RegisterRootCommands, so the root need not name it), and how doctor
finds the credentials
of enabled features (credentialposture.SlotCredential, read through
credentialposture.DeclaredFor(set)). The generator's catalogue is another
reader: templates.Catalogue() is the snapshot narrowed to the scaffoldable
kinds, so there is no second table of features to keep in step.
The set on Props, and the root that owns the rest¶
props.New snapshots the default registry and resolves Props.Features
from Tool.Features; p.GetFeatures() is the nil-safe read (a literal
Props resolves on first read). Every static decision asks it:
p.GetFeatures().Enabled(props.AiCmd). Props.Flags is the request-time
Evaluator, defaulting to the set. The root builds its own middleware chain
(setup.MiddlewareChain) from the set and hands it down the tree, and the
init command binds each enabled feature's flags on itself and hands the run's
flags to the initialiser providers, so two roots in one process share nothing
(D3). root.WithRegistry, WithResolver and WithChain, and
props.WithFeatures, WithSet and WithFlags, are the doors through which a
test or a consumer replaces any of those parts (D12).
Resolving a set¶
Resolve(snapshot, states) applies every descriptor's default, then the
tool's states in order. Enabling an ID nothing declared is an error
(ErrUnknownFeature); disabling one is ignored and listed by Set.Ignored()
so doctor can report it (OQ2: you can switch off what you did not link, you
cannot switch on what you did not link).
Static and dynamic¶
Anything that shapes the command tree, --help, completion, doctor or a
generated project is static and reads Set.Enabled, fixed at construction.
A descriptor reporting IsDynamic() may be overridden at evaluation time by a
Backend; the built-ins never are.
The dynamic half is three layers (D10). Evaluator is what a service holds
(Evaluate(ctx, id, EvalContext) (Decision, error); Decision carries
Enabled, a Reason and a Variant). Backend is what a vendor adapter
implements: Resolve(ctx, id, fallback, ec), where the fallback is the caller's
static state and is what the backend returns for a flag it does not know or has
disabled (OpenFeature's defaultValue), plus Init, Ready, Watch and
Close. features.Dynamic(set, backend, opts...) is the one production
evaluator: static-only features never reach the backend; a dynamic one is
resolved with the Set state as fallback; disabled is a clean answer that
applies it; any error, a not-ready backend or an unknown flag answers the Set
state with ReasonFallback and the error alongside. WithInitTimeout is how a
short-lived process bounds a backend's start; WithOnFallback is where a
service logs. features.SetBackend(set) is the vendor-free backend.
GTB's own backend is pkg/setup/flags.NewConfigBackend(store), which reads
features.<id>.enabled from the tool's config store and forwards every reload
on Watch; flags.RegisterBackend(controller, name, backend) gives any
backend the controller's lifecycle. A vendor is an adapter module that
implements Backend (D11). See the
dynamic feature flags how-to.