Add selected profile overlays

This commit is contained in:
2026-09-06 21:03:45 +00:00
parent 71a73bcfd8
commit ef0e6078f7
17 changed files with 897 additions and 81 deletions

View File

@@ -20,28 +20,29 @@ The zero `Source` is invalid. Constructor argument failures are retained inside
opaque source and become safe structured diagnostics when the source is consumed.
File sources can observe file changes between consumptions; byte sources cannot.
Each source is limited to 8 MiB. An imported load is limited to 64 MiB across all
accepted sources, 1,024 accepted sources, 64 active import edges, and 1,000,000
input nodes in aggregate. Glob expansion separately permits 1,000,000 visited
directory entries, including nonmatches. Reads request at most the remaining
allowance plus one byte so excess data is rejected rather than silently truncated.
Data returned together with a read error is not accepted as partial input.
Each source is limited to 8 MiB. A load using imports or an active profile is limited
to 64 MiB across all accepted sources, 1,024 accepted sources, 64 active import
edges, and 1,000,000 input nodes in aggregate. Glob expansion separately permits
1,000,000 visited directory entries, including nonmatches. Reads request at most the
remaining allowance plus one byte so excess data is rejected rather than silently
truncated. Data returned together with a read error is not accepted as partial input.
## Single-source loading
`Load[T](source, options...)` accepts zero or one `Options[T]`. The configuration
root `T` must satisfy the application schema contract below. With no options it
starts from the Go zero value. A `Definition[T].Defaults` factory, when supplied,
runs exactly once after successful source parsing, schema construction, and optional
schema-version checking. Its result is detached before input is applied.
runs exactly once after successful source parsing, schema construction, optional
schema-version checking, and selected-profile loading. Its result is detached before
input is applied.
The current lifecycle is:
```text
validate options → parse the selected source → traverse/compose enabled imports
→ build schema → check the optional application version → create and clone defaults
→ strictly decode → recorded application resolution → semantic validation
→ snapshot configuration and metadata
→ build schema → check the optional application version → apply a selected profile
→ create and clone defaults → strictly decode → recorded application resolution
→ semantic validation → snapshot configuration and metadata
```
`File`, `Bytes`, and `Reader` each contribute the first base source with deterministic
@@ -51,13 +52,12 @@ contributes no source, and continues through defaults, decode, and projection. A
missing file remains an error; it is never interpreted as no input.
The current `Options` boundary accepts a defaults factory, `VersionRule`, resolver,
semantic validator, explicit path and glob imports, and filesystem context. Active
profiles, overrides, and dynamic secret classification return `options.unavailable`
because their lifecycle stages are not implemented. The reserved profile spelling
`default` selects no active profile. `NoSource`
additionally rejects imports, active profiles, and filesystem context as invalid
combinations. Input containing a reserved `_configkit` mapping key is rejected while
imports are disabled.
semantic validator, explicit path and glob imports, one selected profile, and
filesystem context. Overrides and dynamic secret classification return
`options.unavailable` because their lifecycle stages are not implemented. `NoSource`
rejects imports, active profiles, and filesystem context as invalid combinations.
Input containing a reserved `_configkit` mapping key is rejected while imports are
disabled.
Every source, options, schema, version, defaults, decode, resolution, or validation
failure returns a nil result and a `*LoadError`. Successful results contain caller-owned `Config`, detached
@@ -79,13 +79,16 @@ error without reporting one, ConfigKit emits a generic `version.hook_failed`
diagnostic and suppresses the returned error text. Warnings alone are retained on a
successful result. Callback panics remain programming failures.
### Base/default metadata
### Layered input/default metadata
`Result.Metadata` describes every final canonical path. Exact surviving input paths
are explicit and have a base origin; omitted paths have a default origin. An explicit
parent mapping does not make omitted children explicit. Null state and available
source coordinates are retained. `History` records the Go-zero or application
default baseline followed by exact base definitions, including equal-valued input.
are explicit and have a base or profile origin; omitted paths have a default origin.
An explicit parent mapping does not make omitted children explicit. Null state and
available source coordinates are retained. `History` records the Go-zero or application
default baseline followed by exact base or profile definitions, including
equal-valued input. Profile replacement removes displaced descendants from effective
metadata while retaining their base definition and the parent replacement operation
in history.
All metadata queries and returned nested records are detached.
### Recorded application resolution
@@ -181,6 +184,43 @@ traversal order. Traversal order never authorizes replacement. Composition clone
the resulting tree and its ordered claim records rather than mutating or aliasing an
input document.
## Selected profiles
`Options.Profile` selects at most one overlay by name. Names must match
`[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name
`default` select no profile, and `profiles/default.yml` is never loaded implicitly.
Every other name resolves only to `<configuration-root>/profiles/<name>.yml`, with
no extension fallback, case normalization, inheritance, or profile stack. A selected
missing profile is an error.
The profile is a separately policy-validated YAML mapping. It is loaded after the
base import graph is composed and its schema version is checked, but before defaults
and typed decoding. Profile bytes, nodes, and source records count toward the same
aggregate limits as base inputs. Its canonical path must remain within the configured
root and must not duplicate a file already present in the base graph.
Overlay is intentionally distinct from strict composition:
| Existing lower node | Incoming profile node | Result |
| --- | --- | --- |
| Absent | Value | Retain the profile value |
| Value | Absent | Retain the lower value |
| Mapping | Mapping | Recurse by mapping key |
| Any other pair of present nodes | Any other pair of present nodes | Replace the entire lower node |
Scalars, sequences, and null are atomic. Thus a profile sequence replaces rather
than extends a base sequence, `[]` clears it, null is an explicit replacement, and
cross-kind replacement is allowed when the final application schema accepts it.
Replacing a subtree removes all displaced descendants from effective presence and
origin data; history retains the displaced definitions and identifies the parent
replacement. Surviving profile paths have profile origins, while recursively shared
mapping containers retain their contributing locations.
A profile may not contain `_configkit` anywhere, contribute the configured schema
version, or replace an ancestor of that version with a scalar or null. These rules
apply even if the supplied version text would be unchanged. Profiles cannot mask a
base parse, composition, or version failure because those checks run first.
## Explicit imports
Imports are opt-in. With `Options.Imports` enabled, a document may reserve its root

View File

@@ -3,7 +3,8 @@
This is the contributor entry point for ConfigKit. Read the task-specific owners
below before making changes. The repository is in early implementation: its public
loader currently covers strict selected sources, explicit path/glob import graphs,
composition, typed defaults, schema-version checks, recorded application resolution,
composition, selected profile overlays, typed defaults, schema-version checks,
recorded application resolution,
semantic validation, and metadata. The
[component overview](internal/overview.md) owns the implementation inventory.

View File

@@ -155,6 +155,17 @@ Assigned to selected-profile overlay:
declared defaults, inheritance, arbitrary profile paths, or validating every
unselected overlay.
Implementation record (2026-09-06): `overlayDocuments`, `mergeOverlayMappings`,
`loadProfileDocument`, and `profileDirectiveDiagnostics` adapt the assigned merge,
selection, loading, and ownership mechanics. `TestOverlayDocumentsRecursesMappingsAndReplacesAtomicSubtrees`,
`TestLoadAppliesSelectedProfileAfterBaseAndBeforeDefaults`, profile selection/error,
directive/version, containment, and budget cases adapt the assigned tests. ConfigKit
uses exactly one fixed selected `.yml` source, permits null and cross-kind replacement,
retains displaced descendant history, rejects any profile directive or version-path
contribution, and counts the profile in aggregate budgets. Narratio's declared
default, multi-overlay validation, inheritance-like application conventions, and
null/kind restrictions were not retained.
### Effective inspection and provenance
Assigned to layered metadata, redaction, rendering, and explanation:

View File

@@ -13,6 +13,8 @@ module and package documentation boundary. Its implemented public primitives are
- a private strict composer in `compose.go` that joins only disjoint document-tree
definitions, retains ordered claims and composite mapping origins, reports every
conflicting location, and never aliases an input tree;
- fixed selected-profile loading in `profile.go` and recursive, non-aliasing overlay
with displacement records in `overlay.go`;
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
- descriptor-driven canonical projection, detached typed cloning, cycle/limit
@@ -31,7 +33,7 @@ module and package documentation boundary. Its implemented public primitives are
`validation.go`, completing the public callback lifecycle after composed input.
The [configuration contract](../config.md) owns implemented input, schema, decoding,
loading lifecycle, path imports, composition, version checking, recorded resolution,
loading lifecycle, path imports, composition, profile overlay, version checking, recorded resolution,
semantic validation, metadata, and ownership behavior.
The source-extraction inventory records the application foundations assigned to

View File

@@ -2,7 +2,7 @@
## Status and ownership
Status: implementation in progress; Stages 1–9 are complete and later stages are
Status: implementation in progress; Stages 1–13 are complete and later stages are
pending.
Audience: a `gpt-5.6-terra` coding agent implementing one stage per prompt, in
@@ -982,7 +982,31 @@ Acceptance:
## Stage 13 — Implement profile overlays
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Added a private non-aliasing recursive overlay that merges mappings by key and
atomically replaces scalars, sequences, empty sequences, nulls, and cross-kind
subtrees. Replacement deletes stale descendant origins and records every displaced
definition plus the parent operation for later history.
- Added exact selected-profile loading from `profiles/<name>.yml`, with constrained
names, inactive empty/`default` selection, canonical root containment, base-graph
duplicate rejection, deterministic profile source ownership, and shared aggregate
source/byte/node limits. No path selection, extension fallback, inheritance, or
implicit default file is supported.
- Applied profiles only after base import composition and version checking and before
defaults and strict decoding. Profiles reject `_configkit` at any depth, direct
version contributions, and scalar/null replacements of version ancestors; base
parse and version failures therefore cannot be masked.
- Tests cover recursive siblings, scalar/full/empty-sequence/null/cross-kind
replacement, lower defaults, profile source and origin attribution, removed-child
and stale-index history, selection errors, fixed extension, directives, version
placement, base aliases, parse ordering, filesystem context, symlink escape, input
budgets, and non-aliasing. The
[current profile contract](../config.md#selected-profiles) owns the behavior.
Verified `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`,
and `git diff --check` with Go 1.26.7 on linux/amd64.
Read: feature [profiles](initial.md#profiles), composition/overlay table.

View File

@@ -450,28 +450,9 @@ origin is a resolution operation, with input origins retained separately.
## Composition and overlay
Strict base composition is implemented and now owned by the
[current configuration contract](../config.md#strict-document-composition).
Intentional profile overlay remains target behavior. Its exhaustive operation table
is below; “value” includes scalar, sequence, mapping, and null, while absent means no
node rather than null.
| Existing/lower node | Incoming/higher node | Overlay |
| --- | --- | --- |
| Absent | Absent | Absent |
| Absent | Value | Incoming |
| Value | Absent | Existing |
| Mapping | Mapping | Recurse by key |
| Any other pair of present nodes | Any other pair of present nodes | Replace entire existing node |
A mapping can replace a lower scalar, sequence, or null during overlay, subject to
the final schema. Sequences are atomic and the higher sequence replaces the entire
lower sequence in an overlay. `[]` clears a sequence. There is no append, union,
index merge, or mapping-clear operator.
Replacing a parent must not leave effective metadata for removed children.
Sequence histories describe whole-sequence replacement; index origins refer to
the surviving sequence, not an attempted correspondence with previous indices.
Strict base composition and selected-profile overlay are implemented and owned by
the [current composition contract](../config.md#strict-document-composition) and
[profile contract](../config.md#selected-profiles).
## Explicit imports and source identity
@@ -481,18 +462,9 @@ filesystem context, and budgets are implemented and now owned by the
## Profiles
Load only the composed base unless the application selects a profile. The API
accepts a name matching `[a-zA-Z0-9_-]+`, not an arbitrary path. The empty name
and reserved lowercase name `default` both mean no overlay; a literal
`profiles/default.yml` is never loaded. Other names resolve exactly to
`<configuration-root>/profiles/<name>.yml`, without extension fallback or case
normalization. A selected missing profile is an error.
A profile is one policy-validated YAML mapping applied using the overlay table.
It must not contain `_configkit`, introduce imports, or alter the application's
declared schema-version path. Reject these directives even if their values
would be unchanged. There is no profile inheritance or multiple-profile stack.
Applications own flags and commands that select this behavior.
Selected profiles are implemented and owned by the
[current profile contract](../config.md#selected-profiles). Applications continue
to own the flags and commands that choose a profile name.
## Static defaults and null
@@ -597,8 +569,8 @@ Automatic migration is deferred.
## Consumer API design
The core declarations, callback lifecycle, strict composition, and explicit path
imports are implemented; their current contract is owned by
The core declarations, callback lifecycle, strict composition, explicit imports,
and selected profiles are implemented; their current contract is owned by
[configuration input](../config.md) and Go reference comments. Remaining declarations
and behavior in this section are target design, not implemented Go reference
documentation or a frozen naming commitment. Declarations omit method bodies and
@@ -614,7 +586,7 @@ and verification against consumer models.
`Load`, `Definition`, `Options`, `FilesystemContext`, `Override`, `VersionRule`,
`Version`, `Validation`, and `Result` are declared. The current loader supports the
callback and path-import subset described by the [current contract](../config.md);
later profile, override, secret, and rendering behavior below remains target work and
later override, secret, and rendering behavior below remains target work and
unavailable options fail explicitly rather than being ignored.
```go
@@ -660,8 +632,8 @@ ConfigKit does not synchronize application-owned captured state.
`Source`, its constructors, and import filesystem context are implemented;
[`source.go`](../../source.go) and the [configuration input contract](../config.md#sources)
own their current behavior. Applications must use non-secret source labels because
labels appear in diagnostics and source reports. Profile-related filesystem behavior
remains target work. Filesystem options do not change application field path meanings.
labels appear in diagnostics and source reports. Filesystem options do not change
application field path meanings.
`NoSource()` explicitly selects defaults-only loading. It contributes no document
or file source entry and marks no fields explicit by itself; CLI overrides still