Finalize the ConfigKit roadmap and implementation plan

This commit is contained in:
2026-09-06 13:59:01 -05:00
parent 9fc272f7a9
commit cc40880a22
10 changed files with 904 additions and 109 deletions

View File

@@ -25,8 +25,8 @@ releases.
Preserve unrelated work. This repository initially has documentation only.
2. Read the linked feature sections for the requested stage. Follow links to
current contracts if earlier work has moved a definition out of the roadmap.
3. Extract applicable Narratio code and cases before writing equivalent new
mechanisms. Record source revision, symbols, adaptations, and retained notices.
3. Extract applicable Narratio and WeatherReporter code and cases before writing equivalent new
mechanisms. Record source revision, symbols, and adaptations.
Do not import sibling applications or their internal packages into this module.
4. Implement only the stage boundary. Partial internals may be tested directly;
do not export placeholder APIs, silently ignore unsupported options, or claim
@@ -63,9 +63,9 @@ Promote their externally observable parts into current contracts when implemente
| --- | --- |
| Module and package | Module `gitea.maximumdirect.net/eric/ConfigKit`, matching the existing origin's case; root package `configkit`. No `cmd` application. |
| Go baseline | `go 1.25.0`; use a currently supported patched toolchain at or above that minimum for development, and verify minimum-version compatibility separately. The existing Narratio/Seriatim baselines and local 1.25.5 toolchain support this language baseline. No toolchain directive forcing a newer consumer toolchain. |
| Parser | Pin `go.yaml.in/yaml/v3 v3.0.5` initially, inside `internal/yaml`. It preserves the v3 node-oriented extraction seam while using the YAML organization's maintained module. Do not inherit its scalar coercions. The [upstream v3 reference](https://pkg.go.dev/go.yaml.in/yaml/v3@v3.0.5) documents this version and provenance. |
| Parser | Pin `go.yaml.in/yaml/v4 v4.0.0-rc.6` initially, inside `internal/yaml`. Use the v4 streaming loader and dumper with explicit internal options. This selects upstream's active development line while preserving the node-oriented extraction approach. Do not inherit its scalar coercions. The [exact RC reference](https://pkg.go.dev/go.yaml.in/yaml/v4@v4.0.0-rc.6) documents the selected dependency. |
| Internal representation | A ConfigKit-owned tree in `internal/document`: ordered mapping entries, sequences, typed scalar kind plus original numeric spelling, null, key/value locations, and source ID. It imports neither the public package nor the YAML parser. The YAML adapter translates into it. |
| Dependency direction | Public package coordinates document operations and reflection binding. Private parser imports `internal/document`; document code never imports parser or public package. Keep composition, binding, provenance, hooks, and rendering coordination in root-package private files until a demonstrated boundary needs another package. |
| Dependency direction | Public package coordinates document operations and reflection binding. Only `internal/yaml` and its adapter tests import upstream YAML packages/plugins. The adapter imports `internal/document` and translates parser errors into ConfigKit-owned internal records; document code never imports parser or public package. Adapter signatures expose no upstream types to other ConfigKit packages. Keep composition, binding, provenance, hooks, and rendering coordination in root-package private files until a demonstrated boundary needs another package. |
| Public spelling | Implement the feature roadmap's `Load`, `Definition`, `Options`, sources, path builders, metadata queries, hook helpers, version rule, result methods, and diagnostic methods as written. No functional-options framework or separate builder. |
| Input limits | Fixed initial ceilings: 8 MiB per source, 64 MiB total loaded bytes, depth 256 for input/projected trees, 64 active import edges, and 1,024 accepted sources. Allow at most 1,000,000 input nodes in aggregate and 1,000,000 nodes in each composed/projected tree; copying a snapshot does not consume the input budget again. Bound glob directory-entry visits separately at 1,000,000 per load. Include the selected profile in source/byte counts. Root depth is zero. No public limit-tuning options initially. Test limit mechanisms with smaller private test parameters. |
| Filesystem support | Initial supported targets: Linux and macOS on amd64/arm64. Test real symlink semantics on those systems. No Windows path-compatibility promise in this release. Inputs use slash-separated relative import syntax and native paths for selected file/root/base options. |
@@ -74,11 +74,129 @@ Promote their externally observable parts into current contracts when implemente
| Error boundary | Safe library diagnostics with phase-specific codes; no raw parser/custom-hook error string or raw-value wrapper chain. `LoadError` supports `errors.As`; there is no initial `Unwrap` promise. I/O classifications are exposed as diagnostic codes. |
| Consumer integration scope | Build copied, sanitized consumer-shaped models/fixtures and executable examples in ConfigKit. Actual changes to Narratio, Notarius, WeatherReporter, and Seriatim are separate consumer migration tasks. Do not claim those apps are migrated when these local examples pass. |
| Publication scope | Prepare library source and a release runbook/checker. Do not tag, push, publish, or invent a completed release during these stages. Release-candidate validation/publication follows the adopted release procedure as a separate maintainer action. |
| Test comparisons | Use `github.com/google/go-cmp/cmp`, initially module `github.com/google/go-cmp v0.7.0`, for useful structured test comparisons. Add the requirement when first imported by tests. No production imports or runtime use; do not equate nil and empty or reorder sequences where those differences are contractual. |
The parser dependency choice is based on its own published documentation, not a
promise that v3.0.5 will always be the newest patch. A later verified dependency
fix can change the pin with focused YAML regression checks and normal review.
Do not silently switch parser families or use `@latest` during implementation.
The selected version is a prerelease, deliberately accepted for initial
development. Do not use `@latest`, silently change the pin, or depend on untagged
upstream changes. Reassess the available v4 release before ConfigKit publication;
a newer RC or final release requires an explicit dependency update and rerunning
the adapter, YAML-policy, diagnostics, limits, and rendering regressions. A final
upstream version is preferred when it passes those checks; if the RC is retained,
record that decision in release readiness and compatibility review.
Follow the feature's [upstream isolation contract](initial.md#yaml-dependency-and-public-api-isolation).
Translate upstream API changes internally without changing ConfigKit declarations
or consumer requirements. Never re-export upstream aliases/options/plugins, expose
parser errors through wrapping, or pass parser-created objects through `any`.
Keep ConfigKit's semantic checks independent of upstream convenience options.
The module requirement records the tested dependency; it does not lock a consuming
application's entire module graph. Do not promise compatibility with arbitrary
unverified upstream versions or add a selectable-parser framework.
### Updated extraction map
Use the source revisions and adaptation policies owned by the feature's
[foundation](initial.md#existing-implementation-foundation) and
[secret bootstrap](initial.md#secret-references-and-explicit-bootstrap) sections.
Paths below are relative to the named sibling repository. Reinspect them in
Stage 1 and record the exact revision actually copied. The maintainer is the sole
author of the relevant application code and has authorized its reuse and relicensing
as needed for ConfigKit; no separate license check is required for this extraction.
Keep source provenance for technical traceability. This map assigns reuse
work to existing stages; it does not change stage ordering or public contracts.
| Stages | Source code and tests | Extraction instruction |
| --- | --- | --- |
| 3 | Narratio `internal/config/composition.go`, `load.go`, `loader_strict_test.go`, `composition_test.go` | Extract source parsing and trailing-document regressions. Translate to the private v4 adapter and ConfigKit YAML policy; do not copy raw parser error propagation. |
| 10 | Narratio `composition.go`, `composition_test.go` | Adapt `mergeAdditiveCompositions`, claim validation and recursive merging. Preserve collision/source tests, replacing empty-mapping rejection with ConfigKit's operation table. |
| 11–12 | Narratio `pipeline_composition.go`, `pipeline_composition_test.go` | Reuse explicit import processing and duplicate/source cases. Build recursive graph/glob support and bounded containment; replace root-only directives, extension restrictions, and `os.SameFile` identity rules. |
| 13 | Narratio `mergeOverlayComposition`, `mergeOverlayNodes`, `selectPipelineProfile`, `loadPipelineProfileOverlays`, `pipeline_profiles_test.go` | Adapt overlay mechanics and independence tests. Replace null/kind rejection and declared/all-profile loading with ConfigKit's fixed selected-profile contract. |
| 14–15, 19 | Narratio `effective_pipeline.go`, composition semantic records, `pipeline_profiles_test.go`, `internal/app/config_commands_test.go` | Extract deterministic effective values, composite ownership and safe inspection cases. Add locations, retained history and generic redaction; remove YAML round trips and domain-specific projection. Do not copy diff/digest APIs. |
| 16 | WeatherReporter `internal/config/secrets.go`, secret cases in `config_test.go`; Narratio `internal/app/secrets_env.go`, `secrets_env_test.go`, `internal/fileops/read_bounded.go` | Combine WeatherReporter's read/apply separation and one-newline handling with Narratio's preserve-existing and bounded-read mechanisms. Adapt both to the agreed structured-report state machine. |
| 20 | Narratio inspection/composition/profile tests; WeatherReporter `config_test.go`, `overrides_external_test.go` | Prove side-effect-free inspection, explicit-empty domain validation, loaded/constructed consistency, and nonmutating option normalization with sanitized consumer-shaped fixtures. |
In every owning stage, record copied symbols/cases and intentional expectation
changes in the extraction inventory. Retain domain algorithms in consumer examples;
do not copy application frameworks, CLI commands, logging, digests, or provider
registries into the public library. Existing upstream types, YAML reserialization,
and partial provenance are adaptation work, not reasons to weaken the target.
### Standard-library mechanisms and custom policy
These are implementation choices for the existing contracts, not additional
public extension APIs. Apply each row in its owning stages. The Go 1.25 baseline
supplies the listed primitives; no additional runtime dependency is needed.
| Work and stages | Selected mechanism | ConfigKit work and limits |
| --- | --- | --- |
| Source reads (3, 11) | `os.Open`, `io.LimitReader`, `io.ReadAll` | Read at most `min(perSourceLimit, remainingTotalBudget) + 1` bytes; reject excess before parsing. File size is an optional early check, never the sole enforcement. Preserve read failures rather than accept partial input. |
| Source ownership (3) | `bytes.Clone`, `bytes.NewReader`, `strings.NewReader` | Reject oversized byte arguments before cloning, retaining the constructor error for `Load`. Close only ConfigKit-owned files. Keep a shared private reader-state pointer with `atomic.Bool.CompareAndSwap` to claim a reader before consumption; copying `Source` must not copy an in-use atomic or create another claim. |
| Identity and containment (11–13) | `filepath.Abs`, `EvalSymlinks`, `Rel`; `os.OpenRoot`/`Root.Open` | Canonicalize root and targets, reject a relative result that is absolute, `..`, or begins with a parent component, and open the canonical root-relative target through `Root`. Preserve declared spelling separately. Do not use string-prefix checks, `os.SameFile`, or `os.DirFS` as a confinement guarantee. |
| Glob matching (12) | `path.Match` | Use the feature's matching grammar and a small syntax check for forbidden recursive `**`, respecting escapes and character classes. Do not write another wildcard matcher or treat backslash as a native separator. Validate malformed patterns even when directories are empty or absent. |
| Glob expansion (12) | `os.File.ReadDir(n)` with positive bounded batches | Write a pattern-directed walker with explicit error propagation, canonical containment, and a budget counting every examined entry. Process returned entries and errors correctly, including EOF. Sort bounded candidates before recursion and final accepted relative matches before source traversal. Avoid `filepath.Glob`/`fs.Glob` error suppression and unbounded whole-directory reads. |
| Numeric conversion (3, 6) | `strconv.ParseInt`/`ParseUint` with base 10, `ParseFloat`, `math.IsNaN`/`IsInf` | Check ConfigKit lexical syntax first. Use the destination bit width, including `int`/`uint`; parse floats directly at 32/64 bits. Do not route wide integers through floats. Keep boolean/null recognition in simple switches; `strconv.ParseBool` accepts forms outside our policy. |
| Duration/text scalars (5–6) | `time.ParseDuration`, `Duration.String`, `encoding.TextMarshaler`/`TextUnmarshaler` | Enforce string-only input and paired codecs, canonical projection, detached reconstruction, and safe diagnostics. Date-looking text remains text. |
| Field binding (4) | `reflect.Type`, `StructField`, `StructTag.Lookup`, ordinary reflection accessors | Build our descriptor graph by inspecting declared fields and explicit inline tags. Do not use `reflect.VisibleFields` as the binding algorithm: Go promotion can hide conflicts and does not express our inline rules. No `unsafe` access to private fields. |
| Detached copies and comparison (5, 8–9) | `maps.Clone`, `slices.Clone`, `bytes.Clone` where shallow copies suffice | Recursively clone supported configuration with the descriptors; shallow clones do not detach nested objects. Compare canonical trees with a focused changed-path traversal, preserving atomic sequences and custom scalars. Neither `reflect.DeepEqual` alone nor runtime `go-cmp` supplies this audit. |
| Semantic paths (2) | `strings`, `strconv` for validated index tokens | Implement the small strict JSON Pointer codec and key/index representation. Do not use filesystem normalization or URL escaping/percent decoding. |
| Ordering and import graphs (2, 10–14) | `slices.Sort`/`SortFunc`, `cmp.Compare`, ordinary maps and slices | Define total ordering keys for observable output. Implement bounded DFS with separate active/visited maps and inclusion routes. Keep per-load state local; no global cache, `sync.Map`, or parallel import scheduler initially. |
| Safe diagnostics/output (2–3, 6, 15) | `errors.Is`/`As`, `fs.ErrNotExist`/`ErrPermission`, `fmt`, `io.WriteString`, `strconv.Quote` | Classify underlying failures and emit ConfigKit-owned records. Never forward `strconv.NumError`, duration-parser errors, or other value-bearing error text/unwrap chains. Quoting escapes controls but does not redact secrets. Check writer errors and short writes; flush buffered output if used. |
| Test support (2 onward, 21) | `testing`, `t.TempDir`, native fuzzing, `testing/iotest` | Use standard fragmented/error readers and real filesystem fixtures. Add small custom close-observers or failing writers only where needed. Use test-only `go-cmp` for structured expectations, not a second test framework. |
| Import-boundary checks (3, 20, 22) | `go/parser`, `go/ast`, `strconv.Unquote` | Inspect Go imports, including test files and platform-specific source, rather than regex-match source text. Permit YAML imports only in the adapter and its tests, and `go-cmp` imports only in test files. |
For confined access, canonicalization before `Root.Open` preserves contained
absolute symlink targets that `Root.Open` would reject if passed their original
symlink path. This is an implementation of the feature's stable-input contract,
not a promise of atomic resolution against hostile concurrent replacement.
Retain regular-file checks and safe error classification. Do not introduce custom
syscall-level confinement, path discovery, or a public filesystem-provider API.
### Secret, override, and history implementation bounds
Follow the feature contracts for [secrets](initial.md#secret-references-and-explicit-bootstrap),
[CLI overrides](initial.md#cli-overrides), and
[explanation](initial.md#historical-value-explanation). Introduce these public APIs
only in Stages 16–19. Earlier stages cover ordinary types and file/profile layers;
they must not expose an unresolved secret as a completed load or silently accept
an override option. Extend the shared descriptor/projection graph for `Secret`
in Stage 17, before ordinary struct/text handling; do not add a parallel binder.
- Bootstrap uses `os.LookupEnv`/`os.Setenv`, `os.Open` and bounded `ReadDir`/reads,
`filepath` canonicalization, and `strings.TrimSuffix` for exactly one newline.
Reuse WeatherReporter's actual code and tests, adapting policy rather than
importing its package. Validate names with a small ASCII check or its existing
regular expression. Keep a private environment/file-access seam for failure tests;
do not expose a general environment provider or public mock filesystem.
- Bootstrap ceilings: 1,024 entries, 1 MiB per eligible file, 8 MiB total eligible
bytes. Bound enumeration before sorting, read at most remaining budget plus one,
and reject excess before writes. Skipped existing files consume entry budget,
not content budget, and are not opened. These budgets are independent of YAML
inputs. Errors use `SecretsError` with `Error()` and `Diagnostics() []Diagnostic`;
no raw unwrap chain. `LoadSecretsDir` always returns its detached report alongside
an error. Diagnostics include safely quoted canonical file paths in messages; no config source
ID is fabricated for bootstrap. Reports expose no credential bytes or hashes.
- Stage 16's preflight/apply state machine owns report status assignment and
`ExistingEmpty`. On preflight failure, no `Loaded` entries are possible. On apply
failure, stop with earlier loaded/skipped statuses intact. Never log implicitly.
- Stage 17 reads each referenced name once per load, including failures. Do not
cache across loads, mutate the environment, or retain it in metadata. Application
hooks cannot inject a resolved credential through a public constructor. Store
names in projected nodes and resolved strings only in private `Secret` state.
- Override input shares the depth/node ceilings with document input; count its
nodes toward the aggregate budget. Bound string bytes to 8 MiB per record and
64 MiB combined with source bytes; at most 1,024 records, also counting their
synthetic sources toward the source ceiling. Validate and detach before applying;
no second parser or `flag` framework. Use `reflect` only to reject unsupported
values/detect cycles and standard numeric formatting to create typed scalar nodes.
- Retain structurally shared immutable document roots and audited domain deltas
for history. Cap retained unique history nodes at 4,000,000 per load, including
projected baseline and resolution snapshots; fail safely if exceeded. Explain
projects only the queried subtree for each relevant operation. Do not eagerly
store every rendered path/history combination or expose private roots.
- Test bootstrap real environment integration serially with `t.Setenv`; use the
private seam for unset names, preflight errors, write failures and deterministic
races between preflight and apply. Test load lookup using controlled names and
restore environment; no tests depend on real credentials. `go-cmp` remains test-only.
### Metadata and diagnostics layout
@@ -92,9 +210,9 @@ Expose detached value records for inspection, with these initial fields:
| `Diagnostic` | `Code Code`, `Severity Severity`, `Path Path`, `Message string`, `Primary *Location`, `Related []Location`; absent primary means no known source |
| `Origin` | `Layer Layer`, `Locations []Location`, `Operation ChangeKind`; multiple locations represent a composite origin |
| `FieldInfo` | `Explicit bool`, `Null bool`, `Origin Origin` |
| `Change` | `Path Path`, `Origin Origin`, `Previous []Origin`, `Reasons []string`, `Dependencies []Path`; no public historical values |
| `Change` | `Path Path`, `Origin Origin`, `Previous []Origin`, `Reasons []string`, `Dependencies []Path`; safe values available separately through `Explanation` |
`Layer` is a string enum: `default`, `base`, `profile`, `resolution`. A Go zero
`Layer` is a string enum: `default`, `base`, `profile`, `cli`, `resolution`. A Go zero
baseline uses layer `default` without claiming an application deliberately chose
every zero value. `ChangeKind` has the empty value for input/default origins,
`derived_default`, and `normalization`. `Severity` is `error` or `warning`.
@@ -103,14 +221,16 @@ Define exported constants with the type prefix (for example `LayerBase`,
Use stable string codes grouped by `options.*`, `source.*`, `yaml.*`, `schema.*`,
`decode.*`, `compose.*`, `import.*`, `profile.*`, `version.*`, `resolve.*`, and
`validate.*`. Include distinct missing/permission/read, duplicate, invalid-path,
`validate.*`, `secret.*`, and `override.*`. Bootstrap uses `secrets.*` codes
on a separate `SecretsError`. Include distinct missing/permission/read, duplicate, invalid-path,
limit-exceeded, hook-failed, and unrecorded-change classifications where relevant.
Application reports use their phase's `error`/`warning` code, not arbitrary code
registration. Define exact exported code constants with their owning feature;
private helpers do not need new public codes for every branch.
Sources get deterministic per-load IDs `source-1`, `source-2`, and so on in
traversal order. The selected profile is last. `NoSource` creates no source ID.
traversal order. File traversal is followed by the selected profile, then synthetic CLI sources.
`NoSource` itself creates no source ID; its overrides can create CLI sources.
Public query slices and nested slices/pointers are detached. Default fmt/error
rendering must not reveal hidden value histories.
@@ -125,7 +245,7 @@ rendering must not reveal hidden value histories.
diagnostics permit schema-valid missing fields/map entries; malformed paths or
unknown struct fields produce a hook diagnostic. Version hooks use the untyped
composed tree and nearest known container because decoding has not run yet.
- Defaults apply to the surviving composed/profile input; intermediate overridden
- Defaults apply to the surviving composed/profile/CLI input; intermediate overridden
nodes do not create reset markers. An effective null suppresses defaults at
its path. Missing children of an effective mapping can retain defaults.
- Applying nonempty input to a nil pointer-to-struct creates the object and uses
@@ -172,16 +292,17 @@ Tasks:
1. Create the module and root package documentation with the decisions above.
Keep the existing repository license. Add no application command, runtime
dependencies, framework, or placeholder exported loader.
2. Reinspect Narratio's referenced revision and current worktree. Create
2. Reinspect Narratio's and WeatherReporter's referenced revisions and worktrees. Create
`docs/internal/extraction.md` recording source revisions/symbols and the exact
loading/presence/default/diagnostic cases to extract. Include applicable notices
with copied material. Copy only minimal sanitized fixtures needed by subsequent
parsing/composition/import/profile/inspection/secret and default/presence cases
assigned by the extraction map. Record preserved, adapted and application-only
behavior, including current tests replacing obsolete baseline assumptions.
Copy only minimal sanitized fixtures needed by subsequent
stages; no audio, transcripts, secrets, or entire application package trees.
3. Document the planned parser pin here; add its module requirement only when the
real adapter imports it in Stage 3 so tidy does not leave unused dependencies.
4. Update the developer guide to runnable Go baseline commands and prerequisites.
Correct its stale claim that this plan is deferred. Update documentation-policy
navigation to this now-existing plan without changing ownership rules. Record
Preserve roadmap/current-contract ownership and navigation. Record
the implemented root package in the overview and retain pre-feature status.
Acceptance:
@@ -209,11 +330,15 @@ Tasks:
3. Render control characters in labels/messages/paths as escaped text so source
labels cannot corrupt terminal output. Never include values or parser errors.
4. Document the implemented path and diagnostic API next to declarations.
5. Apply the standard-library path, ordering, and diagnostic mechanisms above.
Introduce test-only `go-cmp` when comparing structured records; simple scalar
assertions can remain ordinary Go checks.
Acceptance:
- Round-trip escaped slash/tilde/empty/dotted/numeric keys and indexed paths;
distinguish a malformed path from a valid missing path.
distinguish a malformed path from a valid missing path. Preserve literal `..`
and percent-containing keys; reject malformed tilde escapes.
- `errors.As` finds `LoadError`; mutation of returned nested records cannot alter
later results. Safe writer failures propagate, including partial output.
- Reordered independent diagnostic submissions render identically; no blanket
@@ -226,21 +351,35 @@ Status: pending.
Read: feature [YAML policy](initial.md#yaml-and-scalar-policy),
[source constructors](initial.md#entry-point-and-reusable-application-definition).
Start with the extraction map's parser and strict-loader cases; the old
second-document acceptance defect is already fixed in Narratio.
Tasks:
1. Adapt Narratio's file/reader loading helpers into the opaque source constructors
and bounded private reader. Add `NoSource`, byte snapshotting, source labels,
regular-file checks, deterministic I/O classification, and reader single-use
state shared by source copies. File loads reopen each time; readers are not closed.
2. Add the pinned YAML adapter and ConfigKit-owned document tree. Decode into parser
nodes only; translate key/value locations and scalar spelling before discarding
parser-specific nodes. Do not use parser typed decode for application structs.
Use the bounded-read and source-ownership recipes above, including the extra-byte
oversize check and an atomic reader claim shared by source copies.
2. Add the pinned v4 YAML adapter and ConfigKit-owned document tree. Use
`yaml.NewLoader` and load each document into `yaml.Node`; translate key/value
locations and scalar spelling before discarding parser-specific nodes. Keep
upstream imports and error translation inside the adapter. Do not use parser
typed decode for application structs or return upstream types from the adapter.
3. Enforce exactly one mapping document, duplicate keys, tag/alias/anchor/merge-key
rejection, and ConfigKit's own scalar grammar. A second successful decode is
an error, including an empty document; do not preserve Narratio's trailing-doc bug.
4. Enforce byte ceilings before parsing and tree depth/node ceilings during adapter
translation. The parser can reject stricter malformed/nesting inputs first, but
none may panic or expose input text. Add private small-limit injection for tests.
Leave `WithSingleDocument` disabled: in rc.6 it stops the streaming loader after
the first document instead of rejecting trailing documents. Check explicitly
for another document. Validate duplicate keys in the ConfigKit tree; rc.6 node
loading preserves duplicates despite upstream typed-decoding uniqueness options.
4. Enforce byte ceilings before parsing. Configure the v4 limit plugin privately
to bound parser nesting, and enforce ConfigKit tree depth/node ceilings during
adapter translation. Calibrate parser-depth counting against ConfigKit's root-zero
limit with block/flow boundary cases rather than assume identical counters.
Parser failures must not panic or expose input text. Add private small-limit
injection for tests. Alias limits do not replace ConfigKit's alias prohibition.
5. Create `docs/config.md` for implemented syntax/input behavior and link from
package documentation. Move the implemented definitions out of the feature
roadmap, preserving navigation for this plan.
@@ -249,11 +388,18 @@ Acceptance:
- Real file and reader tests cover missing/permission/read failures, non-regular
targets, byte snapshots, second reader use, and no caller-reader close.
Cover exact-limit and one-byte-over input, fragmented reads, data returned with
an error, and competing loads of copies of the same reader source.
- Policy tables cover duplicate keys, all forbidden YAML constructs, null spellings,
quoted literals, JSON numeric syntax, legacy booleans/numbers, timestamp-like
strings, multiple/empty documents, and root type. Numeric nodes never round via float.
- Small-limit boundary tests and malformed input tests return safe diagnostics with
available coordinates. This stage does not promise a typed public `Load` yet.
- Retain regressions for duplicate node keys, trailing/empty second documents,
lexical numbers, explicit-tag visibility, and parser-depth enforcement. Add a
focused import-boundary check that rejects direct upstream YAML imports outside
the adapter and its tests and production `go-cmp` imports, using the AST recipe
above; extend it as packages/examples appear.
## Stage 4 — Build the reusable schema binding graph
@@ -266,6 +412,8 @@ Tasks:
1. Implement reflection descriptors for supported root structs/maps, scalar kinds,
pointers, nested structs, string-keyed maps, slices, fixed arrays, and `any`.
Use declared-field reflection and tag lookup as specified above, not Go field
promotion as a substitute for ConfigKit binding.
2. Bind exported field names, tags, inline structs/pointers and the optional inline
map. Detect ambiguous names, multiple inline maps, unsupported tag options,
conflicting defaults-map names (checked against runtime defaults in Stage 6),
@@ -296,6 +444,7 @@ Tasks:
2. Clone ordinary configuration into independently owned values. Reconstruct paired
text scalars through canonical text into fresh instances; preserve type identity
and supported dynamic concrete values without retaining mutable input aliases.
Apply the shallow-versus-recursive copy distinction above and reuse descriptors.
3. Detect cyclic values, non-finite floats, invalid dynamic types, projection limit
violations, and custom codec errors safely. Do not reveal the codec error text.
4. Add structural comparison used later by resolution audit and validator-mutation
@@ -324,6 +473,8 @@ Tasks:
2. Enforce numeric syntax/range and nullability. Support dynamic `int64`/`float64`,
finite floats, exact-duration string parsing, named scalar types, paired text
codecs, arrays with exact length, and strict nested map-of-struct decoding.
Use the standard-library conversion recipes above after lexical validation;
translate failures without retaining value-bearing conversion errors publicly.
3. Reject unknown fields, tag binding conflicts, and invalid defaults consistently.
Collect independent decode errors while leaving the failed working object private.
4. Return a private decoded configuration plus its surviving input presence index,
@@ -390,14 +541,15 @@ Tasks:
derived-default intent using the clarification rules above.
3. Verify final helper values against declared paths. Reject invalid paths, invalid
kinds, invalid projected values, missing records, or use after context closure.
Store reasons/dependencies without raw public history values.
Store reasons/dependencies and private before/after snapshots for later safe explanation.
4. Attach phase diagnostics and stop on reported errors even with nil return. A
non-nil return adds generic safe failure only if no error was already reported.
Leave callback panics as programming failures. Remove the resolver options gate.
Acceptance:
- Execute the feature's timeout and dynamic artifact examples against real code.
- Execute the feature's timeout and dynamic artifact examples against real code,
using their non-secret fields here; add the typed credential field in Stage 17.
- Test explicit-null/zero preservation, baseline replacement for absent input,
normalization of explicit input, map writeback, unrelated pointer/path mismatch,
unrecorded mutation, equal-value intent, repeated helper writes, and subtree removal.
@@ -438,6 +590,9 @@ Status: pending.
Read: feature [composition table](initial.md#composition-and-overlay).
Extract Narratio's additive composer and conflict tests before writing equivalent
code. Change empty-map expectations to the feature table and retain exact locations.
Tasks:
1. Implement a private strict composer over ConfigKit document trees. Recurse only
@@ -463,6 +618,9 @@ Status: pending.
Read: feature [import graph](initial.md#explicit-imports-and-source-identity).
Extract the flat import loader and tests, then generalize its graph, identity and
containment rules. Do not treat its root-only declarations as ConfigKit's contract.
Tasks:
1. Parse the reserved root directive and `path` declarations strictly; preserve
@@ -471,6 +629,8 @@ Tasks:
2. Resolve paths from their declaring source, canonicalize symlinks, enforce root
containment, and perform deterministic depth-first traversal. Keep active-stack
and visited indexes distinct. Track both routes for cycles and repeated imports.
Use the canonicalization/`os.Root` recipe above, including ConfigKit-owned root
handle cleanup; preserve contained relative and absolute symlink targets.
3. Read accepted sources once; enforce count/depth/aggregate budgets. Optional paths
suppress only target nonexistence. Invalid directives, directories, unreadable
files, canonicalization failures, and out-of-root targets remain errors.
@@ -494,11 +654,15 @@ Status: pending.
Read: feature import syntax and source-context API.
Reuse import failure fixtures where applicable; Narratio's flat loader does not
supply generic glob traversal or its bounded filesystem behavior.
Tasks:
1. Implement segment glob matching with sorted slash-relative results and no `**`.
Use an error-reporting directory walker: do not use a convenience glob API that
silently swallows permission/I/O errors. Reuse Stage 11 identity and traversal.
1. Use `path.Match` with the constrained grammar and bounded `File.ReadDir(n)`
expansion recipe above. Return sorted slash-relative matches; no recursive `**`.
Keep directory expansion pattern-directed and error-reporting. Reuse Stage 11
identity and traversal; do not add another glob engine or use `filepath.Glob`.
2. Reject invalid patterns before walking; canonicalize and contain visited
directories/targets, including symlinked directories. Optional zero matches
must not conceal a walk or permission failure. Bound accepted candidates before
@@ -515,15 +679,20 @@ Acceptance:
- Fixtures cover lexical order, multiple declarations, overlapping matches, matches
that are directories, hard links as separate paths, symlink escapes, unmatched
optional/required patterns, invalid patterns, and visible permission failures.
Cover escaped metacharacters, character classes, empty-directory pattern errors,
contained absolute symlinks, and budget exhaustion from nonmatching entries.
- Repeated runs produce identical source IDs, composition, and diagnostic ordering.
Full import contract and baseline pass.
## Stage 13 — Implement profile overlays and wire final precedence
## Stage 13 — Implement profile overlays
Status: pending.
Read: feature [profiles](initial.md#profiles), composition/overlay table.
Extract Narratio's overlay mechanics and profile tests; preserve the intentional
null/kind and profile-selection differences listed in the feature roadmap.
Tasks:
1. Implement private recursive overlay with atomic scalar/sequence/null and
@@ -554,6 +723,9 @@ Status: pending.
Read: feature [presence](initial.md#paths-and-presence),
[history](initial.md#provenance-diagnostics-and-redaction), metadata API.
Use Narratio's effective ownership and inspection tests as extraction evidence.
Its source-only records need locations and complete historical operations.
Tasks:
1. Integrate default, base, profile, and resolution records into a coherent query
@@ -581,6 +753,9 @@ Status: pending.
Read: feature [redaction](initial.md#provenance-diagnostics-and-redaction),
[secret predicate](initial.md#schema-checking-and-secret-classification), result API.
Reuse Narratio's deterministic effective-output and non-disclosure cases. Render
from the owned typed projection, without its marshal/parse reconstruction.
Tasks:
1. Combine descriptor secret tags with the application path predicate over current
@@ -594,9 +769,13 @@ Tasks:
by decoded string, preserve sequence order, use two-space indentation and a final
newline, and substitute the quoted string `[REDACTED]` at secret roots. Preserve
supported nil/empty distinctions. No unredacted option or implicit logging.
4. Marshal only an adapter tree made from the safe projection; never call application
YAML marshal hooks. Finish all text-codec projection work before returning a
successful load. Writer errors may leave partial redacted output.
4. Emit only an adapter tree made from the safe projection through the v4 dumper;
never call application YAML marshal hooks. Set indentation, sequence indentation,
line breaks, and document-marker behavior explicitly inside the adapter rather
than inherit a version preset. Use two-space indentation, noncompact sequences,
LF line breaks, and no document start/end markers for the single output document.
Finish all text-codec projection work before returning a successful load. Writer
errors may leave partial redacted output; translate upstream dump errors safely.
Acceptance:
@@ -610,12 +789,157 @@ Acceptance:
responsibility and that redacted output is not promised reloadable.
- Render actual resolved values including defaults/normalization; baseline passes.
## Stage 16 — Prove consumer ergonomics and migration contracts
## Stage 16 — Extract explicit secrets-directory bootstrap
Status: pending.
Read: [secret bootstrap contract](initial.md#secret-references-and-explicit-bootstrap),
architecture responsibility boundary, testing policy, and WeatherReporter's
`internal/config/secrets.go` and corresponding `config_test.go` cases; also
Narratio's `internal/app/secrets_env.go`, its tests, and bounded-read helper.
Tasks:
1. Record both applications' source revisions and reused symbols/tests in
the extraction inventory. Reuse WeatherReporter's `loadSecrets`/`applySecrets`
separation and Narratio's bounded reads/preserve-existing behavior. Replace
overwrite/rollback, immediate writes, aggregate logging, and incompatible
name/newline/permission/directory policies with ConfigKit's contract. Keep this
operation separate from `Load` and do not add a directory loading option to YAML.
2. Implement bounded, deterministic enumeration and metadata validation; reject
symlinks/nonregular entries, validate names, and preserve existing variables
including empty ones without reading their contents.
3. Implement eligible-file preflight, newline/NUL checks and the apply state
machine, including the presence recheck and partial-failure report. Add the
public report, status enum, safe typed error and report renderer as specified.
4. Document startup-only process mutation, non-atomicity, and how applications
report blocked files and retain the report alongside configuration results.
Acceptance:
- Adapt WeatherReporter's preflight-failure tests and Narratio's existing-empty
environment cases. Replace overwrite/rollback expectations with skip/partial-report
checks; do not drop the underlying failure coverage. Keep bootstrap explicit,
independent of subsequent configuration validation.
- Real temporary-directory tests cover newline preservation, invalid names/types,
bounded reads/enumeration, empty files, and canonical report paths/order.
- Existing nonempty and empty variables win and are distinguishable in reports;
skipped contents are not read. Preflight failures perform no writes. Controlled
apply failure and newly set variable tests prove accurate partial statuses.
- Canary contents are absent from errors, fmt/JSON reports and rendered output.
Writer failures propagate. Run the stage baseline; no actual credentials are used.
## Stage 17 — Add environment-backed typed secret references
Status: pending.
Read: [secret reference contract](initial.md#secret-references-and-explicit-bootstrap),
shared descriptor/projection rules and lifecycle, Stage 16 report boundary.
Tasks:
1. Implement opaque `Secret`, `SecretEnv`, name/value/resolution accessors, safe
fmt behavior and name-only JSON output. Extend existing binding, cloning,
decoding, projection, comparison and historical sensitivity machinery.
2. Resolve effective names after the resolver and before validation. Cache lookup
results only for this load. Handle required versus optional references, invalid
names, absent/null/empty references, unset and empty variables with safe errors.
3. Keep credentials out of every document/history/metadata representation. Rebuild
references from names when cloning defaults, even if they came from a prior
resolved result. Add effective reference-name inspection without claiming a
bootstrap file populated the current environment.
4. Update current contracts and the pipeline example to use typed references;
retain tag/predicate redaction for sensitive derived data. Earlier lifecycle
examples using `Secret` become runnable only at this stage.
Acceptance:
- Test required and optional references in structs, maps, pointers and sequences;
only surviving names are read, once per unique name. Resolution can change a
name and validation sees its resolved value. Reject validator replacement with
a same-name unresolved reference, using a private-state audit against the lookup
cache in addition to projection. Later loads observe changed env.
- Bootstrap plus load tests show a blocked empty variable in the report and an
empty-reference failure at its config path; no fallback to directory contents.
- Secret values appear only through `Value()`, never safe fmt/JSON, diagnostics,
projected trees or history; previously resolved defaults cannot retain stale
values. `Load` never mutates environment. Run the baseline.
## Stage 18 — Apply explicit CLI overrides above profiles
Status: pending.
Read: [override contract](initial.md#cli-overrides), overlay/presence/version rules,
source ordering, and the secret-reference boundary.
Tasks:
1. Add `Override` and `Options.Overrides`. Validate paths, labels, allowed value
types, overlap/version/directive restrictions and budgets; detach input into
private nodes without YAML reparsing or scalar inference.
2. Apply records after profiles and before the existing final typed decode using
the shared overlay primitive. Create synthetic CLI sources and record explicit
presence and before/after operations, including equal-value replacements.
3. Support defaults-only loads with overrides and add a parser-neutral executable
example translating only explicitly set flags. Document general environment
overlays as an explicit non-goal, not a future option.
Acceptance:
- A lifecycle test proves defaults < base/imports < profile < CLI, followed by
recorded normalization and normal secret lookup/validation. Zero, false, empty
string/list and null remain intentional values.
- Test map overlay and whole-list replacement, invalid ancestors/index paths,
duplicate/overlapping paths, type/cycle/limit errors, unknown fields and version
protection. Input mutation after load cannot change result or history.
- CLI diagnostics and provenance identify flag names without raw arguments;
CLI secret input is an environment name. Run the baseline.
## Stage 19 — Expose safe historical-value explanation
Status: pending.
Read: [explanation contract](initial.md#historical-value-explanation), redaction
rules, and history bounds above.
Generalize Narratio's semantic-record and inspection tests. They cover effective
values and source ownership, not the full chronological history required here.
Tasks:
1. Extend retained operation snapshots where necessary to reconstruct known final
and removed paths through defaults, base contributions, profile, CLI, and net
domain changes. Use structural sharing and the history budget, not full copies
per field. Preserve original policy-validated overridden nodes.
2. Implement detached `ValueSnapshot`, `ValueChange`, `Explanation`, and reference
metadata; apply historical secret/dependency closure before any public value
projection. Add `Result.Explain` with absent/null/deleted distinctions.
3. Implement `WriteExplanation` over the safe result, with canonical pointer paths,
composite origins, ordered before/after history, reasons/dependencies and final
source. Keep bootstrap reports separate and show how commands present both.
Acceptance:
- Show the requested concurrency history 4 → 8 → 1 and effective profile file/line;
extend one test with CLI and normalization. Test equal writes, removed paths,
parent replacement, composite maps, atomic sequences and unknown paths.
- Historical input with an overridden invalid destination type stays explainable;
output never claims it was successfully typed. Default null/empty rules agree
with final decoding and metadata. Retained history obeys bounded allocation.
- Canary secrets remain absent even after replacement/deletion/dependency cycles;
returned snapshots cannot mutate internal history. Test safe fmt/JSON of public
records, escaped labels, deterministic output and failing/short writers.
- Run baseline and a representative end-to-end bootstrap/load/explain test.
## Stage 20 — Prove consumer ergonomics and migration contracts
Status: pending.
Read: feature consumer-fit table, module options, external values, and both criteria.
Include the updated consumer tests listed in the extraction map. Preserve domain
rejection of explicit empty lists without making empty lists a library schema error.
Tasks:
1. Create maintained executable examples and minimal synthetic fixtures for four
@@ -623,12 +947,17 @@ Tasks:
object bindings/string versions/dynamic options; WeatherReporter-like defaults-only
and report/module options; Seriatim-like ordered speaker/autocorrect rule files.
2. Generalize the relevant copied Narratio tests and helpers, recording source
revision and notices. Use actual consumer field patterns but no private data or
revision and adaptations. Use actual consumer field patterns but no private data or
dependencies on sibling application modules. Keep runtime algorithms in example
application functions rather than adding library APIs for them.
3. Compare preserved outcomes against consumer evidence and list intentional changes:
3. Add a WeatherReporter-shaped startup example that renders bootstrap outcomes,
loads typed references, and explains configuration with explicitly set CLI flags.
Demonstrate both a blocked empty variable and successful lookup without real
credentials or implicit process mutation in `Load`.
Compare preserved outcomes against consumer evidence and list intentional changes:
explicit empty/null behavior, canonical objects, versions, integer representation,
strict YAML, and retired environment/CLI configuration overrides. Verify imports
strict YAML, retired general environment overrides, explicit CLI records, and
secret-reference migration. Verify imports
and profiles do not absorb application LLM/pipeline-profile semantics.
4. Convert every feature-roadmap API example into a real compiled/run example or a
focused behavioral test; document the happy path in README and `docs/consumers/`.
@@ -643,10 +972,14 @@ Acceptance:
with stable classifications and useful paths, not copied raw parser text.
- Simple consumers need no imports/profiles and no custom parser hooks. Complex
examples preserve domain meaning under the same conventions.
- No external configuration reads, environment mutation, LLM calls, remote services,
- No uncontrolled external configuration reads, LLM calls, remote services,
or sibling checkout requirements occur in default tests. Baseline passes.
- Compile consumer examples using only ConfigKit and standard-library imports.
Review exported signatures, reachable record fields, aliases, generic constraints,
callbacks, and error chains against the upstream isolation contract. Consumers
must not name or configure the YAML dependency to exercise any supported API.
## Stage 17 — Harden boundaries and verify independent loads
## Stage 21 — Harden boundaries and verify independent loads
Status: pending.
@@ -677,7 +1010,7 @@ Acceptance:
- A fresh offline baseline after dependency acquisition passes without reading any
developer-specific configuration path.
## Stage 18 — Prepare release validation and reconcile documentation
## Stage 22 — Prepare release validation and reconcile documentation
Status: pending.
@@ -710,6 +1043,9 @@ Tasks:
Replace implemented feature-roadmap definitions with current contract links;
repair ADR navigation without changing accepted rationale. Keep this execution
plan until release preparation retires completed roadmap material.
Reassess the v4 pin under the dependency-update rule above. Run the import-boundary
check and review the public API for upstream leakage; preserve ConfigKit's
behavioral expectations when adapting to any upstream changes.
6. Update release-readiness evidence for the actual runbook/checker. Keep any
unvalidated release-candidate/publication gap explicit and retain its bootstrap
guard until the maintainer closes it. Do not fabricate release notes for an
@@ -735,20 +1071,24 @@ working loader. The linked feature sections own semantics; stages own work.
| Feature area | Owning stages |
| --- | --- |
| Extraction, ordinary consumer types, package boundaries | 1, 4–6, 16 |
| Extraction, ordinary consumer types, package boundaries | 1, 4–6, 20 |
| YAML subset, precise numeric syntax, safe source failures | 2–3, 6 |
| Paths, presence, origins, definition history | 2, 6–8, 10, 13–14 |
| Static defaults, null, container rules, snapshot ownership | 5–7, 14, 17 |
| Static defaults, null, container rules, snapshot ownership | 5–7, 14, 21 |
| File/bytes/reader/defaults-only inputs and filesystem context | 3, 7, 11–12 |
| Version hook and graph-placement restrictions | 7, 11, 13 |
| Strict imports, glob ordering, identity, limits, collisions | 10–12, 17 |
| Strict imports, glob ordering, identity, limits, collisions | 10–12, 21 |
| Named profiles, precedence, atomic replacement | 13–14 |
| Recorded normalization/derived defaults and hook errors | 8–9, 14 |
| Semantic validation, related diagnostics, safe messages | 2, 7–9 |
| Secret tags/predicate/dependencies and effective inspection | 5, 15, 17 |
| Module-specific options, consistency and intentional migrations | 16 |
| Runnable docs, examples, source-release checks and runbook | 1, 16, 18 |
| Deferred-feature boundaries | Every stage; final audit in 18 |
| Secret tags/predicate/dependencies and effective inspection | 5, 15, 21 |
| Module-specific options, consistency and intentional migrations | 20 |
| Runnable docs, examples, source-release checks and runbook | 1, 20, 22 |
| Secret-directory bootstrap and blocked-file provenance | 16–17 |
| Environment-backed secret references and safe lookup | 17, 19–21 |
| Explicit CLI overrides and complete precedence | 18–21 |
| Safe historical values and explanation renderer | 14–15, 19–21 |
| Deferred-feature boundaries | Every stage; final audit in 22 |
At final handoff, report completed stages, exact checks, current documentation
owners, source-extraction evidence, and remaining release/consumer migration work.
@@ -767,7 +1107,7 @@ are not authorization to silently weaken acceptance:
1. **Is a native Linux runner available for the declared support matrix?**
Recommended: execute filesystem/race tests on native Linux and macOS before
release; cross-builds alone cannot verify symlink/permission behavior. Stage 18
release; cross-builds alone cannot verify symlink/permission behavior. Stage 22
can prepare the checker on the available host while keeping missing native
evidence as an explicit release gate. Alternative: formally narrow the initial
supported platforms to those with execution evidence, with a documented scope