# 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. | | `SearchConfig(application, search, options...)` | Creates an explicit, reusable search source. Loading selects one host file or follows the absent-input lifecycle when an optional search is exhausted. | | `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. ### Configuration discovery Discovery is opt-in. Construct `Search` from literal `Paths`, directory searches, or the supported directory presets, then pass it to `SearchConfig`. Search descriptions are immutable and reusable; each load resolves dynamic directories and checks current candidate files again. `Searches` is ordered concatenation. It preserves the supplied description order and does not deduplicate candidates. `NamedFilenames` selects `.yml`, then `.yaml`. `GenericFilenames` selects `config.yml`, `config.yaml`, `main.yml`, then `main.yaml`. `SearchDirectory` defaults to `[Named, Generic]` when options are omitted or `Filenames` is nil. A non-nil empty filename list contributes no candidates and does not resolve its directory. `CustomFilenames` supplies literal single filename components; it does not expand application-name placeholders. An application slug is required only when a selected directory or filename group uses it. Literal custom paths are not expanded for `~`, variables, or wildcards; relative paths are resolved from the working directory at load time. The zero `Search`, `Searches()`, `Paths()`, and an empty custom filename group contribute no candidates. An explicitly supplied empty path or filename is invalid. Slugs and custom filenames must be usable single host filename components; ConfigKit preserves their case and spelling. Search expansion is ordered concatenation. For each directory search, ConfigKit tries its filename groups and names before advancing to the next directory. When multiple directories use the same filename list, candidates follow the directory-first Cartesian product: every filename in one directory is tried before the next directory. Explicit file candidates stay at their declared positions among directory searches. The presets expand to these ordered directories and filename groups: | Preset | Directory, in order | Filename groups | | --- | --- | --- | | `UnixSystemDirectories` | `/usr/local/etc/`, `/etc/` | Named, then Generic | | `UnixSystemDirectories` | `/etc` | Named only | | `UserConfigDirectory` | Go user-config directory / `` | Named, then Generic | | `UserConfigDirectory` | Go user-config directory | Named only | | `HomeDirectory` | Go home directory / `.` | Named, then Generic | | `HomeDirectory` | Go home directory | Named only | | `WorkingDirectory` | Current working directory | Named only | The Unix system preset is supported on aix, android, darwin, dragonfly, freebsd, hurd, illumos, ios, linux, netbsd, openbsd, and solaris; reaching it on another platform is an option error. Other presets use Go's user-config, home, and working-directory helpers. A reached directory-helper error terminates selection. Bases for later searches are not resolved after an earlier candidate is selected. Missing candidates continue; the first existing candidate is selected and then follows ordinary file loading rules. Errors while acquiring or processing a selected file are terminal and do not resume the search. Searches are optional by default. Exhaustion uses the same absent-main lifecycle as `NoSource()`: there is no main document, source record, source ID, or explicit presence contribution. Imports may be enabled but there are no main declarations to traverse. Defaults, version checks, overlays, CLI overrides, resolution, and validation retain their normal lifecycle. `SearchOptions{Required: true}` makes exhaustion an error; it does not change candidate or selected-file error handling. For an ordinary active overlay after optional exhaustion, provide an explicit filesystem context. A host context with only `Root` is sufficient; ConfigKit loads the overlay at its normal root-relative location and does not infer a root from attempted search directories. The same root-only context can accompany a search that selects a file: the selected file determines its base and must be inside `Root`. A selected host file rejects an explicit `BaseDir` or native filesystem context. Invalid context fails without resuming selection. Layered overlays keep their independent `OverlayFilesystem` requirement. When a search selects a file, inspection reports that actual file with ordinary source IDs, paths, placement, metadata origins, and resource context. Missing candidates create no records. `SearchConfig` can be used as the main source of ordinary or layered loading, including an underlay input, and does not add a new precedence tier. 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 `/overlays/.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`.