Decode typed configuration over defaults

This commit is contained in:
2026-09-06 19:52:45 +00:00
parent 15172294b5
commit 21e3f633bc
8 changed files with 884 additions and 74 deletions

View File

@@ -1,8 +1,9 @@
# Configuration Input Contract
This document owns ConfigKit's currently implemented source-input and YAML syntax
contracts. The parser and owned document representation are private; a public typed
loading lifecycle has not been implemented yet.
This document owns ConfigKit's currently implemented source-input, YAML syntax,
schema, typed-decoding, and ownership contracts. The parser, owned document
representation, and decoder are private; a public typed loading lifecycle has not
been implemented yet.
## Sources
@@ -101,8 +102,50 @@ tokens become indices only in a sequence. Sequence indices cannot have leading
zeroes; fixed-array indices are bounds checked. Dynamic `any` and inline-map entries
remain schema-valid for application-defined descendants.
Descriptors do not yet decode documents. Document decoding will reuse this graph as
its implementation lands.
Descriptors are reused by typed decoding, projection, and cloning so those operations
cannot acquire different field bindings.
## Defaults and typed decoding
The private decoder starts from an independently owned clone of the application's
typed defaults and applies only explicitly present input nodes. Defaults therefore
never become explicit, while equal-valued input remains explicit. Successful decode
produces detached typed state, baseline and final canonical trees, an input-presence
index, and default/input origins for the later public loading lifecycle.
| Configured input | Effect on the typed baseline |
| --- | --- |
| Absent field | Retain the default |
| Explicit false, zero, or empty string | Replace the default |
| Partial mapping or string-keyed map | Recurse and retain omitted default children or entries |
| Empty mapping | Retain existing mapping children |
| Sequence, including an empty sequence | Replace the entire sequence |
| Pointer object | Recurse into a non-nil default; otherwise allocate an object whose omitted children have ordinary Go zero values |
| Allowed null | Replace the value with nil and suppress its default |
Null is allowed only for pointers, maps, slices, and dynamic `any` values. It is an
error for scalars, structs, and arrays. New map entries and sequence elements begin
from their Go zero values; ConfigKit does not invent entry-level defaults.
Decoding is strict at every typed depth. Unknown struct fields, type mismatches,
invalid nulls, numeric overflow, and arrays with the wrong length are errors at the
original semantic path and source coordinates. String-keyed maps accept arbitrary
keys but preserve the strict schema of their values; inline maps accept unmatched
keys according to their element descriptor. Multiple independent field failures are
collected without publishing the partially decoded object.
Booleans and strings are not coerced. Integer destinations require integer syntax
and a value in the destination type's range. Float destinations accept finite JSON
numbers representable by their destination precision. Dynamic numbers become
`int64` for integer syntax and `float64` for fractional or exponent syntax, with
range and finiteness checks. Named scalar types retain their identity. Durations
decode only from strings accepted by `time.ParseDuration`; integers are rejected.
Paired text scalars decode only from strings through `encoding.TextUnmarshaler`, and
codec failures are classified without exposing application error text or input
values.
This decoder is currently an internal lifecycle foundation, not a public conversion
API. Applications cannot call typed loading until `Load` is introduced.
## Typed projection and ownership

View File

@@ -170,6 +170,19 @@ Assigned across schema, decoding, resolution, validation, and consumer examples:
application-owned; every domain mutation is recorded. Campaign/session fallback,
search paths, fetching, storage, and concrete default values remain in Narratio.
Implementation record (2026-09-06): ConfigKit generalized Narratio's fresh-default,
strict nested-type, and explicit-presence cases into `decode.go` and
`decode_test.go`. The decoder applies the owned document tree over a detached typed
baseline, treats mappings recursively and sequences atomically, preserves explicit
false/zero/empty/null, rejects unknown nested fields, and returns private presence
and default/input origin artifacts. Unlike Narratio's custom YAML hooks and
pointer/zero inference, presence comes directly from the parsed tree and equal-valued
input remains explicit. ConfigKit also adds destination-width numeric checks,
dynamic `int64`/`float64`, exact arrays, duration strings, paired text codecs, safe
multi-error collection, and repeated-result isolation. Resolution, validation,
consumer defaults, and all campaign/session algorithms remain assigned to later
work.
### Secrets bootstrap
Assigned to the explicit bootstrap operation:

View File

@@ -7,18 +7,21 @@ module and package documentation boundary. Its implemented public primitives are
- detached provenance and diagnostic record types with empty `Metadata` queries;
- canonical, control-safe diagnostic rendering and structured `LoadError` access;
- opaque file, byte, reader, and no-input source construction and bounded reads;
- a parser-independent ordered tree in `internal/document`; and
- a parser-independent ordered tree in `internal/document`;
- the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary; and
input into that owned tree and exposes no parser types outside its boundary;
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths;
and
- descriptor-driven canonical projection, detached typed cloning, cycle/limit
enforcement, and structural comparison in `projection.go`.
enforcement, and structural comparison in `projection.go`; and
- strict schema-driven decoding over detached defaults in `decode.go`, including
mapping/sequence semantics, null and numeric policy, safe codec errors, presence,
baseline/final trees, and initial default/input origins.
The [configuration contract](../config.md) owns implemented input, schema, and
ownership behavior. ConfigKit intentionally exposes no typed loading or decoding API yet.
Private metadata population will be integrated when that lifecycle exists.
The [configuration contract](../config.md) owns implemented input, schema, decoding,
and ownership behavior. ConfigKit intentionally exposes no typed loading API yet;
the decoder and its initial metadata artifacts remain private until that lifecycle
exists.
The source-extraction inventory records the application foundations assigned to
future implementation work:

View File

@@ -2,7 +2,7 @@
## Status and ownership
Status: implementation in progress; Stages 1–5 are complete and later stages are
Status: implementation in progress; Stages 1–6 are complete and later stages are
pending.
Audience: a `gpt-5.6-terra` coding agent implementing one stage per prompt, in
@@ -564,7 +564,31 @@ Acceptance:
## Stage 6 — Decode scalar and container values over defaults
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Added schema-driven typed decoding in [`decode.go`](../../decode.go), starting
from the Stage 5 detached clone and returning only private typed state, input
presence, projected baseline/final trees, and per-path default/base origins.
- Implemented recursive mapping retention, atomic sequence replacement, exact array
lengths, pointer-object zero allocation versus non-nil retention, nullable-shape
clearing, strict nested map values, inline-map decoding, and unknown-field errors.
- Added checked destination-width integers and floats, dynamic `int64`/`float64`,
named scalars, duration strings, paired text codecs, safe value-free failures, and
collection of independent decode errors at original semantic paths/coordinates.
The [current configuration contract](../config.md#defaults-and-typed-decoding) and
component inventory now own this behavior.
- Adapted Narratio's fresh-default, presence, strict nested-type, and repeated-load
cases, replacing custom YAML hooks and pointer/zero guesses with tree-derived
presence; explicit empty and null are retained and equal-valued input is explicit.
Focused coverage includes the defaults operation table, new/existing map entries,
empty mappings/sequences, nil/non-nil pointer objects, default isolation, numeric
range and coercion failures, integer durations, inline collisions, nested unknown
fields, coordinates, safe codec failures, and independent repeated results.
Verified `go test ./...`, `go vet ./...`, `go build ./...`, `go mod tidy -diff`,
and `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required;
public loading and schema-version checks remain later work.
Read: feature [defaults/null](initial.md#static-defaults-and-null) and typed decoding.

View File

@@ -552,69 +552,21 @@ Applications own flags and commands that select this behavior.
## Static defaults and null
Applications supply a defaults factory that creates a fresh configuration per
load. Mutable maps, slices, and pointers must not be shared between factory
results. ConfigKit must not retain mutable aliases between returned loads or
between the application result and its internal metadata snapshots.
Defaults obey the same recursive mapping/atomic replacement rules as a lower
layer, rather than incidental decoder behavior:
| Configured input | Effect on defaults |
| --- | --- |
| Absent field | Keep the default |
| Explicit false, zero, or empty string | Replace the default |
| Partial mapping, including a string-keyed Go map | Retain omitted default children/entries |
| `{}` over an existing mapping | Retain its default children |
| Sequence, including `[]` | Replace the entire default sequence |
| Partial object represented by a pointer | Retain children of a non-nil default object; otherwise allocate and use ordinary zero values for omitted children |
| Allowed null | Replace with nil; suppress lower defaults |
The factory supplies actual defaults; ConfigKit does not invent per-entry
defaults for newly introduced map entries or sequence elements. Application
resolution may supply those. Go zero values are part of the initial typed
baseline; do not imply that the application explicitly declared every zero as
a domain default. Avoid serialization through `omitempty` that would erase
baseline values or presence information.
Null is valid for pointers, maps, slices, and dynamic `any` values. It is invalid
for ordinary scalar fields, structs, and arrays. Thus `workers: null` cannot
silently leave an integer default intact. Applications can further reject null
through validation. Null never means reset to the application default.
The defaults merge and nullability model is implemented privately. The
[current defaults and typed-decoding contract](../config.md#defaults-and-typed-decoding)
owns recursive mapping, atomic sequence, pointer allocation, explicit-presence,
null, and detached-baseline behavior. The public defaults factory and result
lifecycle remain target work in the sections below.
## Typed decoding
The schema-binding portion of this target is implemented. The [current application
schema contract](../config.md#application-schema-binding) owns root/type support,
field and tag binding, inlining, text-codec pairing, and schema-aware path rules.
The remaining document-to-value behavior below is target decoding work.
Strictness must be enforced by ConfigKit across the effective tree:
- Unknown struct fields, type mismatches, overflow, and invalid null are errors.
- Without an inline map, unmatched keys remain errors. Default inline-map entries
colliding with declared fields are checked when runtime defaults are introduced.
- String-keyed maps accept application-defined keys. Struct values inside them
remain strict. `map[string]any` permits arbitrary keys and recursively accepts
the supported scalar/container model; application validation owns its schema.
- Do not coerce numbers or booleans to strings, or quoted numbers to numeric
fields. Integer targets require integer syntax and range checks. Float targets
accept finite decimal numbers subject to representable range checks.
- Dynamic integer values use checked `int64`, and fractional/exponent values use
checked finite `float64`; values outside those ranges require a typed schema
or a quoted string rather than silent rounding of integer nodes.
- Support `time.Duration` from a duration string. Custom scalar types must
implement both `encoding.TextUnmarshaler` and `encoding.TextMarshaler` so
decoding and inspection have an agreed representation. Duration integers are
rejected. See the [type and projection contract](#supported-types-and-projection).
- Parser-specific unmarshal hooks are not a public extension mechanism. Do not
let them bypass key, null, or type policy. Custom structural decoders are
deferred; unsupported type shapes fail with a clear schema diagnostic.
Keep parser nodes and errors internal under the
[upstream isolation contract](#yaml-dependency-and-public-api-isolation).
The field-binding model must also support defaults, provenance, secret
classification, and rendering consistently.
Schema binding and strict document-to-value decoding are implemented privately.
The [current application schema contract](../config.md#application-schema-binding)
owns root/type support, field and tag binding, inlining, codec pairing, and
schema-aware path rules. The [current defaults and typed-decoding contract](../config.md#defaults-and-typed-decoding)
owns strict conversions, container application, presence, safe failures, and initial
origins. Public lifecycle integration, layered provenance, secret handling, and
rendering remain target work below.
## Recorded resolution and validation