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