diff --git a/docs/roadmap/config-directives.md b/docs/roadmap/config-directives.md new file mode 100644 index 0000000..c891938 --- /dev/null +++ b/docs/roadmap/config-directives.md @@ -0,0 +1,245 @@ +# 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](../config.md) 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](../../internal/engine/directives.go) 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](../../internal/engine/destination.go) rejects + `configkit` in every segment. Its helpers also serve import destinations, + collection declarations, and exact application identity registrations. +- [CLI override validation](../../internal/engine/override.go) rejects the name + in paths and recursively inside supplied values. +- [Source-version processing](../../internal/engine/source_version.go) 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: + +```yaml +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: + +```yaml +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](../config.md), public comments, and +[consumer guide](../consumers/README.md) 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](../policy/release.md#versioning-and-compatibility), 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](../development.md#task-to-test-map) 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 `config` values 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 `configkit` and `_configkit` data 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.