Make consumer documentation easier to follow

This commit is contained in:
2026-11-01 19:04:48 +00:00
parent e74c26a4b3
commit 6db441893d
12 changed files with 1065 additions and 399 deletions

134
README.md
View File

@@ -1,56 +1,14 @@
# ConfigKit # ConfigKit
An opinionated Go library for applications that use YAML configuration files. ConfigKit loads YAML into your application's Go configuration type, combines the
inputs you choose, and lets you inspect where the final values came from. You
supply the types, defaults, and application rules; ConfigKit handles loading,
combination, strict decoding, and inspection.
Linux is release-validated; macOS support is best-effort. See the Linux is release-validated; macOS support is best-effort. See the
[platform support policy](docs/policy/release.md#platform-support) for details. [platform support policy](docs/policy/release.md#platform-support).
ConfigKit's simple `Load` accepts one strict YAML source (or explicit ## Load a configuration file
defaults-only input). `LoadLayered` additionally combines explicit lower-priority
source groups with an independently selected main input. Sources can be host
files, native `fs.FS` files, labeled bytes or readers, or snapshotted mappings.
Enabled explicit imports compose strictly within their group; precedence
between groups combines mappings recursively. Both entry points return a complete,
validated configuration with detached metadata and safe diagnostics. Successful
results and load errors also provide passive `Source(id)` lookups alongside source
enumeration.
Applications may perform audited defaulting and normalization through a resolver,
then report domain errors and warnings through read-only semantic validation. Secret
tags and a path predicate drive deterministic redacted effective YAML. Typed secret
references resolve environment names before validation, and explicit
secrets-directory bootstrap is available as a separate startup operation. Explicit
CLI override records apply after inheritance and typed decoding, before application
resolution. Value explanation is available through
detached structured records and deterministic redacted output. Built-in
`extends` relationships let a mapping inherit from a named sibling during the
same load, retaining explicit presence and source/derivation history. The root
`Diff` API compares frozen effective values from two successful same-type loads;
the [configuration contract](docs/config.md#effective-configuration-diffs) and
[consumer guide](docs/consumers/README.md#effective-configuration-diffs) describe
its safe changed-region records and maintained workflows.
Sources can also be explicitly placed as sequence items. Configuration
declarations and application registrations can give selected sequences scalar
identities and choose how they combine across precedence and inheritance.
Strict peers combine distinct records, while higher-priority collections can
replace, append, or upsert whole records. See the
[sequence contract](docs/config.md#strict-document-composition),
[maintained catalog examples](docs/consumers/README.md#identified-prompt-sequences),
and [ADR 0017](docs/adr/0017-collection-sequence-policy.md).
Imports are also explicit: a source may name files, search declared directories,
or select files with path globs. Applications can set load-wide import defaults
while keeping declaration-specific exceptions. Files are acquired in authored
declaration order; dependency-ready composition uses a shared stable scheduler.
ConfigKit does not search conventional application paths implicitly. See the
[import contract](docs/config.md#explicit-imports), [consumer examples](docs/consumers/README.md),
and [ADR 0015](docs/adr/0015-shared-dependency-ready-ordering.md) and
[ADR 0016](docs/adr/0016-independent-import-controls.md).
Main-file discovery is explicit too: applications choose and order `Search`
descriptions and pass `SearchConfig` to the existing load APIs. See the
[discovery contract](docs/config.md#configuration-discovery), [consumer guide](docs/consumers/README.md#explicit-main-file-discovery),
and [ADR 0019](docs/adr/0019-explicit-configuration-discovery.md).
```go ```go
import "go.maximumdirect.net/configkit" import "go.maximumdirect.net/configkit"
@@ -72,48 +30,54 @@ func ReadConfig(path string) (Config, error) {
Existing consumers should follow the Existing consumers should follow the
[module-path migration guidance](docs/consumers/README.md#vanity-module-path-migration). [module-path migration guidance](docs/consumers/README.md#vanity-module-path-migration).
## Add the features you need
Start with one file. Optional features use the same loading and inspection model:
- [Choose inputs](docs/consumers/README.md#choosing-a-loading-entry-point): read
files, an application-owned `fs.FS`, bytes, a reader, or a Go mapping. Use
explicit search descriptions when the application should look in several places.
- [Supply lower-priority settings](docs/consumers/README.md#native-filesystem-and-layered-input):
use `LoadLayered` for bundled defaults that application configuration can override.
- [Combine configuration](docs/config.md#explicit-imports): add declared imports,
select one overlay, or pass values explicitly set by your CLI parser.
- [Reuse settings](docs/config.md#sibling-inheritance): let one named entry inherit
from a sibling. [Identified lists](docs/config.md#strict-document-composition)
also support record inheritance and replace, append, or upsert operations.
- [Apply application rules](docs/config.md#recorded-application-resolution): derive
or normalize values in a resolver, then check the final configuration in a validator.
- [Handle credentials](docs/config.md#typed-secret-references): use environment-name
references and choose which configuration fields to hide in inspection output.
- [Inspect a load](docs/config.md#layered-metadata): examine sources, presence,
history, safe YAML output, or [changes between two loads](docs/config.md#effective-configuration-diffs).
The [consumer guide](docs/consumers/README.md) walks through these tasks and links
to maintained examples. The [configuration reference](docs/config.md) defines the
exact YAML, combination, and lifecycle rules.
## Design philosophy ## Design philosophy
ConfigKit favors one coherent, predictable configuration model over flexibility ConfigKit favors consistent, predictable rules across applications. A load returns
for reproducing each consumer's conventions. Its goal is to let applications the final configuration and metadata that distinguish supplied values such as
load once, inspect where final values came from, and distinguish explicit values `false` and `0` from omissions. Applications keep their domain rules and runtime
from omissions with minimal configuration bookkeeping. Applications retain their initialization.
domain rules and execution responsibilities.
Loading is synchronous and complete on return. Applications normally load once Within one layer, independent contributions combine strictly: conflicting
near startup and then use the final configuration and metadata; ConfigKit stays values are errors. Across layers, higher-priority settings can replace
inactive until explicitly called again. Applications may initiate additional lower-priority settings, while mappings combine by key. Identified lists have
loads as needed. See the [loading lifecycle](docs/config.md#completion-and-resource-lifetime) their declared combination rules. See the [worked combination
for completion and ownership details. examples](docs/config.md#strict-document-composition) for the details and
exceptions.
Within a configuration tier, independent contributions compose strictly: shared Loading finishes before the call returns. ConfigKit then stays inactive until you
mappings combine, while overlapping non-mapping definitions are errors. Between call it again; inspection uses the saved result. See
precedence tiers, mappings combine recursively, retaining omitted lower-priority [completion and resource lifetime](docs/config.md#completion-and-resource-lifetime).
children; other values replace lower-priority values. The main document and its
imports form one strictly composed tier. Explicit underlay groups sit below it
and above typed defaults; a selected overlay applies before inheritance, while
explicit CLI overrides apply after decoding and before application resolution.
The [configuration contract](docs/config.md#strict-document-composition) owns
the detailed combination rules and input-specific constraints. Declared or
registered collections combine distinct identified records within strict tiers
and use their selected operation at precedence boundaries; other sequences keep
their strict-conflict and whole-value-replacement behavior.
The [architecture goals](docs/policy/architecture.md#success-criteria) explain The [architecture goals](docs/policy/architecture.md#success-criteria) and
these priorities and how genuine new needs can justify extending the shared [ADRs](docs/adr) explain design decisions, including shared import and inheritance
model. The [future feature list](docs/roadmap/future.md) preserves candidates for ordering. [Future work](docs/roadmap/future.md) lists possible extensions.
consideration in future releases. Contributors can start with the [development guide](docs/development.md);
release maintainers use the [release procedure](docs/release.md).
The [configuration contract](docs/config.md) describes implemented behavior,
including [input destinations](docs/config.md#input-destinations). The
[consumer adoption guide](docs/consumers/README.md) provides maintained simple,
layered, mounted-collection, and identified-prompt examples alongside the
migration shapes. Input placement decisions are recorded in
[ADR 0011](docs/adr/0011-input-destinations.md) and sequence placement and
composition in [ADR 0014](docs/adr/0014-sequence-placement-and-composition.md).
See the [development guide](docs/development.md) for repository orientation and
the [release procedure](docs/release.md) for maintainer-only candidate and
publication steps.
ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT). ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT).

20
diff.go
View File

@@ -2,14 +2,13 @@ package configkit
import "go.maximumdirect.net/configkit/internal/engine" import "go.maximumdirect.net/configkit/internal/engine"
// Difference describes one canonical changed region between two successful // Difference describes one changed value or subtree between two successful loads.
// loads. Left is the earlier value and Right is the later value. Each snapshot // Left is the earlier value and Right is the later value. Snapshots are copies;
// is detached; Present distinguishes absence from an explicit null, and // Present distinguishes absence from explicit null. Redacted reports hiding
// Redacted reports whether combined sensitivity affected that side. Mappings // based on sensitivity from either load. Mappings compare by key; a changed
// are compared recursively, while sequences are reported as atomic values. // sequence is reported as a whole value. Records are ordered by canonical JSON
// Records are ordered by canonical JSON pointer. HiddenChanged indicates that // pointer. HiddenChanged reports an actual hidden change, without identifying
// a value concealed by combined sensitivity changed, without revealing which // the hidden member. Redaction alone does not set it.
// concealed member changed.
type Difference struct { type Difference struct {
Path Path Path Path
Left ValueSnapshot Left ValueSnapshot
@@ -17,8 +16,9 @@ type Difference struct {
HiddenChanged bool HiddenChanged bool
} }
// Diff compares the frozen effective values of two successful loads of the // Diff compares the saved final values of two successful loads of the same
// same configuration type. It does not reread sources, environment variables, // configuration type, independently of later changes to Result.Config.
// It does not reread sources, environment variables,
// or rerun defaults or resolvers. Equal values produce no records even when // or rerun defaults or resolvers. Equal values produce no records even when
// their sources, defaults, or resolution histories differ. Credential contents // their sources, defaults, or resolution histories differ. Credential contents
// are not compared: typed secret references compare by reference name only. // are not compared: typed secret references compare by reference name only.

22
doc.go
View File

@@ -1,9 +1,17 @@
// Package configkit provides deterministic, application-owned configuration // Package configkit loads YAML into an application's Go configuration type and
// loading for Go programs. // lets the application inspect where the final values came from.
// //
// Load supports file, byte, reader, and explicit no-input sources with strict YAML, // Start with Load and a File, Bytes, Reader, Mapping, or Filesystem.File source.
// typed defaults, imports, overlays, CLI overrides, schema-version checks, detached // Use NoSource for deliberately absent input or SearchConfig for an explicitly
// metadata, recorded application resolution, typed secret references, safe effective // chosen search. Applications supply types, defaults, version rules, and callbacks
// rendering, read-only semantic validation, and structured load errors. The // for deriving and validating values. LoadLayered adds lower-priority input groups.
// configuration contract is documented in docs/config.md. //
// A successful load returns the final configuration and saved inspection state.
// Metadata tracks supplied values, including false and zero; explanations retain
// safe earlier values. WriteEffective and Diff use the saved state even if the
// application later changes Result.Config. Secret references resolve credentials
// during loading, while inspection follows the configured hiding rules.
//
// The consumer guide is in docs/consumers/README.md. The exact YAML, combination,
// lifecycle, and ownership rules are documented in docs/config.md.
package configkit package configkit

View File

@@ -1,27 +1,66 @@
# Configuration Input Contract # Configuration Input Contract
This document owns ConfigKit's currently implemented source-input, YAML syntax, This reference explains the YAML ConfigKit accepts, how inputs combine, and what
schema, typed-decoding, loading, metadata, redaction, rendering, and ownership an application receives from a load. For a first working example, start with the
contracts. The parser, owned document representation, and decoder remain private [README](../README.md#load-a-configuration-file) or the
behind the public loading lifecycle. [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 ## Sources
ConfigKit exposes opaque sources through these constructors: A source describes the input to pass to `Load` or `LoadLayered`. Choose the
constructor that matches where your configuration comes from:
| Constructor | Current contract | | Constructor | Current contract |
| --- | --- | | --- | --- |
| `File(path)` | Requires a nonempty path, opens a regular file for each consumption, and closes only the library-owned file handle. Missing, inaccessible, unreadable, directory, and other nonregular inputs fail. | | `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. | | `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. | | `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. | | `Bytes(label, data)` | Requires a nonempty, non-secret diagnostic label and snapshots bytes during construction. Later caller mutation cannot affect the source. |
| `Reader(label, input)` | Requires a nonempty, non-secret label and non-nil reader. The reader is claimed once across all copies of the source, consumed but never closed, and cannot be reused. | | `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. Constructor conversion failures become safe deferred diagnostics. | | `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. | | `NoSource()` | Selects defaults-only loading, creates no source record, and marks no paths explicit. |
The zero `Source` is invalid. Constructor argument failures are retained inside the The zero `Source` is invalid. Constructors store argument failures so loading
opaque source and become safe structured diagnostics when the source is consumed. can report them as structured diagnostics. A source's private representation is
File and filesystem sources can observe changes between consumptions; byte and 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 mapping sources cannot. Mapping accepts nil, built-in string, boolean, signed or
unsigned integer, finite float, `[]any`, and `map[string]any` values recursively. unsigned integer, finite float, `[]any`, and `map[string]any` values recursively.
Typed nil maps and slices represent explicit null; empty collections remain empty. Typed nil maps and slices represent explicit null; empty collections remain empty.
@@ -51,12 +90,22 @@ group contribute no candidates. An explicitly supplied empty path or filename
is invalid. Slugs and custom filenames must be usable single host filename is invalid. Slugs and custom filenames must be usable single host filename
components; ConfigKit preserves their case and spelling. components; ConfigKit preserves their case and spelling.
Search expansion is ordered concatenation. For each directory search, ConfigKit Search descriptions are tried in the order supplied. Within a directory search,
tries its filename groups and names before advancing to the next directory. ConfigKit tries every selected filename before moving to the next directory.
When multiple directories use the same filename list, candidates follow the For example, two directories using `NamedFilenames` for application `demo` are
directory-first Cartesian product: every filename in one directory is tried searched in this order:
before the next directory. Explicit file candidates stay at their declared
positions among directory searches. ```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: The presets expand to these ordered directories and filename groups:
@@ -102,6 +151,8 @@ 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 ordinary or layered loading, including an underlay input, and does not add a new
precedence tier. precedence tier.
### Input limits
Each source is limited to 8 MiB. A load using imports or an active overlay is limited 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 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 edges, and 1,000,000 input nodes in aggregate. Glob expansion separately permits
@@ -113,15 +164,19 @@ overrides, and the converted tree uses the same node and depth bounds.
## Single-source loading ## Single-source loading
`Load[T](source, options...)` accepts zero or one `Options[T]`. The configuration `Load[T](source, options...)` returns a configuration of your Go type `T` plus
root `T` must satisfy the application schema contract below. With no options it metadata for inspection. It accepts zero or one `Options[T]`. The root type must
starts from the Go zero value. A `Definition[T].Defaults` factory, when supplied, follow the [schema rules](#application-schema-binding). With no options, loading
runs exactly once after successful source parsing, schema construction, optional starts from the Go zero value. To supply defaults, provide a
schema-version checking, and selected-overlay loading. Its result is detached before `Definition[T].Defaults` function that returns a fresh configuration.
input is applied. Resolver-provided mutable values are detached before secret lookup
and validation, and a final detached copy prevents equal-valued aliases introduced That function runs exactly once after successful source parsing, schema checks,
by validators from escaping into the returned configuration. The publication copy optional version checking, and selected-overlay loading. ConfigKit copies its
preserves already validated secret state without rereading the environment. 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: The current lifecycle is:
@@ -136,11 +191,12 @@ validate options → parse the selected source → extract source controls and
→ snapshot configuration and metadata → snapshot configuration and metadata
``` ```
`File`, `Filesystem.File`, `Bytes`, `Reader`, and `Mapping` each contribute the first base source with deterministic `File`, `Filesystem.File`, `Bytes`, `Reader`, and `Mapping` each contribute
ID `source-1`; enabled imports continue in depth-first order as `source-2` onward. the first base source with deterministic ID `source-1`; enabled imports
`NoSource` skips parsing, supplies an absent version to a configured version checker, continue in depth-first order as `source-2` onward. `NoSource` skips parsing,
contributes no source, and continues through defaults, decode, and projection. A supplies an absent version to a configured version checker, contributes no
missing file remains an error; it is never interpreted as no input. 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 `Options` accepts the application definition, ordinary-source `Destination` and
`PlacementMode`, explicit imports, one selected overlay, filesystem `PlacementMode`, explicit imports, one selected overlay, filesystem
@@ -161,16 +217,23 @@ when imports are disabled. An active ordinary overlay requires this explicit
context. `NoSource` accepts overrides and remains absent rather than contributing context. `NoSource` accepts overrides and remains absent rather than contributing
an empty document. an empty document.
Every source, options, schema, version, defaults, decode, resolution, or validation ### Results and failures
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 You can change `Result.Config` after loading. Inspection methods continue to
record's `Kind` identifies file, filesystem, bytes, reader, mapping, or CLI acquisition describe the configuration as it was when the load finished. Returned source,
independently of its precedence `Layer`. Host file paths remain canonical; diagnostic, and metadata records are copies; changing them cannot change later
filesystem paths are slash-relative within their namespace; synthetic inputs have queries. Separate loads return independently owned configuration and records.
no path. Mutating
the returned configuration or inspection records cannot alter ConfigKit's private Every source, options, schema, version, defaults, decode, resolution, or
snapshot. ConfigKit performs no logging. `Result.WriteEffective` exposes only the validation failure returns a nil result and a `*LoadError`. Successful results
frozen redacted projection. 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 `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 listed by `Result.Sources()`. Source IDs are local to one load; empty and unknown
@@ -186,7 +249,7 @@ or unknown IDs return false.
### Completion and resource lifetime ### Completion and resource lifetime
`Load` is synchronous and complete on return. Success means input assembly, `Load` finishes its work before returning. Success means input assembly,
defaulting, typed decoding, configured application resolution, secret lookup, defaulting, typed decoding, configured application resolution, secret lookup,
validation, and configuration/metadata snapshotting have completed. A reported validation, and configuration/metadata snapshotting have completed. A reported
load failure returns diagnostics and a nil result, as described above. load failure returns diagnostics and a nil result, as described above.
@@ -204,9 +267,9 @@ when explicitly called and use the retained snapshot. Applications normally load
once near startup, but may explicitly load again using sources that satisfy their once near startup, but may explicitly load again using sources that satisfy their
documented reuse rules. Additional loads produce independently owned results. documented reuse rules. Additional loads produce independently owned results.
Completion is not a transaction over external state. Files and environment Files and environment variables are read during processing; they are not all
variables are observed during processing rather than as one simultaneous external captured at one instant. If your callback changes something outside the working
snapshot. ConfigKit does not roll back application callback effects or manage work configuration, ConfigKit does not undo that effect when loading fails or manage work
or resources created by application callbacks. The explicit secrets-directory or resources created by application callbacks. The explicit secrets-directory
bootstrap remains a separate operation invoked by the application. bootstrap remains a separate operation invoked by the application.
@@ -221,6 +284,23 @@ nothing. Underlay groups are listed from lowest to highest priority. Repeating
a file within one group remains an import/repetition error; the same reusable a file within one group remains an import/repetition error; the same reusable
source may be selected again in another tier. 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 ConfigKit acquires the main input first and constructs the schema. It then
acquires and strictly composes underlay groups from lowest to highest priority, acquires and strictly composes underlay groups from lowest to highest priority,
passing each group's accumulated lower collection policy into the next group. passing each group's accumulated lower collection policy into the next group.
@@ -260,7 +340,42 @@ is the sequence container. After assembly assigns an index, nested source
destinations are projected through that item. Physical source locations and destinations are projected through that item. Physical source locations and
resource contexts continue to identify the declaring source. resource contexts continue to identify the declaring source.
#### Input destinations ### 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 `Input.Destination` places the source's application mapping beneath a path
relative to the configuration root. Its zero or empty value means root, so relative to the configuration root. Its zero or empty value means root, so
@@ -303,15 +418,52 @@ acquisition identity, physical source locations, or passive resource contexts.
Introduced ancestor mappings use the contributing document's root location as a Introduced ancestor mappings use the contributing document's root location as a
source fallback; authored content retains its original source coordinates. source fallback; authored content retains its original source coordinates.
The maintained [layered consumer example](consumers/README.md#native-filesystem-and-layered-input) The maintained [layered consumer
shows a main host file, two underlay tiers, embedded defaults, a mapping source, example](consumers/README.md#native-filesystem-and-layered-input) shows a main
and explicit resource access without a second configuration pass. host file, two underlay tiers, embedded defaults, a mapping source, and
The accepted rationale for input placement and its relative-import extension explicit resource access without a second configuration pass. The accepted
is recorded in [ADR 0011](adr/0011-input-destinations.md) and rationale for input placement and its relative-import extension is recorded in
[ADR 0012](adr/0012-relative-import-destinations.md). [ADR 0011](adr/0011-input-destinations.md) and [ADR
0012](adr/0012-relative-import-destinations.md).
### Sibling inheritance ### 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 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 `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 string; lookup does not search ancestor or unrelated scopes. A mapping inside a
@@ -390,6 +542,21 @@ usage.
### Source resource contexts ### 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)` `Result.SourceContext(id)` and callback-scoped `Resolution.SourceContext(id)`
return a passive `ResourceContext` for a source with filesystem context, or return a passive `ResourceContext` for a source with filesystem context, or
false for unknown and contextless IDs. The result query is nil-safe; the false for unknown and contextless IDs. The result query is nil-safe; the
@@ -423,6 +590,12 @@ a caller-owned filesystem closes. This is not a snapshot or universal sandbox.
### Application schema versions ### 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 `VersionRule` declares a semantic path and non-nil checker. Before defaults or typed
decoding, the checker receives a `Version` preserving absent, explicit null, string, decoding, the checker receives a `Version` preserving absent, explicit null, string,
or strict decimal-integer input. Strings and integers supply their exact spelling in or strict decimal-integer input. Strings and integers supply their exact spelling in
@@ -455,15 +628,43 @@ successful result. Callback panics remain programming failures.
#### Shared source-version checks #### 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 `Options.SourceVersionMode` and `LayeredOptions.SourceVersionMode` select how
one `Definition.Version` policy applies across source documents. The zero value one `Definition.Version` policy applies across source documents. The zero
`SourceVersionModeMainOnly` (the zero value) preserves the existing main-only value `SourceVersionModeMainOnly` preserves the existing
check at its effective path and existing protected-path behavior. main-only check at its effective path and existing protected-path behavior.
`SourceVersionModeIfPresent` checks each participating document that declares a `SourceVersionModeIfPresent` checks each participating document that declares
version and permits absent non-main declarations. `SourceVersionModeRequired` rejects a participating a version and permits absent non-main declarations.
document without a declaration. Enabled modes require a usable `VersionRule` `SourceVersionModeRequired` rejects a participating document without a
and checker; an unknown mode or unusable rule is an options error before source declaration. Enabled modes require a usable `VersionRule` and checker; an
access or callbacks. 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 In enabled modes, `Definition.Version.Path` is interpreted in each acquired
document's own coordinates, before placement and composition. The main document, document's own coordinates, before placement and composition. The main document,
@@ -516,7 +717,27 @@ the completed implementation and remediation records in Git history.
### Layered metadata ### Layered metadata
`Result.Metadata` describes every final canonical path. Queries retain explicit 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; key/index distinctions using actual effective and historical container shapes;
only parsed numeric tokens are resolved contextually. A descendant that never only parsed numeric tokens are resolved contextually. A descendant that never
existed is not historical merely because an atomic ancestor was replaced. Exact existed is not historical merely because an atomic ancestor was replaced. Exact
@@ -572,6 +793,18 @@ reports it absent. Queries return detached records.
### Secret classification and effective rendering ### 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 Secret classification combines `configkit:"secret"` field tags with the optional
pure `Definition.Sensitive(Path) bool` predicate. Tags on concrete structs inside pure `Definition.Sensitive(Path) bool` predicate. Tags on concrete structs inside
supported dynamic values are retained in typed projections, including superseded supported dynamic values are retained in typed projections, including superseded
@@ -617,6 +850,15 @@ prose.
### Historical-value explanation ### 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 `Result.Explain(path)` returns a detached `Explanation` for any known effective or
historical path. Its effective value and each chronological before/after value are historical path. Its effective value and each chronological before/after value are
`ValueSnapshot` records containing deterministic YAML text rather than mutable Go or `ValueSnapshot` records containing deterministic YAML text rather than mutable Go or
@@ -700,6 +942,23 @@ kind, or null/container replacements report their affected region. Records use
the existing comparator's canonical pointer ordering. `Present=false` with empty the existing comparator's canonical pointer ordering. `Present=false` with empty
YAML means absence; explicit null is present and renders as `null`. 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 Before either side is returned, sensitivity from both loads is applied to both
snapshots, including historical and dependency-derived classification. A snapshots, including historical and dependency-derived classification. A
classification on either side protects the corresponding values on both sides. classification on either side protects the corresponding values on both sides.
@@ -722,7 +981,24 @@ its normal nil-result/`LoadError` behavior, which callers handle before invoking
## Typed secret references ## Typed secret references
Credential-bearing fields use the opaque `Secret` type and store an environment 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)` variable name in YAML, such as `api_key: OPENROUTER_API_KEY`. `SecretEnv(name)`
constructs the same unresolved reference for application defaults or recorded constructs the same unresolved reference for application defaults or recorded
resolution. Names must match `[A-Za-z_][A-Za-z0-9_]*`; they are public resolution. Names must match `[A-Za-z_][A-Za-z0-9_]*`; they are public
@@ -770,6 +1046,34 @@ reference state without printing its value.
## Explicit secrets-directory bootstrap ## 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. `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 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 configuration validation. Applications choose the native directory path and
@@ -809,6 +1113,44 @@ files when appropriate; ConfigKit stores no implicit global report.
### Recorded application resolution ### 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 An optional `Definition.Resolve` callback runs after successful defaults, typed
decoding, and explicit CLI assignment. It receives the working configuration and a decoding, and explicit CLI assignment. It receives the working configuration and a
per-load `Resolution` context. The context's per-load `Resolution` context. The context's
@@ -871,6 +1213,11 @@ snapshots.
### Semantic validation ### 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 An optional `Definition.Validate` callback runs after successful resolution and
receives the resolved configuration plus a per-load `Validation` context. It can receives the resolved configuration plus a per-load `Validation` context. It can
collect multiple independent `validate.error` and `validate.warning` reports. collect multiple independent `validate.error` and `validate.warning` reports.
@@ -898,6 +1245,24 @@ live services as a substitute for configuration validation.
## Strict document composition ## 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; Ordinary `Load` applies strict document composition to its parsed source scope;
`Options.Imports` adds imported documents as peers in that scope. Application `Options.Imports` adds imported documents as peers in that scope. Application
defaults do not participate. With imports disabled, the single source still has defaults do not participate. With imports disabled, the single source still has
@@ -920,6 +1285,46 @@ Sequences without effective identity conflict when repeated and are never joined
by index. Disjoint entries under shared mappings compose, making stable keyed by index. Disjoint entries under shared mappings compose, making stable keyed
mappings the natural representation for independently contributed entries. 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 `Definition.SequenceIdentities` contains registrations created by
`SequenceIdentityAt` or `SequenceIdentityMatching`. The exact form accepts a `SequenceIdentityAt` or `SequenceIdentityMatching`. The exact form accepts a
mapping-only `Path`; the matching form accepts literal-key and any-mapping-key mapping-only `Path`; the matching form accepts literal-key and any-mapping-key
@@ -976,6 +1381,8 @@ headers are consumed before strict composition and cannot supply a registered
record identity. Final typed decoding and application validation still run on the record identity. Final typed decoding and application validation still run on the
completed configuration. completed configuration.
### YAML declarations and policy lifetime
Ordinary sources, layered inputs, and selected overlays may declare collection Ordinary sources, layered inputs, and selected overlays may declare collection
behavior under `configkit.sequences` at the document root. Each entry has a 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 required literal `destination` and may specify `key` as one string or a nonempty
@@ -1167,6 +1574,52 @@ zero.
## Explicit imports ## 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 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 `configkit` mapping for `imports` and `sequences` controls. Ordinary `Load` may use
`sequences` without enabling imports, but an `imports` field still requires `sequences` without enabling imports, but an `imports` field still requires
@@ -1184,12 +1637,6 @@ reads them as strings. Null, scalar destinations, and non-string members are
key below the root is invalid. With imports disabled, root imports and nested key below the root is invalid. With imports disabled, root imports and nested
`configkit` keys remain `import.disabled`. `configkit` keys remain `import.disabled`.
The directive key is `configkit`, not the `_configkit` spelling used in v0.1.0.
There is no legacy alias: `_configkit` is ordinary application data, subject to
normal schema validation, and does not trigger imports. When upgrading, rename
directive keys in root files and imported fragments, and rename any application
fields previously named `configkit` to avoid the newly reserved key.
Path declarations use slash-separated relative paths. Absolute paths and network Path declarations use slash-separated relative paths. Absolute paths and network
URLs are invalid. Each path resolves from its declaring file's canonical directory. 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 An empty `path: []` selects nothing; it succeeds even when `required: true` because
@@ -1238,6 +1685,17 @@ Sequence identities can be supplied either by these YAML declarations or by
application registrations. The [strict composition contract](#strict-document-composition) application registrations. The [strict composition contract](#strict-document-composition)
defines their agreement, operation, and lifetime rules. 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 Acquisition visits declarations in authored order and each declaration's selected
paths in namespace-path lexical order, preserving source-record and version-callback paths in namespace-path lexical order, preserving source-record and version-callback
order. Ordinary mapping contributions in a main tier are assembled dependency-first: order. Ordinary mapping contributions in a main tier are assembled dependency-first:
@@ -1306,30 +1764,31 @@ Physical paths continue to resolve from the file that declares each import.
An actual empty mapping contributes an empty item, while an unmatched declaration An actual empty mapping contributes an empty item, while an unmatched declaration
with default or explicit `required: false` contributes nothing. with default or explicit `required: false` contributes nothing.
For host files, the configuration root defaults to the selected file's canonical For host files, the configuration root defaults to the selected file's
directory. A host file may supply `FilesystemContext.Root` to widen that root but canonical directory. A host file may supply `FilesystemContext.Root` to widen
may not supply `BaseDir` or a native `FS`. Byte, reader, and mapping inputs with host context that root but may not supply `BaseDir` or a native `FS`. Byte, reader, and
must provide both `BaseDir` and `Root` as absolute existing directories. mapping inputs with host context must provide both `BaseDir` and `Root` as
For `Filesystem.File`, the default root is `.` in its namespace and the base is absolute existing directories. For `Filesystem.File`, the default root is `.`
the selected file's directory. An explicit context must use the same `Filesystem` in its namespace and the base is the selected file's directory. An explicit
namespace and may set `Root` to contain the file and its base; it may not supply context must use the same `Filesystem` namespace and may set `Root` to contain
`BaseDir`. Byte, reader, and mapping inputs with native context provide both `BaseDir` and the file and its base; it may not supply `BaseDir`. Byte, reader, and mapping
`Root` as slash-relative `fs.ValidPath` directories in `FS` (`.` means its root), inputs with native context provide both `BaseDir` and `Root` as slash-relative
with the base inside the root. Context paths are checked even when imports and `fs.ValidPath` directories in `FS` (`.` means its root), with the base inside
overlays are inactive. Diagnostic labels, including URI-looking labels, never the root. Context paths are checked even when imports and overlays are
imply filesystem paths. Nonregular targets are rejected before opening to avoid blocking on FIFOs; inactive. Diagnostic labels, including URI-looking labels, never imply
post-open regular-file checks remain in place under the stable-input assumption. filesystem paths. Nonregular targets are rejected before opening to avoid
ConfigKit canonicalizes symlinks, blocking on FIFOs; post-open regular-file checks remain in place under the
checks containment with relative paths, and opens the canonical root-relative target stable-input assumption. ConfigKit canonicalizes symlinks, checks containment
through a library-owned root handle. Contained relative or absolute symlink targets with relative paths, and opens the canonical root-relative target through a
are accepted; escapes are rejected. Canonical paths define identity, so hard links at library-owned root handle. Contained relative or absolute symlink targets are
distinct paths remain distinct sources. Native filesystems use slash-path accepted; escapes are rejected. Canonical paths define identity, so hard links
operations. Where the supplied `fs.FS` implements `fs.ReadLinkFS`, ConfigKit uses at distinct paths remain distinct sources. Native filesystems use slash-path
that capability to inspect and resolve discovered links; otherwise it follows operations. Where the supplied `fs.FS` implements `fs.ReadLinkFS`, ConfigKit
the filesystem's ordinary `fs.FS` behavior. ConfigKit does not require every uses that capability to inspect and resolve discovered links; otherwise it
native filesystem to expose link inspection, and native filesystem behavior is follows the filesystem's ordinary `fs.FS` behavior. ConfigKit does not require
not a general sandbox guarantee. Imports stay in their selected namespace and every native filesystem to expose link inspection, and native filesystem
root. 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 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 sites. A previously visited canonical path is `import.repeated` and reports both
@@ -1339,9 +1798,39 @@ application's schema-version path or replace one of its ancestors; the version m
come from the selected source. No directory is scanned implicitly, including one come from the selected source. No directory is scanned implicitly, including one
named `conf.d`, and ConfigKit performs no remote fetching. 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 ## YAML documents
The private YAML boundary currently accepts exactly one nonempty document whose root 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: is a mapping. Comments and block or flow collections are supported. It rejects:
- empty input or an empty document; - empty input or an empty document;
@@ -1386,9 +1875,8 @@ keys that otherwise classify as a boolean, number, or null.
## Application schema binding ## Application schema binding
ConfigKit builds a private reflection descriptor graph for each schema operation; Your Go configuration type defines the accepted fields and value types. The
it keeps no global mutable descriptor cache. A configuration root must be a struct root must be a struct or a map with string keys. Nested supported shapes are:
or string-keyed map. Nested supported shapes are:
- booleans, strings, signed and unsigned integers, and finite floating-point types; - booleans, strings, signed and unsigned integers, and finite floating-point types;
- `time.Duration`, typed `Secret` references, and paired - `time.Duration`, typed `Secret` references, and paired
@@ -1396,6 +1884,8 @@ or string-keyed map. Nested supported shapes are:
- pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and - pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and
- recursive type definitions whose eventual application values are acyclic. - 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, Named scalar types retain their Go identity. `uintptr`, complex numbers, functions,
channels, unsafe pointers, non-string map keys, and nonempty interfaces are not 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 configuration types. A custom text scalar must provide both directions on its value
@@ -1415,16 +1905,16 @@ ConfigKit field tag and marks its bound field or container for secret classifica
and redacted inspection. and redacted inspection.
Schema path traversal uses these same bindings. Explicit key segments address Schema path traversal uses these same bindings. Explicit key segments address
mappings, explicit index segments address sequences, and parsed numeric pointer mappings, explicit index segments address sequences, and parsed numeric
tokens become indices only in a sequence. Sequence indices must use canonical pointer tokens become indices only in a sequence. Sequence indices must use
nonnegative decimal spelling: no signs or leading zeroes except `0` itself. canonical nonnegative decimal spelling: no signs or leading zeroes except `0`
Tokens such as `+0` and `-0` remain literal keys in mappings but do not identify itself. Tokens such as `+0` and `-0` remain literal keys in mappings but do
sequence elements. Fixed-array indices are bounds checked. Dynamic `any` and inline-map entries not identify sequence elements. Fixed-array indices are bounds checked.
remain schema-valid for application-defined descendants, but dynamic values cannot Dynamic `any` and inline-map entries remain schema-valid for
introduce typed secret references. application-defined descendants, but dynamic values cannot introduce typed
secret references.
Descriptors are reused by typed decoding, projection, and cloning so those operations Decoding, inspection output, and copied values use the same field bindings.
cannot acquire different field bindings.
### Canonical module options ### Canonical module options
@@ -1446,8 +1936,24 @@ examples.
## Defaults and typed decoding ## Defaults and typed decoding
The private decoder starts from an independently owned clone of the application's Defaults are starting values for omitted settings. Suppose your defaults are
typed defaults and applies only explicitly present input nodes. Defaults therefore `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 never become explicit, while equal-valued input remains explicit. Successful decode
produces detached typed state, baseline and final canonical trees, an input-presence produces detached typed state, baseline and final canonical trees, an input-presence
index, and default/input origins for the later public loading lifecycle. index, and default/input origins for the later public loading lifecycle.
@@ -1492,13 +1998,30 @@ conversion API.
## Typed projection and ownership ## Typed projection and ownership
ConfigKit can internally project a schema-valid application value directly into its ConfigKit uses a consistent representation of typed values for inspection and
owned canonical tree without YAML serialization. Projection includes every bound comparison, called a projection. It includes every field bound by the schema,
field, including ordinary zero values; `omitempty` has no effect. Struct fields keep even ordinary zero values. `omitempty` does not remove these fields from
binding order, dynamic map keys sort by decoded string, and sequences keep their output. For example, a nil slice displays as `null`, while a non-nil empty
order. Integers retain exact base-10 values, finite floats use their destination slice displays as `[]`. Creating this representation does not serialize and
precision, durations use `time.Duration.String`, and paired text scalars use their reparse YAML. Struct fields keep binding order, dynamic map keys sort by
canonical marshaled text. 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 Nil pointers, maps, slices, and dynamic values project as null. Non-nil empty maps
and slices remain empty containers, preserving the distinction. Recursive acyclic and slices remain empty containers, preserving the distinction. Recursive acyclic
@@ -1523,6 +2046,15 @@ and rendering. They are not a public generic conversion or cloning API.
## Safe failures ## 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.*` Source failures use stable `source.*` codes and YAML failures use stable `yaml.*`
codes declared by the root package. Diagnostics contain safe classifications, codes declared by the root package. Diagnostics contain safe classifications,
semantic paths when applicable, and available source coordinates. Raw parser, semantic paths when applicable, and available source coordinates. Raw parser,

View File

@@ -1,24 +1,35 @@
# Consumer Adoption Guide # Consumer Adoption Guide
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and ConfigKit loads configuration into your application's Go types. You provide
selected-overlay traversal, precedence, provenance, secret lookup, and safe inspection from those types, defaults, and application rules. ConfigKit reads and combines the
applications. Consumers still own their configuration structs, defaults, semantic selected inputs, checks their types, and returns values with source information.
validation, schema-version policy, CLI parsing, runtime matching and conversion, Your application still owns CLI parsing, resource reads, remote acquisition, and
resource checks, remote acquisition, and service initialization. runtime initialization.
Import the public root package as `"go.maximumdirect.net/configkit"`; its package Import `"go.maximumdirect.net/configkit"`; the package name is `configkit`.
name remains `configkit`. Start with one file, then choose the tasks your application needs:
| Task | Start here |
| --- | --- |
| Load one file | [Happy path](#happy-path) and the [README example](../../README.md#load-a-configuration-file) |
| Supply fixed defaults and check application rules | [Defaults and application rules](#defaults-and-application-rules) |
| Add bundled defaults beneath application settings | [Native filesystem and layered input](#native-filesystem-and-layered-input) |
| Select files or an overlay | [Discovery](#explicit-main-file-discovery), [import defaults](#import-defaults-and-selection-inspection), and the [overlay reference](../config.md#selected-overlays) |
| Reuse settings or combine record lists | [Inheritance](#named-sibling-inheritance) and [identified sequences](#identified-prompt-sequences) |
| Handle credentials | [Typed-secret reference](../config.md#typed-secret-references) and the [startup example](../../examples/consumers/consumers_test.go) |
| Inspect sources, earlier values, or changes | [Inspect a completed load](#inspect-a-completed-load) and [diffs](#effective-configuration-diffs) |
| Upgrade an existing consumer | [Migration guidance](#intentional-migration-changes) |
## Choosing a loading entry point ## Choosing a loading entry point
- Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration. - Use [`Load`](../../lifecycle.go) with one source for an ordinary configuration.
[`SearchConfig`](../../discovery.go) is also a source, so its explicit [`SearchConfig`](../../discovery.go) is also a source; pass it to `Load` or use
discovery result can be passed to `Load` or used as the main input to it as the main input to [`LoadLayered`](../../layered.go).
[`LoadLayered`](../../layered.go). - Use `LoadLayered` when several inputs should supply lower-priority defaults
- Use `LoadLayered` when inputs must be composed in underlay groups before the beneath the main source. Each underlay group combines its inputs strictly.
main source. Each [`Input`](../../layered.go) owns its source, filesystem, Each [`Input`](../../layered.go) owns its source, filesystem, destination, and
destination, and import choice. The overlay filesystem context is selected import choice. The overlay filesystem context is selected independently in
independently in [`LayeredOptions`](../../layered.go). [`LayeredOptions`](../../layered.go).
- Load-wide rules belong to [`Options`](../../lifecycle.go) or - Load-wide rules belong to [`Options`](../../lifecycle.go) or
[`LayeredOptions`](../../layered.go): the application definition (including [`LayeredOptions`](../../layered.go): the application definition (including
version policy), import defaults, and explicit overrides. These do not turn version policy), import defaults, and explicit overrides. These do not turn
@@ -33,68 +44,68 @@ file and layered cases. For import-focused adoption, see the
discovery composed with sequences, see the discovery composed with sequences, see the
[sequence catalog](../../examples/consumers/sequence_catalog.go). [sequence catalog](../../examples/consumers/sequence_catalog.go).
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
sanitized shapes using only ConfigKit and the Go standard library:
| Shape | Demonstrated fit | Application code deliberately retained |
| --- | --- | --- |
| Narratio-like | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
| Notarius-like | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
| Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
executable examples and ordinary tests. It contains no private application data,
credentials, network calls, or dependency on a sibling checkout.
The Notarius-shaped fixtures deliberately load both `version: 4` and
`version: "4"` into the same string-backed application field. Its version checker
still owns the supported identifier; fractional numeric input and unsupported
integer identifiers remain negative examples.
## Happy path ## Happy path
An application defines ordinary types and passes one explicit source. Optional Define the Go configuration type, choose one source, and call `Load`. Handle a
features appear only when the application needs them: load error before starting the application with `result.Config`. The
[README example](../../README.md#load-a-configuration-file) shows this complete
minimal loading function. An explicitly missing file is an error; use `NoSource`
when you deliberately want defaults without a main document.
### Defaults and application rules
Using the README's `Config` type, an application can add reusable rules:
```go ```go
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{ definition := configkit.Definition[Config]{
Definition: rules, Defaults: func() Config { return Config{Enabled: true, Workers: 2} },
Imports: useDeclaredImports, Validate: func(value *Config, validation *configkit.Validation) {
Overlay: selectedConfigOverlay, if value.Workers < 0 {
Overrides: explicitlySetFlags, validation.Error(configkit.Keys("workers"), "must be nonnegative")
}) }
if err != nil { },
return err
} }
return start(result.Config)
``` ```
Simple consumers use neither imports nor selected overlays. A complex consumer can opt into Pass this definition in `Options[Config]{Definition: definition}` when calling
both without redefining their syntax. ConfigKit overlays are configuration overlays; `Load`. Supplied `enabled: false` or `workers: 0` replaces those defaults.
they do not select or replace an application's LLM, model, pipeline, report, or Use `Resolve` when a value must be derived or normalized before validation, and
execution profile concepts. record that change with the provided helpers. The
[callback reference](../config.md#recorded-application-resolution) compares the
helpers; [executable examples](../../example_test.go) show full definitions.
### Inspect a completed load
Keep the result when you need to explain configuration. Use
`result.Metadata().Lookup(configkit.Keys("workers"))` to inspect whether input
supplied the value and which source or application step supplied it. Follow an
origin's source ID with `result.Source(id)` when you need its source record.
Use `Explain` for earlier values, `WriteExplanation` for a readable explanation,
and `WriteEffective` for safe YAML describing the final saved state. These
operations describe the completed load even if your application later changes
`result.Config`. The [inspection reference](../config.md#layered-metadata)
explains returned values, copies, and hidden information.
Add imports, a selected overlay, or explicitly set CLI values through options
only when needed. An overlay changes configuration; choosing an LLM, report, or
execution profile remains application logic.
## Explicit main-file discovery ## Explicit main-file discovery
Applications choose the search conventions and their precedence, then pass the Applications choose the search conventions and their precedence, then pass the
resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry point. resulting `SearchConfig` source to the same `Load` or `LoadLayered` entry
For example, the maintained sequence catalog puts a caller-selected file before point. For example, the maintained sequence catalog puts a caller-selected
a custom directory fallback while keeping its underlay and overlay choices file before a custom directory fallback while keeping its underlay and overlay
independent. See [`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go) choices independent. See
and its [behavior tests](../../examples/consumers/sequence_catalog_test.go), plus [`sequence_catalog.go`](../../examples/consumers/sequence_catalog.go) and its
the [working-directory executable example](../../examples/consumers/discovery_test.go). [behavior tests](../../examples/consumers/sequence_catalog_test.go), plus the
Together, the root-owned [discovery API](../../discovery.go) and [configuration [working-directory executable
discovery contract](../config.md#configuration-discovery) define ordered example](../../examples/consumers/discovery_test.go). Together, the [discovery
concatenation, directory-first Cartesian expansion, optional exhaustion, and API](../../discovery.go) and [configuration discovery
selected-file behavior. [ADR 0019](../adr/0019-explicit-configuration-discovery.md) contract](../config.md#configuration-discovery) explain the order of attempted
files, what happens when no file is found, and why a selected-file failure
stops the search. [ADR 0019](../adr/0019-explicit-configuration-discovery.md)
records why discovery is explicitly selected. records why discovery is explicitly selected.
## Import defaults and selection inspection ## Import defaults and selection inspection
@@ -143,13 +154,14 @@ priority values remain present. Its resolver records a derived endpoint URL afte
overlay application, and its nested typed secret reference is classified before overlay application, and its nested typed secret reference is classified before
the resulting records are presented. the resulting records are presented.
The maintained [behavior tests and executable examples](../../examples/consumers/diff_test.go) The maintained [behavior tests and executable
use temporary files and disposable secret values. Presenters should use examples](../../examples/consumers/diff_test.go) use temporary files and
`ValueSnapshot.Present` for absence, record membership for change, and disposable secret values. Presenters should use `ValueSnapshot.Present` for
`HiddenChanged` for a withheld change indicator. Do not parse the redaction marker absence, record membership for change, and `HiddenChanged` for a withheld
or add separately rendered full contexts that lack combined sensitivity. The change indicator. Do not parse the redaction marker or add separately rendered
[configuration contract](../config.md#effective-configuration-diffs) owns shared full contexts that lack combined sensitivity. The [configuration
equality, granularity, and disclosure semantics. contract](../config.md#effective-configuration-diffs) owns shared equality,
granularity, and disclosure semantics.
## Named sibling inheritance ## Named sibling inheritance
@@ -276,10 +288,9 @@ path-bearing example. Its application resolver uses `Resolution.EntryInfo` and
`Resolution.Source` before normalizing: relative CLI input uses an explicitly `Resolution.Source` before normalizing: relative CLI input uses an explicitly
supplied invocation directory, retained defaults use the canonical main source, supplied invocation directory, retained defaults use the canonical main source,
and a file-authored scalar uses its one declaring source. Absolute paths remain and a file-authored scalar uses its one declaring source. Absolute paths remain
unchanged. Its executable test covers the matching effective YAML and explanation unchanged. Its executable test checks the resulting YAML and explanation
history, source-file ownership, and a symlink opened through an alias; canonical history. When the configuration filename is a symlink, this example uses the
target-file directories, rather than the alias directory or process working actual target file's directory as the base for file-authored paths.
directory, are the selected bases.
The selected example policy leaves a relative value unchanged and emits a warning The selected example policy leaves a relative value unchanged and emits a warning
when a CLI base is absent or file provenance is missing or composite. That is an when a CLI base is absent or file provenance is missing or composite. That is an
@@ -289,7 +300,15 @@ existence checks nor confinement checks.
## Intentional migration changes ## Intentional migration changes
The examples preserve domain meaning while adopting one configuration convention: When upgrading, start with the changes relevant to your dependency and inputs:
1. Update the module requirement and Go imports together as described below.
2. Replace old public names using the old/new table.
3. Update any layer or diagnostic-code strings stored or compared by your code.
4. Review YAML and startup changes that apply to your application, then rebuild
and run its configuration tests.
The maintained examples illustrate these configuration changes:
- Explicit empty strings, lists, zeroes, false values, and nulls remain intentional. - Explicit empty strings, lists, zeroes, false values, and nulls remain intentional.
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
@@ -323,11 +342,13 @@ Go imports together, selecting a revision or release that declares the new modul
path. The package name remains `configkit`; the configuration model and public path. The package name remains `configkit`; the configuration model and public
symbol names are unchanged by this identity migration. symbol names are unchanged by this identity migration.
Old and new import paths identify distinct Go packages, including distinct named Go treats the old and new import paths as different packages, with different
types. Coordinate adoption with any dependencies that exchange ConfigKit values named types. For example, if another library returns a ConfigKit `Path` or
with your application; changing a requirement alone does not make those types `Secret` using the old import, your application cannot use it as the new
interchangeable. The maintained examples use the new path, but other repositories package's type just by changing a module requirement. Coordinate migration
must manage their own migrations. with dependencies that pass ConfigKit values to or from your application. The
maintained examples use the new path, but other repositories must manage their
own migrations.
Historical tags keep their original module declarations and contents. Older Historical tags keep their original module declarations and contents. Older
releases such as v0.10.2 still use the old path and cannot be installed under the releases such as v0.10.2 still use the old path and cannot be installed under the
@@ -363,12 +384,13 @@ compatibility aliases:
| History diagnostic string `"explain.limit_exceeded"` | `"history.limit_exceeded"` | | History diagnostic string `"explain.limit_exceeded"` | `"history.limit_exceeded"` |
The YAML import keys `follow` and `hidden`, the `configkit:"secret"` tag, and The YAML import keys `follow` and `hidden`, the `configkit:"secret"` tag, and
typed `Secret` references are unchanged. Consumers that compare literal strings typed `Secret` references are unchanged. If your application compares or
or keep serialized diagnostic/provenance expectations may need to update those stores layer names or diagnostic codes as strings, update those strings
alongside Go references. This repository guidance does not claim that downstream alongside Go references. This repository guidance does not claim that
applications have migrated and does not select a release version for this downstream applications have migrated and does not select a release version
breaking pre-v1 change. `ChangeKind` and its inspection constants remain; use for this breaking pre-v1 change. `ChangeKind` and its inspection constants
`Default` and `Normalize` when the helper should both assign and record a value. remain; use `Default` and `Normalize` when the helper should both assign and
record a value.
### Selected-overlay migration ### Selected-overlay migration
@@ -380,10 +402,40 @@ The [configuration contract](../config.md#selected-overlays) is the canonical
behavioral reference. Application-owned profile concepts, such as `model_profile`, behavioral reference. Application-owned profile concepts, such as `model_profile`,
are unchanged, and any CLI flag rename remains the application's decision. are unchanged, and any CLI flag rename remains the application's decision.
These examples are migration evidence, not completed changes to Narratio, Notarius, ### Background on the examples
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
wiring, runtime catalogs, and operational documentation remains separate downstream These examples demonstrate migration shapes. They do not claim that Narratio,
work. Notarius, WeatherReporter, or Seriatim has been migrated. Updating those
repositories, their deployed YAML, CLI wiring, runtime catalogs, and
operational documentation remains separate downstream work.
## Example background
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
sanitized shapes using only ConfigKit and the Go standard library:
| Shape | Demonstrated fit | Application code deliberately retained |
| --- | --- | --- |
| Artifact defaults and normalization (Narratio-like) | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
| Versions and dynamic options (Notarius-like) | Canonical object bindings, a string-backed schema version accepting string or integer YAML, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
| Module validation and credential startup (WeatherReporter-like) | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
| Independent ordered rule files (Seriatim-like) | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
| Bundled file | Simple native `fs.FS` source through ordinary `Load` | Selection and lifetime of the application-owned embedded filesystem |
| Layered catalog | Embedded defaults with an import, a mapping underlay, an independent host main file, explicit false/zero input, and source-relative resource access | Domain validation and opening/interpreting the template resource |
| Mounted collections | Entry-declared relative destinations, nested metadata/prompt/profile imports, local inheritance, and a version-only higher-tier override | Collection IDs, profile meaning, and opening/interpreting prompt resources |
| Named presets | One-load sibling inheritance, explicit false, nested destination resolution, and source/derivation inspection | Choosing and executing a preset |
| Effective configuration diff | Same-type frozen effective comparisons for two selected files or base-versus-overlay impact | Source/options selection and consumer-owned safe presentation |
| Shared source-version checks | One `Definition.Version` policy for accepted declarations across participating source documents, with optional or required declarations and sanitized evidence | Choosing which declarations the application accepts; final assembled schema and domain validation |
| Sequence catalogs | Declared collection operations, record inheritance, typed CLI and resolution, inspection, exact and wildcard application registrations | Catalog meaning and final domain validation |
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
executable examples and ordinary tests. It contains no private application data,
credentials, network calls, or dependency on a sibling checkout.
The Notarius-shaped fixtures deliberately load both `version: 4` and
`version: "4"` into the same string-backed application field. Its version checker
still owns the supported identifier; fractional numeric input and unsupported
integer identifiers remain negative examples.
## Maintained API examples ## Maintained API examples

View File

@@ -179,3 +179,9 @@ reflection behavior by `schema_test.go`, `projection_test.go`, and
`decode_test.go`. The [development test map](../development.md#task-to-test-map) `decode_test.go`. The [development test map](../development.md#task-to-test-map)
links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed links focused commands. [`clone.go`](../../internal/engine/clone.go) owns typed
copying, using the shared descriptors and per-operation traversal helpers. copying, using the shared descriptors and per-operation traversal helpers.
Each schema operation builds its private reflection descriptor graph with
operation-local caching; there is no global mutable descriptor cache. Decoding,
projection, and cloning reuse these descriptors so field bindings agree. The
public reference describes the resulting type, output, and ownership behavior;
this descriptor storage is an implementation detail.

View File

@@ -1,9 +1,16 @@
# ConfigKit Release Procedure # ConfigKit Release Procedure
This is the maintainer runbook for ConfigKit's [release policy](policy/release.md). This is the maintainer runbook for ConfigKit's [release policy](policy/release.md).
ConfigKit publishes a source-only Go module. The immutable annotated tag and A release publishes the Go source under a new Git tag, with a matching release
matching checked-in release note are the deliverables; there is no binary, note committed in the repository. The tag is annotated: it includes the release
container image, generated package, or separate registry publication. note text and points to the released commit. Published tags and contents are
immutable. There is no binary, container image, generated package, or separate
registry publication.
A **candidate** is the committed revision you intend to release. The
**authoritative remote** is the repository where ConfigKit publishes releases;
its exact push URL is listed below. The **consumer minimum** is the oldest Go
version supported for applications using ConfigKit.
## Release identity and support ## Release identity and support
@@ -20,12 +27,14 @@ container image, generated package, or separate registry publication.
- Best-effort targets: macOS amd64 and arm64, under the - Best-effort targets: macOS amd64 and arm64, under the
[platform support policy](policy/release.md#platform-support) [platform support policy](policy/release.md#platform-support)
Release validation runs one offline race-enabled Go suite, one full vet pass, Release validation runs the offline Go test suite once with the race detector,
and CGO-disabled package compilation for both Linux targets. macOS and CI are then a full vet pass and package compilation for both Linux targets with cgo
disabled (`CGO_ENABLED=0`). macOS and CI are
not release gates; do not wait for or poll CI. not release gates; do not wait for or poll CI.
The vanity module path identifies Go imports. Source/tag browser links and public The vanity module path is the name applications put in Go imports. Source/tag
Go retrieval use the Forge HTTPS repository independently of SSH publication. browser links and public Go retrieval use the Forge HTTPS repository
independently of SSH publication.
## 1. Select Go and prepare the tool ## 1. Select Go and prepare the tool
@@ -36,10 +45,10 @@ For a patch release, retain the series recorded in `go.mod` and leave its consum
minimum unchanged. Follow the minimum unchanged. Follow the
[toolchain selection policy](policy/release.md#configkit-toolchain-selection). [toolchain selection policy](policy/release.md#configkit-toolchain-selection).
Put the selected series' latest locally available compatible patch first on Choose the latest compatible patch installed locally in that Go series. Put
`PATH`, ensuring it meets `go.mod`'s minimum. The same `go` executable must build its `go` executable first on `PATH` and ensure it meets the minimum in
ReleaseKit and run its child validation commands. Set local toolchain selection `go.mod`. The same `go` executable must build ReleaseKit and run its child
and verify it: validation commands. Set local toolchain selection and verify it:
```sh ```sh
export GOTOOLCHAIN=local export GOTOOLCHAIN=local
@@ -64,8 +73,17 @@ use that required version instead and document the reason. When the series is
unchanged, leave the minimum unchanged unless a minor or major release unchanged, leave the minimum unchanged unless a minor or major release
deliberately changes it under the deliberately changes it under the
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain). [consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
For example, validating with Go 1.26.8 leaves `go 1.26.0` unchanged. There is no For example:
separate requirement to validate with Go 1.26.0.
| Release situation | Go used for validation | Consumer minimum |
| --- | --- | --- |
| Patch release in the 1.26 series | Latest compatible patch installed locally, such as 1.26.8 | Keep the existing `go.mod` minimum unchanged. |
| Minor/major release staying in 1.26 | Latest compatible patch installed locally | Normally retain the minimum; any deliberate change follows the linked policy. |
| Minor/major release adopting a newer installed stable series | Latest compatible patch installed locally in that series | Normally declare that series' initial stable patch, or the higher patch required by a dependency or identified fix. |
Using Go 1.26.8 for validation can therefore leave `go 1.26.0` unchanged. Users
need the declared minimum, while maintainers validate with their selected patch.
There is no separate requirement to validate with Go 1.26.0.
Prepare dependencies whether or not the minimum changes: Prepare dependencies whether or not the minimum changes:
@@ -73,7 +91,8 @@ Prepare dependencies whether or not the minimum changes:
go mod download go mod download
``` ```
Build the tool selected by `go.mod` into temporary storage outside the checkout. Build the ReleaseKit version recorded in `go.mod` into temporary storage outside
the checkout.
Ensure `$TMPDIR` names a system temporary location, not the repository. These Ensure `$TMPDIR` names a system temporary location, not the repository. These
commands never install another ReleaseKit version: commands never install another ReleaseKit version:
@@ -85,8 +104,8 @@ go list -m gitea.maximumdirect.net/eric/ReleaseKit
"$releasekit_bin" version "$releasekit_bin" version
``` ```
The module-selected executable is ReleaseKit v0.4.2. Use the built executable This repository selects ReleaseKit v0.4.2. Use the compiled program above for
for publication to preserve its exit status and publication-state diagnostics. publication so its exit status and publication report reach you unchanged.
`go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands `go run` maps a nonzero child exit status to 1. ReleaseKit's child Go commands
use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected use the `go` found on `PATH` with `GOTOOLCHAIN=local`; keep the selected
executable first on `PATH` through the publication command. executable first on `PATH` through the publication command.
@@ -102,10 +121,10 @@ not authorize a push or release.
## 2. Prepare and commit the candidate ## 2. Prepare and commit the candidate
Choose an unused stable version locally and on the authoritative remote. Compare Choose a stable version that has not been used locally or on the publication
the candidate with the previous tag and review exported Go signatures and remote. Compare the candidate with the previous tag. Review public Go APIs and
ownership, YAML and resolution semantics, diagnostic contracts, minimum Go value ownership, YAML and resolution rules, diagnostic contracts, the minimum
version, and platform support. Review maintained consumer examples and the Go version, and platform support. Review maintained consumer examples and the
migration guidance. Explain any consumer-minimum increase and required consumer migration guidance. Explain any consumer-minimum increase and required consumer
action under the action under the
[consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain). [consumer minimum policy](policy/release.md#consumer-minimum-and-release-toolchain).
@@ -142,32 +161,40 @@ candidate to `main`, and invoke validation again.
## 4. Publish the committed candidate ## 4. Publish the committed candidate
With the authorized candidate already on authoritative `main`, invoke the Once publication is authorized and the candidate is on the publication remote's
module-selected native executable exactly once: `main`, run the compiled ReleaseKit program once to validate and publish it:
```sh ```sh
"$releasekit_bin" publish vMAJOR.MINOR.PATCH "$releasekit_bin" publish vMAJOR.MINOR.PATCH
``` ```
Replace the placeholder with the selected version. ReleaseKit checks the Replace the placeholder with the selected version. ReleaseKit checks the
effective push URL against `expected_push_url`, verifies the candidate on push URL Git will actually use, after URL rewrites, against `expected_push_url`.
It verifies the candidate on
`main`, and checks the release-note H1 for the product and version. It then `main`, and checks the release-note H1 for the product and version. It then
runs the selected race-enabled standard suite once with test-time vet disabled, runs the selected race-enabled standard suite once with test-time vet disabled,
one full vet pass, and CGO-disabled package compilation for Linux amd64 and one full vet pass, and CGO-disabled package compilation for Linux amd64 and
arm64. It publishes only the annotated tag for that candidate. Confirmed arm64. It publishes only the annotated tag for that candidate. Confirmed
publication of the intended tag completes the release. The tag points to the publication of the intended tag completes the release. The tag points to the
captured commit and uses the complete checked-in note as its annotation message. captured commit and uses the complete checked-in note as its annotation message.
The optional `check` Use the optional `check` command for diagnosis when useful. It validates without
command is available for diagnosis but is not a separate required pass: publishing and is not required before `publish`, which performs its own checks:
```sh ```sh
"$releasekit_bin" check vMAJOR.MINOR.PATCH "$releasekit_bin" check vMAJOR.MINOR.PATCH
``` ```
Status 0 confirms publication, status 1 reports a known publication failure or For `publish`, interpret the program's exit status as follows:
conflict, status 2 reports usage, profile, or version errors, and status 3 means
the push outcome remains unknown. Preserve both the native exit status and its | Status | Meaning | Next action |
diagnostics. | --- | --- | --- |
| `0` | Publication succeeded. | The release is complete. Optional verification can follow. |
| `1` | A known publication failure or conflict was reported. | Read the report and correct the stated problem. Preserve any published tag. |
| `2` | The command, profile, or version is invalid. | Correct the command or configuration before trying again. |
| `3` | The tag push outcome is still unknown. | Follow the recovery procedure below before retrying or declaring success. |
Keep both the program's exit status and its diagnostic report. Using `go run`
would lose the distinction between its nonzero exit statuses.
The command does not push or repair `main`, wait for CI, or publish binaries or The command does not push or repair `main`, wait for CI, or publish binaries or
a hosted release page. A successful intended-tag publication does not require a hosted release page. A successful intended-tag publication does not require
@@ -176,12 +203,17 @@ remote read-back or consumer retrieval; those checks remain optional.
## 5. Recover from a failed or uncertain tag push ## 5. Recover from a failed or uncertain tag push
The executable reports the publication state, phase, version, candidate commit, The executable reports the publication state, phase, version, candidate commit,
tag-object ID, effective endpoint, and error on standard error. It performs one tag-object ID, effective endpoint, and error on standard error. After a failed
bounded authoritative reconciliation after a failed tag push. Exit status 3 tag push, it makes one bounded check of the exact publication remote to find out
whether the tag arrived. Exit status 3
means the tag outcome remains unknown; do not blindly retry or move a tag. means the tag outcome remains unknown; do not blindly retry or move a tag.
For an uncertain result, use the exact endpoint, candidate commit, version, and An annotated tag is a Git object containing the release note and a pointer to
tag-object ID from the report. Copy the captured endpoint into the variable the source commit. Its tag-object ID identifies that complete object. Checking
only the commit ID would not establish that the intended tag object arrived.
For an uncertain result, use the exact endpoint (remote URL), candidate commit,
version, and tag-object ID from the report. Copy the captured endpoint into the variable
below using normal shell quoting. Check that Git still resolves it to itself, below using normal shell quoting. Check that Git still resolves it to itself,
then query the authoritative refs once: then query the authoritative refs once:
@@ -198,7 +230,8 @@ cannot establish absence at the captured endpoint.
Compare the `main` object ID with the reported candidate commit and the tag Compare the `main` object ID with the reported candidate commit and the tag
object ID with the reported tag-object ID. `--refs` returns the annotated tag object ID with the reported tag-object ID. `--refs` returns the annotated tag
object itself, not merely its peeled commit. If the tag object matches, the object itself. A peeled commit is the source commit reached by following that
tag; it is a different identity. If the tag object matches, the
release is published. If the query fails, the result is conflicting, or the release is published. If the query fails, the result is conflicting, or the
endpoint or candidate differs, stop and resolve the state before retrying. If endpoint or candidate differs, stop and resolve the state before retrying. If
the endpoint is reachable, the tag is confirmed absent, and `main` still equals the endpoint is reachable, the tag is confirmed absent, and `main` still equals

View File

@@ -2,10 +2,34 @@
Reviewed revision: `08bb3a20c6a565646d98f68688d268c7bae39573`. Reviewed revision: `08bb3a20c6a565646d98f68688d268c7bae39573`.
Status: proposed revisions only. This catalog identifies opportunities to make Status: complete. All D01–D18 revisions have been applied and reviewed together
existing documentation easier to understand. None of the proposals has been for consistency. The catalog below preserves the proposals that guided the
applied to its target page. The previous consistency audit remains in Git work; the completion record identifies their implemented locations. The previous
history; its two findings were corrected in `94d1e33`. consistency audit remains in Git history; its two findings were corrected in
`94d1e33`.
## Completion record
| Proposal | Implemented result |
| --- | --- |
| D01 | [README](../../README.md) leads with the loading outcome and minimal example, followed by optional tasks and platform/licensing links. |
| D02 | [Consumer guide](../consumers/README.md) starts with task routes, defaults, validation, and inspection; example background and the test inventory follow usage. |
| D03 | [Reference introduction](../config.md) supplies topic routes, YAML illustrations, path definitions, and common-versus-detailed subheadings. |
| D04 | [Sources](../config.md#sources), [results](../config.md#results-and-failures), and [resource lifetime](../config.md#completion-and-resource-lifetime) explain copying, reuse, saved inspection, and caller-owned resources. |
| D05 | [Priority](../config.md#priority-and-processing-order), [strict combination](../config.md#combining-mappings), and [defaults](../config.md#defaults-and-typed-decoding) have visible examples and retain collection exceptions. |
| D06 | [Discovery](../config.md#configuration-discovery) shows the four attempted paths and distinguishes optional absence, required absence, and selected-file failure. |
| D07 | [Imports](../config.md#explicit-imports) introduce the three selectors, option applicability, requiredness, ordering, and earlier syntax separately. |
| D08 | [Placement](../config.md#input-destinations) illustrates files and assembled YAML; [contexts](../config.md#source-resource-contexts) explain later resource access and ownership. |
| D09 | [Identified lists](../config.md#identified-lists-a-worked-example) show replace, append, and whole-record upsert before registration and policy-lifetime details. |
| D10 | [Inheritance](../config.md#sibling-inheritance) starts with parent/child settings, then cancellation and identified records before exact scheduling rules. |
| D11 | [Versions](../config.md#application-schema-versions) introduce application acceptance, a mode table, mounted headers, and the limits of observed evidence. |
| D12 | [Callbacks](../config.md#recorded-application-resolution) have task and helper tables, a derived-default example, and entry-state/lifetime guidance. |
| D13 | [Secrets](../config.md#typed-secret-references) separate lookup, hiding, and bootstrap; [bootstrap](../config.md#explicit-secrets-directory-bootstrap) explains empty existing variables and partial reports. |
| D14 | [Inspection](../config.md#layered-metadata) starts with reader needs and a default/main/overlay example; [diffs](../config.md#effective-configuration-diffs) explain snapshot flags and hidden changes. |
| D15 | [YAML](../config.md#yaml-documents), [defaults](../config.md#defaults-and-typed-decoding), [projection](../config.md#typed-projection-and-ownership), and [failures](../config.md#safe-failures) have practical examples; descriptor caching moved to the [internal overview](../internal/overview.md). |
| D16 | [Package documentation](../../doc.go) and the targeted public declarations explain purpose, paths, returned copies, and saved state while retaining precise contracts. |
| D17 | [Migration guidance](../consumers/README.md#intentional-migration-changes) leads with numbered consumer actions and explains package type identity alongside the retained old/new table. |
| D18 | [Release procedure](../release.md) defines operational terms and explains Go selection, publication statuses, and tag-object recovery without changing command blocks or gates. |
## Audience and Scope ## Audience and Scope
@@ -400,14 +424,34 @@ The highest-impact starting points are the README and consumer entry routes
(D13–D14). The other proposals develop a selected task or reference topic. These (D13–D14). The other proposals develop a selected task or reference topic. These
priorities are editorial recommendations, not additional release gates. priorities are editorial recommendations, not additional release gates.
Later rewrites should preserve existing contracts, diagnostic identifiers, These revisions preserve existing contracts, diagnostic identifiers,
defaults, limits, security boundaries, and exceptions. Keep precise material defaults, limits, security boundaries, and exceptions. Keep precise material
available after its plain-language introduction. A simpler explanation must not available after its plain-language introduction. A simpler explanation must not
imply universal last-value replacement, universal null support, transactional imply universal last-value replacement, universal null support, transactional
loading, refreshed inspection after caller mutation, or stronger filesystem loading, refreshed inspection after caller mutation, or stronger filesystem
isolation. isolation.
This pass changes only this catalog. No user-facing page, Go comment, example, Implementation changes consumer-facing prose and Go declaration comments, with
policy, command, test, or implementation has been revised. Later implementation the relocated descriptor-caching explanation in the existing internal overview.
can validate changed examples and commands at their existing owners and inspect Policies, accepted ADRs, release history, license notices, dependencies, and
rendered tables and navigation under the documentation policy. production behavior remain unchanged. Validation includes exercising the revised
illustrations against the library, checking local links and Markdown tables,
comparing Go tokens to confirm comment-only edits, and verifying that release
command blocks remain identical to the implementation baseline `e74c26a`.
Completed validation with Go 1.26.10 and dependency downloads disabled:
- `./scripts/check.sh` passed formatting, whitespace, all tests (including local
links and executable examples), vet, and build.
- Temporary programs outside the repository exercised the revised README,
callbacks, YAML, discovery, imports, collection operations, placement,
inheritance, version modes, inspection, secret bootstrap, defaults, and
projection illustrations against the current library; all passed.
- A Go scanner comparison confirmed identical non-comment tokens in all six
changed Go files. All 12 release command blocks are byte-for-byte unchanged.
- The six affected Markdown pages rendered successfully with the project's
Markdown parser; all 27 tables have consistent column counts. Existing
navigation anchors remain available.
The final consistency pass checked terminology, worked results, public comments,
cross-page links, documentation ownership, and the preserved release workflow.

View File

@@ -6,15 +6,18 @@ import (
"go.maximumdirect.net/configkit/internal/engine" "go.maximumdirect.net/configkit/internal/engine"
) )
// ValueSnapshot is a detached, deterministic YAML representation of one value. // ValueSnapshot describes a saved value as consistently formatted YAML text.
// Present distinguishes absence or deletion from an explicit null value. // It is a copy; changing it cannot affect later inspection. Present is false for
// absence or deletion, while explicit null is present. Redacted reports whether
// the value or a descendant was hidden.
type ValueSnapshot struct { type ValueSnapshot struct {
Present bool Present bool
Redacted bool Redacted bool
YAML string YAML string
} }
// ValueChange describes one explicit definition or audited domain operation. // ValueChange shows values before and after an input contribution or recorded
// application operation, together with its origin, reasons, and dependencies.
type ValueChange struct { type ValueChange struct {
Before ValueSnapshot Before ValueSnapshot
After ValueSnapshot After ValueSnapshot
@@ -29,7 +32,8 @@ type SecretReferenceInfo struct {
Name string Name string
} }
// Explanation is a detached safe account of one effective or historical path. // Explanation describes a path's final value and earlier contributions, with
// sensitive values hidden. It is a copy that cannot alter later inspection.
type Explanation struct { type Explanation struct {
Path Path Path Path
Effective ValueSnapshot Effective ValueSnapshot
@@ -38,9 +42,10 @@ type Explanation struct {
SecretReference *SecretReferenceInfo SecretReference *SecretReferenceInfo
} }
// Explain returns safe effective and chronological values for a known final or // Explain returns the saved final value and earlier values for a known current
// historical path. Structural inheritance appears between source applications // or historical path, hiding sensitive information. History follows processing
// and typed reconciliation. Unknown and malformed paths return false. // order: inheritance appears after source combination and before the transition
// to typed values. Unknown and malformed paths return false.
func (r *Result[T]) Explain(path Path) (Explanation, bool) { func (r *Result[T]) Explain(path Path) (Explanation, bool) {
if r == nil { if r == nil {
return Explanation{}, false return Explanation{}, false
@@ -49,8 +54,9 @@ func (r *Result[T]) Explain(path Path) (Explanation, bool) {
return fromEngineExplanation(value), ok return fromEngineExplanation(value), ok
} }
// WriteEffective writes deterministic redacted YAML from the frozen resolved // WriteEffective writes consistently formatted YAML for the saved final
// snapshot. The output is intended for inspection and is not promised reloadable. // configuration, with sensitive values hidden. Later changes to Result.Config
// do not change the output. It is for inspection and is not promised reloadable.
// Writer errors, including short writes, are returned and may leave partial output. // Writer errors, including short writes, are returned and may leave partial output.
func (r *Result[T]) WriteEffective(output io.Writer) error { func (r *Result[T]) WriteEffective(output io.Writer) error {
if r == nil { if r == nil {
@@ -61,7 +67,7 @@ func (r *Result[T]) WriteEffective(output io.Writer) error {
} }
// WriteExplanation writes a deterministic human-readable rendering of Explain. // WriteExplanation writes a deterministic human-readable rendering of Explain.
// Unknown paths produce no output. Writer failures may leave partial output. // Unknown paths return an error without output. Writer failures may leave partial output.
func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error { func (r *Result[T]) WriteExplanation(output io.Writer, path Path) error {
if r == nil { if r == nil {
var inspection *engine.Inspection var inspection *engine.Inspection

View File

@@ -6,14 +6,19 @@ import (
"go.maximumdirect.net/configkit/internal/engine" "go.maximumdirect.net/configkit/internal/engine"
) )
// Definition contains reusable application rules for a configuration type. // Definition supplies the defaults and application rules used when loading T.
// Sensitive is a pure predicate that can add sensitivity to semantic paths. // Treat a definition as read-only after sharing it. Sensitive is a pure predicate
// Definitions should be treated as read-only after being shared. // that can mark additional configuration paths for hiding in inspection output.
type Definition[T any] struct { type Definition[T any] struct {
Defaults func() T // Defaults returns starting values for settings omitted from input.
Resolve func(*T, *Resolution) error Defaults func() T
Validate func(*T, *Validation) // Resolve derives or normalizes values and records those changes.
Version *VersionRule Resolve func(*T, *Resolution) error
// Validate reports application errors and warnings without changing values.
Validate func(*T, *Validation)
// Version chooses a version path and the application's acceptance rule.
Version *VersionRule
// Sensitive adds paths to those already hidden by configkit:"secret" tags.
Sensitive func(Path) bool Sensitive func(Path) bool
// SequenceIdentities declares the scalar fields used to combine records and // SequenceIdentities declares the scalar fields used to combine records and
// resolve record inheritance at selected sequence destinations. Registration // resolve record inheritance at selected sequence destinations. Registration
@@ -132,9 +137,10 @@ type Override struct {
Flag string Flag string
} }
// Resolution exposes entry-state input provenance, surviving-input presence, and // Resolution lets a resolver inspect supplied values and record derived defaults
// audited application resolution changes. A resolver must not retain or // or normalization. Its input queries describe the state at callback entry,
// concurrently use the context. // even after the resolver changes the configuration. Use the context only during
// the callback; do not retain it or use it concurrently.
type Resolution struct{ value *engine.Resolution } type Resolution struct{ value *engine.Resolution }
// VersionRule declares the input path and application checker for a schema version. // VersionRule declares the input path and application checker for a schema version.
@@ -155,21 +161,27 @@ type Version struct {
Text string Text string
} }
// Validation collects safe application diagnostics during a supported callback. // Validation lets an application report value-free errors and warnings during
// A callback must not retain the context or use it concurrently. // a version or validation callback. Use it only during the callback; do not
// retain the context or use it concurrently.
type Validation struct{ value *engine.Validation } type Validation struct{ value *engine.Validation }
// Result is a successful independently owned configuration load. // Result contains a successful load's configuration and saved inspection state.
// The application owns Config and may change it. Inspection continues to describe
// the completed load; it does not follow later changes to Config. Separate loads
// return independently owned results.
type Result[T any] struct { type Result[T any] struct {
Config T Config T
inspection *engine.Inspection inspection *engine.Inspection
} }
// Load parses and strictly decodes zero or one selected source, explicitly enabled // Load reads the selected input and returns a configuration of type T with
// imports, and one selected overlay. After source precedence, built-in sibling // inspection metadata. NoSource deliberately omits the main document. Enabled
// `extends` relationships resolve before defaults and typed decoding. It accepts // imports and one selected overlay participate in loading. Sources combine first,
// at most one Options value and returns no result when any error is reported. // then sibling extends relationships resolve before defaults and strict typed
// decoding. Load accepts at most one Options value and returns a nil result when
// any error is reported. See docs/config.md for the complete processing order.
func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) { func Load[T any](source Source, supplied ...Options[T]) (*Result[T], error) {
options := make([]engine.Options[T], len(supplied)) options := make([]engine.Options[T], len(supplied))
for index := range supplied { for index := range supplied {
@@ -190,7 +202,8 @@ func fromEngineLoaded[T any](loaded *engine.Loaded[T], err error) (*Result[T], e
return &Result[T]{Config: loaded.Config, inspection: loaded.Inspection}, nil return &Result[T]{Config: loaded.Config, inspection: loaded.Inspection}, nil
} }
// Metadata returns detached effective presence, origin, and history records. // Metadata provides queries for final input presence, value origins, and history.
// Records returned by its queries are copies that cannot change the saved load.
func (r *Result[T]) Metadata() Metadata { func (r *Result[T]) Metadata() Metadata {
if r == nil || r.inspection == nil { if r == nil || r.inspection == nil {
return Metadata{} return Metadata{}
@@ -198,7 +211,8 @@ func (r *Result[T]) Metadata() Metadata {
return Metadata{value: r.inspection.Metadata()} return Metadata{value: r.inspection.Metadata()}
} }
// Sources returns a detached list of sources used by the load. // Sources returns a copied list of sources used by the load. Changing the list
// or its nested records cannot change later queries.
func (r *Result[T]) Sources() []SourceInfo { func (r *Result[T]) Sources() []SourceInfo {
if r == nil || r.inspection == nil { if r == nil || r.inspection == nil {
return nil return nil

View File

@@ -4,8 +4,12 @@ import (
"go.maximumdirect.net/configkit/internal/engine" "go.maximumdirect.net/configkit/internal/engine"
) )
// Path identifies a configuration value using YAML field names. Its textual form // Path names a value inside configuration. For example, Keys("servers").Index(0)
// is a JSON Pointer: the root is empty, ~ escapes as ~0, and / escapes as ~1. // names the first server, while Keys("servers", "0") names a literal mapping key.
// They have the same pointer text but distinct key/index intent.
//
// Its textual form is a JSON Pointer: the root is empty, ~ escapes as ~0, and /
// escapes as ~1.
// Paths are semantic rather than filesystem or URL paths: empty, dot, dot-dot, // Paths are semantic rather than filesystem or URL paths: empty, dot, dot-dot,
// percent-containing, and numeric keys retain their literal meaning. Parsed // percent-containing, and numeric keys retain their literal meaning. Parsed
// numeric tokens are resolved only when traversal knows their containing type. // numeric tokens are resolved only when traversal knows their containing type.

View File

@@ -180,10 +180,10 @@ type Location struct {
Column int Column int
} }
// SourceInfo describes a source discovered during a load. Kind identifies its // SourceInfo describes one input acquired during loading. Kind says how it was
// acquisition, independent of Layer. Path is canonical for a host file and // read; Layer says its priority role. Path is canonical for a host file and
// namespace-relative for a filesystem file; synthetic sources have no Path. // namespace-relative for a filesystem file; synthetic sources have no Path.
// Destination is the effective mapping path where this source contributes. // Destination says where the source contributes in the assembled configuration.
// UnderlayIndex is the one-based original underlay position, or zero for other // UnderlayIndex is the one-based original underlay position, or zero for other
// sources. ImportedFrom identifies the declaration that included this source. // sources. ImportedFrom identifies the declaration that included this source.
type SourceInfo struct { type SourceInfo struct {
@@ -240,12 +240,14 @@ type EmptyImportSelection struct {
Selector ImportSelector Selector ImportSelector
} }
// SourceVersionEvidence describes a source-local version declaration observed // SourceVersionEvidence records an observed version declaration in one input
// during an enabled load. A non-nil record with Present false records observed // document when source-version checks are enabled. A non-nil record with
// absence, but does not imply that a checker ran or accepted a value. Text is // Version.Present false records absence; it does not mean a checker ran or
// empty when sensitivity classification or conservative callback-time/failed- // accepted a value. Version.Text is empty when final sensitivity classification
// load redaction withholds it; Present and Null retain their meaning. Copies // or conservative masking during callbacks or failed loads hides it.
// returned by source queries are detached. See docs/config.md for disclosure // Version.Present and Version.Null retain their meaning.
// Source queries return copies you can change without changing stored evidence.
// See docs/config.md for disclosure
// and lifecycle details. // and lifecycle details.
type SourceVersionEvidence struct { type SourceVersionEvidence struct {
Path Path Path Path
@@ -265,9 +267,9 @@ type Diagnostic struct {
Related []Location Related []Location
} }
// InheritanceStep identifies a concrete payload transfer. From and To name // InheritanceStep identifies values copied from From to To by inheritance.
// semantic value paths; Reference names the controlling destination `extends` // These are configuration paths. Reference names the controlling destination
// path. Locations belong to that directive, not the transferred value. // extends path; Locations identify that directive rather than the copied value.
type InheritanceStep struct { type InheritanceStep struct {
From Path From Path
To Path To Path
@@ -275,10 +277,10 @@ type InheritanceStep struct {
Locations []Location Locations []Location
} }
// Origin describes where an effective definition came from. Multiple locations // Origin describes where a final value came from. Multiple locations identify
// represent a composite origin; resolution origins are synthetic and identify their // contributors to a combined value. Resolution identifies an application operation
// operation instead of a file location. Inheritance records the concrete // instead of a file location. Inheritance describes the copying steps, while
// derivation path while Layer and Locations continue to identify value sources. // Layer and Locations still identify the value's sources.
type Origin struct { type Origin struct {
Layer Layer Layer Layer
Locations []Location Locations []Location
@@ -286,7 +288,8 @@ type Origin struct {
Inheritance []InheritanceStep Inheritance []InheritanceStep
} }
// FieldInfo describes an effective configuration path. // FieldInfo describes a final configuration path: whether input supplied it,
// whether it is null, and where it came from.
type FieldInfo struct { type FieldInfo struct {
Explicit bool Explicit bool
Null bool Null bool