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

@@ -585,6 +585,24 @@ introduce typed secret references.
Descriptors are reused by typed decoding, projection, and cloning so those operations
cannot acquire different field bindings.
### Canonical module options
Applications with a known module catalog represent each configured item as an object
with an explicit module identifier and an options mapping. Ordinary typed structs,
including a small union of optional option fields, use the same strict binding above;
application validation decides which fields and combinations apply to each module.
String shorthand, a library module registry, and custom structural decoder hooks are
not supported alternatives.
When a runtime-selected catalog genuinely requires dynamic options,
`map[string]any` retains ConfigKit's canonical dynamic scalars. In particular,
integers are `int64`; application validation owns allowed keys, value constraints,
range checks, and conversion to runtime `int` or catalog objects. ConfigKit does not
provide generic subtree conversion, and consumers must not add a YAML marshal/decode
round trip to exercise supported behavior. The
[consumer adoption guide](consumers/README.md) contains compiled typed and dynamic
examples.
## Defaults and typed decoding
The private decoder starts from an independently owned clone of the application's

98
docs/consumers/README.md Normal file
View File

@@ -0,0 +1,98 @@
# Consumer Adoption Guide
ConfigKit removes parser setup, strict decoding, presence bookkeeping, import and
profile traversal, precedence, provenance, secret lookup, and safe inspection from
applications. Consumers still own their configuration structs, defaults, semantic
validation, schema-version policy, CLI parsing, runtime matching and conversion,
resource checks, remote acquisition, and service initialization.
The maintained [`examples/consumers`](../../examples/consumers) package demonstrates
four sanitized shapes using only ConfigKit and the Go standard library:
| Shape | Demonstrated fit | Application code deliberately retained |
| --- | --- | --- |
| Narratio-like | Fresh defaults, explicit partial imports, artifact maps, recorded inherited defaults, normalization, and source ownership | Artifact meaning and the separate `model_profile` domain field |
| Notarius-like | Canonical object bindings, a string schema version, strict dynamic options, and `int64` dynamic integers | Catalog selection, allowed option keys, range-checked conversion to runtime `int` |
| WeatherReporter-like | Defaults-only loading, report maps, canonical module objects, typed option unions, explicit CLI records, and typed secret startup | Report/module compatibility, rejection of explicitly empty domain lists, CLI parsing, and startup ordering |
| Seriatim-like | Independent strict ordered speaker and autocorrect rule files | First-match and ordered replacement algorithms; no central application configuration file |
The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the
executable examples and ordinary tests. It contains no private application data,
credentials, network calls, or dependency on a sibling checkout.
## Happy path
An application defines ordinary types and passes one explicit source. Optional
features appear only when the application needs them:
```go
result, err := configkit.Load[Config](configkit.File(path), configkit.Options[Config]{
Definition: rules,
Imports: useDeclaredImports,
Profile: selectedConfigProfile,
Overrides: explicitlySetFlags,
})
if err != nil {
return err
}
return start(result.Config)
```
Simple consumers use neither imports nor profiles. A complex consumer can opt into
both without redefining their syntax. ConfigKit profiles are configuration overlays;
they do not select or replace an application's LLM, model, pipeline, report, or
execution profile concepts.
Known module catalogs should use canonical object entries with a module identifier
and typed options. A small union struct is sufficient when the available option
fields are known; validation selects which fields apply to each module. Runtime-
selected catalogs may retain `map[string]any`, whose integers are `int64`; applications
validate keys and ranges and convert only at their runtime boundary. Neither shape
needs YAML reserialization, a parser hook, a ConfigKit registry, or a generic subtree
conversion API.
## Intentional migration changes
The examples preserve domain meaning while adopting one configuration convention:
- Explicit empty strings, lists, zeroes, false values, and nulls remain intentional.
ConfigKit accepts an empty list structurally; WeatherReporter-shaped validation
rejects an explicitly or programmatically supplied empty module/template list as a
domain rule. Loaded and constructed values use the same validator.
- Canonical module and object bindings replace string shorthand. Unknown fields and
incompatible option shapes fail at their semantic paths.
- Schema versions are strings. Consumers with integer versions migrate their files
instead of enabling another scalar convention.
- Integers inside dynamic options are portable `int64` values. Runtime `int`
conversion is explicit and range checked.
- Strict YAML intentionally rejects duplicate keys, aliases, merge keys, unsupported
tags/scalars, trailing documents, and unknown typed fields. Diagnostics expose
stable classifications and safe paths, not copied parser text.
- General environment-variable configuration overlays retire. Application CLI
parsers translate only explicitly set configuration flags into `Override` records.
Separate invocation state remains outside configuration.
- Credential fields migrate to environment-name `Secret` references. Applications
explicitly call `LoadSecretsDir` before `Load` when they choose file bootstrap;
blocked empty variables remain blocked, and ordinary `Load` never changes the
process environment.
These examples are migration evidence, not completed changes to Narratio, Notarius,
WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, CLI
wiring, runtime catalogs, and operational documentation remains separate downstream
work.
## Maintained roadmap examples
Every nontrivial API sketch from the feature roadmap has a compiled example or a
focused behavioral owner:
| Roadmap surface | Maintained owner |
| --- | --- |
| `Load`, `NoSource`, validation, versions, resolution, `SecretEnv`, overrides, and explanation | Executable examples in [`example_test.go`](../../example_test.go) |
| Source/import/profile pipeline rules and artifact defaults | Narratio-shaped executable example and fixture |
| Canonical module objects and dynamic option handling | WeatherReporter- and Notarius-shaped examples and migration tests |
| `LoadSecretsDir`, bootstrap reporting, typed lookup, CLI explanation | `Example_weatherStartup` |
| Results, metadata, diagnostics, effective output, and writer failures | Focused root-package load, provenance, rendering, and explanation tests |
The package-level import-boundary test covers the consumer examples too: supported
usage names no YAML dependency, parser type, parser option, or parser error chain.

View File

@@ -25,7 +25,7 @@ redacted effective rendering and historical value explanation. The
| Documentation changes | [Documentation policy](policy/documentation.md) and the owning document |
| Architecture decisions or future work | [Documentation policy](policy/documentation.md), [initial target design](roadmap/initial.md), and relevant ADRs listed below |
| Code and test changes | [Architecture policy](policy/architecture.md), [testing policy](policy/testing.md), and owning contracts/tests as they are introduced |
| Examples or copyable assets | The owning implemented contract and related maintained examples, once available |
| Examples or copyable assets | The owning implemented contract and [consumer adoption guide](consumers/README.md) |
| Release preparation | [Release policy](policy/release.md) and [release-readiness gaps](roadmap/release-readiness.md); the operational runbook will be added before release |
Accepted decisions:

View File

@@ -11,6 +11,8 @@ govern every adaptation.
| --- | --- | --- | --- |
| Narratio | `b68435128be1e24b94fc7ec259d6e9093669fd99` | Clean `main` tracking `origin/main` | Primary parsing, composition, import, profile, presence, defaults, resolution, and inspection source |
| WeatherReporter | `5d306a202471e10fe98923a1df27d0b0fce5c666` | Clean `main` tracking `origin/main` | Secrets bootstrap and simple-consumer/defaults/options evidence |
| Notarius | `77ae132e5609916da40dcd4fcc749c000a63cbac` | Clean `main` tracking `origin/main` | Canonical binding, string-version, and dynamic-option migration evidence |
| Seriatim | `0fc92f364316c874d366b00cdc884a33e1900b25` | Clean `main` tracking `origin/main` | Independent ordered speaker/autocorrect rule evidence |
These revisions were inspected on 2026-09-06. Paths below are relative to each
application repository. Record later source revisions and adaptations here when
@@ -369,10 +371,42 @@ Assigned to consumer-shaped examples and migration checks:
semantics, registries, rendering, and module option validation remain application
responsibilities.
Consumer-example implementation record (2026-09-06): at Narratio revision
`b68435128be1e24b94fc7ec259d6e9093669fd99`, the maintained consumer cases adapt
`TestMaintainedExtractionExamplesPreservePublishedContracts`,
`TestConfigValidateAndShowPipelineOnlyAreSideEffectFree`, and the stable source-
ownership tests into a sanitized partial-import/artifact fixture. ConfigKit replaces
application composition envelopes and command output with `_configkit.imports`,
typed defaults, recorded resolution, metadata, and explanation. Artifact meaning,
model/pipeline profiles, commands, and generated-domain attribution remain in the
example application layer; no production function was copied.
At WeatherReporter revision `5d306a202471e10fe98923a1df27d0b0fce5c666`,
`TestDefaults`, explicit-empty-list cases, loaded/constructed consistency cases, and
nonmutating module-option normalization were adapted into the WeatherReporter-shaped
fixtures and tests in [`examples/consumers`](../../examples/consumers). ConfigKit's
detached defaults replace source mutation, canonical module objects replace string
shorthand, ordinary validation retains empty-list and option compatibility rules,
and explicit `Override` records replace external configuration-field overlays. The
startup example composes the separately implemented bootstrap report, typed secret
lookup, and explanation APIs without copying WeatherReporter's registries or
rendering/runtime modules.
Notarius revision `77ae132e5609916da40dcd4fcc749c000a63cbac` supplied
binding/version/dynamic-option migration evidence only. The local example uses an
ordinary canonical object, a string `VersionRule`, ConfigKit's `int64` dynamic
integer convention, and application range checking; it does not copy a catalog or
environment overlay. Seriatim revision
`0fc92f364316c874d366b00cdc884a33e1900b25` supplied ordered rule-file evidence.
The local examples load speaker and autocorrect files independently and keep their
matching/replacement algorithms outside ConfigKit. Neither source is a build or test
dependency.
## Application-only material
Do not extract application commands, filesystem discovery, logging, runtime
initialization, remote clients, LLM/provider setup, audio/transcript fixtures,
configuration digests, report rendering, campaign/session algorithms, or consumer
resource validation into ConfigKit. Later consumer examples may use small synthetic
types and fixtures to prove that these responsibilities remain straightforward.
resource validation into ConfigKit. Maintained consumer examples use only small
synthetic types and fixtures to prove that these responsibilities remain
straightforward.

View File

@@ -42,13 +42,17 @@ module and package documentation boundary. Its implemented public primitives are
- frozen-snapshot redaction and canonical effective output in `render.go`;
- bounded immutable input applications, safe historical replay, detached explanation
records, and deterministic human rendering in `explain.go`, with the
only upstream dumper integration isolated in `internal/yaml/write.go`.
only upstream dumper integration isolated in `internal/yaml/write.go`; and
- a separate maintained `examples/consumers` package with four sanitized consumer
shapes, fixtures, domain validators, and runtime-boundary functions.
The [configuration contract](../config.md) owns implemented input, schema, decoding,
loading lifecycle, path imports, composition, profile/CLI overlay, version checking,
recorded resolution, typed secret references/bootstrap, semantic validation,
metadata, secret classification, redacted effective rendering, historical
explanation, and ownership behavior.
The [consumer adoption guide](../consumers/README.md) owns current application-fit
and migration guidance.
The source-extraction inventory records the application foundations assigned to
future implementation work:

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