Decode typed configuration over defaults
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user