Build reusable schema descriptors

This commit is contained in:
2026-09-06 19:36:14 +00:00
parent 7c78da9cd2
commit 2b65876a7d
7 changed files with 789 additions and 17 deletions

View File

@@ -69,6 +69,41 @@ boolean/null words are rejected unless quoted.
Mapping keys use the same scalar classification and must classify as strings. Quote
keys that otherwise classify as a boolean, number, or null.
## Application schema binding
ConfigKit builds a private reflection descriptor graph for each schema operation;
it keeps no global mutable descriptor cache. A configuration root must be a struct
or string-keyed map. Nested supported shapes are:
- booleans, strings, signed and unsigned integers, and finite floating-point types;
- `time.Duration` and paired `encoding.TextMarshaler`/`TextUnmarshaler` scalars;
- pointers, structs, string-keyed maps, slices, fixed arrays, and `any`; and
- recursive type definitions whose eventual application values are acyclic.
Named scalar types retain their Go identity. `uintptr`, complex numbers, functions,
channels, unsafe pointers, non-string map keys, and nonempty interfaces are not
configuration types. A custom text scalar must provide both directions on its value
or pointer method set. Building a descriptor only inspects method sets; it never
executes application methods or parser-specific hooks.
Exported struct fields use an explicit `yaml` name or the lowercase Go field name.
Unexported fields and `yaml:"-"` are ignored. `omitempty` is accepted but does not
change binding or future projection. An explicit `yaml:",inline"` flattens a struct
or pointer-to-struct, or designates at most one string-keyed inline map for otherwise
unmatched names. Ambiguous names, named inline fields, multiple inline maps, and
unsupported or malformed tag options are schema errors. Embedded fields are ordinary
named fields unless explicitly inlined. `configkit:"secret"` is the only current
ConfigKit field tag and records sensitivity for later lifecycle work.
Schema path traversal uses these same bindings. Explicit key segments address
mappings, explicit index segments address sequences, and parsed numeric pointer
tokens become indices only in a sequence. Sequence indices 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, clone defaults, or project application
values. Those operations will reuse this graph as their implementation lands.
## Safe failures
Source failures use stable `source.*` codes and YAML failures use stable `yaml.*`

View File

@@ -9,7 +9,9 @@ module and package documentation boundary. Its implemented public primitives are
- opaque file, byte, reader, and no-input source construction and bounded reads;
- a parser-independent ordered tree in `internal/document`; and
- the sole upstream YAML integration in `internal/yaml`, which translates validated
input into that owned tree and exposes no parser types outside its boundary.
input into that owned tree and exposes no parser types outside its boundary; and
- a per-operation private reflection descriptor graph in `schema.go` for supported
types, YAML field binding, inlining, secret tags, recursion, and semantic paths.
The [configuration input contract](../config.md) owns implemented source and YAML
behavior. ConfigKit intentionally exposes no typed loading or decoding API yet.

View File

@@ -2,7 +2,7 @@
## Status and ownership
Status: implementation in progress; Stages 1–3 are complete and later stages are
Status: implementation in progress; Stages 1–4 are complete and later stages are
pending.
Audience: a `gpt-5.6-terra` coding agent implementing one stage per prompt, in
@@ -463,7 +463,27 @@ Acceptance:
## Stage 4 — Build the reusable schema binding graph
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Implemented a per-operation private descriptor graph in [`schema.go`](../../schema.go)
for structural roots, ordinary/named scalars, duration and paired text scalars,
pointers, structs, string-keyed maps, slices, arrays, dynamic values, and recursive
definitions. No global cache or application method execution is involved.
- Implemented declared-field YAML binding, ignored/private fields, explicit struct/
pointer/map inlining, ambiguity and tag validation, one inline map, secret-tag
retention, and schema-aware key/index path traversal. Runtime default collisions,
cyclic values, cloning, projection, and decoding remain assigned to later stages.
- Added the [current schema contract](../config.md#application-schema-binding) and
updated the component inventory and target-roadmap ownership links.
- Focused tests cover nested and inline conflicts, ignored/private fields, maps of
structs, named scalars, pointer text method sets, missing codec pairs, root and
unsupported-kind rejection, recursive descriptors, side-effect freedom, dynamic
and inline-map paths, parsed numeric indices, and array bounds. 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;
typed value operations remain later work.
Read: feature [typed decoding](initial.md#typed-decoding) and
[type/projection contract](initial.md#supported-types-and-projection).

View File

@@ -584,17 +584,16 @@ through validation. Null never means reset to the application default.
## 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.
- Support ordinary exported fields and `yaml` names/ignore tags. Untagged field
names use the lowercase Go field name. Embedded structs follow the same rule
unless explicitly tagged `yaml:",inline"`; inline struct fields contribute to
the containing mapping. Permit at most one inline string-keyed map per object,
which receives otherwise unmatched keys. Duplicate declared field names and
default inline-map entries colliding with declared fields are schema errors.
Without an inline map, unmatched keys remain errors. Ignore `omitempty` for
decoding, defaults, and effective inspection.
- 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.
@@ -1298,12 +1297,10 @@ model, not the application's separately constructed runtime module objects.
### Supported types and projection
The supported root `T` is a struct or string-keyed map. Within it, support ordinary
exported scalar fields, pointers, structs, string-keyed maps, slices, fixed-length
arrays, dynamic `any`, and the explicitly supported scalar conversions. Reject
unsupported kinds, non-string map keys, cyclic object graphs, and ambiguous field
bindings as schema/projection errors. Arrays require matching sequence length.
`yaml:"-"` fields and unexported fields are outside the configuration projection;
Supported schema types and field bindings are implemented and owned by the
[current application schema contract](../config.md#application-schema-binding).
Projection must additionally reject cyclic object graphs. Arrays require matching
sequence length. Ignored and unexported fields stay outside configuration projection;
hooks must not use them to carry runtime resources or configuration needing
inspection. Non-configuration runtime state belongs in separate application types.