1441 lines
90 KiB
Markdown
1441 lines
90 KiB
Markdown
# Configuration Input Contract
|
|
|
|
This document owns ConfigKit's currently implemented source-input, YAML syntax,
|
|
schema, typed-decoding, loading, metadata, redaction, rendering, and ownership
|
|
contracts. The parser, owned document representation, and decoder remain private
|
|
behind the public loading lifecycle.
|
|
|
|
## Sources
|
|
|
|
ConfigKit exposes opaque sources through these constructors:
|
|
|
|
| Constructor | Current contract |
|
|
| --- | --- |
|
|
| `File(path)` | Requires a nonempty path, opens a regular file for each consumption, and closes only the library-owned file handle. Missing, inaccessible, unreadable, directory, and other nonregular inputs fail. |
|
|
| `NewFilesystem(label, filesystem).File(name)` | Borrows a caller-owned `fs.FS` under a safe nonempty display label. `name` must be a non-root slash-relative `fs.ValidPath`. Each load reopens the selected regular file; copies of the `Filesystem` preserve namespace identity. Separate constructors create separate namespaces even for the same label or filesystem. |
|
|
| `Bytes(label, data)` | Requires a nonempty, non-secret diagnostic label and snapshots bytes during construction. Later caller mutation cannot affect the source. |
|
|
| `Reader(label, input)` | Requires a nonempty, non-secret label and non-nil reader. The reader is claimed once across all copies of the source, consumed but never closed, and cannot be reused. |
|
|
| `Mapping(label, values)` | Requires a safe nonempty label and a non-nil root `map[string]any`. Snapshots supported built-in values into an owned mapping document at construction; `{}` is valid. Constructor conversion failures become safe deferred diagnostics. |
|
|
| `NoSource()` | Selects defaults-only loading, creates no source record, and marks no paths explicit. |
|
|
|
|
The zero `Source` is invalid. Constructor argument failures are retained inside the
|
|
opaque source and become safe structured diagnostics when the source is consumed.
|
|
File and filesystem sources can observe changes between consumptions; byte and
|
|
mapping sources cannot. Mapping accepts nil, built-in string, boolean, signed or
|
|
unsigned integer, finite float, `[]any`, and `map[string]any` values recursively.
|
|
Typed nil maps and slices represent explicit null; empty collections remain empty.
|
|
Keys are sorted for stable provenance, integer decimal spelling stays exact, and
|
|
cycles or unsupported types are rejected without exposing values. Mapping input
|
|
uses the same directive and version rules as YAML, not CLI override restrictions.
|
|
|
|
Each source is limited to 8 MiB. A load using imports or an active overlay 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.
|
|
For mapping sources, key and string bytes use the same accounting as programmatic
|
|
overrides, and the converted tree uses the same node and depth bounds.
|
|
|
|
## 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, optional
|
|
schema-version checking, and selected-overlay loading. Its result is detached before
|
|
input is applied. Resolver-provided mutable values are detached before secret lookup
|
|
and validation, and a final detached copy prevents equal-valued aliases introduced
|
|
by validators from escaping into the returned configuration. The publication copy
|
|
preserves already validated secret state without rereading the environment.
|
|
|
|
The current lifecycle is:
|
|
|
|
```text
|
|
validate options → parse the selected source → extract source controls and
|
|
traverse/strictly compose enabled imports
|
|
→ build schema → check the optional application version → apply a selected overlay
|
|
→ expand sibling inheritance
|
|
→ create and clone defaults → strictly decode → apply explicit CLI overrides
|
|
→ recorded application resolution
|
|
→ resolve typed secret references → semantic validation → classify secrets
|
|
→ snapshot configuration and metadata
|
|
```
|
|
|
|
`File`, `Filesystem.File`, `Bytes`, `Reader`, and `Mapping` each contribute the first base source with deterministic
|
|
ID `source-1`; enabled imports continue in depth-first order as `source-2` onward.
|
|
`NoSource` skips parsing, supplies an absent version to a configured version checker,
|
|
contributes no source, and continues through defaults, decode, and projection. A
|
|
missing file remains an error; it is never interpreted as no input.
|
|
|
|
`Options` accepts the application definition, ordinary-source `Destination` and
|
|
`PlacementMode`, explicit imports, one selected overlay, filesystem
|
|
context, explicit CLI overrides, and shared source-version controls. The definition
|
|
may register sequence identities through `Definition.SequenceIdentities`. Its
|
|
zero-valued placement mode places the source mapping
|
|
at the destination; `sequence-item` explicitly wraps the mapping as one item at a
|
|
non-root destination. Unknown modes, invalid destinations, and invalid or
|
|
conflicting sequence registrations fail before source acquisition. Overlay and
|
|
CLI coordinates remain rooted at the application root. Ordinary `Load` recognizes
|
|
source-root collection declarations whether or not imports are enabled; an
|
|
`imports` declaration still requires imports to be enabled. `NoSource` may enable
|
|
imports, but it has no main declarations to traverse. It accepts an explicit
|
|
filesystem context: a host context requires an absolute existing `Root` and may
|
|
omit `BaseDir` to use that root as its internal import base; a native context
|
|
requires its namespace, `BaseDir`, and `Root`. Supplied context is validated even
|
|
when imports are disabled. An active ordinary overlay requires this explicit
|
|
context. `NoSource` accepts overrides and remains absent rather than contributing
|
|
an empty document.
|
|
|
|
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
|
|
`Metadata`, a defensive source list, and defensive nonfatal diagnostics. Each source
|
|
record's `Kind` identifies file, filesystem, bytes, reader, mapping, or CLI acquisition
|
|
independently of its precedence `Layer`. Host file paths remain canonical;
|
|
filesystem paths are slash-relative within their namespace; synthetic inputs have
|
|
no path. Mutating
|
|
the returned configuration or inspection records cannot alter ConfigKit's private
|
|
snapshot. ConfigKit performs no logging. `Result.WriteEffective` exposes only the
|
|
frozen redacted projection.
|
|
|
|
### Completion and resource lifetime
|
|
|
|
`Load` is synchronous and complete on return. Success means input assembly,
|
|
defaulting, typed decoding, configured application resolution, secret lookup,
|
|
validation, and configuration/metadata snapshotting have completed. A reported
|
|
load failure returns diagnostics and a nil result, as described above.
|
|
|
|
Before returning on success or failure, ConfigKit closes the filesystem handles
|
|
it opened for loading, including files and directory/root handles. Caller-owned
|
|
filesystems are borrowed, never closed. The returned
|
|
configuration and inspection metadata require no close operation and remain
|
|
usable independently of the source files. A supplied `Reader` remains
|
|
caller-owned and is never closed by ConfigKit.
|
|
|
|
ConfigKit performs no background work after returning: it does not watch inputs,
|
|
refresh secrets, or mutate published results. Inspection and rendering run only
|
|
when explicitly called and use the retained snapshot. Applications normally load
|
|
once near startup, but may explicitly load again using sources that satisfy their
|
|
documented reuse rules. Additional loads produce independently owned results.
|
|
|
|
Completion is not a transaction over external state. Files and environment
|
|
variables are observed during processing rather than as one simultaneous external
|
|
snapshot. ConfigKit does not roll back application callback effects or manage work
|
|
or resources created by application callbacks. The explicit secrets-directory
|
|
bootstrap remains a separate operation invoked by the application.
|
|
|
|
## Layered loading
|
|
|
|
`LoadLayered[T](main, options...)` accepts zero or one `LayeredOptions[T]` value.
|
|
Its `main` is an `Input` containing a source, import opt-in, and optional
|
|
filesystem context. A zero source is invalid; `Input{Source: NoSource()}` omits
|
|
the main document. Each `Underlay` contains inputs that compose strictly as
|
|
one tier, including each input's enabled imports. Empty groups contribute
|
|
nothing. Underlay groups are listed from lowest to highest priority. Repeating
|
|
a file within one group remains an import/repetition error; the same reusable
|
|
source may be selected again in another tier.
|
|
|
|
ConfigKit acquires the main input first and constructs the schema. It then
|
|
acquires and strictly composes underlay groups from lowest to highest priority,
|
|
passing each group's accumulated lower collection policy into the next group.
|
|
Each group is applied once as a precedence tier. The main tier is then composed
|
|
with the accumulated underlay policy, and the optional application version is
|
|
checked against that composed main tier only. Underlay roots cannot claim that
|
|
version path (`underlay.version`); imported fragments retain `import.version`
|
|
protection. ConfigKit then applies the selected overlay and resolves inheritance.
|
|
Scalar, sequence, and null definitions replace lower values between tiers unless
|
|
a collection policy defines the sequence operation; mappings combine recursively.
|
|
After successful defaults and typed decoding, CLI overrides are applied to that
|
|
typed configuration before application resolution. Syntax, strict-composition,
|
|
inheritance, and decoding errors cannot be masked by a CLI override. Resolution,
|
|
secret lookup, validation, and publication run once on the resulting values.
|
|
|
|
An active layered overlay requires `LayeredOptions.OverlayFilesystem` explicitly,
|
|
independent of the main source's namespace. Its name and root-relative lookup
|
|
follow the selected-overlay rules below; inactive overlays ignore the context.
|
|
The same path in a different filesystem namespace is not a repeated file.
|
|
Ordinary `Load` infers overlay context from sources that provide one; with
|
|
`NoSource`, it requires the explicit `Options.Filesystem` context.
|
|
|
|
Source IDs follow acquisition order—main and its depth-first imports, then each
|
|
underlay tier's inputs and depth-first imports, then overlay and CLI—even though
|
|
history follows precedence order. Default main-version checking happens after
|
|
underlay composition because the main tier inherits the accumulated collection
|
|
policy. Enabled per-source version checks retain acquisition order and source
|
|
local headers. Source `Layer` describes precedence while
|
|
`Kind` describes acquisition. Underlay records use `LayerUnderlay` and a
|
|
one-based `UnderlayIndex` giving their original tier position; all other records
|
|
have index zero. Explicit empty groups and `NoSource` consume no source ID.
|
|
Byte, source, node, and glob-entry budgets are shared across the load, while
|
|
the existing per-source and import-depth bounds still apply.
|
|
|
|
When a source mapping is explicitly placed as an item, its effective destination
|
|
is the sequence container. After assembly assigns an index, nested source
|
|
destinations are projected through that item. Physical source locations and
|
|
resource contexts continue to identify the declaring source.
|
|
|
|
#### Input destinations
|
|
|
|
`Input.Destination` places the source's application mapping beneath a path
|
|
relative to the configuration root. Its zero or empty value means root, so
|
|
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
|
|
produce a structured `LoadError` before source acquisition.
|
|
|
|
Source-root `configkit.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
|
|
append to the importing document's effective destination. Omission and `[]`
|
|
add no keys. Nested imports continue relative to their immediate importing
|
|
document, each declaration is independent of its siblings, and all matches of
|
|
one glob receive the same resulting destination. This logical placement does
|
|
not change how the physical import path resolves from its declaring file.
|
|
|
|
A valid destination on `NoSource()` contributes nothing; an actual empty
|
|
mapping materializes its destination containers. Once placed, contributions
|
|
follow the same strict composition within a tier and recursive precedence
|
|
behavior described above. A selected overlay and CLI overrides remain rooted at
|
|
the application root and are not moved beneath the main input's destination.
|
|
|
|
`SourceInfo.Destination` reports each acquired source's effective mapping or
|
|
sequence-container path, including imports. `PlacementMode` reports how that
|
|
source was placed; mapping is the zero value, and CLI/overlay sources report
|
|
mapping unless item placement was applied. `DestinationUnresolved` is true only
|
|
when an enclosing item index needed to name the source's destination has not yet
|
|
been assigned. In that case `Destination` reports the nearest resolved enclosing
|
|
collection container and omits the unresolved local suffix. A source's own item
|
|
placement still has an exact destination at its sequence container. Completed
|
|
loads project nested source destinations through their owning items. Callback and
|
|
failed-load snapshots preserve the evidence available at that point; a fallback
|
|
snapshot remains unresolved, while a later exact snapshot is false. Returned
|
|
source records and paths are detached. Placement does not change source IDs,
|
|
acquisition identity, physical source locations, or passive resource contexts.
|
|
Introduced ancestor mappings use the contributing document's root location as a
|
|
source fallback; authored content retains its original source coordinates.
|
|
|
|
The maintained [layered consumer example](consumers/README.md#native-filesystem-and-layered-input)
|
|
shows a main host file, two underlay tiers, embedded defaults, a mapping source,
|
|
and explicit resource access without a second configuration pass.
|
|
The accepted rationale for input placement and its relative-import extension
|
|
is recorded in [ADR 0011](adr/0011-input-destinations.md) and
|
|
[ADR 0012](adr/0012-relative-import-destinations.md).
|
|
|
|
### Sibling inheritance
|
|
|
|
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
|
|
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
|
|
identity declaration have no sibling namespace for non-null string
|
|
relationships, though mappings nested inside them can have one. An `extends`
|
|
value of null cancels a relationship without looking up a parent. Other value
|
|
types, incomplete or extra reference fields, missing parents, and cycles fail
|
|
the load with value-free diagnostics.
|
|
|
|
Within each mapping sibling scope or identified sequence, direct definitions
|
|
resolve by dependency readiness. A parent is resolved before a child; among
|
|
currently eligible definitions, the lexically smallest canonical semantic
|
|
pointer is selected, including newly eligible definitions as soon as their
|
|
parent completes. This ordering governs inheritance operations and their
|
|
history, controls, and transfers. It does not reorder mappings or sequence
|
|
records in the resulting configuration.
|
|
|
|
`extends` is reserved structural input across ordinary and layered loads, every
|
|
source kind, imports, selected overlays, and explicit CLI overrides. It is
|
|
consumed before typed decoding and need not appear in the application's Go
|
|
types. Applications previously using `extends` as an ordinary YAML field must
|
|
rename that key and its field. No collection registration or keyword setup is
|
|
required for mapping-sibling relationships; identified-sequence relationships
|
|
require effective identity fields for that collection.
|
|
|
|
ConfigKit completes strict composition within tiers and precedence assembly
|
|
between tiers before resolving relationships. Only records that survive that
|
|
assembly participate in identified-sequence lookup: an upserted parent supplies
|
|
its final record, a replaced parent is unavailable, and a higher replacement
|
|
without `extends` supersedes a lower relationship. An omitted `extends` retains
|
|
a lower-priority mapping-sibling relationship; an identified record's effective
|
|
directive depends on the record that survives assembly. `extends: null` cancels
|
|
a relationship while preserving the child's own settings. Superseded references
|
|
are historical evidence, not active relationships to validate. Each child
|
|
recursively overlays its own settings over its resolved parent: explicit zero,
|
|
false, empty string, and
|
|
null values replace inherited values, while mappings combine recursively. For
|
|
identified records, the child's identity fields and their origins remain its
|
|
own; parent identity fields do not contribute to the payload.
|
|
Sequences at collection destinations use the shared collection operation with
|
|
the parent as lower input and the child as higher input. The destination's
|
|
explicit mode applies; otherwise a copied identity selects upsert and an
|
|
unkeyed destination replaces. A parent's explicit mode does not move to a
|
|
different destination. Sequences outside collection destinations remain atomic
|
|
replacements.
|
|
All effective relationships resolve, including
|
|
entries the application may never select for execution. Partial parent
|
|
definitions undergo normal typed decoding and application-owned validation,
|
|
not a special structural exemption.
|
|
|
|
Enclosing relationships resolve first. Nested instructions carried by a parent
|
|
are then resolved separately at each destination, so a child's nested sibling
|
|
override can change an inherited nested relationship. Typed defaults and
|
|
application-derived values do not start another inheritance pass. Inherited
|
|
input counts as explicit presence for `Metadata.IsExplicit`,
|
|
`Resolution.IsExplicit`, and absence-based resolver defaults, even for false,
|
|
zero, empty, or null input. `Origin.Inheritance` distinguishes it from a direct
|
|
declaration with concrete source, destination, and controlling directive paths.
|
|
A direct child replacement has its own effective origin; displaced ancestry
|
|
remains inspectable in history. An inherited relative resource keeps the
|
|
declaring source's passive `SourceContext`, not the child's directory.
|
|
|
|
The optional schema-version checker still runs once against the main input at
|
|
its original point. Inheritance cannot add, replace, or consume the protected
|
|
version path; an unrelated relationship inside an enclosing mapping does not
|
|
invalidate an unchanged version. Expansion uses the decoded projection
|
|
node/depth limits without charging copied values as acquired sources;
|
|
retained evidence uses the existing history-node limit. See the
|
|
[consumer example](consumers/README.md#named-sibling-inheritance) for one-load
|
|
usage.
|
|
|
|
### Source resource contexts
|
|
|
|
`Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
|
|
return a passive `ResourceContext` for a source with filesystem context, or
|
|
false for unknown and contextless IDs. The result query is nil-safe; the
|
|
resolution query follows the callback's lifetime guard. Queries perform no
|
|
filesystem access. Obtain IDs from exact origin locations rather than guessing
|
|
one context for a composite origin or an application-derived value.
|
|
|
|
`ResourceContext.FS` is rooted at the source's configured root, and `BaseDir`
|
|
is its slash-relative directory inside that root. This includes imported
|
|
sources, selected overlays, host files even without enabled imports, native
|
|
files, and byte, reader, or mapping inputs with an explicit context. CLI and
|
|
contextless synthetic inputs have no resource context. To open a relative
|
|
resource, join `BaseDir` with the application resource name using slash-path
|
|
operations and open that path through `FS`.
|
|
|
|
For a resource-valued field, query `result.Metadata().Lookup(fieldPath)`, inspect
|
|
its origin locations, choose a source ID only when the application's policy can
|
|
identify the intended contributor, then call `result.SourceContext(id)`. The
|
|
context belongs to that source even when the field survived from a lower-priority
|
|
underlay. A composite origin has no automatically selected resource base.
|
|
|
|
Native contexts borrow the caller's filesystem within the configured root.
|
|
Host contexts retain a canonical root path, not an open root handle; each
|
|
resource `Open` checks containment and opens a fresh handle that the returned
|
|
file's `Close` releases. Applications own and close the files they open.
|
|
Resources are application data, not configuration input, and do not consume
|
|
configuration byte or node budgets. The loaded configuration and metadata stay
|
|
usable if source files later change or disappear. Later resource operations
|
|
observe the backing filesystem then and may fail if files move, disappear, or
|
|
a caller-owned filesystem closes. This is not a snapshot or universal sandbox.
|
|
|
|
### Application schema versions
|
|
|
|
`VersionRule` declares a semantic path and non-nil checker. Before defaults or typed
|
|
decoding, the checker receives a `Version` preserving absent, explicit null, string,
|
|
or strict decimal-integer input. Strings and integers supply their exact spelling in
|
|
`Version.Text`; ConfigKit does not impose a machine-integer range. Fractional or
|
|
exponent numbers, booleans, mappings, and sequences produce `version.type`, and the
|
|
checker does not run. ConfigKit does not interpret the text or impose a universal
|
|
version path.
|
|
|
|
Integer versions use the strict grammar `^-?(0|[1-9][0-9]*)$`. Their spelling is
|
|
preserved exactly, including `-0`, and application policy decides which identifiers
|
|
are supported. A numeric version has no built-in `int64` range limit. Existing parser
|
|
policy still rejects leading zeroes, leading plus signs, hexadecimal notation, and
|
|
other non-JSON scalar forms before version checking.
|
|
|
|
At exactly the configured semantic path, accepted integer input can decode into a
|
|
plain Go string or named string without a text codec, including pointer wrappers.
|
|
This is a load-local exception: siblings and unrelated fields, dynamic values,
|
|
integer and float destinations, durations, `Secret`, and text codecs retain ordinary
|
|
strict decoding. The retained source and history stay numeric while typed projection
|
|
and effective YAML contain the resulting string; explanation records this through
|
|
the normal decoding reconciliation event. Fragment, overlay, and CLI ownership
|
|
protections apply before this conversion.
|
|
|
|
The checker reports safe, value-free errors and warnings through `Validation.Error`
|
|
and `Validation.Warning`; ConfigKit resolves primary and related paths to exact or
|
|
nearest input locations. Reported errors fail loading. If the checker returns an
|
|
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.
|
|
|
|
#### Shared source-version checks
|
|
|
|
`Options.SourceVersionMode` and `LayeredOptions.SourceVersionMode` select how
|
|
one `Definition.Version` policy applies across source documents. The zero value
|
|
`SourceVersionModeDefault` preserves the existing main-only check at its
|
|
effective path and existing protected-path behavior. `SourceVersionModeIfPresent`
|
|
checks each participating document that declares a version and permits absent
|
|
non-main declarations. `SourceVersionModeRequired` rejects a participating
|
|
document without a declaration. Enabled modes require a usable `VersionRule`
|
|
and checker; an unknown mode or unusable rule is an options error before source
|
|
access or callbacks.
|
|
|
|
In enabled modes, `Definition.Version.Path` is interpreted in each acquired
|
|
document's own coordinates, before placement and composition. The main document,
|
|
direct underlay documents, imported documents (including transitive and glob
|
|
matches), and the selected overlay participate. `NoSource`, absent optional
|
|
imports, empty underlay groups, defaults, resolution output, inheritance, and
|
|
synthetic CLI overrides are not documents to check. An absent declaration in an
|
|
actual main document still reaches the checker in if-present mode; required mode
|
|
rejects it first. With `NoSource`, the existing single absent-main checker
|
|
behavior remains in both enabled modes. If-present omissions in non-main
|
|
documents skip the checker. Explicit null is present and reaches the checker;
|
|
unsupported selected values produce `version.type`. No equality with the main
|
|
declaration or non-null rule is imposed. Acceptance establishes only that the
|
|
declaration is accepted by the consumer's checker; it does not establish that
|
|
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
|
|
validation and main-version ownership protection continue independently.
|
|
|
|
Accepted non-main declarations are consumed from their owned payload before
|
|
composition; their authored ancestors remain, even if empty. The main
|
|
declaration remains in the application. Ordinary release metadata should use a
|
|
different path. Enabled main protection uses the main input's effective path:
|
|
its `Destination` prefixed to the source-local `VersionRule.Path`. This protects
|
|
the remaining payload against imports, underlays, inheritance, and CLI
|
|
overrides. Thus a mounted main looks up a local header while ownership checks
|
|
protect its destination-prefixed application path. Main checker diagnostics and
|
|
declaration locations use source-local coordinates; ordinary payload
|
|
diagnostics continue to use effective paths. Default-mode diagnostics are
|
|
unchanged.
|
|
|
|
`SourceInfo.VersionCheck` is nil when enabled declaration processing did not
|
|
occur. Otherwise it records a source-local `Path`, the observed `Version` state
|
|
(`Present`, `Null`, and, when disclosed, exact `Text`), an actual or nearest
|
|
available `Location`, and whether text was redacted. A non-nil record with
|
|
`Present == false` records observed absence; it does not say the checker ran or
|
|
accepted a declaration. Absent evidence still identifies the full requested
|
|
header path, while its location identifies the nearest available source node.
|
|
During callbacks and in failed-load source records,
|
|
text is conservatively withheld because final classification may not be
|
|
available. Successful final source records reflect completed sensitivity
|
|
classification at the declaration's mounted path and ancestors, including
|
|
historical classification. Redaction empties `Text` while retaining presence
|
|
and null state. Returned copies are detached; evidence does not reopen inputs.
|
|
Consult [ADR 0013](adr/0013-shared-source-version-checks.md) for the accepted
|
|
decision and the [development guide](development.md#baseline-validation) for
|
|
the completed implementation and remediation records in Git history.
|
|
|
|
### Layered metadata
|
|
|
|
`Result.Metadata` describes every final canonical path. Queries retain explicit
|
|
key/index distinctions using actual effective and historical container shapes;
|
|
only parsed numeric tokens are resolved contextually. A descendant that never
|
|
existed is not historical merely because an atomic ancestor was replaced. Exact
|
|
surviving direct or inherited input paths are explicit and retain their source
|
|
layer and derivation; 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 overlay definitions, including
|
|
equal-valued input. Overlay 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.
|
|
|
|
`Metadata.HistoricalPaths` enumerates observed paths, including removed paths,
|
|
without reloading input or resolving unselected credentials. Consumers can use
|
|
it with `History` to validate ownership of declarations hidden by an overlay;
|
|
domain ownership rules remain application responsibilities. The method's Go
|
|
documentation in [metadata.go](../metadata.go) defines enumeration and path-ordering
|
|
details.
|
|
|
|
History is chronological: a path can have a synthetic default record, its exact
|
|
base definition, an overlay replacement, structural inheritance, and a recorded
|
|
resolution operation. A descendant
|
|
query uses the ancestor's path for a whole-container or whole-sequence replacement;
|
|
the descendant can therefore retain history after `Lookup` stops reporting it as
|
|
effective. Equal-valued input is still an explicit base definition, while a derived
|
|
value for absent input remains non-explicit.
|
|
|
|
Strict composition assigns contributed records their assembled sequence positions
|
|
in history. Across tiers, history follows the selected
|
|
[collection operation](#strict-document-composition), including append and
|
|
whole-record upsert. Incoming contributions use their assigned positions, and
|
|
replaced payload remains available as historical evidence.
|
|
|
|
A mapping assembled from the selected file and imports has one composite base origin
|
|
whose locations follow [scoped assembly order](#explicit-imports), not source-record
|
|
acquisition order. A mapping also visited by an overlay has an overlay-layer
|
|
composite origin with the lower contributing locations followed by the overlay
|
|
location; an empty mapping is still an explicit container even though it contributes
|
|
no children. Resolution origins are synthetic: they have layer
|
|
`resolution`, no fabricated file location, and a `derived_default` or `normalization`
|
|
operation. Reasons and dependency paths retain first occurrence order with duplicates
|
|
removed. Source records and IDs retain [acquisition order](#explicit-imports),
|
|
including the underlay ordering described in [layered loading](#layered-loading).
|
|
|
|
Inheritance adds no acquired source or precedence layer. `Origin.Inheritance`
|
|
lists concrete transfers from contributor paths through destinations, with
|
|
each step's controlling `extends` path and location. `History` retains source
|
|
applications at their original paths and projects parent contributions, child
|
|
replacements, and consumed controls at destination paths after source assembly
|
|
and before typed decoding. `HistoricalPaths` includes consumed directives and
|
|
displaced inherited descendants. A control can have history even when `Lookup`
|
|
reports it absent. Queries return detached records.
|
|
|
|
### Secret classification and effective rendering
|
|
|
|
Secret classification combines `configkit:"secret"` field tags with the optional
|
|
pure `Definition.Secret(Path) bool` predicate. Tags on concrete structs inside
|
|
supported dynamic values are retained in typed projections, including superseded
|
|
defaults and pre-resolution values. A tagged or predicate-classified
|
|
container makes all descendants sensitive. ConfigKit evaluates final and historical
|
|
paths, so replacing or deleting a definition does not erase its classification. The
|
|
predicate can add sensitivity but cannot declassify a tagged path.
|
|
|
|
Recorded resolution dependencies propagate sensitivity to their derived target.
|
|
Propagation reaches a fixed point, so record/map iteration order does not affect the
|
|
result and dependency cycles terminate. A dependency on a classified descendant or
|
|
on a container that contains one is treated conservatively as sensitive. Applications
|
|
must classify secret-producing transformations whose dependencies are not recorded.
|
|
Inherited value and control paths link source and destination classifications
|
|
through chains and nested copies; unrelated sibling fields are not classified
|
|
merely because another field is secret. Displaced historical evidence remains
|
|
redacted. Secret references are copied only as unresolved names; credential
|
|
lookup stays after decoding and application resolution.
|
|
|
|
`Result.WriteEffective` writes the frozen resolved snapshot as deterministic redacted
|
|
YAML. Mappings sort by decoded key string, sequences retain order, indentation is two
|
|
spaces with noncompact sequence indentation, line endings are LF, the document has no
|
|
start or end marker, and output ends with a newline. Nil pointers/maps/slices remain
|
|
`null`; non-nil empty maps and sequences remain `{}` and `[]`. A secret root is
|
|
replaced by the quoted string `'[REDACTED]'`. Redacted output is intended for
|
|
inspection and is not promised to be reloadable.
|
|
|
|
Rendering uses only a safe adapter tree built from the already completed canonical
|
|
typed projection. It does not invoke application YAML marshal hooks or text codecs;
|
|
all text-codec projection has succeeded before `Load` returns. Caller mutation of
|
|
`Result.Config` cannot change later output. Writer failures, including short writes,
|
|
are returned and may leave partial redacted output; internal dumper failures are
|
|
translated to a fixed safe error. There is no unredacted rendering option and
|
|
ConfigKit performs no implicit logging.
|
|
|
|
`Result.Config` is the explicit trusted boundary and contains ordinary typed values,
|
|
including secrets. Metadata records, diagnostics, and load errors contain no
|
|
configuration values. Explanation snapshots deliberately expose non-secret
|
|
configuration history after applying the redaction contract below. Applications
|
|
must still keep source labels, resolution reasons, and diagnostic messages value-free
|
|
because ConfigKit cannot infer and sanitize secrets embedded in arbitrary application
|
|
prose.
|
|
|
|
### Historical-value explanation
|
|
|
|
`Result.Explain(path)` returns a detached `Explanation` for any known effective or
|
|
historical path. Its effective value and each chronological before/after value are
|
|
`ValueSnapshot` records containing deterministic YAML text rather than mutable Go or
|
|
parser values. `Present=false` with empty YAML means absence or deletion; explicit
|
|
null is present and renders as `null`. The history starts with the canonical typed
|
|
application-default or Go-zero baseline, followed by relevant underlay and main-tier
|
|
contributions in precedence order, the selected overlay, and structural inheritance
|
|
events. Typed defaulting and decoding reconcile that source history before supplied
|
|
CLI records and audited net resolution operations. Within each strict tier, replay follows
|
|
[scoped assembly order](#explicit-imports), not source-record acquisition order.
|
|
Inheritance events include consumed or canceled `extends` input and equal-valued
|
|
child replacements. Overlapping parent/child resolution records
|
|
are rendered
|
|
as one net subtree event with combined reasons and dependencies, rather than
|
|
pretending that the recorded assignments ran in lexical order. Explicit equal
|
|
writes remain visible. When structural replay differs from the pre-CLI typed input,
|
|
a `ChangeDecoding` event records retained defaults and canonical decoding. CLI
|
|
contributions then retain their raw input representation while effective and
|
|
resolution values use the typed projection. This built-in inspection operation is
|
|
not accepted by `Resolution.Record`.
|
|
|
|
Base, overlay, and CLI history preserves the policy-validated input representation,
|
|
including a superseded value that could not have decoded into the final Go field.
|
|
Baseline, effective, and resolution values use canonical typed projection. A
|
|
decoding event bridges replayed input and canonical typed values, so intermediate
|
|
null/scalar replacements cannot incorrectly imply permanent default removal. Mapping
|
|
queries reconstruct each source or override contribution. A sequence snapshot
|
|
represents the collection at that point in replay, and sequence diffs remain
|
|
atomic. Descendant queries describe their presence and values before and after
|
|
containing sequence operations. Combination follows the
|
|
[collection operation tables](#strict-document-composition); upsert replaces
|
|
matching records in full. Removed paths remain explainable; unknown and malformed
|
|
paths return false.
|
|
|
|
Every snapshot applies the same historical tag, predicate, and recorded-dependency
|
|
sensitivity closure as effective rendering before YAML is exposed. A redacted scalar
|
|
is the quoted marker `'[REDACTED]'`, and a container snapshot has `Redacted=true` if
|
|
it contains a masked descendant. Typed `Secret` snapshots contain only the configured
|
|
environment name. `SecretReference` reports that name only for a successfully
|
|
resolved effective reference, and is omitted when its path is classified. Credential
|
|
values are never retained in explanation history.
|
|
|
|
`Result.WriteExplanation(output, path)` renders the safe structured result with the
|
|
canonical JSON-pointer path, canonical-versus-input representation labels, effective
|
|
source, composite locations, chronological values, resolution reasons and
|
|
dependencies, and successful secret-reference lookup attribution. Control characters
|
|
in paths, source identifiers and labels, reasons, dependencies, and reference names
|
|
are escaped. An unknown path returns a fixed safe error before writing anything;
|
|
writer errors and short writes are returned and may leave partial output. There is no
|
|
unredacted mode.
|
|
|
|
Retained history consists of immutable source applications and audited subtree
|
|
deltas, rather than a full tree copy per field. Shared nodes count once toward a
|
|
4,000,000-node retained-history limit; exceeding it fails loading with
|
|
`explain.limit_exceeded`. Replaying one requested path is performed on demand, and
|
|
the returned YAML strings and nested records cannot mutate later explanations.
|
|
|
|
Application commands should render the separate `SecretsReport` immediately after
|
|
an explicit `LoadSecretsDir` bootstrap, then use `WriteDiagnostics`,
|
|
`WriteEffective`, or `WriteExplanation` on the later load result as appropriate.
|
|
ConfigKit does not infer bootstrap outcomes from a secret reference and does not
|
|
bundle these independent reports into a command framework.
|
|
|
|
## Effective configuration diffs
|
|
|
|
`configkit.Diff(left, right)` compares two completed successful loads of the same
|
|
configuration type. `left` is the earlier state and `right` is the later state.
|
|
Applications load both sides with the sources and options they choose; to inspect
|
|
an overlay's impact, load the same base once without the overlay and once with it
|
|
selected. The [consumer guide](consumers/README.md#effective-configuration-diffs)
|
|
links to maintained examples. The public API returns safe changed-region records;
|
|
applications own their presentation.
|
|
|
|
Comparison uses the frozen canonical effective inspection values, not mutable
|
|
`Result.Config`, source text, provenance, or history. It does not reopen sources,
|
|
reread environment variables, or rerun defaults or callbacks. Equal effective
|
|
values produce no records even when sources, explicit-input status, defaults, or
|
|
resolution histories differ; for example, explicit zero and defaulted zero are
|
|
equal. Mappings compare recursively, sequences compare atomically, and scalar,
|
|
kind, or null/container replacements report their affected region. Records use
|
|
the existing comparator's canonical pointer ordering. `Present=false` with empty
|
|
YAML means absence; explicit null is present and renders as `null`.
|
|
|
|
Before either side is returned, sensitivity from both loads is applied to both
|
|
snapshots, including historical and dependency-derived classification. A
|
|
classification on either side protects the corresponding values on both sides.
|
|
`Redacted` reports masking, while `HiddenChanged` reports an actual concealed
|
|
value or presence change; it is not implied merely by redaction. A structural
|
|
replacement that prevents safe correspondence masks the complete reported
|
|
region, and an absent side remains absent. Record membership establishes that a
|
|
change occurred even when safe snapshots render identically. Literal
|
|
marker-looking input is ordinary input; consumers use the record fields rather
|
|
than interpreting marker text.
|
|
|
|
Typed secret references contribute their configured environment names to the
|
|
frozen comparison, never resolved credential bytes. Credential rotation behind
|
|
an unchanged reference is therefore not detected. The records contain changed
|
|
regions only, not paired full-document context; independently calling
|
|
`WriteEffective` on each result does not create cross-protected context. Nil or
|
|
uninitialized results return an error with no records. A failed `Load` retains
|
|
its normal nil-result/`LoadError` behavior, which callers handle before invoking
|
|
`Diff`.
|
|
|
|
## Typed secret references
|
|
|
|
Credential-bearing fields use the opaque `Secret` type and store an environment
|
|
variable name in YAML, such as `api_key: OPENROUTER_API_KEY`. `SecretEnv(name)`
|
|
constructs the same unresolved reference for application defaults or recorded
|
|
resolution. Names must match `[A-Za-z_][A-Za-z0-9_]*`; they are public
|
|
configuration and remain visible through `EnvName`, canonical projection, history,
|
|
and effective output unless the path is separately classified for redaction.
|
|
|
|
A non-pointer `Secret` is required. An absent, null, or invalid reference, or an
|
|
unset or empty environment value, fails with a safe `secret.*` diagnostic at the
|
|
configuration path. `*Secret` is optional only when nil; every non-nil reference
|
|
must resolve to a nonempty value. Secret references work in typed structs, maps,
|
|
pointers, slices, and arrays. A dynamic `any` value does not establish secret
|
|
semantics and cannot contain `Secret`; applications must use a typed slot.
|
|
|
|
ConfigKit resolves only the effective names after CLI assignment and recorded
|
|
application resolution, and before semantic validation. CLI overrides may supply
|
|
or replace a typed secret reference; lookup uses that effective name and retains
|
|
its effective source location for diagnostics. Each unique name is read with
|
|
`os.LookupEnv` once per load. Validation can inspect the credential through the
|
|
deliberately named `Value()` accessor and distinguish success with `IsResolved()`.
|
|
Later loads read the environment again, and cloned defaults are rebuilt from names
|
|
so a previously resolved result cannot retain a stale credential. `Load` never
|
|
writes to the environment.
|
|
|
|
Credential bytes are held only in the per-load lookup state and the trusted returned
|
|
configuration, and are returned only by `Value`. Secret formatting uses a safe
|
|
public-name representation, JSON marshals only the name, and projection, metadata,
|
|
histories, diagnostics, effective output,
|
|
and resolution snapshots never retain the credential. Validation remains read-only:
|
|
in addition to ordinary projected-value comparison, ConfigKit checks the private
|
|
resolved state against this load's lookup result. Validator mutation checks also
|
|
reject changes to projected field sensitivity, even when a replacement dynamic
|
|
concrete type preserves the same textual values. Applications must keep their
|
|
validation messages value-free and must not include `Value()` in diagnostics.
|
|
|
|
For example, a typed pipeline field is declared as:
|
|
|
|
```go
|
|
type PipelineConfig struct {
|
|
APIKey configkit.Secret `yaml:"api_key"`
|
|
}
|
|
```
|
|
|
|
The executable `ExampleSecretEnv` demonstrates loading and inspecting the safe
|
|
reference state without printing its value.
|
|
|
|
## Explicit secrets-directory bootstrap
|
|
|
|
`LoadSecretsDir(directory)` is a separate, opt-in startup operation. It is never
|
|
called by `Load`, is not a YAML option, and does not participate in configuration
|
|
validation. Applications choose the native directory path and should invoke the
|
|
operation once before starting concurrent work. The selected path may itself be a
|
|
symlink; ConfigKit resolves it to an existing canonical directory before examining
|
|
its immediate entries.
|
|
|
|
The bootstrap enumerates at most 1,024 entries, including dot-prefixed entries.
|
|
Entries whose names begin with `.` (files, directories, or symlinks, such as `.git`
|
|
and `.gitignore`) are ignored before name or metadata validation, are never opened,
|
|
and are omitted from the report. Remaining entries are sorted by filename; each
|
|
must have an ASCII environment-variable name and be a regular, nonsymlink
|
|
file. Metadata for all remaining entries is validated before file content is read
|
|
or process state is changed. Existing environment variables win, including variables whose
|
|
value is empty; their files are not opened. Each eligible file is limited to 1 MiB,
|
|
and eligible content is limited to 8 MiB in total. ConfigKit removes exactly one
|
|
terminal LF or CRLF, preserves all other whitespace, accepts an empty value, and
|
|
rejects NUL bytes.
|
|
|
|
All eligible files are read successfully before application begins. Presence is
|
|
checked again immediately before each `os.Setenv`: a variable populated since
|
|
preflight is skipped. A write failure stops application without rollback, so prior
|
|
writes remain and later eligible entries are `not_applied`. This operation is not
|
|
atomic and does not provide compare-and-set semantics for concurrent environment
|
|
mutation.
|
|
|
|
`SecretsReport.Entries` returns detached, filename-ordered `SecretFileResult`
|
|
records with `loaded`, `skipped_existing`, `not_applied`, or `failed` status.
|
|
`ExistingEmpty` distinguishes an empty existing winner. `SecretsReport.WriteTo`
|
|
renders only quoted names, canonical paths, statuses, and that empty-state flag;
|
|
it never retains or renders file contents. Failures return the non-nil partial
|
|
report with a `*SecretsError` containing detached `secrets.*` diagnostics and no
|
|
raw filesystem or environment error chain. Applications should retain the report
|
|
alongside the later configuration result and explicitly present blocked and failed
|
|
files when appropriate; ConfigKit stores no implicit global report.
|
|
|
|
### Recorded application resolution
|
|
|
|
An optional `Definition.Resolve` callback runs after successful defaults, typed
|
|
decoding, and explicit CLI assignment. It receives the working configuration and a
|
|
per-load `Resolution` context. The context's
|
|
`IsExplicit` query uses surviving input presence, so explicit false, zero, empty, and
|
|
null are never confused with absence or static defaults.
|
|
|
|
`Resolution.InputInfo(path)` returns detached effective metadata from the fixed
|
|
post-decode entry state, including explicit CLI overrides, before application
|
|
resolution runs. It includes retained static defaults, preserves explicit null, and
|
|
uses the same exact semantic path matching as `Metadata.Lookup`; missing,
|
|
malformed, and key/index-shape-mismatched paths return false without diagnostics.
|
|
Its `Explicit` value agrees with `IsExplicit` for every found entry path. Resolution
|
|
changes, additions, removals, and moves do not alter this entry view.
|
|
|
|
`Resolution.Source(id)` returns a detached record for an already acquired source in
|
|
the same load: the main source, imports, selected overlay, and supplied CLI records
|
|
where applicable. Empty and unknown IDs return false. It never reloads input, reads
|
|
the filesystem, resolves symlinks, or looks up credentials. A source path exists
|
|
only when the acquired source has one; defaults and byte, reader, and CLI sources do
|
|
not acquire an invented file path. Applications select a base directory from exact
|
|
origin locations and these source records according to their own policy.
|
|
|
|
Both queries are callback-scoped like the rest of `Resolution`: nil, zero, or
|
|
retained contexts panic with the fixed closed-context failure outside the callback.
|
|
Returned field and source records are detached and remain usable after callback
|
|
exit; changing them cannot affect later queries, results, or another load.
|
|
|
|
`Default` assigns and records a derived value only when its path is not explicit.
|
|
`Normalize` always assigns and records. For transformations performed directly by
|
|
application code, `Resolution.Record` declares a derived-default or normalization
|
|
operation; a recorded container covers changes to its descendants. Records require
|
|
a schema-valid path, valid kind, safe nonempty reason, and schema-valid dependency
|
|
paths. Repeated records at one path are coalesced: reasons and dependencies retain
|
|
their first occurrence, the last helper value is authoritative, and any normalization
|
|
makes the combined operation a normalization.
|
|
|
|
ConfigKit projects the configuration before and after the callback. Every net change
|
|
must have an exact or ancestor record. Helper-assigned values must agree with the
|
|
final value at their declared path, which supports ordinary map-entry copy/writeback
|
|
while detecting forgotten writeback or unrelated targets. Invalid paths, kinds,
|
|
values, records, helper targets, and unrecorded mutations fail the load. Equal-valued
|
|
derived assignments may still record application intent even when there is no
|
|
structural change.
|
|
|
|
`Resolution.Error` and `Resolution.Warning` attach exact or nearest available input
|
|
locations. Reported errors fail the load; warnings survive on a successful result. A
|
|
returned resolver error adds one generic `resolve.hook_failed` error only when the
|
|
callback did not report an error, and its text is never exposed. Callback panics are
|
|
programming failures. Resolution and validation contexts are callback-scoped; using
|
|
a nil, zero, or retained context outside its active callback panics with fixed,
|
|
value-free text.
|
|
|
|
Successful records update effective origins and append detached metadata history
|
|
without changing input explicitness. Removed paths leave effective metadata but
|
|
retain history. ConfigKit privately snapshots each recorded before/after subtree for
|
|
later safe explanation; caller mutations of `Result.Config` cannot alter those
|
|
snapshots.
|
|
|
|
### Semantic validation
|
|
|
|
An optional `Definition.Validate` callback runs after successful resolution and
|
|
receives the resolved configuration plus a per-load `Validation` context. It can
|
|
collect multiple independent `validate.error` and `validate.warning` reports.
|
|
Warnings continue the lifecycle and are returned on success; any error returns a nil
|
|
result with the full safe diagnostic and source report.
|
|
|
|
Primary and related paths must be valid for the application schema. Omitted declared
|
|
fields and missing string-map or dynamic keys are valid report targets; unknown
|
|
struct fields and malformed paths produce `validate.invalid_path`. ConfigKit attaches
|
|
the exact surviving input location when one exists, otherwise the nearest input
|
|
container location, or no primary location for defaults-only synthetic state.
|
|
|
|
Validation is read-only. ConfigKit projects the configuration immediately before and
|
|
after the callback and returns `validate.mutation` at every detected net-change path,
|
|
including nested mutable state. A value that can no longer be projected is also a
|
|
mutation failure. No mutated or otherwise invalid configuration is published.
|
|
Callback panics remain programming failures, and retained contexts use the same
|
|
fixed closed-context panic as version and resolution callbacks.
|
|
|
|
Only schema-bound exported state participates in loading, resolution audit,
|
|
validation audit, metadata, and snapshots. Ignored or unexported ordinary fields are
|
|
zero in returned detached values. Applications retain responsibility for domain
|
|
rules and runtime initialization; validators should not start clients or depend on
|
|
live services as a substitute for configuration validation.
|
|
|
|
## Strict document composition
|
|
|
|
Ordinary `Load` applies strict document composition to its parsed source scope;
|
|
`Options.Imports` adds imported documents as peers in that scope. Application
|
|
defaults do not participate. With imports disabled, the single source still has
|
|
its controls extracted and any declared or registered collection identity is
|
|
validated, but there are no imported peers to combine.
|
|
|
|
| Existing node | Incoming node | Result |
|
|
| --- | --- | --- |
|
|
| Absent | Absent | Absent |
|
|
| Absent | Present | Retain the incoming node |
|
|
| Present | Absent | Retain the existing node |
|
|
| Mapping | Mapping | Recurse by mapping key |
|
|
| Any other pair of present nodes | Any other pair of present nodes | `compose.conflict` |
|
|
|
|
Every non-mapping overlap is a conflict, even when values are equal, except for
|
|
sequence/sequence contributions at a destination with effective identity from an
|
|
application registration, a current YAML `key` declaration, or retained lower
|
|
policy. Nulls remain atomic conflicts.
|
|
Sequences without effective identity conflict when repeated and are never joined
|
|
by index. Disjoint entries under shared mappings compose, making stable keyed
|
|
mappings the natural representation for independently contributed entries.
|
|
|
|
`Definition.SequenceIdentities` contains registrations created by
|
|
`SequenceIdentityAt` or `SequenceIdentityMatching`. The exact form accepts a
|
|
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
|
|
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
|
|
arbitrary prefix or suffix. Each registration requires one or more literal,
|
|
direct field names; compound field sets are normalized as sets, and field names
|
|
are not otherwise normalized. The same field-set and scalar-equality rules apply
|
|
to YAML `key` declarations.
|
|
|
|
At each selected path, records must provide every effective identity field as a
|
|
scalar string, number, boolean, or explicit null. Field names form a set, so
|
|
order and repetition do not affect compound identity. The type-tagged tuple
|
|
preserves category distinctions: strings compare exactly, including empty
|
|
strings; numbers, booleans, and null remain distinct categories. Finite numbers
|
|
compare by exact mathematical value, so `1`, `1.0`, and `1e0` are equal, as are
|
|
`0` and `-0`, without floating-point rounding. Distinct tuples append in source
|
|
and list order; duplicate tuples conflict even when records are otherwise equal
|
|
or disjoint. Malformed records produce `compose.invalid`; duplicate identities
|
|
produce `compose.conflict`, with paths and source locations identifying a
|
|
duplicate without exposing identity values. Missing paths and non-sequence values
|
|
do not fail by themselves. Equivalent overlapping registrations coalesce.
|
|
Incompatible overlaps and invalid registrations produce `options.invalid` before
|
|
source acquisition. Unused and root registrations are allowed and have no effect.
|
|
|
|
Registrations provide identity for strict composition, sequence operations,
|
|
inheritance lookup, and final validation. Dynamic selectors apply at concrete
|
|
strict-composition, inheritance, and final-result destinations without requiring
|
|
the application to enumerate mapping keys. Each strict tier has independent
|
|
duplicate-identity state among its peers; layered precedence and inheritance
|
|
then apply the collection operation described below.
|
|
|
|
Final registration checks run in both ordinary and layered loads after effective
|
|
secret lookup and before application semantic validation. They examine actual
|
|
final typed sequences, including values introduced by defaults, CLI, or recorded
|
|
resolution; they do not require path existence or sequence shape. Diagnostics
|
|
use concrete collection and item paths and available source locations, without
|
|
exposing identity or credential values. The guarantee ends when loading publishes
|
|
successfully and does not cover later caller mutations.
|
|
YAML-only identity is enforced during structural composition, precedence, and
|
|
identified inheritance; it carries no final-result guarantee after typed
|
|
defaults, CLI, or application resolution. Applications that need that guarantee
|
|
register the identity constraint.
|
|
The [maintained catalog examples](consumers/README.md#identified-prompt-sequences)
|
|
show strict peers, precedence operations, inheritance, dynamic destinations, and
|
|
the distinction between YAML-only declarations and application registrations.
|
|
When an enabled source-version mode is selected, declarations are checked in each
|
|
source's own coordinates before placement and composition. Accepted non-main
|
|
headers are consumed before strict composition and cannot supply a registered
|
|
record identity. Final typed decoding and application validation still run on the
|
|
completed configuration.
|
|
|
|
Ordinary sources, layered inputs, and selected overlays may declare collection
|
|
behavior under `configkit.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
|
|
relative to that source's placement, so a mounted fragment's declaration is
|
|
mounted with its values. Declarations from strict peers must agree on identity
|
|
fields and on any explicitly supplied mode; omission expresses no mode
|
|
preference. They apply even when the declaring source has no value at that
|
|
destination: a key validates an existing retained sequence or remains pending
|
|
until records arrive, while a mode-only declaration affects later combinations
|
|
without recombining earlier tiers. A key supplies the identity used by `append`
|
|
and `upsert` and is checked against any application registration.
|
|
|
|
Strict peers at an identified destination first combine distinct records in
|
|
source and list order. Duplicate identities among peers or within one
|
|
contribution fail under every mode. After strict composition, precedence and
|
|
inheritance use one operation for the assembled incoming collection:
|
|
|
|
| Mode | Behavior at source-precedence and inheritance boundaries |
|
|
| --- | --- |
|
|
| `replace` | Replace the complete lower sequence with the assembled higher sequence. |
|
|
| `append` | Retain lower records and append higher records; duplicate identities fail. |
|
|
| `upsert` | Replace matching records in full at their lower positions, retain unmatched lower records, and append new identities. |
|
|
|
|
Upsert replaces the complete matched record; it does not recursively merge
|
|
record fields. Append and upsert require identity. With no explicit mode in the
|
|
current tier, an explicit lower mode persists; if none exists, effective identity
|
|
selects upsert and its absence selects replace. Inferred modes do not persist.
|
|
Explicit modes persist at their destination until changed by a higher explicit
|
|
mode, independently of YAML identity lifetime. A declaration alone does not
|
|
clear or recombine a collection.
|
|
|
|
The presence and value kind at a precedence or inheritance boundary determine
|
|
whether a collection changes:
|
|
|
|
| Higher contribution | `replace` | `append` / `upsert` |
|
|
| --- | --- | --- |
|
|
| Absent | Leave the lower value unchanged. | Leave the lower value unchanged. |
|
|
| Explicit `[]` | Clear the sequence. | Contribute no records. |
|
|
| Explicit null or another non-sequence value | Replace the lower value, subject to application schema. | Error; the operation requires a sequence. |
|
|
|
|
An absent lower value can be initialized by an incoming sequence under any mode.
|
|
A present lower null or other non-sequence value is not absence: append and
|
|
upsert cannot combine with it. An empty sequence still requires a valid mode and
|
|
identity where applicable. Null used as an identity component inside a record is
|
|
distinct from null used as the collection value.
|
|
|
|
Declarations govern ordinary strict source composition, including loads with
|
|
imports disabled, and each strict scope in layered loading. Before composing a
|
|
higher tier's peers, ConfigKit makes applicable lower identity and explicit mode
|
|
available; after peer composition, the tier applies its resolved operation
|
|
against the lower collection. Selected overlays use the same collection policy
|
|
after the base source graph is composed. During mapping-sibling inheritance, a
|
|
parent's YAML identity can follow its collection to the child destination, while
|
|
the parent's explicit operation stays at the parent destination. The child
|
|
destination selects its operation; absent one, effective identity implies
|
|
upsert and its absence implies replace. Whole-sequence replacement discards
|
|
lower YAML identity and uses the replacing scope's key declaration, if any;
|
|
without one, the collection is unkeyed unless an application registration still
|
|
supplies identity. Replacement discards policies owned by removed records, while
|
|
an independently declared explicit mode and application registration remain
|
|
applicable. Item-local
|
|
policies follow their owning records: retaining a record retains its rules, while
|
|
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`;
|
|
`NoSource` has no declaration scope.
|
|
|
|
An empty mapping contributes no child definitions. Two empty mappings compose, and
|
|
an empty mapping neither clears nor conflicts with another mapping's children.
|
|
Shared mapping containers retain all contributing value locations as their composite
|
|
base origin. A conflict reports the first claim in
|
|
[scoped assembly order](#explicit-imports) as its primary location and every
|
|
remaining claim as related locations in the same order. Assembly order never
|
|
authorizes replacement. Composition clones the resulting tree and its ordered
|
|
claim records rather than mutating or aliasing an input document.
|
|
|
|
## Selected overlays
|
|
|
|
`Options.Overlay` 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 overlay, and `overlays/default.yml` is never loaded implicitly.
|
|
Every other name resolves only to `<configuration-root>/overlays/<name>.yml`, with
|
|
no extension fallback, case normalization, inheritance between overlay files,
|
|
or overlay stack. A selected missing overlay is an error.
|
|
|
|
With `NoSource`, the main input stays absent and the explicit filesystem context
|
|
supplies the overlay root. The overlay is recorded as the first acquired source
|
|
when no other source exists. Host contexts may use the root-only form described
|
|
under [single-source loading](#single-source-loading); native contexts retain
|
|
their complete namespace, base, and root requirements.
|
|
|
|
The overlay 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. Overlay 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.
|
|
|
|
Collection destinations follow the
|
|
[collection operation and presence rules](#strict-document-composition) above.
|
|
The following table describes ordinary overlay behavior elsewhere:
|
|
|
|
| Existing lower node | Incoming overlay node | Result |
|
|
| --- | --- | --- |
|
|
| Absent | Value | Retain the overlay 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 |
|
|
|
|
Under these ordinary rules, scalars, sequences, and null are atomic. An overlay
|
|
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 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
|
|
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
|
|
not 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. Overlays cannot mask a base parse, composition, or
|
|
version failure because those checks run first.
|
|
|
|
## Explicit CLI overrides
|
|
|
|
`Options.Overrides` accepts only records the application knows were explicitly set
|
|
by its CLI parser. Each `Override` supplies a non-root mapping `Path`, a detached
|
|
`Value`, and a required safe display `Flag` such as `--workers`; ConfigKit has no CLI
|
|
parser dependency. Parser defaults must not be submitted as overrides, and `Flag`
|
|
must never contain the raw argument or configured value. General environment
|
|
variable overlays are an explicit non-goal: environment access is reserved for
|
|
typed secret references.
|
|
|
|
Overrides are prepared after source assembly and inheritance, then assigned after
|
|
typed defaults and decoding and before application resolution. The order is:
|
|
defaults and decoded sources < CLI < application resolution. A CLI mapping
|
|
recursively overlays the decoded mapping; a CLI sequence replaces the complete
|
|
decoded sequence regardless of source collection operations. Inheritance and
|
|
typed defaulting do not run again after CLI assignment. Earlier parsing,
|
|
composition, inheritance, and decoding failures remain errors even if an override
|
|
would replace the failing value. Recorded resolution, typed secret lookup, and
|
|
semantic validation use the effective post-override values. Overrides also work
|
|
with `NoSource`.
|
|
|
|
Values are converted directly into ConfigKit's owned tree without YAML parsing or
|
|
string coercion. Supported values are `nil`, built-in strings and booleans, all
|
|
built-in signed and unsigned integer widths except `uintptr`, finite `float32` and
|
|
`float64`, and recursive `[]any`/`map[string]any`. Strings stay strings; duration and
|
|
secret fields therefore receive string values. Nil is explicit null; empty strings,
|
|
false, and zero remain intentional; empty sequences replace whole lists; and mapping
|
|
values recursively overlay existing mappings. Map keys are sorted canonically and
|
|
sequence order is preserved.
|
|
|
|
All records are detached and validated before any is applied. They cannot create
|
|
`configkit` or `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
|
|
ordinary strict-decoder errors.
|
|
|
|
At most 1,024 records are accepted, and each record creates one synthetic CLI
|
|
source after file/overlay sources in supplied order. Override sources share the
|
|
1,024-source ceiling. A record has an 8 MiB string-byte limit; override strings and
|
|
source bytes share the 64 MiB aggregate limit. Synthetic mapping keys and values
|
|
count toward the existing 1,000,000-node and depth-256 ceilings. Cycles, named or
|
|
unsupported types, nonfinite floats, and limit excesses fail with safe `override.*`
|
|
diagnostics. Diagnostics and provenance identify the safe `Flag` source, never raw
|
|
arguments or values.
|
|
|
|
Override origins are explicit CLI origins. Resolution receives CLI values with
|
|
their explicit-presence and source information. Absence-based defaults preserve
|
|
those values, while recorded normalization and derivation may change them. History
|
|
retains replaced source definitions and records equal-value replacements as well
|
|
as structural changes. Secret lookup and validation use post-resolution values
|
|
with their effective source locations.
|
|
Caller-owned maps and slices are snapshotted during loading and must not be mutated
|
|
concurrently with the call. The executable `ExampleOverride` shows translating a
|
|
parser's explicit-set state into records while preserving an explicitly supplied
|
|
zero.
|
|
|
|
## Explicit imports
|
|
|
|
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
|
|
`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`,
|
|
`dir`, or `glob` selector. `path` and `dir` accept a string or a list of strings;
|
|
`glob` accepts one path pattern string. The optional `required` boolean controls
|
|
whether an empty selection or a missing named path is an error. Its default is
|
|
false, so missing imports are absent unless `required: true` is specified. The
|
|
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`.
|
|
|
|
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.
|
|
|
|
Path declarations use slash-separated relative paths. Absolute paths and network
|
|
URLs are invalid. Each path resolves from its declaring file's canonical directory.
|
|
An empty `path: []` selects nothing; it succeeds even when `required: true` because
|
|
there are no named targets to be missing. A nonempty required path list verifies
|
|
every named target, including targets later excluded from selection.
|
|
|
|
Directory declarations search the listed literal directory roots. Missing roots
|
|
contribute no candidates; supplied inaccessible or invalid roots fail. `pattern`
|
|
accepts one basename pattern or a list, matching filenames only. Omission selects
|
|
all filenames; an explicit empty list selects none. `exclude` accepts one basename
|
|
pattern or a list and removes matching final candidates. Patterns use Go's
|
|
case-sensitive `path.Match` grammar. `recursive` enables descendant traversal;
|
|
`depth` is valid only when recursion is effective, with direct files at depth 1 and
|
|
depth 0 or omission meaning unlimited traversal subject to library budgets.
|
|
`follow` controls discovered symlink entries and `hidden` controls discovered
|
|
names beginning with a dot. Both default to true. Explicit roots retain their
|
|
normal access behavior.
|
|
|
|
Glob declarations use the slash-based Go `path.Match` grammar: `*`, `?`, character
|
|
classes, and backslash escapes are supported, while recursive `**` syntax is
|
|
invalid. Backslashes escape pattern characters rather than acting as directory
|
|
separators. ConfigKit validates the complete pattern before filesystem traversal,
|
|
walks only directories selected by preceding segments, and reads wildcard
|
|
directories in bounded batches. Every examined entry consumes the separate visit
|
|
budget, including nonmatches. Literal components are explicit path components;
|
|
`follow` and `hidden` filter only wildcard-discovered entries. `exclude` filters
|
|
final candidate basenames.
|
|
|
|
`Options.ImportDefaults` and `LayeredOptions.ImportDefaults` provide one
|
|
application-owned load-wide policy for enabled imports in the main source,
|
|
underlays, and transitive imports; they do not apply to selected overlays or
|
|
discover sources by themselves. Each applicable field resolves independently:
|
|
an explicit declaration value wins, then the application default, then the
|
|
library default (`required: false`, `follow: true`, `hidden: true`,
|
|
`recursive: false`, and unrestricted filenames). A declaration's `pattern`
|
|
replaces rather than extends the default pattern. Nil `Pattern` leaves directory
|
|
filenames unrestricted; a non-nil empty slice selects none. Defaults do not make
|
|
a field applicable to selectors that do not support it, and load calls snapshot
|
|
the supplied pointers and slice before source acquisition.
|
|
|
|
A source's `configkit.sequences` declarations apply independently of import
|
|
enablement; a `configkit.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.
|
|
|
|
Acquisition visits declarations in authored order and each declaration's selected
|
|
paths in namespace-path lexical order, preserving source-record and version-callback
|
|
order. Ordinary mapping contributions in a main tier are assembled dependency-first:
|
|
ConfigKit selects the lexically smallest stable source key among the contributions
|
|
whose imports are complete. Independent branches may interleave, and the main body
|
|
may be assembled after its imports. Sequence-item contributions in that tier are
|
|
nodes in the same schedule; each selected item first assembles its own strict
|
|
mapping scope, with helpers before the bodies that import them. Those local scopes
|
|
do not use application collection policies and are not flattened into the outer
|
|
schedule. Item positions and diagnostic paths follow the resulting assembly order.
|
|
Each underlay group likewise acquires its inputs before dependency-ready assembly
|
|
in one strict scope. Independent groups remain separate precedence tiers; at a
|
|
declared or registered destination, the assembled higher sequence uses the
|
|
resolved collection operation against the lower sequence. Other sequences retain
|
|
whole-value replacement between tiers. Assembly order never permits replacement.
|
|
|
|
Acquisition order and resolution order serve different purposes: acquisition
|
|
assigns source records and callback order, while the shared scheduler determines
|
|
composition order among currently dependency-ready contributions. Inheritance
|
|
uses the same eligibility rule within each destination-local sibling scope; it
|
|
does not reorder the authored output mapping. The private namespace and stable-key
|
|
construction is described in the [import implementation overview](internal/imports.md).
|
|
|
|
An empty selection is allowed by default. `required: true` makes an empty directory
|
|
or glob selection fail with `import.missing`; it also makes missing named paths
|
|
fail. Requiredness does not suppress invalid paths, containment failures,
|
|
directories selected as files, permissions, parse errors, or composition errors.
|
|
|
|
For inspection, `SourceInfo.ImportSelections` records one outcome for each
|
|
completed optional declaration whose entire selected set is empty. The record is
|
|
attached to its declaring source, keeps the declaration path local to that source,
|
|
identifies the declaration mapping (including its `SourceID`) and selector, and
|
|
uses outcome `none`. Records remain in declaration order and are available
|
|
through the existing result, load
|
|
error, and callback source snapshots. They do not enumerate candidates, explain
|
|
why a path was absent, or describe partial omissions. Empty `path: []` succeeds
|
|
regardless of requiredness: it produces a `none` record when effectively optional,
|
|
and no record when effectively required. Snapshots detach the record paths and
|
|
slices.
|
|
|
|
Each declaration may include `placement: mapping` or
|
|
`placement: sequence-item`. Omission means `mapping` for that declaration,
|
|
including every match of a glob; placement is not inherited. Sequence-item
|
|
placement requires a nonempty effective destination and wraps the imported
|
|
mapping as one element of a sequence there. It does not split source sequences
|
|
or infer identity fields. Repeated sequences combine only when the destination
|
|
has an applicable YAML identity declaration or application registration; source
|
|
placement remains independent of collection identity and operation. Sequence
|
|
records retain source locations through placement and history; indexes describe
|
|
their assembled positions while source coordinates continue to identify the
|
|
declaring files.
|
|
Header/version policy retains its existing owner and timing. Application
|
|
registrations also enforce identity on the final effective typed configuration;
|
|
YAML-only declarations are enforced at structural combination and inheritance
|
|
boundaries. The [configuration contract](#strict-document-composition) defines
|
|
these validation boundaries.
|
|
|
|
An item import first opens a local mapping scope. Its body and ordinary helper
|
|
imports compose within that record. An omitted helper destination or
|
|
`destination: []` places the helper at the record root; other destinations are
|
|
relative to the immediate importing source. A helper may supply fields such as
|
|
the record's `id`. An explicit nested `sequence-item` import creates a sequence
|
|
inside the record, and its placement is independent of the enclosing item.
|
|
At the record root, an item import must name a nonempty nested destination.
|
|
Physical paths continue to resolve from the file that declares each import.
|
|
An actual empty mapping contributes an empty item, while an unmatched declaration
|
|
with default or explicit `required: false` contributes nothing.
|
|
|
|
For host files, the configuration root defaults to the selected file's canonical
|
|
directory. A host file may supply `FilesystemContext.Root` to widen that root but
|
|
may not supply `BaseDir` or a native `FS`. Byte, reader, and mapping inputs with host context
|
|
must provide both `BaseDir` and `Root` as absolute existing directories.
|
|
For `Filesystem.File`, the default root is `.` in its namespace and the base is
|
|
the selected file's directory. An explicit context must use the same `Filesystem`
|
|
namespace and may set `Root` to contain the file and its base; it may not supply
|
|
`BaseDir`. Byte, reader, and mapping inputs with native context provide both `BaseDir` and
|
|
`Root` as slash-relative `fs.ValidPath` directories in `FS` (`.` means its root),
|
|
with the base inside the root. Context paths are checked even when imports and
|
|
overlays are inactive. Diagnostic labels, including URI-looking labels, never
|
|
imply filesystem paths. Nonregular targets are rejected before opening to avoid blocking on FIFOs;
|
|
post-open regular-file checks remain in place under the stable-input assumption.
|
|
ConfigKit canonicalizes symlinks,
|
|
checks containment with relative paths, and opens the canonical root-relative target
|
|
through a library-owned root handle. Contained relative or absolute symlink targets
|
|
are accepted; escapes are rejected. Canonical paths define identity, so hard links at
|
|
distinct paths remain distinct sources. Native filesystems use slash-path
|
|
operations. Where the supplied `fs.FS` implements `fs.ReadLinkFS`, ConfigKit uses
|
|
that capability to inspect and resolve discovered links; otherwise it follows
|
|
the filesystem's ordinary `fs.FS` behavior. ConfigKit does not require every
|
|
native filesystem to expose link inspection, and native filesystem behavior is
|
|
not a general sandbox guarantee. Imports stay in their selected namespace and
|
|
root.
|
|
|
|
An active-stack repeat is `import.cycle` and reports the complete cycle's import
|
|
sites. A previously visited canonical path is `import.repeated` and reports both
|
|
inclusion routes, including diamonds and aliases. Each accepted file is read once.
|
|
Imported fragments may contain their own root directives, but may not claim the
|
|
application's schema-version path or replace one of its ancestors; the version must
|
|
come from the selected source. No directory is scanned implicitly, including one
|
|
named `conf.d`, and ConfigKit performs no remote fetching.
|
|
|
|
## YAML documents
|
|
|
|
The private YAML boundary currently accepts exactly one nonempty document whose root
|
|
is a mapping. Comments and block or flow collections are supported. It rejects:
|
|
|
|
- empty input or an empty document;
|
|
- every additional document, including an empty one;
|
|
- duplicate decoded mapping keys;
|
|
- non-string mapping keys;
|
|
- explicit tags, anchors, aliases, and merge keys;
|
|
- a quoted or plain mapping key equal to `<<`; and
|
|
- input exceeding the fixed depth or node limits.
|
|
|
|
Mapping key comparison is exact: it performs no case folding or Unicode
|
|
normalization. The private document tree retains declaration order, exact validated
|
|
number spelling, source identifiers, and separate key/value line and column
|
|
locations. It contains no upstream parser nodes or types.
|
|
|
|
Root depth is zero. Input depth is limited to 256 and each parsed input tree to
|
|
1,000,000 nodes, counting mapping keys as input nodes. The upstream parser also has
|
|
a private calibrated depth guard; passing that guard never bypasses ConfigKit's tree
|
|
limits.
|
|
|
|
## Scalars
|
|
|
|
Scalar meaning is determined by ConfigKit rather than upstream coercion:
|
|
|
|
| Input form | Meaning |
|
|
| --- | --- |
|
|
| Quoted, literal, or folded scalar | String |
|
|
| Plain `true` or `false` | Boolean |
|
|
| Plain `null`, `~`, or an empty mapping value | Null |
|
|
| JSON decimal number | Number, retaining its exact spelling |
|
|
| Date/time-looking or other ordinary plain text | String |
|
|
|
|
JSON numbers permit an optional minus, a zero or nonzero decimal integer without a
|
|
leading zero, an optional fraction with at least one digit, and an optional decimal
|
|
exponent. Hexadecimal, octal, digit separators, leading plus, leading/trailing-dot
|
|
forms, incomplete exponents, and non-finite values are rejected. Legacy boolean
|
|
words (`yes`, `no`, `on`, and `off`) and non-lowercase variants of the reserved
|
|
boolean/null words are rejected unless quoted.
|
|
|
|
Mapping keys use the same scalar classification and must classify as strings. Quote
|
|
keys that otherwise classify as a boolean, number, or null.
|
|
|
|
## Application schema binding
|
|
|
|
ConfigKit builds a private reflection descriptor graph for each schema operation;
|
|
it keeps no global mutable descriptor cache. A configuration root must be a struct
|
|
or string-keyed map. Nested supported shapes are:
|
|
|
|
- booleans, strings, signed and unsigned integers, and finite floating-point types;
|
|
- `time.Duration`, typed `Secret` references, and paired
|
|
`encoding.TextMarshaler`/`TextUnmarshaler` scalars;
|
|
- pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and
|
|
- recursive type definitions whose eventual application values are acyclic.
|
|
|
|
Named scalar types retain their Go identity. `uintptr`, complex numbers, functions,
|
|
channels, unsafe pointers, non-string map keys, and nonempty interfaces are not
|
|
configuration types. A custom text scalar must provide both directions on its value
|
|
or pointer method set. Building a descriptor only inspects method sets; it never
|
|
executes application methods or parser-specific hooks.
|
|
|
|
Exported struct fields use an explicit `yaml` name or the lowercase Go field name.
|
|
Unexported fields and `yaml:"-"` are ignored. `omitempty` is accepted but does not
|
|
change binding or projection. An explicit `yaml:",inline"` flattens a struct
|
|
or pointer-to-struct, or designates at most one string-keyed inline map for otherwise
|
|
unmatched names. Ambiguous names, named inline fields, multiple inline maps, and
|
|
unsupported or malformed tag options are schema errors. Inline pointers must
|
|
terminate in a struct; pointer-to-map and recursive pointer-only inline declarations
|
|
are rejected before the defaults factory runs. Embedded fields are ordinary
|
|
named fields unless explicitly inlined. `configkit:"secret"` is the only current
|
|
ConfigKit field tag and marks its bound field or container for secret classification
|
|
and redacted inspection.
|
|
|
|
Schema path traversal uses these same bindings. Explicit key segments address
|
|
mappings, explicit index segments address sequences, and parsed numeric pointer
|
|
tokens become indices only in a sequence. Sequence indices must use canonical
|
|
nonnegative decimal spelling: no signs or leading zeroes except `0` itself.
|
|
Tokens such as `+0` and `-0` remain literal keys in mappings but do not identify
|
|
sequence elements. Fixed-array indices are bounds checked. Dynamic `any` and inline-map entries
|
|
remain schema-valid for application-defined descendants, but dynamic values cannot
|
|
introduce typed secret references.
|
|
|
|
Descriptors are reused by typed decoding, projection, and cloning so those operations
|
|
cannot acquire different field bindings.
|
|
|
|
### Canonical module options
|
|
|
|
Applications with a known module catalog represent each configured item as an object
|
|
with an explicit module identifier and an options mapping. Ordinary typed structs,
|
|
including a small union of optional option fields, use the same strict binding above;
|
|
application validation decides which fields and combinations apply to each module.
|
|
String shorthand, a library module registry, and custom structural decoder hooks are
|
|
not supported alternatives.
|
|
|
|
When a runtime-selected catalog genuinely requires dynamic options,
|
|
`map[string]any` retains ConfigKit's canonical dynamic scalars. In particular,
|
|
integers are `int64`; application validation owns allowed keys, value constraints,
|
|
range checks, and conversion to runtime `int` or catalog objects. ConfigKit does not
|
|
provide generic subtree conversion, and consumers must not add a YAML marshal/decode
|
|
round trip to exercise supported behavior. The
|
|
[consumer adoption guide](consumers/README.md) contains compiled typed and dynamic
|
|
examples.
|
|
|
|
## Defaults and typed decoding
|
|
|
|
The private decoder starts from an independently owned clone of the application's
|
|
typed defaults and applies only explicitly present input nodes. Defaults therefore
|
|
never become explicit, while equal-valued input remains explicit. Successful decode
|
|
produces detached typed state, baseline and final canonical trees, an input-presence
|
|
index, and default/input origins for the later public loading lifecycle.
|
|
|
|
| Configured input | Effect on the typed baseline |
|
|
| --- | --- |
|
|
| Absent field | Retain the default |
|
|
| Explicit false, zero, or empty string | Replace the default |
|
|
| Partial mapping or string-keyed map | Recurse and retain omitted default children or entries |
|
|
| Empty mapping | Retain existing mapping children |
|
|
| Sequence, including an empty sequence | Replace the entire sequence |
|
|
| Pointer object | Recurse into a non-nil default; otherwise allocate an object whose omitted children have ordinary Go zero values |
|
|
| Allowed null | Replace the value with nil and suppress its default |
|
|
|
|
Null is allowed only for pointers, maps, slices, and dynamic `any` values. It is an
|
|
error for scalars, structs, and arrays. New map entries and sequence elements begin
|
|
from their Go zero values; ConfigKit does not invent entry-level defaults.
|
|
|
|
Decoding is strict at every typed depth. Unknown struct fields, type mismatches,
|
|
invalid nulls, numeric overflow, and arrays with the wrong length are errors at the
|
|
original semantic path and source coordinates. String-keyed maps accept arbitrary
|
|
keys but preserve the strict schema of their values; inline maps accept unmatched
|
|
keys according to their element descriptor. Multiple independent field failures are
|
|
collected without publishing the partially decoded object.
|
|
|
|
Booleans and strings are not generally coerced. At exactly a configured
|
|
`VersionRule.Path`, strict decimal-integer input may decode into a plain `string`, a
|
|
named string without a text codec, or pointer wrappers around those types. The
|
|
exception is load-local and does not apply to unrelated fields or loads without a
|
|
version rule. Integer destinations require integer syntax
|
|
and a value in the destination type's range. Float destinations accept finite JSON
|
|
numbers representable by their destination precision. Dynamic numbers become
|
|
`int64` for integer syntax and `float64` for fractional or exponent syntax, with
|
|
range and finiteness checks. Named scalar types retain their identity. Durations
|
|
decode only from strings accepted by `time.ParseDuration`; integers are rejected.
|
|
Paired text scalars decode only from strings through `encoding.TextUnmarshaler`, and
|
|
codec failures are classified without exposing application error text or input
|
|
values.
|
|
|
|
This decoder is available only through `Load`; ConfigKit exposes no generic document
|
|
conversion API.
|
|
|
|
## Typed projection and ownership
|
|
|
|
ConfigKit can internally project a schema-valid application value directly into its
|
|
owned canonical tree without YAML serialization. Projection includes every bound
|
|
field, including ordinary zero values; `omitempty` has no effect. Struct fields keep
|
|
binding order, dynamic map keys sort by decoded string, and sequences keep their
|
|
order. Integers retain exact base-10 values, finite floats use their destination
|
|
precision, durations use `time.Duration.String`, and paired text scalars use their
|
|
canonical marshaled text.
|
|
|
|
Nil pointers, maps, slices, and dynamic values project as null. Non-nil empty maps
|
|
and slices remain empty containers, preserving the distinction. Recursive acyclic
|
|
objects are supported. Cyclic values, non-finite floats, unsupported dynamic concrete
|
|
types, invalid codec output, and depth/node-limit violations fail with safe schema
|
|
classifications that omit codec error text.
|
|
|
|
The same descriptor graph creates detached typed copies. Pointers, maps, slices,
|
|
arrays, nested dynamic values, and shared references are recursively copied; shared
|
|
pointer identity is not preserved as configuration meaning. Paired text scalars are
|
|
rebuilt by unmarshaling their canonical text into fresh values, detaching private
|
|
backing storage. Ignored and unexported struct fields are zeroed rather than copied.
|
|
|
|
Private structural comparison operates on canonical trees. Mapping additions,
|
|
removals, and descendant replacements retain semantic paths. A sequence is atomic:
|
|
any sequence change reports the sequence path and never invents correspondence
|
|
between old and new indices. Cross-kind and null/container changes replace the
|
|
affected path.
|
|
|
|
These mechanisms support decoding, resolution auditing, metadata,
|
|
and rendering. They are not a public generic conversion or cloning API.
|
|
|
|
## Safe failures
|
|
|
|
Source failures use stable `source.*` codes and YAML failures use stable `yaml.*`
|
|
codes declared by the root package. Diagnostics contain safe classifications,
|
|
semantic paths when applicable, and available source coordinates. Raw parser,
|
|
conversion, reader-error, and input text is not forwarded. Source labels and
|
|
diagnostic messages are application-visible and therefore must never contain
|
|
secrets; terminal control characters are escaped by `WriteDiagnostics`.
|