Files
configkit/docs/roadmap/config-directives.md

239 lines
12 KiB
Markdown

# Document-Root Configuration Controls
Status: proposed implementation of the agreed `config` spelling and
source-document-root-only reservation. Ordinary application-root `config` data
created through placement or later lifecycle operations is permitted.
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. The
[implementation plan](implementation.md) owns staged execution and progress.
Neither document changes 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.
### Application-root data from non-document operations
Input placement, mounted imports, typed defaults, CLI overrides, and application
resolution may produce ordinary `config` data at the final application root,
following their existing schema and lifecycle rules. Controls are recognized
only during source-document extraction. For example, a fragment containing
`timeout: 10s` can be placed at destination `[config]`, producing application
data at `/config/timeout`. A CLI assignment to `Keys("config", "timeout")`
addresses that data rather than introducing a control. Collection declarations
and registrations can likewise address actual data at that application path.
This keeps the reservation document-local. Placement and later lifecycle
operations do not reinterpret their output as another source document, and no
additional schema or callback restriction reserves the final root namespace.
Inspection can show a root `config` value without reinterpreting it; effective
YAML retains its existing lack of a reloadability guarantee. Explain
source-local control coordinates and assembled application coordinates
separately, even when their path text is the same; this requires no new
inspection-record format.
### Consistent application-path handling
Allow application paths containing literal `config`, at root or nested, in
input placement, relative import destinations, collection declarations and
registrations, CLI paths and values, version fields, and inspection queries.
Each operation retains its 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.
An assembled application path beginning with `config` does not by itself
establish overlap with an actual source-document control block.
`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 root or 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. Allow ordinary application-root `config` data through
placement and later lifecycle operations without re-extracting controls.
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, ordinary nested fields, and application-root data
created through non-document operations 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.
- Placement, defaults, CLI overrides, and recorded resolution can produce
ordinary application-root `config` data. Collection declarations,
registrations, and inspection can address it. Source controls are still
extracted before mounting and are not rediscovered in the resulting data.
- 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.
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.