2064 lines
118 KiB
Markdown
2064 lines
118 KiB
Markdown
# Configuration Input Contract
|
|
|
|
This reference explains the YAML ConfigKit accepts, how inputs combine, and what
|
|
an application receives from a load. For a first working example, start with the
|
|
[README](../README.md#load-a-configuration-file) or the
|
|
[consumer guide](consumers/README.md).
|
|
|
|
Configuration authors can start with [YAML values](#scalars),
|
|
[combination rules](#strict-document-composition), [imports](#explicit-imports),
|
|
and [inheritance](#sibling-inheritance). Go API users can start with
|
|
[sources](#sources), [defaults and decoding](#defaults-and-typed-decoding),
|
|
[application callbacks](#recorded-application-resolution), and
|
|
[inspection](#layered-metadata). [Credentials](#typed-secret-references) and
|
|
[secrets-file startup](#explicit-secrets-directory-bootstrap) have separate roles.
|
|
|
|
## Terms used in this reference
|
|
|
|
A **mapping** is a group of named YAML values; a **sequence** is a list. A
|
|
**scalar** is a single value such as a string, number, or boolean. For example:
|
|
|
|
```yaml
|
|
service: # mapping
|
|
enabled: false # scalar
|
|
names: [primary, spare] # sequence
|
|
```
|
|
|
|
A **source** supplies an input document, such as a file or an in-memory mapping.
|
|
A **tier** is a group of inputs at the same priority. **Precedence** describes
|
|
which tier takes priority when several tiers supply a setting. Inputs within
|
|
one tier follow strict combination rules.
|
|
|
|
A **configuration path** names a value inside the configuration, rather than a
|
|
file on disk. For example, `/service/names/0` identifies the first name in the
|
|
YAML above. Go callers can construct it with `Keys("service", "names").Index(0)`.
|
|
Its text uses JSON Pointer escaping: `~0` represents `~` and `~1` represents `/`
|
|
inside a key. Explicit mapping keys and sequence indexes remain distinct even
|
|
when their pointer text looks the same.
|
|
|
|
**Metadata** describes a value's presence and **origin**: where it came from,
|
|
including application processing that changed it. A returned **detached** record
|
|
is a copy you can change without changing ConfigKit's stored record. A **snapshot**
|
|
is saved configuration state used for inspection after loading finishes.
|
|
Borrowed filesystems have their own [resource lifetime](#source-resource-contexts).
|
|
|
|
## Sources
|
|
|
|
A source describes the input to pass to `Load` or `LoadLayered`. Choose the
|
|
constructor that matches where your configuration comes from:
|
|
|
|
| Constructor | Current contract |
|
|
| --- | --- |
|
|
| `File(path)` | Requires a nonempty path, opens a regular file for each load, 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. Copies of the source share one read attempt. ConfigKit never closes the reader; you remain responsible for closing it if needed. |
|
|
| `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. Conversion failures are reported when loading uses the source. |
|
|
| `NoSource()` | Selects defaults-only loading, creates no source record, and marks no paths explicit. |
|
|
|
|
The zero `Source` is invalid. Constructors store argument failures so loading
|
|
can report them as structured diagnostics. A source's private representation is
|
|
not part of the API.
|
|
File and filesystem sources can observe changes between loads; 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 `SearchPaths`, 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 `<application>.yml`, then `<application>.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()`, `SearchPaths()`, 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 descriptions are tried in the order supplied. Within a directory search,
|
|
ConfigKit tries every selected filename before moving to the next directory.
|
|
For example, two directories using `NamedFilenames` for application `demo` are
|
|
searched in this order:
|
|
|
|
```text
|
|
first/demo.yml
|
|
first/demo.yaml
|
|
second/demo.yml
|
|
second/demo.yaml
|
|
```
|
|
|
|
Explicit file candidates keep their declared positions among directory searches.
|
|
If no candidate exists, an optional search continues as absent input and a
|
|
required search fails. Once a file is selected, a failure to load it is an error;
|
|
ConfigKit does not continue looking for another configuration file.
|
|
|
|
The presets expand to these ordered directories and filename groups:
|
|
|
|
| Preset | Directory, in order | Filename groups |
|
|
| --- | --- | --- |
|
|
| `UnixSystemDirectories` | `/usr/local/etc/<application>`, `/etc/<application>` | Named, then Generic |
|
|
| `UnixSystemDirectories` | `/etc` | Named only |
|
|
| `UserConfigDirectory` | Go user-config directory / `<application>` | Named, then Generic |
|
|
| `UserConfigDirectory` | Go user-config directory | Named only |
|
|
| `HomeDirectory` | Go home directory / `.<application>` | 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.
|
|
|
|
### Input limits
|
|
|
|
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...)` returns a configuration of your Go type `T` plus
|
|
metadata for inspection. It accepts zero or one `Options[T]`. The root type must
|
|
follow the [schema rules](#application-schema-binding). With no options, loading
|
|
starts from the Go zero value. To supply defaults, provide a
|
|
`Definition[T].Defaults` function that returns a fresh configuration.
|
|
|
|
That function runs exactly once after successful source parsing, schema checks,
|
|
optional version checking, and selected-overlay loading. ConfigKit copies its
|
|
result before applying input. Mutable values supplied by a resolver are also
|
|
copied before secret lookup and validation. A final copy prevents even
|
|
equal-valued replacements made by validators from leaving shared mutable storage
|
|
in the returned configuration. It preserves validated secret state without
|
|
reading credentials again.
|
|
|
|
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.
|
|
|
|
### Results and failures
|
|
|
|
You can change `Result.Config` after loading. Inspection methods continue to
|
|
describe the configuration as it was when the load finished. Returned source,
|
|
diagnostic, and metadata records are copies; changing them cannot change later
|
|
queries. Separate loads return independently owned configuration and records.
|
|
|
|
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.
|
|
|
|
`Result.Source(id)` returns the detached record for an acquired source already
|
|
listed by `Result.Sources()`. Source IDs are local to one load; empty and unknown
|
|
IDs return false. This passive lookup uses retained evidence and does not reacquire
|
|
the source, access the filesystem, or look up credentials. A source ID can be
|
|
obtained from the source records or exact origin locations.
|
|
|
|
`LoadError.Source(id)` performs the same lookup over the partial, already-sanitized
|
|
inventory returned by `LoadError.Sources()`. It preserves the source evidence and
|
|
placement available when loading failed, which may be incomplete; it does not
|
|
promise a record for every attempted source. Nil or zero-valued errors and empty
|
|
or unknown IDs return false.
|
|
|
|
### Completion and resource lifetime
|
|
|
|
`Load` finishes its work before returning. 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.
|
|
|
|
Files and environment variables are read during processing; they are not all
|
|
captured at one instant. If your callback changes something outside the working
|
|
configuration, ConfigKit does not undo that effect when loading fails 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.
|
|
|
|
### Priority and processing order
|
|
|
|
The input priority order is shown below, from lowest to highest. Mappings combine
|
|
by key across these boundaries; identified lists use their collection operation.
|
|
|
|
```text
|
|
typed defaults → underlay groups (first to last) → main + imports → overlay → CLI
|
|
```
|
|
|
|
This is a priority order, rather than a schedule of processing steps. Defaults
|
|
have low priority but are applied during typed decoding, after source assembly
|
|
and inheritance. Application resolution runs after CLI assignment and can make
|
|
recorded changes. Secret lookup and validation then assess the resulting values.
|
|
See the [load timeline](#single-source-loading) for the processing sequence.
|
|
|
|
### Source order and version checks
|
|
|
|
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
|
|
|
|
A destination controls where settings appear in the configuration. It is
|
|
independent of where the source file lives on disk. Consider this file layout:
|
|
|
|
```text
|
|
/etc/demo/
|
|
config.yml
|
|
fragments/
|
|
prompt.yml
|
|
welcome.txt
|
|
```
|
|
|
|
With imports enabled, `config.yml` can contain:
|
|
|
|
```yaml
|
|
configkit:
|
|
imports:
|
|
- path: fragments/prompt.yml
|
|
destination: [prompts, welcome]
|
|
```
|
|
|
|
If `prompt.yml` contains `template: welcome.txt`, the assembled input is:
|
|
|
|
```yaml
|
|
prompts:
|
|
welcome:
|
|
template: welcome.txt
|
|
```
|
|
|
|
The import filename is relative to `config.yml`; the destination places its
|
|
contents beneath `/prompts/welcome`. A later application read of `welcome.txt`
|
|
can use the imported source's context, as described under
|
|
[resource contexts](#source-resource-contexts).
|
|
|
|
#### Destination rules and inspection evidence
|
|
|
|
`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. `DestinationIsFallback` 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
|
|
|
|
Inheritance lets one entry reuse another entry's settings. A sibling is another
|
|
entry in the same enclosing mapping. For example:
|
|
|
|
```yaml
|
|
presets:
|
|
base:
|
|
width: 10
|
|
length: 20
|
|
custom:
|
|
extends: base
|
|
width: 30
|
|
```
|
|
|
|
The resulting `custom` has `width: 30` and `length: 20`; `base` remains an
|
|
ordinary entry. ConfigKit resolves this after combining source layers, so the
|
|
child uses the parent's settings that remain after that combination.
|
|
`extends: null` cancels a relationship and keeps the child's own settings.
|
|
|
|
An identified list uses a mapping reference instead of a sibling name:
|
|
|
|
```yaml
|
|
configkit:
|
|
sequences:
|
|
- destination: [presets]
|
|
key: id
|
|
presets:
|
|
- {id: base, width: 10, length: 20}
|
|
- {id: custom, extends: {id: base}, width: 30}
|
|
```
|
|
|
|
Here the child keeps `id: custom` and receives the parent's other settings.
|
|
It has `width: 30` and `length: 20`. Every parent remains subject to ordinary
|
|
application decoding and validation.
|
|
|
|
#### Lookup, processing order, and limits
|
|
|
|
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
|
|
|
|
Use a source context when a configuration value names another file, such as a
|
|
template. It tells the application which filesystem and directory belonged to
|
|
the source that supplied the value. Obtaining the context does not open files.
|
|
|
|
For the [placement example](#input-destinations), inspect the origin of
|
|
`/prompts/welcome/template` and take its source ID. `result.SourceContext(id)`
|
|
provides a filesystem rooted at `/etc/demo` and a `BaseDir` of `fragments`.
|
|
Joining that base with `welcome.txt` gives `fragments/welcome.txt`, which the
|
|
application can open through the returned `FS`. Close the returned file after
|
|
reading it. The maintained [ReadWelcome example](../examples/consumers/layered.go)
|
|
shows this workflow. If a value has several contributing sources, the application
|
|
must choose the intended source according to its own rules.
|
|
|
|
#### Context queries and lifetime
|
|
|
|
`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
|
|
|
|
Your application chooses which version field to check and which identifiers it
|
|
accepts. ConfigKit passes the declared value to that checker; it does not assign
|
|
meaning to a version number. For a rule at `version`, both `version: 4` and
|
|
`version: "4"` supply text `4`. A plain string field can receive that spelling
|
|
under the conversion rules below.
|
|
|
|
`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
|
|
|
|
By default, version checking concerns the assembled main input. Enable a shared
|
|
source-version mode when the application needs to check declarations in individual
|
|
main, imported, underlay, and selected-overlay documents with the same rule.
|
|
With a configured usable version rule, the main cases are:
|
|
|
|
| Situation | `MainOnly` (default) | `IfPresent` | `Required` |
|
|
| --- | --- | --- | --- |
|
|
| Header absent in an actual main document | Call the checker with an absent version. | Call the checker with an absent version. | Reject before calling the checker. |
|
|
| Header absent in another participating document | No source-local check; existing version-path protection applies. | Skip its checker call. | Reject the document. |
|
|
| Main header is explicit null | Call the checker with `Null` true. | Call the checker with `Null` true. | Call the checker with `Null` true. |
|
|
| Main input is `NoSource()` | Keep the absent-main checker call. | Keep the absent-main checker call. | Keep the absent-main checker call. |
|
|
|
|
The short column labels stand for the `SourceVersionMode` constants below.
|
|
In enabled modes, an explicit null in another participating document also reaches
|
|
the checker. The application's rule decides whether null is accepted.
|
|
|
|
For example, a main document with `version: 4` placed at destination `[service]`
|
|
is checked at its own `/version` path in enabled modes. Its accepted value remains
|
|
at `/service/version` in the application configuration, and protection applies
|
|
there. Accepted non-main headers are removed before documents combine, so they
|
|
cannot overwrite the main header. The maintained
|
|
[versioned-service example](../examples/consumers/source_version.go) demonstrates
|
|
this placement. Source evidence records what was observed; its existence alone
|
|
is not proof that a checker ran or accepted a declaration.
|
|
|
|
##### Mode rules and version evidence
|
|
|
|
`Options.SourceVersionMode` and `LayeredOptions.SourceVersionMode` select how
|
|
one `Definition.Version` policy applies across source documents. The zero
|
|
value `SourceVersionModeMainOnly` 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.VersionEvidence` 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
|
|
|
|
Use inspection to understand a completed load without reading the files again:
|
|
|
|
| Need | API to use |
|
|
| --- | --- |
|
|
| Check whether input supplied a final value, including false or zero | `Metadata().Lookup` or `IsExplicit` |
|
|
| Find the sources that supplied it | The field's `Origin`, then `Source(id)` or `Sources()` |
|
|
| Inspect attribution for earlier definitions | `Metadata().History` |
|
|
| Inspect earlier values as safe YAML | `Explain` or `WriteExplanation` |
|
|
| Show the saved final configuration as safe YAML | `WriteEffective` |
|
|
| Compare final values from two loads | `Diff` |
|
|
|
|
For example, suppose a typed default sets `workers` to `2`, the main input
|
|
supplies `workers: 3`, and an overlay supplies `workers: 4`. The final value is
|
|
`4`, it is explicit, and its origin identifies the overlay. History retains the
|
|
earlier default and main contributions. `History` returns attribution records;
|
|
`Explain` adds their before/after values. Changing `result.Config.Workers` later
|
|
does not change any of these saved inspection results.
|
|
|
|
#### Presence, origins, and historical paths
|
|
|
|
`Result.Metadata()` describes every final configuration 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
|
|
|
|
Choose which configuration values to hide when displaying a load. A
|
|
`configkit:"secret"` field tag or `Definition.Sensitive` predicate marks a path
|
|
as sensitive. Marking a container also hides its descendants. This choice is
|
|
separate from using a typed `Secret` to read a credential, described below.
|
|
|
|
The saved output continues to hide sensitive historical values even when a
|
|
higher layer replaces them. When a resolver records a dependency on a sensitive
|
|
value, that sensitivity also protects the derived target. The detailed rules
|
|
below explain how classification spreads and how output is formatted.
|
|
|
|
#### Classification and output details
|
|
|
|
Secret classification combines `configkit:"secret"` field tags with the optional
|
|
pure `Definition.Sensitive(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
|
|
|
|
An explanation shows the final value and the earlier contributions that led to
|
|
it. In the `workers` example above, a caller can inspect `/workers` to see the
|
|
default, main input, and overlay along with their source attribution. The
|
|
returned values are YAML strings in copies of the saved records; editing them
|
|
cannot change a later explanation. Sensitive values are hidden before those
|
|
strings are returned.
|
|
|
|
#### Value representations and history
|
|
|
|
`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 exposed as a manual recording method.
|
|
|
|
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
|
|
`history.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 `LoadSecretsIntoEnvironment` 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`.
|
|
|
|
The result fields help distinguish absence from hidden output:
|
|
|
|
| Field | Meaning |
|
|
| --- | --- |
|
|
| `ValueSnapshot.Present` | False means the value is absent. Explicit null is present and renders as `null`. |
|
|
| `ValueSnapshot.Redacted` | This value, or something inside it, was hidden. |
|
|
| `Difference.HiddenChanged` | A hidden value or its presence actually changed. Redaction alone does not set this flag. |
|
|
|
|
For example, a sensitive string changing from one value to another can produce
|
|
the same `'[REDACTED]'` text on both sides. The difference record still exists and
|
|
`HiddenChanged` is true. A literal input string that looks like that marker is
|
|
ordinary data; use the fields rather than parsing marker text. A typed secret's
|
|
reference name is compared, so rotating its environment credential without
|
|
changing that name does not produce a difference.
|
|
|
|
### Combined protection and comparison details
|
|
|
|
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
|
|
|
|
There are three separate tasks when working with credentials:
|
|
|
|
| Task | Mechanism |
|
|
| --- | --- |
|
|
| Read a credential from an environment variable | A typed `Secret` field whose YAML value is the variable's name |
|
|
| Hide a configuration value in inspection output | A `configkit:"secret"` tag or `Definition.Sensitive` |
|
|
| Populate environment variables from local files before loading | An explicit `LoadSecretsIntoEnvironment` call |
|
|
|
|
For example, YAML `api_key: DEMO_API_KEY` names an environment variable. Its
|
|
credential is available to trusted application code through `Secret.Value()`
|
|
after loading succeeds. The name `DEMO_API_KEY` remains visible in inspection
|
|
unless the path is separately classified as sensitive. The credential itself is
|
|
never retained in inspection history. The optional file helper below populates
|
|
the environment; ordinary configuration loading does not write it.
|
|
|
|
### Reference requirements and credential lifetime
|
|
|
|
Fields that need credentials use the `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
|
|
|
|
If an application stores credentials in files, it can call this helper before
|
|
loading configuration. A file named `DEMO_API_KEY` containing the dummy text
|
|
`example-value` supplies environment variable `DEMO_API_KEY`. YAML can then
|
|
refer to that variable with `api_key: DEMO_API_KEY`. Applications that already
|
|
receive environment variables do not need this file step.
|
|
|
|
Existing environment variables take priority over files. If `DEMO_API_KEY` is
|
|
already set to an empty string, its file is skipped and it stays empty. A later
|
|
required `Secret` reference will still fail on that empty value. File bootstrap
|
|
accepting an empty file is different from a required reference resolving to an
|
|
empty credential.
|
|
|
|
Read the report even when the helper returns an error:
|
|
|
|
| Status | How to interpret it |
|
|
| --- | --- |
|
|
| `loaded` | This file supplied the environment value. |
|
|
| `skipped_existing` | The variable was already set; `ExistingEmpty` says whether that existing value was empty. |
|
|
| `failed` | This entry could not be processed; inspect the safe diagnostics. |
|
|
| `not_applied` | No value from this eligible entry was applied, for example because preflight failed or an earlier write failed. |
|
|
|
|
Render the returned `SecretsReport` separately from a later configuration load.
|
|
It contains filenames, paths, and statuses, not credential contents. The
|
|
[maintained startup example](../examples/consumers/consumers_test.go) shows how to
|
|
keep bootstrap and configuration reports together in the application.
|
|
|
|
### Preflight, limits, and partial writes
|
|
|
|
`LoadSecretsIntoEnvironment(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
|
|
|
|
Application rules fit into three tasks:
|
|
|
|
| Task | Definition field |
|
|
| --- | --- |
|
|
| Supply fixed starting values | `Defaults`, which returns a fresh configuration |
|
|
| Derive or normalize values while recording why they changed | `Resolve` |
|
|
| Report domain errors and warnings without changing values | `Validate` |
|
|
|
|
Within `Resolve`, choose a helper according to whether it should assign a value:
|
|
|
|
| Helper | Assigns a value? | Records the change? |
|
|
| --- | --- | --- |
|
|
| `Default` | Only when input did not explicitly supply the path. | When it assigns. |
|
|
| `Normalize` | Always. | Yes. |
|
|
| `RecordDerivedDefault` | No; the application has already performed the mutation. | Yes, including for explicit input. |
|
|
| `RecordNormalization` | No; the application has already performed the mutation. | Yes. |
|
|
|
|
For a configuration with `Workers` and `QueueLimit`, a resolver can use this call:
|
|
|
|
```go
|
|
configkit.Default(resolution, configkit.Keys("queue_limit"),
|
|
&configuration.QueueLimit, configuration.Workers*2,
|
|
"size queue from workers", configkit.Keys("workers"))
|
|
```
|
|
|
|
If input omits `queue_limit`, this derives it from `workers` and records the
|
|
reason and dependency. An explicitly supplied `queue_limit: 0` is preserved.
|
|
Dependency paths explain where a derived value came from and support sensitivity
|
|
propagation. Use value-free reasons. See the [executable API examples](../example_test.go)
|
|
for complete callback definitions.
|
|
|
|
Use a `Resolution` or `Validation` context only inside its callback. Do not retain
|
|
it or use it concurrently. Returned metadata records are copies you can keep.
|
|
`EntryInfo` keeps describing the configuration at the start of resolution,
|
|
including defaults and CLI assignments, even after your resolver changes values.
|
|
|
|
#### Resolution recording and failure rules
|
|
|
|
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.EntryInfo(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.RecordDerivedDefault` and
|
|
`Resolution.RecordNormalization` declare the operation already performed; a
|
|
recorded container covers changes to its descendants. These methods do not assign
|
|
values, and a derived-default record does not suppress work for explicit input.
|
|
Records require a schema-valid path, 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, 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
|
|
|
|
Use validation for application rules such as a nonnegative worker count or a
|
|
required combination of fields. Report errors and warnings through the supplied
|
|
context. Make any normalization or derived assignment in `Resolve` first;
|
|
validation examines the final configuration without changing it.
|
|
|
|
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
|
|
|
|
### Combining mappings
|
|
|
|
The main document and its imports are peers in one tier. For example, a main
|
|
file containing `service: {workers: 2}` and an import containing
|
|
`service: {enabled: false}` produce:
|
|
|
|
```yaml
|
|
service:
|
|
workers: 2
|
|
enabled: false
|
|
```
|
|
|
|
If the import also defined `workers`, strict composition would fail, even if its
|
|
value were also `2`. An overlay containing `service: {workers: 4}` intentionally
|
|
changes the result to `workers: 4`, retaining `enabled: false`. This shows the
|
|
difference between combining peers and applying a higher-priority layer.
|
|
Identified lists have their own rules below.
|
|
|
|
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.
|
|
|
|
### Identified lists: a worked example
|
|
|
|
A record is a mapping inside a list. An identity field such as `id` lets ConfigKit
|
|
tell which records refer to the same entry. Declare the identity and desired
|
|
operation at the source root, for example:
|
|
|
|
```yaml
|
|
configkit:
|
|
sequences:
|
|
- destination: [records]
|
|
key: id
|
|
mode: upsert
|
|
```
|
|
|
|
Suppose a lower layer supplies
|
|
`records: [{id: a, width: 10, length: 20}, {id: c, width: 50}]`, and
|
|
its higher layer supplies `records: [{id: a, width: 30}, {id: b, width: 40}]`.
|
|
The result during source assembly depends on the selected operation:
|
|
|
|
| Operation | Result for this example |
|
|
| --- | --- |
|
|
| `replace` | `[{id: a, width: 30}, {id: b, width: 40}]` |
|
|
| `append` | Error: both layers contain identity `a`. |
|
|
| `upsert` | `[{id: a, width: 30}, {id: c, width: 50}, {id: b, width: 40}]` |
|
|
|
|
Upsert replaces the complete matching record, so `length` from the lower `a`
|
|
record is gone. The unmatched lower `c` record stays at its original position;
|
|
new identities append. If the higher layer supplied only `[{id: b, width: 40}]`,
|
|
append would succeed with
|
|
`[{id: a, width: 10, length: 20}, {id: c, width: 50}, {id: b, width: 40}]`.
|
|
Typed defaults and decoding happen later.
|
|
|
|
Within a strict tier, distinct identities combine and duplicate identities fail
|
|
under every operation. For example, two imported peers may each supply different
|
|
records, but cannot both define record `a`. Across layers, the operation table
|
|
below applies. Without identity, repeated peer lists conflict and higher-layer
|
|
lists replace whole lower lists.
|
|
|
|
### Application registrations and identity details
|
|
|
|
`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.
|
|
|
|
### YAML declarations and policy lifetime
|
|
|
|
Ordinary sources, layered inputs, and selected overlays may declare collection
|
|
behavior under `configkit.sequences` at the document root. Each entry has a
|
|
required literal `destination` and may specify `key` as one string or a nonempty
|
|
list of strings, plus a `mode` of `replace`, `append`, or `upsert`. These
|
|
controls are removed before application decoding. A declaration destination is
|
|
relative to that source's placement, so a mounted fragment's declaration is
|
|
mounted with its values. Declarations from strict peers must agree on identity
|
|
fields and on any explicitly supplied mode; omission expresses no mode
|
|
preference. They apply even when the declaring source has no value at that
|
|
destination: a key validates an existing retained sequence or remains pending
|
|
until records arrive, while a mode-only declaration affects later combinations
|
|
without recombining earlier tiers. A key supplies the identity used by `append`
|
|
and `upsert` and is checked against any application registration.
|
|
|
|
Strict peers at an identified destination first combine distinct records in
|
|
source and list order. Duplicate identities among peers or within one
|
|
contribution fail under every mode. After strict composition, precedence and
|
|
inheritance use one operation for the assembled incoming collection:
|
|
|
|
| Mode | Behavior at source-precedence and inheritance boundaries |
|
|
| --- | --- |
|
|
| `replace` | Replace the complete lower sequence with the assembled higher sequence. |
|
|
| `append` | Retain lower records and append higher records; duplicate identities fail. |
|
|
| `upsert` | Replace matching records in full at their lower positions, retain unmatched lower records, and append new identities. |
|
|
|
|
Upsert replaces the complete matched record; it does not recursively merge
|
|
record fields. Append and upsert require identity. With no explicit mode in the
|
|
current tier, an explicit lower mode persists; if none exists, effective identity
|
|
selects upsert and its absence selects replace. Inferred modes do not persist.
|
|
Explicit modes persist at their destination until changed by a higher explicit
|
|
mode, independently of YAML identity lifetime. A declaration alone does not
|
|
clear or recombine a collection.
|
|
|
|
The presence and value kind at a precedence or inheritance boundary determine
|
|
whether a collection changes:
|
|
|
|
| Higher contribution | `replace` | `append` / `upsert` |
|
|
| --- | --- | --- |
|
|
| Absent | Leave the lower value unchanged. | Leave the lower value unchanged. |
|
|
| Explicit `[]` | Clear the sequence. | Contribute no records. |
|
|
| Explicit null or another non-sequence value | Replace the lower value, subject to application schema. | Error; the operation requires a sequence. |
|
|
|
|
An absent lower value can be initialized by an incoming sequence under any mode.
|
|
A present lower null or other non-sequence value is not absence: append and
|
|
upsert cannot combine with it. An empty sequence still requires a valid mode and
|
|
identity where applicable. Null used as an identity component inside a record is
|
|
distinct from null used as the collection value.
|
|
|
|
Declarations govern ordinary strict source composition, including loads with
|
|
imports disabled, and each strict scope in layered loading. Before composing a
|
|
higher tier's peers, ConfigKit makes applicable lower identity and explicit mode
|
|
available; after peer composition, the tier applies its resolved operation
|
|
against the lower collection. Selected overlays use the same collection policy
|
|
after the base source graph is composed. During mapping-sibling inheritance, a
|
|
parent's YAML identity can follow its collection to the child destination, while
|
|
the parent's explicit operation stays at the parent destination. The child
|
|
destination selects its operation; absent one, effective identity implies
|
|
upsert and its absence implies replace. Whole-sequence replacement discards
|
|
lower YAML identity and uses the replacing scope's key declaration, if any;
|
|
without one, the collection is unkeyed unless an application registration still
|
|
supplies identity. Replacement discards policies owned by removed records, while
|
|
an independently declared explicit mode and application registration remain
|
|
applicable. Item-local
|
|
policies follow their owning records: retaining a record retains its rules, while
|
|
whole-record upsert replacement discards the displaced record's rules. Matching
|
|
identity or reusing an index does not transfer them. These policies do not change
|
|
CLI behavior: CLI sequence assignments replace whole sequences after decoding.
|
|
A root `configkit.imports` declaration still requires `Options.Imports`;
|
|
`NoSource` has no declaration scope.
|
|
|
|
An empty mapping contributes no child definitions. Two empty mappings compose, and
|
|
an empty mapping neither clears nor conflicts with another mapping's children.
|
|
Shared mapping containers retain all contributing value locations as their composite
|
|
base origin. A conflict reports the first claim in
|
|
[scoped assembly order](#explicit-imports) as its primary location and every
|
|
remaining claim as related locations in the same order. Assembly order never
|
|
authorizes replacement. Composition clones the resulting tree and its ordered
|
|
claim records rather than mutating or aliasing an input document.
|
|
|
|
## Selected overlays
|
|
|
|
`Options.Overlay` selects at most one overlay by name. Names must match
|
|
`[a-zA-Z0-9_-]+`; they are not paths. The empty name and exact lowercase name
|
|
`default` select no overlay, and `overlays/default.yml` is never loaded implicitly.
|
|
Every other name resolves only to `<configuration-root>/overlays/<name>.yml`, with
|
|
no extension fallback, case normalization, inheritance between overlay files,
|
|
or overlay stack. A selected missing overlay is an error.
|
|
|
|
With `NoSource`, the main input stays absent and the explicit filesystem context
|
|
supplies the overlay root. The overlay is recorded as the first acquired source
|
|
when no other source exists. Host contexts may use the root-only form described
|
|
under [single-source loading](#single-source-loading); native contexts retain
|
|
their complete namespace, base, and root requirements.
|
|
|
|
The overlay is a separately policy-validated YAML mapping. It is loaded after the
|
|
base import graph is composed and its schema version is checked, but before defaults
|
|
and typed decoding. Overlay bytes, nodes, and source records count toward the same
|
|
aggregate limits as base inputs. Its canonical path must remain within the configured
|
|
root and must not duplicate a file already present in the base graph.
|
|
|
|
Collection destinations follow the
|
|
[collection operation and presence rules](#strict-document-composition) above.
|
|
The following table describes ordinary overlay behavior elsewhere:
|
|
|
|
| Existing lower node | Incoming overlay node | Result |
|
|
| --- | --- | --- |
|
|
| Absent | Value | Retain the overlay value |
|
|
| Value | Absent | Retain the lower value |
|
|
| Mapping | Mapping | Recurse by mapping key |
|
|
| Any other pair of present nodes | Any other pair of present nodes | Replace the entire lower node |
|
|
|
|
Under these ordinary rules, scalars, sequences, and null are atomic. An overlay
|
|
sequence replaces rather than extends a base sequence, `[]` clears it, null is an
|
|
explicit replacement, and cross-kind replacement is allowed when the final
|
|
application schema accepts it.
|
|
Replacing a subtree removes all displaced descendants from effective presence and
|
|
origin data; history retains the displaced definitions and identifies the parent
|
|
replacement. Surviving overlay paths have overlay origins, while recursively shared
|
|
mapping containers retain their contributing locations.
|
|
|
|
An overlay may contain only root `configkit.sequences` declarations; overlay
|
|
imports and nested directives are rejected. Sequence declarations are removed
|
|
before application decoding. At declared or registered sequence destinations,
|
|
`replace`, `append`, and `upsert` use the same identity and policy rules as source
|
|
composition. Other overlay sequences remain atomic replacements. An overlay may
|
|
not contribute the configured schema version or replace an ancestor of that
|
|
version with a scalar or null. These rules apply even if the supplied version
|
|
text would be unchanged. Overlays cannot mask a base parse, composition, or
|
|
version failure because those checks run first.
|
|
|
|
## Explicit CLI overrides
|
|
|
|
`Options.Overrides` accepts only records the application knows were explicitly set
|
|
by its CLI parser. Each `Override` supplies a non-root mapping `Path`, a detached
|
|
`Value`, and a required safe display `Flag` such as `--workers`; ConfigKit has no CLI
|
|
parser dependency. Parser defaults must not be submitted as overrides, and `Flag`
|
|
must never contain the raw argument or configured value. General environment
|
|
variable overlays are an explicit non-goal: environment access is reserved for
|
|
typed secret references.
|
|
|
|
Overrides are prepared after source assembly and inheritance, then assigned after
|
|
typed defaults and decoding and before application resolution. The order is:
|
|
defaults and decoded sources < CLI < application resolution. A CLI mapping
|
|
recursively overlays the decoded mapping; a CLI sequence replaces the complete
|
|
decoded sequence regardless of source collection operations. Inheritance and
|
|
typed defaulting do not run again after CLI assignment. Earlier parsing,
|
|
composition, inheritance, and decoding failures remain errors even if an override
|
|
would replace the failing value. Recorded resolution, typed secret lookup, and
|
|
semantic validation use the effective post-override values. Overrides also work
|
|
with `NoSource`.
|
|
|
|
Values are converted directly into ConfigKit's owned tree without YAML parsing or
|
|
string coercion. Supported values are `nil`, built-in strings and booleans, all
|
|
built-in signed and unsigned integer widths except `uintptr`, finite `float32` and
|
|
`float64`, and recursive `[]any`/`map[string]any`. Strings stay strings; duration and
|
|
secret fields therefore receive string values. Nil is explicit null; empty strings,
|
|
false, and zero remain intentional; empty sequences replace whole lists; and mapping
|
|
values recursively overlay existing mappings. Map keys are sorted canonically and
|
|
sequence order is preserved.
|
|
|
|
All records are detached and validated before any is applied. They cannot create
|
|
`configkit` or `extends` controls. Root and explicit
|
|
sequence-index paths are invalid; parsed numeric tokens remain mapping keys. A path
|
|
may create missing mapping ancestors but cannot traverse an existing scalar, null,
|
|
or sequence. Duplicate and ancestor/descendant-overlapping records are rejected.
|
|
Neither paths nor values may create `configkit`, and no record may overlap the
|
|
configured schema-version path. Unknown fields and effective type mismatches remain
|
|
ordinary strict-decoder errors.
|
|
|
|
At most 1,024 records are accepted, and each record creates one synthetic CLI
|
|
source after file/overlay sources in supplied order. Override sources share the
|
|
1,024-source ceiling. A record has an 8 MiB string-byte limit; override strings and
|
|
source bytes share the 64 MiB aggregate limit. Synthetic mapping keys and values
|
|
count toward the existing 1,000,000-node and depth-256 ceilings. Cycles, named or
|
|
unsupported types, nonfinite floats, and limit excesses fail with safe `override.*`
|
|
diagnostics. Diagnostics and provenance identify the safe `Flag` source, never raw
|
|
arguments or values.
|
|
|
|
Override origins are explicit CLI origins. Resolution receives CLI values with
|
|
their explicit-presence and source information. Absence-based defaults preserve
|
|
those values, while recorded normalization and derivation may change them. History
|
|
retains replaced source definitions and records equal-value replacements as well
|
|
as structural changes. Secret lookup and validation use post-resolution values
|
|
with their effective source locations.
|
|
Caller-owned maps and slices are snapshotted during loading and must not be mutated
|
|
concurrently with the call. The executable `ExampleOverride` shows translating a
|
|
parser's explicit-set state into records while preserving an explicitly supplied
|
|
zero.
|
|
|
|
## Explicit imports
|
|
|
|
Imports include other documents as peers of the declaring document. Enable
|
|
imports in the application (`Options.Imports` or the layered input's `Imports`)
|
|
and choose one selector per declaration. These are alternatives for selecting
|
|
files, not three declarations that must be used together:
|
|
|
|
Named file:
|
|
|
|
```yaml
|
|
configkit:
|
|
imports:
|
|
- path: fragments/service.yml
|
|
```
|
|
|
|
Directory search, restricted here to YAML filenames:
|
|
|
|
```yaml
|
|
configkit:
|
|
imports:
|
|
- dir: fragments
|
|
pattern: "*.yml"
|
|
```
|
|
|
|
Path pattern:
|
|
|
|
```yaml
|
|
configkit:
|
|
imports:
|
|
- glob: "fragments/*.yml"
|
|
```
|
|
|
|
Each selected document follows the same strict combination rules. A missing
|
|
optional input can be absent; invalid declarations, unreadable selected files,
|
|
and conflicting settings still fail loading.
|
|
|
|
| Option | `path` | `dir` | `glob` |
|
|
| --- | --- | --- | --- |
|
|
| `required`, `exclude`, `destination`, `placement` | Supported | Supported | Supported |
|
|
| `pattern`, `recursive`, `depth` | Not supported | Supported (`depth` needs effective recursion) | Not supported |
|
|
| `follow`, `hidden` | Not supported | Supported | Supported for wildcard-discovered entries |
|
|
|
|
Each applicable option is chosen independently: its declaration value wins over
|
|
an application import default, which wins over the library default. The detailed
|
|
rules below explain absence, patterns, placement, and ordering.
|
|
|
|
### Declaration and selection rules
|
|
|
|
Imports are opt-in. With `Options.Imports` enabled, a document may use its root
|
|
`configkit` mapping for `imports` and `sequences` controls. Ordinary `Load` may use
|
|
`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`.
|
|
|
|
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.
|
|
The Go fields are named `FollowSymlinks` and `IncludeHidden`; the YAML keys
|
|
remain `follow` and `hidden`.
|
|
|
|
A source's `configkit.sequences` declarations apply independently of import
|
|
enablement; a `configkit.imports` declaration still requires `Options.Imports`.
|
|
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.
|
|
|
|
### Ordering and inspection
|
|
|
|
Reading sources and combining their settings are separate steps. Source IDs
|
|
follow the reading order: declarations in the order written, with selected
|
|
paths in lexical order. Combination waits for dependencies. If `main.yml`
|
|
imports `extra.yml`, `extra.yml` is ready before the body of `main.yml`.
|
|
Among all ready contributions, ConfigKit uses its stable source-key ordering;
|
|
independent branches can therefore interleave. This ordering determines list
|
|
assembly and reporting, but never allows conflicting peer values to overwrite
|
|
one another.
|
|
|
|
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.EmptyImportSelections` records 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, and identifies
|
|
the declaration mapping (including its `SourceID`) and selector. 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 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.
|
|
|
|
### Earlier directive spelling
|
|
|
|
The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0.
|
|
There is no legacy alias: `_configkit` is ordinary application data, subject to
|
|
normal schema validation, and does not trigger imports. When upgrading, rename
|
|
directive keys in root files and imported fragments, and rename any application
|
|
fields previously named `configkit` to avoid the newly reserved key.
|
|
|
|
## YAML documents
|
|
|
|
A configuration document is a YAML mapping at the top level. For example, these
|
|
values can be loaded into Go fields of the matching types:
|
|
|
|
```yaml
|
|
enabled: false
|
|
wait: 5s
|
|
title: "yes"
|
|
```
|
|
|
|
```go
|
|
type Settings struct {
|
|
Enabled bool `yaml:"enabled"`
|
|
Wait time.Duration `yaml:"wait"`
|
|
Title string `yaml:"title"`
|
|
}
|
|
```
|
|
|
|
Here `5s` is a string that decodes into `time.Duration`; ordinary text `yes`
|
|
needs quotes because the unquoted legacy boolean spelling is rejected. Use
|
|
lowercase `true` and `false` for booleans. The following reference lists the
|
|
complete accepted and rejected forms.
|
|
|
|
ConfigKit accepts exactly one nonempty YAML 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
|
|
|
|
Your Go configuration type defines the accepted fields and value types. The
|
|
root must be a struct or a map with string keys. 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.
|
|
|
|
### Advanced type and field rules
|
|
|
|
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.
|
|
|
|
Decoding, inspection output, and copied values use the same 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
|
|
|
|
Defaults are starting values for omitted settings. Suppose your defaults are
|
|
`enabled: true`, `workers: 2`, `names: [primary]`, and a non-nil pointer value
|
|
for `optional`. The following independent examples illustrate the effect of input:
|
|
|
|
| Input | Result |
|
|
| --- | --- |
|
|
| `enabled` omitted | Retain `true`; it is not explicit input. |
|
|
| `enabled: false` | Use `false` and mark the path explicit. |
|
|
| `workers: 0` | Use `0` and mark the path explicit. |
|
|
| `names: []` | Use an empty list instead of the default list. |
|
|
| `optional: null` | Use nil and suppress the pointer default. |
|
|
|
|
Null requires a field type that allows nil. For example, `enabled: null` fails
|
|
for a non-pointer boolean; it does not mean "use the default." The full table
|
|
below also covers partial mappings, new entries, and pointer objects.
|
|
|
|
Decoding starts from a copy of the application's typed defaults and applies only
|
|
settings that input supplied. 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 uses a consistent representation of typed values for inspection and
|
|
comparison, called a projection. It includes every field bound by the schema,
|
|
even ordinary zero values. `omitempty` does not remove these fields from
|
|
output. For example, a nil slice displays as `null`, while a non-nil empty
|
|
slice displays as `[]`. Creating this representation does not serialize and
|
|
reparse YAML. 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.
|
|
|
|
For example, loading `{}` into this type leaves `Workers` at zero and ignores
|
|
`Internal`:
|
|
|
|
```go
|
|
type OutputSettings struct {
|
|
Workers int `yaml:"workers,omitempty"`
|
|
Internal string `yaml:"-"`
|
|
}
|
|
```
|
|
|
|
The effective YAML includes `workers: 0`; `omitempty` does not hide it. There is
|
|
no `Internal` field in that output. Ignored fields are zeroed in the returned
|
|
configuration, including when the defaults factory supplied a value for them.
|
|
|
|
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
|
|
|
|
A failed load returns a `LoadError` with structured diagnostics. Inspect the
|
|
code to identify the kind of problem, the path to identify the setting, and the
|
|
source location to find it in the input. For example, misspelling a declared
|
|
`enabled` field as `enabeld` produces `decode.unknown_field` at `/enabeld`, with
|
|
available source coordinates. Human-readable messages explain the problem;
|
|
application code can compare the diagnostic code instead of parsing that text.
|
|
See [WriteDiagnostics](../diagnostic.go) and the
|
|
[maintained error-handling example](../example_test.go).
|
|
|
|
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`.
|