12 KiB
Document-Root Configuration Controls
Status: proposed implementation of the agreed config spelling and
document-root-only reservation. The remaining application-root boundary choice
is recorded under Open Decisions.
Planning baseline: 1e69abc88cf748537cd7febc85e0743755fa2fc1.
Purpose
Use config for ConfigKit's document-level controls while allowing applications
to use nested config fields for their own settings. An application that selects
ConfigKit adopts one control syntax; it should not need a configurable keyword
or a second configuration-processing pass.
This roadmap defines the target and the work needed to reach it. It does not change current behavior. The configuration contract and public Go declarations remain authoritative until implementation lands.
Current Baseline
ConfigKit uses a root configkit mapping for imports and sequences controls.
The directive implementation extracts
those controls before source placement and composition. Nested occurrences of
configkit are rejected, including within application mappings and sequences.
Imports-disabled and selected-overlay paths have their own checks enforcing
that reservation.
The reservation also reaches beyond parsing:
- Destination validation rejects
configkitin every segment. Its helpers also serve import destinations, collection declarations, and exact application identity registrations. - CLI override validation rejects the name in paths and recursively inside supplied values.
- Source-version processing protects actual source-root controls from overlapping version declarations.
- Source inspection records, diagnostics, examples, and documentation use
source-local paths such as
/configkit/imports/0.
A constant rename alone would retain the broad prohibition under a much more common application field name. The scope change is an essential part of this migration.
Targeted End State
One source-document control block
config replaces configkit as the exact, case-sensitive name of a control
mapping at a source document's root:
config:
imports:
- path: fragments/provider.yml
destination: [providers, primary, config]
sequences:
- destination: [jobs]
key: id
Recognize the block in each participating input document before mounting its application payload. This covers host files, native filesystem files, byte and reader input, mapping sources, imported fragments, underlays, and selected overlays through the existing loading paths. A mapping source's root is its source-document root even though it was not parsed from YAML.
Retain the existing control fields and their context-specific rules. Imports
remain opt-in; sequence declarations remain available where currently supported;
selected overlays continue to allow collection declarations and reject imports.
Malformed blocks and unknown control fields remain errors under the existing
diagnostic families. This change adds no shape-based fallback that treats an
unrecognized root config as application data.
Remove the control block from the source payload as part of existing directive extraction. Placement, composition, inheritance, decoding, CLI assignment, and application callbacks retain their established order. They do not start another document-control discovery pass.
Nested application fields
Below a source's root, config is ordinary application data. For example:
providers:
primary:
config:
timeout: 10s
imports: application-owned-value
Neither nested config nor the field named imports above acquires control
semantics. Its contents follow normal schema, combination, presence, validation,
and inspection rules. The same applies inside sequence records, overlays, and
inherited application payloads. Fields inside the actual root control block
continue to follow that block's grammar.
The boundary belongs to the source document, rather than its eventual mounting
location. An imported file's own root config is still its control block even
when that file becomes a nested mapping or a sequence item. To supply the
provider settings in the first example, fragments/provider.yml can contain
timeout: 10s; the destination places that payload under
/providers/primary/config. Its filesystem paths still resolve from the
declaring file under the existing resource-context rules.
Consistent application-path handling
Allow nested application paths containing literal config in input placement,
relative import destinations, collection declarations and registrations, CLI
paths and values, version fields, and inspection queries, subject to each
operation's existing rules. In particular, a relative destination beginning
with config can legitimately refer to a nested application mapping after the
parent destination is applied. Do not decide its meaning from that relative
segment alone.
Protect actual source-root controls during source-version extraction while
allowing version fields below an ordinary nested config mapping. Preserve
main-version ownership, non-main header consumption, and existing version modes.
Root application values produced through placement or later lifecycle inputs
are the distinct boundary choice recorded under Open Decisions.
extends keeps its existing inheritance meaning, scope, and CLI restrictions.
Allowing config in application data does not make other invalid paths, unknown
typed fields, malformed controls, or overlapping version writes valid.
Direct migration and observable records
Use one control spelling. configkit and the earlier _configkit spelling are
ordinary application keys, with no compatibility alias or special legacy-key
rejection. They are accepted or rejected by normal application schema rules.
Consumers must update their actual control blocks; a permissive mapping schema
may accept an old block as data without executing its imports.
Directive-related paths in diagnostics and source evidence change to the new
spelling, such as /config/imports/0. Preserve authored locations, source IDs,
effective placement, history, sensitivity, and resource ownership. Application
values under nested config receive the same metadata and safe inspection as
other ordinary fields, including explicit false, zero, empty, and null values
where supported by the schema.
The Go package name, vanity module path, exported symbol names,
configkit:"secret" struct tag, and library diagnostic prefix remain unchanged.
This is a configuration-control syntax and reservation-scope migration.
Work Needed From the Baseline
Engine integration
Update directive extraction and imports-disabled/overlay checks together so
they recognize source-root config and stop rejecting ordinary nested keys.
Review the old recursive rejection helpers for removal or simplification where
they no longer serve the selected rules.
Audit destination and collection-selector validation, CLI path/value checks, and version/control overlap detection as a coordinated change. Retain shared helpers when their callers need the same rule; distinguish source-local control coordinates from effective application coordinates where their responsibilities differ. Do not carry a global name ban into a helper merely by replacing its string constant. Resolve the application-root decision below before preparing the detailed implementation plan.
Keep the existing engine and facade boundaries. No configurable control name, new provider abstraction, or change to composition algorithms is needed.
Documentation and migration
Record the document-root boundary and its rationale in a new ADR, keeping existing accepted decisions as historical records. Update applicable local architecture-policy descriptions when the behavior lands. The current configuration reference, public comments, and consumer guide should explain the new syntax, per-source root boundary, and ordinary nested fields together.
Migrate maintained examples, fixtures, and behavioral expectations for genuine
control blocks. Preserve literal configkit occurrences that identify the Go
package, struct tag, diagnostic prefix, or ordinary application data. Include
consumer guidance for renaming old controls, moving conflicting root application
fields, and updating stored source-local directive paths. Historical tags and
release records retain their original syntax.
Treat the syntax migration as a breaking pre-v1 change under the release policy, intended to land before v1.0.0. Explain it in that release's eventual upgrade guidance; this roadmap does not prepare or publish a release.
Behavioral verification
Use the existing test owners and extend behavioral coverage at the boundaries this change affects:
- New root controls work across the supported source forms, enabled imports, layered inputs, mounted records, and selected overlays. Invalid controls and imports-disabled behavior retain their established classifications.
- Nested
configvalues work with imports enabled and disabled, in mappings and sequence records, and through composition, overlays, inheritance, CLI, defaults, and recorded resolution. Control-looking children remain data. - Mounted inputs and relative destinations distinguish source roots from
application paths, including a relative destination beginning with
config. Collection registrations and nested version fields follow that distinction. - Ordinary
configkitand_configkitdata no longer trigger controls or name-based rejection. Genuine old control syntax is not executed as an alias. - Source-local directive evidence uses
/config/...; ordinary application presence, origins, explanations, sensitivity, and resource contexts agree with final values. Existing inheritance and protected-version checks remain effective.
Add cases for the application-root outcome once selected. Verify behavior with independent expectations, rather than a test that only checks a renamed constant. Reuse existing coverage for unchanged semantics, run the appropriate focused checks and everyday validation, and check documentation links. No new release gate, security guarantee, or performance threshold follows from this roadmap.
Open Decisions
May non-document operations produce application-root config data?
The agreed source-document rule settles authored input: root config is a
control block and nested config is data. It does not by itself settle whether
an effective root /config may be produced by Input.Destination, mounted
imports, typed defaults, CLI overrides, or application resolution.
Recommendation: Allow these operations to produce ordinary root application
data, following their existing schema and lifecycle rules. Recognize controls
only at source-document extraction. For example, a fragment containing
timeout: 10s could be placed at destination [config]; a CLI assignment to
Keys("config", "timeout") would address data rather than introduce a control.
Collection declarations and registrations could likewise address actual data
at that application path. This keeps the reservation strictly document-local,
avoids adding a separate schema or callback restriction, and treats later
operations as data processing rather than new document parsing. Inspection can
show a root config value without reinterpreting it; effective YAML already has
no reloadability guarantee. The cost is that the same textual path can describe
source controls or assembled application data, so documentation must distinguish
those coordinates clearly.
Alternative: Also reserve /config as an application-root namespace,
while allowing nested paths such as /providers/primary/config. This keeps
source-root and final-root vocabulary aligned. It requires defining consistent
behavior for destinations, collection selectors, CLI values, defaults,
resolution output, and relevant version paths; changing only the source parser
or CLI checks would leave gaps. It also restricts application data beyond the
agreed source-document reservation. Choose this alternative only as an explicit
additional policy decision, with its validation boundary defined before
implementation.