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

@@ -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