From 68ec552b52bd8d68fe71c4bba6707bdce6095b4e Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 00:45:36 +0000 Subject: [PATCH] Document source-root configuration controls --- docs/config.md | 78 +++++++++++++++++++++------------- docs/consumers/README.md | 10 +++++ docs/development.md | 1 + docs/internal/overview.md | 4 +- docs/policy/architecture.md | 4 +- docs/roadmap/implementation.md | 2 + layered.go | 3 +- lifecycle.go | 5 ++- 8 files changed, 73 insertions(+), 34 deletions(-) diff --git a/docs/config.md b/docs/config.md index 3750fb6..07b8866 100644 --- a/docs/config.md +++ b/docs/config.md @@ -356,7 +356,7 @@ independent of where the source file lives on disk. Consider this file layout: With imports enabled, `config.yml` can contain: ```yaml -configkit: +config: imports: - path: fragments/prompt.yml destination: [prompts, welcome] @@ -383,10 +383,10 @@ existing inputs keep their behavior. Main inputs and underlay inputs use the same destination model. Destination segments are literal mapping keys: dots, slashes, and numeric-looking text are not split or interpreted as sequence indexes. Explicit index segments and paths that introduce the reserved -`configkit` or mapping-valued `extends` structure are invalid input options and +mapping-valued `extends` structure are invalid input options and produce a structured `LoadError` before source acquisition. -Source-root `configkit.imports` directives are processed before placement. An +Source-root `config.imports` directives are processed before placement. An import without an explicit destination inherits the effective destination of its importing document, including through nested imports. An import may instead declare a relative destination as a sequence of literal mapping keys; its keys @@ -449,7 +449,7 @@ child uses the parent's settings that remain after that combination. An identified list uses a mapping reference instead of a sibling name: ```yaml -configkit: +config: sequences: - destination: [presets] key: id @@ -468,7 +468,7 @@ Any input mapping may declare `extends: name` to inherit from the mapping named `name` among its immediate siblings. Names are exact keys, including the empty string; lookup does not search ancestor or unrelated scopes. A mapping inside a sequence can also participate in record inheritance when that sequence has an -effective identity declaration from `configkit.sequences` or an application +effective identity declaration from `config.sequences` or an application registration. In that case, `extends` is a mapping containing exactly the sequence's identity fields, with scalar values; lookup is limited to that final sequence. The document root and a sequence-element mapping without an effective @@ -683,7 +683,8 @@ each fragment conforms to a complete application schema. Final assembled decoding and application validation still run once. A header that is absent below a structural control follows the same omission -policy. Actual header/control overlap is rejected, and ordinary structural +policy. Actual source-local header/control overlap is rejected; an application +path beginning with `config` alone does not imply overlap. Ordinary structural validation and main-version ownership protection continue independently. Accepted non-main declarations are consumed from their owned payload before @@ -1292,7 +1293,7 @@ tell which records refer to the same entry. Declare the identity and desired operation at the source root, for example: ```yaml -configkit: +config: sequences: - destination: [records] key: id @@ -1330,7 +1331,7 @@ lists replace whole lower lists. mapping-only `Path`; the matching form accepts literal-key and any-mapping-key segments created by `SequenceLiteralKey` and `SequenceAnyMappingKey`. A literal `"*"` key is distinct from the wildcard constructor. Parsed numeric path tokens -remain literal mapping keys; explicit index segments and reserved `configkit` or +remain literal mapping keys; explicit index segments and reserved mapping-valued `extends` segments are invalid. The root selector is harmless under the mapping-root schema. Selector segments consume one mapping level and match a complete destination; an any-mapping-key segment matches exactly one key, not an @@ -1384,7 +1385,7 @@ completed configuration. ### YAML declarations and policy lifetime Ordinary sources, layered inputs, and selected overlays may declare collection -behavior under `configkit.sequences` at the document root. Each entry has a +behavior under `config.sequences` at the document root. Each entry has a required literal `destination` and may specify `key` as one string or a nonempty list of strings, plus a `mode` of `replace`, `append`, or `upsert`. These controls are removed before application decoding. A declaration destination is @@ -1450,7 +1451,7 @@ policies follow their owning records: retaining a record retains its rules, whil whole-record upsert replacement discards the displaced record's rules. Matching identity or reusing an index does not transfer them. These policies do not change CLI behavior: CLI sequence assignments replace whole sequences after decoding. -A root `configkit.imports` declaration still requires `Options.Imports`; +A root `config.imports` declaration still requires `Options.Imports`; `NoSource` has no declaration scope. An empty mapping contributes no child definitions. Two empty mappings compose, and @@ -1503,8 +1504,8 @@ origin data; history retains the displaced definitions and identifies the parent replacement. Surviving overlay paths have overlay origins, while recursively shared mapping containers retain their contributing locations. -An overlay may contain only root `configkit.sequences` declarations; overlay -imports and nested directives are rejected. Sequence declarations are removed +An overlay may contain only root `config.sequences` declarations; overlay +imports are rejected and nested `config` fields are ordinary data. Sequence declarations are removed before application decoding. At declared or registered sequence destinations, `replace`, `append`, and `upsert` use the same identity and policy rules as source composition. Other overlay sequences remain atomic replacements. An overlay may @@ -1544,12 +1545,12 @@ values recursively overlay existing mappings. Map keys are sorted canonically an sequence order is preserved. All records are detached and validated before any is applied. They cannot create -`configkit` or `extends` controls. Root and explicit +`extends` controls. Root and explicit sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path may create missing mapping ancestors but cannot traverse an existing scalar, null, or sequence. Duplicate and ancestor/descendant-overlapping records are rejected. -Neither paths nor values may create `configkit`, and no record may overlap the -configured schema-version path. Unknown fields and effective type mismatches remain +Paths and values may contain ordinary `config` and `configkit` data; no record may +overlap the configured schema-version path. Unknown fields and effective type mismatches remain ordinary strict-decoder errors. At most 1,024 records are accepted, and each record creates one synthetic CLI @@ -1582,7 +1583,7 @@ files, not three declarations that must be used together: Named file: ```yaml -configkit: +config: imports: - path: fragments/service.yml ``` @@ -1590,7 +1591,7 @@ configkit: Directory search, restricted here to YAML filenames: ```yaml -configkit: +config: imports: - dir: fragments pattern: "*.yml" @@ -1599,7 +1600,7 @@ configkit: Path pattern: ```yaml -configkit: +config: imports: - glob: "fragments/*.yml" ``` @@ -1621,7 +1622,7 @@ rules below explain absence, patterns, placement, and ordering. ### Declaration and selection rules Imports are opt-in. With `Options.Imports` enabled, a document may use its root -`configkit` mapping for `imports` and `sequences` controls. Ordinary `Load` may use +`config` mapping for `imports` and `sequences` controls. Ordinary `Load` may use `sequences` without enabling imports, but an `imports` field still requires `Options.Imports`. ConfigKit removes recognized root controls before application decoding. Each imports sequence item is a mapping with exactly one `path`, @@ -1633,9 +1634,26 @@ removed `optional` field is invalid. `destination` is an optional sequence of strings. Every destination string is one literal mapping key and follows the input-destination validation rules above; quote numeric-looking keys so YAML reads them as strings. Null, scalar destinations, and non-string members are -`import.invalid`, as are all other unsupported shapes and fields. A `configkit` -key below the root is invalid. With imports disabled, root imports and nested -`configkit` keys remain `import.disabled`. +`import.invalid`, as are all other unsupported shapes and fields. Nested `config` +keys are ordinary application data. With imports disabled, root imports remain +`import.disabled`. + +Each acquired document has its own root, including imported files mounted below +another value or as sequence items. Controls are extracted before placement; +assembled application-root `config` data from placement, defaults, CLI, or +resolution is not reinterpreted. For example: + +```yaml +config: + sequences: [] +service: + config: + imports: application-data +``` + +The [mounted-root example](../examples/consumers/config_controls_test.go) shows +an imported payload becoming application-root `config`. Inspection YAML can +contain that data and retains its existing lack of a reloadability guarantee. Path declarations use slash-separated relative paths. Absolute paths and network URLs are invalid. Each path resolves from its declaring file's canonical directory. @@ -1679,8 +1697,8 @@ the supplied pointers and slice before source acquisition. The Go fields are named `FollowSymlinks` and `IncludeHidden`; the YAML keys remain `follow` and `hidden`. -A source's `configkit.sequences` declarations apply independently of import -enablement; a `configkit.imports` declaration still requires `Options.Imports`. +A source's `config.sequences` declarations apply independently of import +enablement; a `config.imports` declaration still requires `Options.Imports`. Sequence identities can be supplied either by these YAML declarations or by application registrations. The [strict composition contract](#strict-document-composition) defines their agreement, operation, and lifetime rules. @@ -1800,11 +1818,13 @@ named `conf.d`, and ConfigKit performs no remote fetching. ### Earlier directive spelling -The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0. -There is no legacy alias: `_configkit` is ordinary application data, subject to -normal schema validation, and does not trigger imports. When upgrading, rename -directive keys in root files and imported fragments, and rename any application -fields previously named `configkit` to avoid the newly reserved key. +The directive key is `config`. Neither `configkit` nor `_configkit` is an alias: +both are ordinary application data subject to normal schema validation and do +not trigger imports. Rename genuine old control blocks in each source; move, +rename, or separately mount source-root application fields named `config`. +Stored source-local declaration paths change from `/configkit/...` to +`/config/...`. Permissive schemas may decode old blocks as data without +executing their imports. ## YAML documents diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 87d62bd..25323f9 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -7,6 +7,16 @@ Your application still owns CLI parsing, resource reads, remote acquisition, and runtime initialization. Import `"go.maximumdirect.net/configkit"`; the package name is `configkit`. + +Document controls now use root `config` in every source. Rename genuine old +`configkit` or `_configkit` control blocks and update stored declaration paths +from `/configkit/...` to `/config/...`. Old blocks become ordinary data and a +permissive schema can accept them without executing imports. Move, rename, or +separately mount application fields previously authored as source-root `config`. +Nested and assembled root `config` values remain ordinary application data; the +[mounted-root example](../../examples/consumers/config_controls_test.go) shows +the boundary. See the [configuration contract](../config.md#declaration-and-selection-rules) +for the complete rules. Start with one file, then choose the tasks your application needs: | Task | Start here | diff --git a/docs/development.md b/docs/development.md index 846f622..1132fb5 100644 --- a/docs/development.md +++ b/docs/development.md @@ -64,6 +64,7 @@ Accepted decisions: - [Define identity and merge rules for each collection](adr/0017-collection-sequence-policy.md) - [Apply CLI overrides after inheritance and decoding](adr/0018-cli-after-structural-resolution.md) - [Keep main-file discovery explicit and composable](adr/0019-explicit-configuration-discovery.md) +- [Scope configuration controls to document roots](adr/0020-document-root-controls.md) For cross-cutting changes, follow every applicable row. Completed implementation and remediation plans are preserved in Git history at `cb5b435`; use current diff --git a/docs/internal/overview.md b/docs/internal/overview.md index d538455..4d35b9a 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -110,7 +110,9 @@ source, filesystem, destination, and import settings. [`acquisition.go`](../../internal/engine/acquisition.go) defines acquisition policy, load-wide accounting, and tier-scoped repetition/namespace state. Each -input is acquired under that policy; grouped requests retain acquired underlay +input is acquired under that policy. Source-root controls are extracted from +each acquired tree before placement; assembled application data is not parsed +again for controls. Grouped requests retain acquired underlay scopes until their tier is composed in [`load_request.go`](../../internal/engine/load_request.go). Completed tiers then share one assembly handoff that applies collection declarations and projects diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 1c6c6c8..5acc23c 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -207,7 +207,9 @@ records this ordering. CLI overrides are the highest-priority input to application resolution. Supplied sequences replace whole sequences, and mappings combine recursively. Inheritance and typed defaulting do not run again after CLI application. CLI paths and values -must not introduce structural directives; earlier source, inheritance, and +must not introduce inheritance controls; source-document controls are interpreted +only at acquisition, before placement, and later application `config` data is +not reinterpreted. Earlier source, inheritance, and decoding failures remain errors even if a CLI assignment would replace the value. Semantic validation assesses the final configuration after overrides and resolution. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 62c859e..84f31ef 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -289,6 +289,8 @@ executable example. Existing scenarios still exercise their original features. ## Stage 9 — Update current documentation and public comments +**Status:** Complete. Current documentation, navigation, and public comments reflect the implemented boundary; local links and examples pass. + **Scope:** canonical behavior, local architectural descriptions, consumer migration guidance, and navigation after the implementation is in place. diff --git a/layered.go b/layered.go index d84a063..bf29b00 100644 --- a/layered.go +++ b/layered.go @@ -5,7 +5,8 @@ import "go.maximumdirect.net/configkit/internal/engine" // Input selects one source, its application destination, whether its imports are // enabled, and any explicit filesystem context. A zero Source is invalid; use // NoSource() for an omitted main input. Sources keep their documented reuse and -// reader ownership rules. +// reader ownership rules. Each source's root controls are interpreted before +// placement; a destination named config remains ordinary application data. type Input struct { Source Source // Destination establishes the mapping path for Source. Imports inherit this diff --git a/lifecycle.go b/lifecycle.go index 2b2b28c..9e83f24 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -89,8 +89,9 @@ type ImportDefaults struct { } // Options contains the application definition and per-load input selections. -// Imports enables traversal of declarations in a selected main document; an -// absent source has no main declarations to traverse. Overlay selects one fixed +// Imports enables traversal of source-root declarations; an absent source has +// no main declarations to traverse. Controls are interpreted before placement, +// while nested and assembled application config values remain data. Overlay selects one fixed // overlay and requires filesystem context for NoSource. Overrides contain only // values explicitly supplied by an application CLI. // Enabled source-version modes interpret Definition.Version.Path in each