Add maintained consumer migration examples

This commit is contained in:
2026-09-06 22:26:14 +00:00
parent 6ef240b1a0
commit a8bfb8848d
23 changed files with 855 additions and 95 deletions

View File

@@ -1437,7 +1437,56 @@ Acceptance:
## Stage 20 — Prove consumer ergonomics and migration contracts
Status: pending.
Status: complete.
Completion evidence (2026-09-06):
- Added the maintained [`examples/consumers`](../../examples/consumers) package and
sanitized local fixtures for four application shapes. The Narratio-like example
uses fresh defaults, partial imports, artifacts, recorded inherited defaults, and
an application model profile distinct from an optional ConfigKit profile. The
Notarius-like example uses canonical object bindings, a string version rule,
dynamic options, and range-checked `int64` conversion at the runtime boundary.
- Added WeatherReporter-like defaults-only and file loads, report maps, canonical
module objects, typed option unions, and shared loaded/constructed validation.
Application validation—not the ConfigKit schema—rejects explicit empty module and
path-template lists and null required module options. Recorded normalization acts
on detached values without mutating caller-owned constructed options. Added
independent ordered Seriatim-like speaker and autocorrect rule loaders while
retaining matching/replacement algorithms in the example package.
- Added executable examples for all four shapes and a WeatherReporter-shaped startup
flow. The startup explicitly renders `SecretsReport`, demonstrates an existing
empty variable blocking its file and a separate successful disposable lookup,
translates only an explicitly set `--units` flag into an `Override`, and renders
its explanation. It verifies ordinary `Load` preserves the process environment
and never uses a real credential.
- Generalized the assigned Narratio inspection/default/artifact outcomes and
WeatherReporter defaults, explicit-empty, loaded/constructed consistency, and
option-normalization cases. [`docs/internal/extraction.md`](../internal/extraction.md)
records the rechecked clean revisions, adapted cases, intentional differences,
and no-copy boundaries for Narratio `b68435128be1e24b94fc7ec259d6e9093669fd99`,
WeatherReporter `5d306a202471e10fe98923a1df27d0b0fce5c666`, Notarius
`77ae132e5609916da40dcd4fcc749c000a63cbac`, and Seriatim
`0fc92f364316c874d366b00cdc884a33e1900b25`. No sibling checkout is a build or test
dependency.
- Added negative migration fixtures for integer versions, string module shorthand,
explicit empty lists, explicit null required options, and strict unknown fields.
They fail with stable `version.type`, `decode.type`, `validate.error`, and
`decode.unknown_field` classifications at semantic paths without parser text.
Dynamic options bind directly without YAML reserialization or a generic subtree
conversion API.
- Added the [consumer adoption guide](../consumers/README.md), current canonical
module-options contract, README navigation, and component inventory. The guide
maps every feature-roadmap API sketch to an executable example or focused test,
describes removed plumbing versus retained domain code, and lists explicit
empty/null, object, version, integer, strict-YAML, environment-overlay, CLI, and
secret-reference migrations without claiming downstream repositories are migrated.
- Confirmed the example package directly imports only ConfigKit and the standard
library, then reviewed the exported surface and existing AST import-boundary test
for aliases, generic constraints, callbacks, record fields, and safe error chains.
Verified `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`,
`go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. No
deviations were required; broader hardening remains later work.
Read: feature consumer-fit table, module options, external values, and both criteria.

View File

@@ -989,95 +989,10 @@ public records, replay ordering, redaction, rendering, and allocation bounds.
### Module-specific options
Use a canonical object entry with an explicit module identifier and options
mapping. Shorthand strings and custom structural decoders are not required to
express the domain information. For a known application module catalog, model
the available option fields with ordinary typed structs; application validation
selects the meaningful fields and rejects incompatible combinations.
Illustrative YAML for an application that supports two modules:
```yaml
modules:
- module: chunker
options:
window_units: 12
- module: discussion
options:
sections: [short_term]
```
The corresponding typed configuration and validator need no second parser:
```go
type ModuleOptions struct {
WindowUnits *int `yaml:"window_units"`
Sections []string `yaml:"sections"`
}
type ModuleSpec struct {
Module string `yaml:"module"`
Options ModuleOptions `yaml:"options"`
}
type ModuleConfig struct {
Modules []ModuleSpec `yaml:"modules"`
}
func validateModules(cfg *ModuleConfig, v *configkit.Validation) {
for index, spec := range cfg.Modules {
path := configkit.Keys("modules").Index(index)
switch spec.Module {
case "chunker":
if spec.Options.WindowUnits == nil || *spec.Options.WindowUnits < 1 {
v.Error(path.Key("options").Key("window_units"), "must be a positive integer")
}
if spec.Options.Sections != nil {
v.Error(path.Key("options").Key("sections"), "not supported by this module")
}
case "discussion":
if spec.Options.WindowUnits != nil {
v.Error(path.Key("options").Key("window_units"), "not supported by this module")
}
if len(spec.Options.Sections) == 0 {
v.Error(path.Key("options").Key("sections"), "must contain at least one section")
}
default:
v.Error(path.Key("module"), "unsupported module")
}
}
}
func ReadModuleConfig(path string) (*configkit.Result[ModuleConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[ModuleConfig]{
Definition: configkit.Definition[ModuleConfig]{Validate: validateModules},
})
}
```
The pointer and nil-slice states above express optional domain values; this
application treats explicit null as unselected for those optional alternatives.
ConfigKit rejects unknown fields and invalid scalar types. The application owns
which supported options apply to a module and their domain constraints. Larger
catalogs may use nested typed option objects rather than one flat options struct.
Neither shape requires a library registry, parser hook, or configurable merge rule.
For an application whose option schema is genuinely selected at runtime,
`map[string]any` remains available. ConfigKit owns parsing, paths, and the single
dynamic scalar representation; application validation owns the selected schema,
including allowed keys and value constraints. Conversion into runtime module
arguments is application code. Dynamic integers remain checked `int64`; existing
`int` assertions must be adapted with range checks when converting to `int`.
The initial API does not add a generic subtree-conversion helper. Prefer typed
configuration where option shapes are known, and direct domain validation where
they are dynamic. Do not introduce a second YAML marshal/decode round trip as the
blessed pattern. Reconsider a narrow conversion helper only if a concrete dynamic
catalog demonstrates substantial duplicated type-binding infrastructure that
ordinary models cannot remove. Such a helper would have to retain strictness and
source attribution; that is a separate justified extension, not an escape hatch
for legacy shorthand. Configuration inspection describes the validated input
model, not the application's separately constructed runtime module objects.
Canonical typed and dynamic module options are implemented through the ordinary
schema. The [current module-options contract](../config.md#canonical-module-options)
and [consumer adoption guide](../consumers/README.md) own the maintained object
forms, validation boundary, and executable examples.
### Supported types and projection