From f42ecc70c330a399aa6dada87658694402a515ec Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 6 Sep 2026 22:47:12 +0000 Subject: [PATCH] Prepare source release validation --- README.md | 11 +- .../0001-separate-composition-and-overlay.md | 4 +- docs/adr/0002-own-yaml-semantics.md | 3 +- ...reserve-presence-and-resolution-history.md | 5 +- ...ecret-references-and-explicit-bootstrap.md | 5 +- ...explicit-overrides-and-safe-explanation.md | 5 +- docs/config.md | 4 +- docs/consumers/README.md | 4 +- docs/development.md | 6 +- docs/internal/extraction.md | 23 +- docs/internal/overview.md | 30 +- docs/policy/architecture.md | 21 +- docs/policy/documentation.md | 14 +- docs/policy/release.md | 24 +- docs/policy/testing.md | 2 +- docs/release.md | 240 ++++ docs/roadmap/implementation.md | 114 +- docs/roadmap/initial.md | 1139 +---------------- docs/roadmap/release-readiness.md | 46 +- documentation_test.go | 151 +++ scripts/release-check.sh | 75 ++ 21 files changed, 721 insertions(+), 1205 deletions(-) create mode 100644 docs/release.md create mode 100644 documentation_test.go create mode 100755 scripts/release-check.sh diff --git a/README.md b/README.md index 068df6d..613dbd3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ An opinionated Go library for applications that use YAML configuration files. -ConfigKit is in early implementation. Its current loader accepts one strict YAML +ConfigKit's initial loader accepts one strict YAML source (or explicit defaults-only input), optionally traverses explicit path and glob imports, strictly composes their disjoint definitions, applies typed defaults, and can overlay one selected fixed profile and check an application schema version. It @@ -33,6 +33,9 @@ func ReadConfig(path string) (Config, error) { The [configuration contract](docs/config.md) describes implemented behavior. The [consumer adoption guide](docs/consumers/README.md) provides maintained Narratio-, Notarius-, WeatherReporter-, and Seriatim-shaped examples and migration guidance. -The [initial target design](docs/roadmap/initial.md) describes planned behavior. -The [implementation plan](docs/roadmap/implementation.md) owns the pending stages. -See the [development guide](docs/development.md) for repository orientation. +The [initial target record](docs/roadmap/initial.md) links implemented feature owners +and preserves deliberately deferred scope. The [implementation plan](docs/roadmap/implementation.md) +records completion evidence until release-candidate preparation retires it. See the +[development guide](docs/development.md) for repository orientation and the +[release procedure](docs/release.md) for maintainer-only candidate and publication +steps. diff --git a/docs/adr/0001-separate-composition-and-overlay.md b/docs/adr/0001-separate-composition-and-overlay.md index cca27c1..5ab8e22 100644 --- a/docs/adr/0001-separate-composition-and-overlay.md +++ b/docs/adr/0001-separate-composition-and-overlay.md @@ -20,8 +20,8 @@ Use distinct strict-composition and intentional-overlay operations. Composition rejects conflicting definitions; overlay provides explicit precedence. Keep the operations opinionated rather than adding arbitrary per-field merge strategies. The [architecture policy](../policy/architecture.md) owns the governing boundary; -the [target design](../roadmap/initial.md#composition-and-overlay) owns the planned -operation table until its current contract lands. +the [current configuration contract](../config.md#strict-document-composition) owns +the operation table and selected-profile overlay behavior. ## Alternatives considered diff --git a/docs/adr/0002-own-yaml-semantics.md b/docs/adr/0002-own-yaml-semantics.md index b7c1654..49c07fa 100644 --- a/docs/adr/0002-own-yaml-semantics.md +++ b/docs/adr/0002-own-yaml-semantics.md @@ -20,7 +20,8 @@ ConfigKit defines its supported YAML policy and keeps parser integration private Application types and diagnostics must not require parser-specific public types. Use a small internal boundary rather than a general parser/provider framework. The [architecture policy](../policy/architecture.md) owns this constraint; the -[target design](../roadmap/initial.md#yaml-and-scalar-policy) owns planned syntax. +[current YAML document](../config.md#yaml-documents) and +[scalar](../config.md#scalars) contracts own the implemented syntax. ## Alternatives considered diff --git a/docs/adr/0003-preserve-presence-and-resolution-history.md b/docs/adr/0003-preserve-presence-and-resolution-history.md index fee68c0..b5e3474 100644 --- a/docs/adr/0003-preserve-presence-and-resolution-history.md +++ b/docs/adr/0003-preserve-presence-and-resolution-history.md @@ -22,8 +22,9 @@ values. Keep application defaults and normalization application-owned, but recor resolution changes so final inspection and metadata describe the actual resolved snapshot. Do not promise language-enforced immutability of exported Go values. The [architecture policy](../policy/architecture.md) owns these invariants; the -[target design](../roadmap/initial.md#recorded-resolution-and-validation) owns the -planned recording mechanism and ownership semantics. +[current resolution contract](../config.md#recorded-application-resolution) and +[ownership contract](../config.md#typed-projection-and-ownership) own the implemented +recording mechanism and ownership semantics. ## Alternatives considered diff --git a/docs/adr/0004-secret-references-and-explicit-bootstrap.md b/docs/adr/0004-secret-references-and-explicit-bootstrap.md index 6e20b66..ddc97a3 100644 --- a/docs/adr/0004-secret-references-and-explicit-bootstrap.md +++ b/docs/adr/0004-secret-references-and-explicit-bootstrap.md @@ -21,8 +21,9 @@ Existing environment variables win even when empty. Report skipped files and partial failures without values. Keep bootstrap provenance separate from later lookup provenance rather than imply an unchanged environment between calls. General environment-variable configuration overlays are not on the roadmap. -The [feature roadmap](../roadmap/initial.md#secret-references-and-explicit-bootstrap) -owns the unimplemented contract; stages remain in the implementation plan. +The [current reference contract](../config.md#typed-secret-references) and +[bootstrap contract](../config.md#explicit-secrets-directory-bootstrap) own the +implemented behavior. ## Alternatives considered diff --git a/docs/adr/0005-explicit-overrides-and-safe-explanation.md b/docs/adr/0005-explicit-overrides-and-safe-explanation.md index 9564f2b..09b5c30 100644 --- a/docs/adr/0005-explicit-overrides-and-safe-explanation.md +++ b/docs/adr/0005-explicit-overrides-and-safe-explanation.md @@ -19,8 +19,9 @@ node tree before typed decoding, retaining the existing parser adapter boundary. Recorded domain resolution follows input selection. Provide safe structured historical values and an explanation renderer in the initial release, building on ADR 0003's presence and audited resolution decision. Applications own commands. -The [feature roadmap](../roadmap/initial.md#cli-overrides) owns the exact override -contract and [explanation contract](../roadmap/initial.md#historical-value-explanation). +The [current override contract](../config.md#explicit-cli-overrides) and +[explanation contract](../config.md#historical-value-explanation) own the implemented +behavior. ## Alternatives considered diff --git a/docs/config.md b/docs/config.md index c7a571a..40399af 100644 --- a/docs/config.md +++ b/docs/config.md @@ -567,7 +567,7 @@ 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 +change binding or 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 @@ -673,7 +673,7 @@ any sequence change reports the sequence path and never invents correspondence between old and new indices. Cross-kind and null/container changes replace the affected path. -These mechanisms currently support later decoding, resolution auditing, metadata, +These mechanisms support decoding, resolution auditing, metadata, and rendering. They are not a public generic conversion or cloning API. ## Safe failures diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 4e72e65..44cf5c1 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -81,9 +81,9 @@ WeatherReporter, or Seriatim. Updating those repositories, their deployed YAML, wiring, runtime catalogs, and operational documentation remains separate downstream work. -## Maintained roadmap examples +## Maintained API examples -Every nontrivial API sketch from the feature roadmap has a compiled example or a +Every nontrivial initial API sketch has a compiled example or a focused behavioral owner: | Roadmap surface | Maintained owner | diff --git a/docs/development.md b/docs/development.md index 34becf4..3c6ce55 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,7 +1,7 @@ # Development This is the contributor entry point for ConfigKit. Read the task-specific owners -below before making changes. The repository is in early implementation: its public +below before making changes. Its public loader currently covers strict selected sources, explicit path/glob import graphs, composition, selected profile overlays, typed defaults, schema-version checks, explicit CLI overrides, recorded application resolution, semantic validation, @@ -23,10 +23,10 @@ redacted effective rendering and historical value explanation. The | --- | --- | | Repository orientation or component responsibility | [Architecture policy](policy/architecture.md) and [component overview](internal/overview.md) | | 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 | +| Architecture decisions or future work | [Documentation policy](policy/documentation.md), [initial target record](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 [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 | +| Release preparation | [Release policy](policy/release.md), [release procedure](release.md), and [release-readiness evidence](roadmap/release-readiness.md) | Accepted decisions: diff --git a/docs/internal/extraction.md b/docs/internal/extraction.md index ed433e7..c1c63f3 100644 --- a/docs/internal/extraction.md +++ b/docs/internal/extraction.md @@ -28,8 +28,8 @@ the owning implementation work extracts a symbol or case. - Use sanitized local fixtures only. Audio, transcripts, credentials, private configuration, and whole application package trees are application-only. - Record the exact copied/generalized symbols and tests in the owning section when - extraction occurs. An entry below assigns evidence; it does not claim extraction - or behavior is implemented. + extraction occurs. Assignment prose records the original work boundary; dated + implementation records state what was actually generalized. ## Narratio evidence @@ -86,8 +86,8 @@ collection, conflict construction, and recursive mapping join in `compose.go` ad the assigned production symbols. `TestComposeDocumentsJoinsDisjointMappingsAndTracksClaims`, `TestComposeDocumentsRejectsEveryPresentNonMappingOverlap`, and `TestComposeDocumentsReportsAllClaimantsAtExactLocations` adapt the assigned cases. -The planned empty-mapping difference is covered separately: empty mappings retain -container provenance but contribute no child claims. No unplanned semantic deviation +The intentional empty-mapping difference is covered separately: empty mappings retain +container provenance but contribute no child claims. No other semantic deviation from the extracted additive composer was required. ### Imports @@ -122,9 +122,9 @@ declaration-order, disjoint composition, keyed-entry, all-source conflict, unsaf target, alias, imported parse/schema, and relative-resolution cases. ConfigKit adds recursive bounded DFS, `os.OpenRoot` confinement, canonical symlink identity, cycle/diamond route locations, optional-path semantics, aggregate budgets, and -fragment version-placement enforcement. The planned glob syntax is parsed but -remains explicitly unavailable for its next owning work; hard links intentionally -replace Narratio's same-file rejection with path identity. +fragment version-placement enforcement. That checkpoint parsed glob syntax behind an +explicit unsupported gate that the subsequent glob implementation removed; hard +links intentionally replace Narratio's same-file rejection with path identity. Glob-import implementation record (2026-09-06): ConfigKit retains the extracted declaration order, import failures, composition, and identity cases while adding the @@ -133,7 +133,7 @@ new `expandImportGlob`, `readGlobMatches`, and pattern-validation mechanisms in directories, hard links, contained and escaping symlinks, unmatched declarations, malformed and recursive patterns, escaped metacharacters, character classes, permission failures, explicit `conf.d` use, and entry/candidate budgets. This work -has no Narratio production equivalent; it uses the planned standard-library +has no Narratio production equivalent; it uses the selected standard-library `path.Match` and bounded `File.ReadDir` adaptation rather than its flat loader. ### Profiles and overlay @@ -199,8 +199,8 @@ inspection cases across one default/import/profile/resolution workflow. ConfigKi adds chronological descendant history for parent and whole-sequence operations, composite empty-mapping origins, canonical resolution reasons/dependencies, private snapshot agreement, defensive nested copies, and concurrent load isolation. Value -rendering, redaction, explanation, commands, generated attribution, diffs, and -digests remain assigned to their later owning work. +That checkpoint deliberately left rendering, redaction, explanation, commands, +generated attribution, diffs, and digests outside its scope. Effective-rendering implementation record (2026-09-06): ConfigKit adapts Narratio's deterministic effective-output and side-effect-free inspection cases in @@ -212,7 +212,8 @@ resolved values, text-scalar snapshot reuse, application YAML-hook isolation, non-disclosure through failures and record formatting, caller mutation, and writer failures. Unlike Narratio's marshal/reparse path, ConfigKit renders only its owned frozen tree and substitutes redaction before the upstream dumper boundary. Historical -value explanation and secret references remain assigned to later work. +value explanation and secret references were implemented at their later owning +boundaries and are exercised by the consumer startup example below. ### Defaults, presence, and domain resolution diff --git a/docs/internal/overview.md b/docs/internal/overview.md index ba2498d..dc6d4a0 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -20,17 +20,17 @@ module and package documentation boundary. Its implemented public primitives are - a per-operation private reflection descriptor graph in `schema.go` for supported types, YAML field binding, inlining, secret tags, recursion, and semantic paths; - descriptor-driven canonical projection, detached typed cloning, cycle/limit - enforcement, and structural comparison in `projection.go`; and + enforcement, and structural comparison in `projection.go`; - 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; and + baseline/final trees, and initial default/input origins; - public lifecycle coordination in `load.go`, bounded canonical import DFS in `imports.go`, pattern-directed glob expansion in `glob.go`, version-report contexts in `validation.go`, and detached layered metadata/history assembly in - `load_metadata.go`; and + `load_metadata.go`; - recorded application defaulting and normalization in `resolution.go`, including explicitness queries, helper verification, structural mutation audit, coalesced - intent, private before/after snapshots, and resolution metadata integration; and + intent, private before/after snapshots, and resolution metadata integration; - post-resolution schema-aware report collection and validator mutation detection in `validation.go`, completing the public callback lifecycle after composed input; - opaque typed environment references and per-load name resolution in @@ -38,13 +38,15 @@ module and package documentation boundary. Its implemented public primitives are validator-state auditing; - explicit bounded startup environment bootstrap and safe partial reporting in `secrets.go`; -- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`; and +- fixed-point tag/predicate/dependency sensitivity classification in `secret.go`; - 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`; and + only upstream dumper integration isolated in `internal/yaml/write.go`; - a separate maintained `examples/consumers` package with four sanitized consumer - shapes, fixtures, domain validators, and runtime-boundary functions. + shapes, fixtures, domain validators, and runtime-boundary functions; and +- a fail-fast source-candidate checker in `scripts/release-check.sh`, with local + documentation-link and public import-boundary gates owned by root-package tests. The [configuration contract](../config.md) owns implemented input, schema, decoding, loading lifecycle, path imports, composition, profile/CLI overlay, version checking, @@ -54,15 +56,13 @@ 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: +The source-extraction inventory records the application foundations used by the +implementation: - [Extraction inventory](extraction.md) -This document will inventory additional components and link to their current -contracts and focused internal documentation as they are introduced. - The [architecture policy](../policy/architecture.md) owns binding boundaries and -invariants. The [initial target design](../roadmap/initial.md) describes planned -behavior; it is not an inventory of implemented features. See the -[development guide](../development.md) for contributor orientation and validation. +invariants. The [initial target record](../roadmap/initial.md) links current feature +owners and preserves deferred scope; it is not an implementation inventory. See the +[development guide](../development.md) for contributor orientation and validation +and the [release procedure](../release.md) for source-candidate gates. diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 74f0f6f..07ddb39 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -7,10 +7,10 @@ ## Purpose and applicability ConfigKit is an opinionated Go library for consistent YAML configuration loading -and resolution across consuming applications. This policy governs contributions -now; it does not claim that the target features are implemented. The -[component overview](../internal/overview.md) owns implementation inventory and -[initial roadmap](../roadmap/initial.md) owns unimplemented target behavior. +and resolution across consuming applications. This policy governs contributions. +The [component overview](../internal/overview.md) owns implementation inventory, the +[configuration contract](../config.md) owns implemented behavior, and the +[initial target record](../roadmap/initial.md) preserves deferred scope. ## Success criteria @@ -48,8 +48,8 @@ existing primitives cannot accommodate it cleanly, and show how the extension preserves a coherent model. Application-specific interpretation and validation remain appropriate. Reuse Narratio's applicable infrastructure under these boundaries, and keep simple consumers independent of optional composition -features. The [consumer API design](../roadmap/initial.md#consumer-api-design) -describes the proposed surface; this policy does not claim it is implemented. +features. The [consumer adoption guide](../consumers/README.md) demonstrates the +implemented surface against representative simple and complex shapes. ## Responsibility boundaries @@ -103,12 +103,12 @@ decoding. Preserve presence and source information through processing. Ordinary values must not be used to infer whether input was absent. Final configuration, metadata, and effective inspection must agree, including application resolution changes; see [ADR 0003](../adr/0003-preserve-presence-and-resolution-history.md). -Safe historical values and an explanation renderer belong to the initial target, +Safe historical values and an explanation renderer are part of the implemented contract, including superseded/deleted paths and effective source attribution. Inspection must agree with the final typed snapshot and apply historical sensitivity rules. See [ADR 0005](../adr/0005-explicit-overrides-and-safe-explanation.md). -Detailed syntax and operation tables belong to the target roadmap until their -current configuration contract is implemented. +Detailed syntax and operation tables belong to the +[current configuration contract](../config.md). ## Ownership and confidentiality @@ -136,6 +136,7 @@ for safe messages supplied by their hooks. Do not introduce automatic directory discovery, network providers, arbitrary merge strategies, additional configuration languages, or application runtime management without concrete consumer evidence and an explicit design decision. -The roadmap owns the complete deferred-feature inventory. Significant boundary +The [initial target record](../roadmap/initial.md#explicit-non-goals-and-deferred-scope) +owns the deferred-feature inventory. Significant boundary changes require an ADR and corresponding policy/contract updates; implementation convenience alone is not a reason to broaden the public API. diff --git a/docs/policy/documentation.md b/docs/policy/documentation.md index 9353c40..4366db4 100644 --- a/docs/policy/documentation.md +++ b/docs/policy/documentation.md @@ -62,24 +62,24 @@ handling, not secret values. | Documentation rules | `docs/policy/documentation.md` | Ownership and document lifecycle | | Testing rules | `docs/policy/testing.md` | Risk-based sufficiency, test boundaries, and maintenance principles | | Release rules | `docs/policy/release.md` | Compatibility, integrity, gates, immutability, and exceptions | -| Release procedure | `docs/release.md`, when operational | Exact maintainer commands, prerequisites, supported targets, and verification | +| Release procedure | `docs/release.md` | Exact maintainer commands, prerequisites, supported targets, and verification | | Exported Go API | Go documentation beside declarations | Symbol contracts, errors, ownership, and lifecycle API behavior | | YAML and resolution semantics | `docs/config.md`, as behavior lands | Syntax, composition, precedence, conversion, secret references/bootstrap, and explanation semantics; Go reference docs link here for shared rules | | Consumer walkthroughs | `docs/consumers/`, when useful | Task-oriented usage and consumer responsibilities, not duplicate API reference | | Component inventory | `docs/internal/overview.md` | Implemented components and links to their owners | | Component mechanics | Other `docs/internal/` files, when needed | Detailed implementation behavior and change recipes | | Architectural decisions | `docs/adr/` | Context, decisions, alternatives, consequences, and supersession history | -| Initial target | `docs/roadmap/initial.md` | Unimplemented end-state and deferred feature scope | +| Initial target record | `docs/roadmap/initial.md` | Implemented-feature owner links and deferred feature scope until roadmap retirement | | Implementation plan | `docs/roadmap/implementation.md` | Stages, dependencies, acceptance gates, and progress | -| Release readiness gaps | `docs/roadmap/release-readiness.md` | Outstanding release prerequisites and closure evidence | +| Release readiness evidence | `docs/roadmap/release-readiness.md` | Closed preparation gaps and outstanding candidate/publication evidence until roadmap retirement | | Historical release notes | `docs/releases/` | Version-specific historical summaries required by release policy | | Complete examples | `examples/` and executable Go examples | Maintained artifacts; prose links instead of duplicating full examples | -A listed future owner is not a requirement to create an empty file now. Create -API, consumer, example, and component documents when the corresponding behavior +A listed owner is not a requirement to create an empty file before it has useful +content. Create API, consumer, example, and component documents when the corresponding behavior exists and there is useful content to own. Link only to existing files; use plain -code paths for planned files. Release prerequisites have the explicit temporary -exception recorded in the release policy. +code paths for planned files. Unvalidated candidate and publication work has the +explicit temporary exception recorded in the release policy. ConfigKit has no owned HTTP service, application CLI, or runtime operations interface. Do not create those reference documents for consuming applications. diff --git a/docs/policy/release.md b/docs/policy/release.md index 6f3178f..2104e5f 100644 --- a/docs/policy/release.md +++ b/docs/policy/release.md @@ -222,17 +222,21 @@ restating or redefining its requirements. ### Temporary bootstrap deferral -The release procedure (`docs/release.md`) and complete release-check command are -not yet present because ConfigKit has no Go module or implementation to validate. -Their creation is temporarily deferred during initial development. This exception -permits implementation work; it does not permit publishing a release without -any gate required above. +The release procedure and automated source-candidate checker now exist, but no exact +release candidate, version-matched release note, native macOS execution evidence, or +post-publication module retrieval has been validated. Initial implementation may be +completed without fabricating those later facts. This exception does not authorize +publication or waive any candidate, platform, remote, or consumer-verification gate. -Compensating controls: no release may be published until the -[release-readiness gaps](../roadmap/release-readiness.md) are closed, the runbook -and checker exist, and the exact candidate passes all applicable gates. The -maintainer preparing the first release owns closure. Remove this temporary -exception when readiness is verified. Any other exception requires a documented repository constraint, rationale, +Compensating controls: the [release procedure](../release.md) keeps every unvalidated +step as an explicit maintainer gate, and the +[release-readiness evidence](../roadmap/release-readiness.md) records what is closed +and outstanding. No release may be published until an exact committed candidate has +the required release note, passes all applicable automated and native-platform gates, +is verified against `origin/main`, and completes guarded tag publication and clean +consumer retrieval. The maintainer preparing the first release owns closure. Remove +this temporary exception when that evidence is recorded and obsolete roadmap files +are retired. Any other exception requires a documented repository constraint, rationale, compensating controls, scope, and closure condition here; none is currently adopted. Do not silently weaken the base integrity, validation, documentation, verification, or immutability guarantees. diff --git a/docs/policy/testing.md b/docs/policy/testing.md index 682d3ee..d769c3b 100644 --- a/docs/policy/testing.md +++ b/docs/policy/testing.md @@ -39,7 +39,7 @@ boundaries. No credential bytes may appear in inspection or failure fixtures' expected output; use disposable canaries to prove non-disclosure. Baseline commands belong in the [development guide](../development.md). Release -gates belong in the [release policy](release.md) and its eventual runbook. This +gates belong in the [release policy](release.md) and [maintainer runbook](../release.md). This policy does not imply that those tools or tests already exist. ## Every test has a cost diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..706d260 --- /dev/null +++ b/docs/release.md @@ -0,0 +1,240 @@ +# ConfigKit Release Procedure + +This is the maintainer runbook for ConfigKit's [release policy](policy/release.md). +ConfigKit publishes one source-only Go module: the immutable source tag and the +matching checked-in release note are the deliverables. There is no application +binary, container image, generated package, or separate registry publication. + +## Release identity and support + +- Module: `gitea.maximumdirect.net/eric/ConfigKit` +- Primary branch: `main` +- Authoritative remote: `origin`, expected to fetch from + `https://gitea.maximumdirect.net/eric/ConfigKit.git` +- Tags: stable Semantic Versions such as `v0.1.0`; prerelease tags are not supported +- Minimum Go language/toolchain line: Go 1.25, tested with a supported patched + Go 1.25 toolchain and one newer supported toolchain +- Build targets: Linux and macOS on amd64 and arm64 + +The automated checker cross-builds all four targets. Before release, the maintainer +must also obtain native test, race, symlink, and permission-fixture evidence on Linux +and macOS; cross-compilation is not native execution evidence. + +## 1. Prepare access and tools + +The release maintainer owns this procedure. Work in a dedicated clone with Git, Bash, +and two supported Go toolchains available. Configure private-host Git and Go module +access through the normal credential manager before starting. Keep tokens, passwords, +SSH material, and environment dumps outside the repository and command transcript. +Verify access without displaying credentials: + +```sh +git remote get-url origin +git ls-remote origin HEAD +``` + +The remote URL must equal the authoritative URL above. Select the latest supported +patch in the Go 1.25 line and a newer supported toolchain. The values below are the +toolchains used while this runbook was introduced; update them to supported patch +releases during candidate preparation and record the actual versions in the release +note or validation record. + +```sh +export CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5 +export CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7 +GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go version +GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go version +GOTOOLCHAIN="$CONFIGKIT_BASELINE_TOOLCHAIN" go mod download +GOTOOLCHAIN="$CONFIGKIT_NEWER_TOOLCHAIN" go mod download +``` + +Those commands explicitly acquire toolchains and dependencies. The checker itself +sets `GOPROXY=off` and fails if its required cache is incomplete. + +## 2. Select the version and review compatibility + +Choose an unused stable version and compare the candidate with the previous tag. +For the first release, review the whole public surface and current contracts. + +```sh +release_version=v0.1.0 +printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || : +if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then + printf 'remote tag already exists: %s\n' "$release_version" >&2 + exit 1 +else + test $? -eq 2 +fi +git log --oneline --decorate --no-merges +``` + +Review exported Go signatures and ownership, YAML and scalar behavior, precedence, +diagnostic codes, minimum Go version, and platform support. Before `v1.0.0`, any +intentional incompatible minor-release change must be explained with consumer action; +patch releases must remain compatible within their minor line. Review the maintained +consumer examples and the migration guide even when signatures did not change. + +Reassess `go.yaml.in/yaml/v4` rather than using `@latest` implicitly: + +```sh +go list -m -versions go.yaml.in/yaml/v4 +go list -m -json go.yaml.in/yaml/v4@latest +``` + +If changing the explicit pin, make that a reviewed dependency change and rerun the +complete checker, especially parser policy, limits, diagnostics, and rendering tests. +If retaining a prerelease pin, record that decision in Compatibility. + +## 3. Write the release note + +Create `docs/releases/$release_version.md` before selecting the candidate. It must +begin with `# ConfigKit $release_version` and contain `Summary`, `Compatibility`, +`Upgrade`, and `Changes` sections. Use “No action required” where appropriate. Link +to current canonical contracts instead of copying their definitions. Do not create a +release note until there is a real candidate and reviewed content. + +## 4. Validate the source candidate + +Run the complete checker from the repository root after dependencies have been +acquired. It validates module manifests in a disposable copy, both supported Go +toolchains, offline default tests, the native race detector, vet, native builds, +CGO-disabled cross-builds, formatting and whitespace (including untracked files), +examples, package isolation, and local documentation links. + +```sh +./scripts/release-check.sh +``` + +`go vet` is the repository's static-analysis baseline. No additional linter is +claimed. Links to current ConfigKit files and headings are strict. Recorded links to +sibling consumer source are checked when those checkouts exist and are otherwise +inapplicable in an isolated clone; their immutable revisions remain recorded in the +extraction inventory. + +Complete these manual gates and record their evidence with the candidate: + +- The maintainer reviews compatibility and downstream migration impact against + `docs/config.md`, Go API comments, and the consumer guide. +- The maintainer inspects `git status --short`, `git diff`, and the candidate file + list for credentials, private data, generated output, unintended files, and + unrelated changes. +- The maintainer records successful native checker runs on Linux and macOS, including + filesystem capability tests. If either host is unavailable, release remains + blocked unless platform support is formally narrowed in current documentation. +- The maintainer removes completed, obsolete documents under `docs/roadmap/`, repairs + inbound links, and reruns the checker. ADR rationale remains historical. +- The maintainer reviews the exact version-matched release note for accuracy and + confirms it describes only deliverables that will actually be published. + +## 5. Commit, push, and record the candidate + +Commit all candidate source, contract, example, roadmap-retirement, and release-note +changes, rerun the checker against that clean commit, then push only `main` and record +the exact commit in Git metadata local to the clone: + +```sh +git add . +git commit --message "Prepare ConfigKit $release_version" +./scripts/release-check.sh +test -z "$(git status --porcelain=v1 --untracked-files=all)" +git push origin HEAD:refs/heads/main +git rev-parse HEAD | tee .git/configkit-release-candidate +candidate_commit=$(cat .git/configkit-release-candidate) +``` + +If review changes the source after this point, create and validate a new candidate; +do not reuse the recorded commit. + +## 6. Run publication guards + +Immediately before tagging, fetch the authoritative branch and tags, restore the +recorded variables if this is a new shell, and execute every guard: + +```sh +release_version=v0.1.0 +candidate_commit=$(cat .git/configkit-release-candidate) +git fetch --prune origin main +git fetch --tags origin +test "$(git branch --show-current)" = main +test -z "$(git status --porcelain=v1 --untracked-files=all)" +test "$(git rev-parse HEAD)" = "$candidate_commit" +test "$(git rev-parse refs/remotes/origin/main)" = "$candidate_commit" +test "$(git remote get-url origin)" = 'https://gitea.maximumdirect.net/eric/ConfigKit.git' +printf '%s\n' "$release_version" | grep -Eq '^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$' +test -f "docs/releases/$release_version.md" +test "$(sed -n '1p' "docs/releases/$release_version.md")" = "# ConfigKit $release_version" +for heading in Summary Compatibility Upgrade Changes; do + grep -Fxq "## $heading" "docs/releases/$release_version.md" +done +git show-ref --verify --quiet "refs/tags/$release_version" && exit 1 || : +if git ls-remote --exit-code --tags origin "refs/tags/$release_version" >/dev/null; then + printf 'remote tag already exists: %s\n' "$release_version" >&2 + exit 1 +else + test $? -eq 2 +fi +``` + +Confirm the recorded automated and manual approvals apply to exactly +`$candidate_commit`. A successful older run is not evidence for a changed commit. + +## 7. Tag and publish the exact ref + +Create one annotated tag at the guarded commit, verify its peeled target, and push +only that ref. Do not use `git push --tags`. + +```sh +git tag --annotate "$release_version" --message "ConfigKit $release_version" "$candidate_commit" +test "$(git rev-list -n 1 "$release_version")" = "$candidate_commit" +git push origin "refs/tags/$release_version:refs/tags/$release_version" +``` + +The pushed tag completes the declared source-only publication. No Gitea release +object or binary upload is required. + +## 8. Verify publication and clean consumer retrieval + +Verify the authoritative tag and then retrieve that exact version into a new module +without a local `replace` directive. Private-host credentials must already be +configured. Use direct origin retrieval unless an explicitly approved internal Go +proxy is the publication channel. + +```sh +remote_tag_commit=$(git ls-remote origin "refs/tags/$release_version^{}" | awk '{print $1}') +test "$remote_tag_commit" = "$candidate_commit" +consumer_dir=$(mktemp -d) +( + cd "$consumer_dir" + go mod init example.invalid/configkit-release-check + GOPRIVATE=gitea.maximumdirect.net GOPROXY=direct go get "gitea.maximumdirect.net/eric/ConfigKit@$release_version" + cat > main.go <<'EOF' +package main + +import configkit "gitea.maximumdirect.net/eric/ConfigKit" + +func main() { + _ = configkit.NoSource() +} +EOF + gofmt -w main.go + go test ./... + go build ./... + go list -m -f '{{.Path}} {{.Version}}' gitea.maximumdirect.net/eric/ConfigKit +) +rm -rf -- "$consumer_dir" +``` + +The final `go list` output must show the exact module path and `$release_version`. +Record the result with the release evidence. + +## 9. Handle failures without replacement + +Before publication, fix the candidate, rerun every gate, commit, push, and record a +new exact candidate. An unpublished local tag may be deleted only after verifying it +was never pushed or used by another release channel. + +After publication, never move, delete, or recreate the tag and never replace its +release note. Correct the defect on `main`, choose a new semantically appropriate +version, and repeat this procedure. A supported advisory or deprecation mechanism +may warn consumers about the old version but cannot change its contents. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index aab4008..5a41172 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -2,8 +2,8 @@ ## Status and ownership -Status: implementation in progress; Stages 1–15 are complete and later stages are -pending. +Status: implementation complete; Stages 1–22 are complete. Release-candidate +selection and publication remain separate maintainer work. Audience: a `gpt-5.6-terra` coding agent implementing one stage per prompt, in ascending order. Each stage depends on every earlier stage being complete. @@ -85,7 +85,7 @@ the adapter, YAML-policy, diagnostics, limits, and rendering regressions. A fina 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). +Follow the feature's [upstream isolation contract](../policy/architecture.md#public-api-and-dependency-direction). 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`. @@ -97,8 +97,8 @@ 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. +[foundation](../internal/extraction.md) and +[secret bootstrap](../config.md#typed-secret-references) 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 @@ -154,9 +154,9 @@ 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 +Follow the feature contracts for [secrets](../config.md#typed-secret-references), +[CLI overrides](../config.md#explicit-cli-overrides), and +[explanation](../config.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` @@ -303,8 +303,8 @@ Completion evidence (2026-09-06): No deviations were required. Paths, diagnostics, parsing, and all later behavior remain assigned to subsequent stages. -Read: feature [foundation](initial.md#existing-implementation-foundation), -[consumer fit](initial.md#consumer-fit-and-intentional-migrations), all policies. +Read: feature [foundation](../internal/extraction.md), +[consumer fit](../consumers/README.md), all policies. Tasks: @@ -353,8 +353,8 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; metadata population and configuration lifecycle behavior remain later work. -Read: feature [paths](initial.md#paths-and-presence), -[diagnostics](initial.md#provenance-diagnostics-and-redaction), public metadata API. +Read: feature [paths](../config.md#layered-metadata), +[diagnostics](../config.md#safe-failures), public metadata API. Tasks: @@ -408,8 +408,8 @@ Completion evidence (2026-09-06): `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; typed binding and decoding remain later work. -Read: feature [YAML policy](initial.md#yaml-and-scalar-policy), -[source constructors](initial.md#entry-point-and-reusable-application-definition). +Read: feature [YAML policy](../config.md#yaml-documents), +[source constructors](../config.md#single-source-loading). Start with the extraction map's parser and strict-loader cases; the old second-document acceptance defect is already fixed in Narratio. @@ -485,8 +485,8 @@ Completion evidence (2026-09-06): `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). +Read: feature [typed decoding](../config.md#defaults-and-typed-decoding) and +[type/projection contract](../config.md#application-schema-binding). Tasks: @@ -537,8 +537,8 @@ Completion evidence (2026-09-06): Go 1.26.7 on linux/amd64. No deviations were required; defaults and typed decoding remain later work. -Read: feature [ownership](initial.md#recorded-resolution-and-validation) and -[type/projection contract](initial.md#supported-types-and-projection). +Read: feature [ownership](../config.md#recorded-application-resolution) and +[type/projection contract](../config.md#application-schema-binding). Tasks: @@ -590,7 +590,7 @@ Completion evidence (2026-09-06): 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. +Read: feature [defaults/null](../config.md#defaults-and-typed-decoding) and typed decoding. Tasks: @@ -651,9 +651,9 @@ Completion evidence (2026-09-06): `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; resolution and semantic validation remain later work. -Read: feature [lifecycle](initial.md#resolution-lifecycle), -[version hook](initial.md#schema-checking-and-secret-classification), -[results](initial.md#results-errors-and-inspection). +Read: feature [lifecycle](../config.md#single-source-loading), +[version hook](../config.md#application-schema-binding), +[results](../config.md#safe-failures). Tasks: @@ -715,7 +715,7 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; semantic validation remains later work. -Read: feature [resolution callbacks](initial.md#resolution-and-validation-callbacks). +Read: feature [resolution callbacks](../config.md#recorded-application-resolution). Tasks: @@ -776,8 +776,8 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; imports and profiles remain deliberately unavailable. -Read: feature [diagnostic callbacks](initial.md#resolution-and-validation-callbacks) -and [schema reports](initial.md#schema-checking-and-secret-classification). +Read: feature [diagnostic callbacks](../config.md#recorded-application-resolution) +and [schema reports](../config.md#application-schema-binding). Tasks: @@ -824,7 +824,7 @@ Completion evidence (2026-09-06): [current composition contract](../config.md#strict-document-composition) owns the implemented semantics; public import wiring remains deferred to subsequent work. -Read: feature [composition table](initial.md#composition-and-overlay). +Read: feature [composition table](../config.md#strict-document-composition). Extract Narratio's additive composer and conflict tests before writing equivalent code. Change empty-map expectations to the feature table and retain exact locations. @@ -883,7 +883,7 @@ Completion evidence (2026-09-06): `go test -race ./...`, `go vet ./...`, `go build ./...`, `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. -Read: feature [import graph](initial.md#explicit-imports-and-source-identity). +Read: feature [import graph](../config.md#explicit-imports). 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. @@ -1008,7 +1008,7 @@ Completion evidence (2026-09-06): Verified `go test ./...`, `go test -race ./...`, `go vet ./...`, `go build ./...`, and `git diff --check` with Go 1.26.7 on linux/amd64. -Read: feature [profiles](initial.md#profiles), composition/overlay table. +Read: feature [profiles](../config.md#selected-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. @@ -1064,8 +1064,8 @@ Completion evidence (2026-09-06): 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. -Read: feature [presence](initial.md#paths-and-presence), -[history](initial.md#provenance-diagnostics-and-redaction), metadata API. +Read: feature [presence](../config.md#layered-metadata), +[history](../config.md#safe-failures), metadata API. Use Narratio's effective ownership and inspection tests as extraction evidence. Its source-only records need locations and complete historical operations. @@ -1123,8 +1123,8 @@ Completion evidence (2026-09-06): `go build ./...`, `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. -Read: feature [redaction](initial.md#provenance-diagnostics-and-redaction), -[secret predicate](initial.md#schema-checking-and-secret-classification), result API. +Read: feature [redaction](../config.md#safe-failures), +[secret predicate](../config.md#application-schema-binding), result API. Reuse Narratio's deterministic effective-output and non-disclosure cases. Render from the owned typed projection, without its marshal/parse reconstruction. @@ -1195,7 +1195,7 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; environment-backed typed secret references remain later work. -Read: [secret bootstrap contract](initial.md#secret-references-and-explicit-bootstrap), +Read: [secret bootstrap contract](../config.md#typed-secret-references), 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. @@ -1267,7 +1267,7 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; CLI overrides remain later work. -Read: [secret reference contract](initial.md#secret-references-and-explicit-bootstrap), +Read: [secret reference contract](../config.md#typed-secret-references), shared descriptor/projection rules and lifecycle, Stage 16 report boundary. Tasks: @@ -1337,7 +1337,7 @@ Completion evidence (2026-09-06): `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; safe historical-value explanation remains later work. -Read: [override contract](initial.md#cli-overrides), overlay/presence/version rules, +Read: [override contract](../config.md#explicit-cli-overrides), overlay/presence/version rules, source ordering, and the secret-reference boundary. Tasks: @@ -1403,7 +1403,7 @@ Completion evidence (2026-09-06): `go build ./...`, `go mod tidy -diff`, and `git diff --check` with Go 1.26.7 on linux/amd64. No deviations were required; consumer ergonomics remain later work. -Read: [explanation contract](initial.md#historical-value-explanation), redaction +Read: [explanation contract](../config.md#historical-value-explanation), redaction rules, and history bounds above. Generalize Narratio's semantic-record and inspection tests. They cover effective @@ -1598,7 +1598,47 @@ Acceptance: ## Stage 22 — Prepare release validation and reconcile documentation -Status: pending. +Status: complete. + +Completion evidence (2026-09-06): + +- Added the [maintainer release procedure](../release.md) for the existing + `origin/main`, module `gitea.maximumdirect.net/eric/ConfigKit`, source-only stable + tags, Go 1.25 minimum and newer-toolchain validation, and Linux/macOS amd64/arm64 + targets. It supplies candidate recording, clean branch/index/worktree and exact + remote-commit guards, version/release-note/tag checks, precise single-ref tag + publication, clean consumer retrieval without `replace`, and immutable correction + steps. Private-host access is a prerequisite without embedding credentials or + environment dumps. +- Added fail-fast [`scripts/release-check.sh`](../../scripts/release-check.sh). It + verifies cached dependencies and runs offline disposable-copy module hygiene, + default tests on Go 1.25.5 and Go 1.26.7, vet and native builds on both, a native + race run, CGO-disabled builds for linux/amd64, linux/arm64, darwin/amd64, and + darwin/arm64, repository-wide Go formatting and text whitespace including + untracked files, executable examples, consumer fixtures, public import isolation, + and local documentation links. Toolchain/dependency acquisition remains an + explicit preceding runbook step. +- Added `TestLocalDocumentationLinks`, which strictly verifies repository-local files + and Markdown headings while treating recorded sibling-source links as applicable + only when those optional checkouts exist. The runbook makes immutable extraction + revisions authoritative in isolated release clones. +- Reconciled README, development, configuration, consumer, architecture/testing/ + documentation/release policies, component and extraction inventories, and ADR + navigation with current behavior. Reduced the implemented feature roadmap to a + target record linking current owners and retaining only foundations plus explicit + non-goals/deferred scope. The implementation plan and readiness record remain until + an actual candidate retires completed roadmap files as required by release policy. +- Reassessed the YAML v4 dependency with `go list -m -versions` and + `go list -m -json ...@latest`; `v4.0.0-rc.6` remained the newest available release + on 2026-09-06, so the tested explicit pin was retained. `go doc -all` and + `TestDependencyImportBoundary` confirmed public documentation is renderable and no + upstream YAML type/import leaks outside the private adapter or maintained examples. +- The complete checker passed on linux/amd64 with Go 1.25.5 and Go 1.26.7. The + [release-readiness evidence](release-readiness.md) keeps exact candidate/release + note, native macOS execution, compatibility/content approval, roadmap retirement, + authoritative remote/tag guards, and post-publication retrieval explicitly open + under the revised bootstrap exception. No tag, push, downstream migration, release + note, or publication occurred. Read: release policy in full and [release-readiness gaps](release-readiness.md). diff --git a/docs/roadmap/initial.md b/docs/roadmap/initial.md index 7c19023..2f6f88b 100644 --- a/docs/roadmap/initial.md +++ b/docs/roadmap/initial.md @@ -1,1098 +1,83 @@ -# ConfigKit — Initial Target Design +# ConfigKit Initial Target Record ## Status and ownership -This roadmap describes the targeted end-state for ConfigKit's initial release, -provisionally `v0.1.0`. It is future design, not a claim of implemented behavior -or a stable v1 compatibility promise. Audience: maintainers and implementation -agents. +The initial feature target is implemented. This document now records the intended +outcome, source foundations, and deliberately deferred scope without duplicating the +current configuration contract or Go API reference. Ordered implementation history +and acceptance evidence remain in the [implementation plan](implementation.md) until +the maintainer retires completed roadmaps while preparing a release candidate. -This document owns target behavior, boundaries, and deferred scope. The -[implementation plan](implementation.md) owns stages, dependencies, acceptance -gates, and progress. Do not add implementation steps or stage status here. +The [architecture policy](../policy/architecture.md) owns responsibilities, +dependency direction, confidentiality, and the two success criteria. The +[configuration contract](../config.md) and Go declarations own implemented behavior. +The [consumer adoption guide](../consumers/README.md) owns consumer fit and migration +guidance. Accepted rationale remains in the [ADRs](../development.md#task-specific-reading-guide). -The [architecture policy](../policy/architecture.md) owns binding contributor -constraints. Accepted rationale is recorded in ADRs for -[composition versus overlay](../adr/0001-separate-composition-and-overlay.md), -[parser independence](../adr/0002-own-yaml-semantics.md), and -[presence and resolution history](../adr/0003-preserve-presence-and-resolution-history.md) -under the [documentation policy](../policy/documentation.md). As features land, -move their normative behavior to current contracts and replace the corresponding roadmap -text with links. Retire completed roadmap material during release preparation; -these files are not permanent duplicate specifications. +## Initial outcome -## Purpose and principles +ConfigKit is one opinionated Go library for strict YAML configuration loading. It +removes source handling, parsing, explicit import/profile traversal, composition, +precedence, presence tracking, provenance, secret lookup/bootstrap, diagnostics, and +safe inspection plumbing from applications. Applications retain their types, +defaults, schema-version policy, recorded normalization, semantic validation, CLI +parsing, remote acquisition, domain algorithms, resource checks, and runtime setup. -ConfigKit provides a consistent configuration lifecycle for Go applications -using YAML, including Seriatim, Audita, Narratio, WeatherFeeder, WeatherReporter, -and Distributor. Its goal is one deterministic, explainable resolution model -that works for a single file and scales to explicitly imported fragments. +The maintained Narratio-, Notarius-, WeatherReporter-, and Seriatim-shaped examples +demonstrate both architecture success criteria: simple consumers avoid optional +composition machinery, complex consumers use the same conventions, and domain logic +remains recognizable application code. These examples are migration evidence, not a +claim that any sibling application has migrated. -- YAML supplies syntax; ConfigKit owns composition, precedence, presence, - conversion, and error policy. -- Preserve a source-located tree through composition and overlay. Do not first - collapse input into `map[string]any` or application structs. -- Keep application types ordinary. Pointers express application nullability, - not a universal workaround for distinguishing absent from false or zero. -- Separate strict composition from intentional overlay. -- Keep the public API small and the parser private. Avoid a general provider or - merge-strategy framework. +## Implementation foundations -### Success criteria +The implementation was generalized from maintainer-authorized application code and +tests at fixed clean revisions. The [extraction inventory](../internal/extraction.md) +records exact revisions, symbols, adapted cases, intentional semantic differences, +and the boundary that keeps sibling checkouts out of builds and tests. Links from +that inventory or historical implementation evidence to sibling source are optional +in isolated clones; immutable revision references remain authoritative evidence. -1. **Reduce application infrastructure:** Can applications delete substantial - configuration infrastructure while retaining clear ownership of their domain - rules? -2. **Unify configuration conventions:** Do consumers adopt one consistent - configuration model, with only the flexibility needed to express genuine - application requirements? +The module uses `go.yaml.in/yaml/v4` only behind `internal/yaml`. ConfigKit owns the +document model, field binding, scalar grammar, errors, limits, and output behavior. +No upstream type, option, plugin, error chain, hidden parser value, or generic +constraint crosses the public API. The package-level import-boundary test enforces +this isolation for production, tests, and maintained examples. -The [architecture policy](../policy/architecture.md#success-criteria) owns these -coequal criteria and their governing rules. The target must satisfy both: the -shared lifecycle removes configuration plumbing, while common syntax, presence, -composition, precedence, and decoding rules establish consistent conventions. -Application types, interpretation, defaults, and semantic validation retain the -flexibility needed to describe domain requirements. +## Current feature owners -Consumer migrations are expected, including intentional configuration syntax -changes. Existing code and tests are extraction foundations and evidence of needs, -not compatibility obligations for ConfigKit. Prefer canonical object forms over -consumer-specific shorthand where they express the same information. Do not add -alternate parsers or merge rules merely to preserve an existing convention. - -Evaluate an extension by identifying its concrete domain requirement, explaining -why the existing primitives cannot accommodate it cleanly, and showing how it -preserves one coherent model. If application code must remain, distinguish domain -interpretation and validation from duplicated configuration infrastructure. A -restriction that makes useful information inexpressible is not justified by -consistency alone. - -The consumer examples and adoption work must demonstrate both reduced plumbing -and convergence on the model. Narratio extraction and simpler consumers should -supply concrete evidence; feature count, unchanged legacy syntax, and fewer lines -inside ConfigKit are not measures of success. Detailed consumer migrations and -execution steps belong in the [implementation plan](implementation.md). - -## Responsibility boundary - -| ConfigKit owns | Applications own | +| Initial feature area | Current owner | | --- | --- | -| Local file and labeled bytes/reader inputs | Main-file search paths, bootstrap timing, and CLI parsing/wiring | -| YAML policy, explicit import traversal, composition, profiles, and explicit CLI overrides | Configuration types and field meanings | -| Presence, source locations, resolution records, and safe diagnostics | Static defaults, derived defaults, and normalization rules | -| Secret-reference resolution and explicit directory bootstrap | Credential use and process startup coordination | -| Strict typed decoding and lifecycle coordination | Semantic validation and schema-version policy | -| Source reporting, safe history/explanation, and redacted effective output | Remote acquisition, migrations, runtime initialization, and resource checks | - -Applications may fetch remote data and pass labeled bytes to ConfigKit. This -provides useful provenance without making ConfigKit responsible for network -availability, credentials, or caching. Narratio's pipeline/campaign/session -fallback rules remain application semantics; the extraction boundary is described -below. - -## Existing implementation foundation - -Narratio is the primary extraction source and intended first substantial consumer -of ConfigKit. Prefer copying and generalizing applicable existing code and tests -over independently rebuilding equivalent mechanisms. Reuse includes the behavior -captured by Narratio's tests, not just production code. ConfigKit's target -contracts govern the result; reuse does not require preserving incidental -application structure or incompatible behavior. - -The references below were inspected on 2026-09-06 at Narratio commit -`b68435128be1e24b94fc7ec259d6e9093669fd99`. Paths refer to the sibling checkout -`../narratio` relative to the ConfigKit repository root; the links require that -checkout layout. They are extraction references, not a runtime or build dependency. -Recheck them against the source revision actually used for extraction. - -| Foundation | Source references | Intended treatment | -| --- | --- | --- | -| File and labeled reader/bytes loading; strict field checking | [load.go](../../../narratio/internal/config/load.go): `decodeStrictYAML`, `decodeStrictYAMLFromReader`, `LoadSessionBytesWithOptions` | Extract reusable input handling and generalize decoding behind ConfigKit's private parser boundary | -| Node parsing, strict composition, overlays, and canonical rendering | [composition.go](../../../narratio/internal/config/composition.go): `parseCompositionDocument`, `buildCompositionNode`, `mergeAdditiveCompositions`, `mergeOverlayComposition`, `canonicalYAML` | Extract algorithms and tests; replace upstream node kinds, path spelling, scalar semantics, and incompatible merge rules | -| Explicit imports and profile selection | [pipeline_composition.go](../../../narratio/internal/config/pipeline_composition.go): `loadPipelineCompositionSources`, `loadPipelineImports`, `loadPipelineProfileOverlays`, `selectPipelineProfile` | Reuse traversal/selection mechanics with ConfigKit's recursive graph, containment, directive syntax, and fixed selected-profile convention | -| Effective value/source inspection | [effective_pipeline.go](../../../narratio/internal/config/effective_pipeline.go): `EffectivePipelineValues`, `EffectivePipelineSources`, `MarshalEffectivePipeline`; [config_commands.go](../../../narratio/internal/app/config_commands.go) | Reuse deterministic projection, ownership, and side-effect-free inspection cases; retain application commands and generated-domain attribution in Narratio | -| Explicit field presence | [normalize.go](../../../narratio/internal/config/normalize.go): `NormalizeConfig.UnmarshalYAML` | Generalize the requirement into library metadata; replace the application-specific parser hook | -| Default application | [load.go](../../../narratio/internal/config/load.go): `applyPipelineDefaults` and related helpers; [defaults.go](../../../narratio/internal/config/defaults.go) | Reuse applicable mechanics and behavioral cases while keeping actual default values and domain rules application-owned | -| Resolution and source attribution | [load.go](../../../narratio/internal/config/load.go): `Resolve`, `mergeCampaignSession`, `selectStableInput`; [config.go](../../../narratio/internal/config/config.go): `ResolvedInputFile`, `SessionSource` | Adapt source tracking into generic provenance; retain campaign/session fallback and domain types in Narratio | -| Application-supplied remote bytes | [config_loader.go](../../../narratio/internal/app/config_loader.go) | Use as evidence for labeled input support; keep fetching, storage clients, and remote metadata acquisition outside ConfigKit | -| Existing behavioral coverage | [normalize_test.go](../../../narratio/internal/config/normalize_test.go), [load_validate_test.go](../../../narratio/internal/config/load_validate_test.go), [campaign_config_test.go](../../../narratio/internal/config/campaign_config_test.go) | Extract relevant cases and minimal fixtures; preserve domain-specific assertions in consumer tests | - -Additional extraction coverage includes [composition tests](../../../narratio/internal/config/composition_test.go), -[import tests](../../../narratio/internal/config/pipeline_composition_test.go), -[profile tests](../../../narratio/internal/config/pipeline_profiles_test.go), -[strict-loader tests](../../../narratio/internal/config/loader_strict_test.go), and -[inspection tests](../../../narratio/internal/app/config_commands_test.go). -These are reusable behavior evidence, not an instruction to preserve every assertion. - -### Compatibility and adaptation - -Extraction must distinguish preserved behavior, intentionally changed behavior, -and application-only behavior. Record the source revision and relevant symbols -with the extraction work so reviewers can compare the implementation and tests. -Generalized code must not import Narratio's internal packages or copy its domain -model into ConfigKit's public API. - -Known adaptation points include: - -- Some Narratio defaulting helpers use an empty string to trigger a default. - Adapt these to ConfigKit's [presence and defaults rules](#static-defaults-and-null) - rather than losing explicitly configured empty values. -- `decodeStrictYAMLFromReader` now rejects trailing documents. Preserve and - generalize its strict-loader regressions under ConfigKit's single-document policy. -- The custom normalization decoder and pointer-based presence conventions need - to give way to generic metadata while preserving genuine domain nullability. -- Existing composition nodes and effective-field ownership are substantial - foundations, but do not retain the full line/column and historical-value records - ConfigKit requires. Keep application-generated family/campaign attribution as - domain resolution with recorded dependencies. -- Narratio rejects overlapping empty mappings, null overlays, and cross-kind - overlays. ConfigKit's operation table deliberately permits those cases. Adapt - the implementation and expectations together. -- Narratio declares import paths and profiles in a root `composition` envelope, - supports a declared default profile, and loads every declared overlay. ConfigKit - uses `_configkit.imports`, recursive imports/globs, and only the selected fixed - profile path; `default` means no overlay. Do not preserve an alternate convention. -- Narratio uses `os.SameFile` to reject filesystem aliases. ConfigKit uses canonical - path identity, treats hard links as distinct paths, and enforces root containment. -- Narratio's owned tree still embeds upstream YAML kinds/tags and dotted/indexed - paths. Its final binding and effective inspection reserialize/reparse YAML. - Reuse algorithms without importing those dependencies into ConfigKit's document - model or replacing direct descriptor-driven decoding and projection. -- Narratio's configuration diffs and digests are useful consumer evidence, but - remain outside ConfigKit's initial scope. Their implementation does not supply - full historical-value explanation or authorize a new public API. - -Each intentional behavior change needs focused tests and an explicit migration -account for the affected consumer. Do not copy an old expectation merely because -it passes in Narratio, or discard a useful case because the implementation moves. -Use representative Narratio configurations to verify preserved outcomes and -identify intentional differences. Exercise a simpler consumer as well so the -public API remains practical for single-document applications. - -### Capabilities requiring broader implementation - -The inspected Narratio loader supplies explicit flat imports, strict composition, -profile overlays, source ownership, and effective inspection in addition to its -pipeline/campaign/session domain coordination. Prefer extracting those foundations -rather than rebuilding equivalent mechanisms. - -ConfigKit still requires new or substantially revised work for recursive import -graphs and globs, bounded canonical containment, its conservative YAML policy, -schema-aware defaults/direct decoding, comprehensive location and value history, -recorded domain resolution, generic secret classification, and safe structured -reporting. Existing provenance is a starting point, not full explainability. - -Automatic `conf.d` discovery remains deferred. Existing application relationships -and declared profile paths do not redefine ConfigKit's import/profile conventions. -The [implementation plan](implementation.md) owns extraction tasks and ordering. - -## Consumer fit and intentional migrations - -These are adoption targets, not promises of unchanged consumer behavior or -completed integrations. Source links refer to sibling checkouts as in the -Narratio foundation section. The findings reflect the inspected implementations -on 2026-09-06 and must be rechecked at migration time. - -| Consumer and evidence | Useful ConfigKit fit | Intentional migration or retained application responsibility | -| --- | --- | --- | -| Narratio: [configuration loader](../../../narratio/internal/config/load.go) | Shared loading, presence, defaults coordination, and source attribution | Replace ad hoc presence/empty-value defaults; retain campaign/session domain resolution | -| Notarius: [file model](../../../notarius/internal/core/config/file_config.go), [environment handling](../../../notarius/internal/core/config/env.go) | Strict nested models, named pipelines, presence-aware values, and diagnostics | Use canonical object bindings/references and string schema versions; adapt dynamic integer consumers to `int64`; migrate configuration-field environment overrides to files/profiles; keep catalog resolution and runtime invocation choices application-owned | -| WeatherReporter: [loader](../../../weatherreporter/internal/config/load.go), [module options](../../../weatherreporter/internal/config/reports.go) | Defaults-only loading, durations, report maps, list presence, and typed module options | Replace string shorthand with object entries and private presence flags with metadata; translate explicitly set configuration flags into ConfigKit overrides; reuse secrets-directory loading through explicit ConfigKit bootstrap | -| Seriatim: [configuration surfaces](../../../seriatim/docs/config.md), [speaker rules](../../../seriatim/internal/speaker/map.go), [autocorrect rules](../../../seriatim/internal/autocorrect/autocorrect.go) | Simple typed loading of ordered YAML rule files, normalization, and cross-rule diagnostics | Adopt strict YAML rejection deliberately; retain matching/replacement algorithms, resource checks, and CLI/environment runtime settings; no central application YAML file is required | - -WeatherReporter's updated [configuration tests](../../../weatherreporter/internal/config/config_test.go) -and [external constructed-config tests](../../../weatherreporter/internal/config/overrides_external_test.go) -cover explicit empty lists, consistent loaded/constructed behavior, and normalization -without mutating source options. Preserve these domain outcomes through ConfigKit's -presence, detached ownership, and application validation. ConfigKit permits an empty -list structurally; a particular consumer may reject it semantically. - -The initial model standardizes syntax and resolution while preserving application -field meanings. Module shorthand and integer schema versions are syntax migration -issues; module-specific option schemas are genuine domain variation addressed -[below](#module-specific-options). ConfigKit configuration profiles are overlays, -not substitutes for application-owned LLM or pipeline profile concepts. - -## External values and invocation state - -Configuration-field CLI flags use the explicit [override API](#cli-overrides). -Applications own parsing flags and selecting sources/profiles. Separate invocation -state, such as an input artifact or execution lane, remains application-owned. -General environment-variable configuration overlays are an explicit non-goal, -not deferred work. Environment access is reserved for named secret references. - -Do not inject hidden overrides through captured environment/CLI values in default -factories, resolver hooks, or validators. Domain resolution follows the declared -input layers. Mutating `Result.Config` cannot update a validated snapshot. - -## Secret references and explicit bootstrap - -Secret-bearing configuration fields use ConfigKit's opaque `Secret` type. Their -YAML value is an environment-variable name, for example `api_key: OPENROUTER_API_KEY`. -Names are public configuration; resolved credentials are not configuration-tree -values. Literal-secret input, interpolation, fallback literal values, dotenv -parsing, and implicit directory discovery are unsupported. Names must match -`[A-Za-z_][A-Za-z0-9_]*`. A string that happens to be a valid name is always a -reference; ConfigKit cannot detect credentials mistakenly written in other fields. - -```go -func SecretEnv(name string) Secret -func (s Secret) EnvName() string -func (s Secret) Value() string -func (s Secret) IsResolved() bool -``` - -`SecretEnv` creates an unresolved reference; invalid names fail the containing -load. The zero value is an absent reference, never a literal secret. A non-pointer -`Secret` is required: absent, null, empty/invalid name, unset environment variable, -or an empty environment value fails with a safe `secret.*` diagnostic. Use -`*Secret` for optional credentials: nil is allowed, but every non-nil reference -must resolve to a nonempty value. Applications validate conditional requirements. -Only typed fields/maps/slices establish secret-reference semantics; dynamic `any` -strings are ordinary values and must migrate to typed secret slots when needed. - -Recognize `Secret` before ordinary struct/text binding. Projection, comparison, -cloning for defaults, and resolution auditing operate on the name only. Resolve -the surviving references after recorded application resolution and before semantic -validation, using one `os.LookupEnv` result per unique name per load. A copied -previous result used as defaults is re-resolved; it cannot carry stale credentials. -Validation can use `Value()`; resolution works with names. In addition to ordinary -projection auditing, reject validator changes to a secret's private resolved state -by comparing it with this load's lookup result. A same-name unresolved replacement -must not evade the read-only validator contract. `Value()` returns empty -on unresolved references; `IsResolved()` distinguishes that state. Private credential -storage is excluded from every tree, historical snapshot, diagnostic, default -format, JSON/text serialization, and inspection path. Provide safe formatting for -all fmt verbs and name-only JSON marshaling; expose no credential decoder/codec. -Only the deliberately named `Value()` accessor returns a credential. Applications -own its use and must not embed it in hook messages. Secret references themselves -need no `configkit:"secret"` tag: the existing tag/predicate classifies additional -sensitive or derived values and can intentionally hide a reference name. - -The separate, opt-in bootstrap operation below loads a directory into the process -environment. `Load` never calls it or changes process-global state. Applications -choose a directory and call bootstrap once during startup before concurrent work. -Bootstrap paths are native application-selected paths, independent of import roots. -An empty or missing directory path is an error; an existing empty directory succeeds. - -```go -func LoadSecretsDir(directory string) (*SecretsReport, error) -func (r *SecretsReport) Entries() []SecretFileResult -func (r *SecretsReport) WriteTo(output io.Writer) (int64, error) -type SecretsError struct { /* private safe diagnostic storage */ } -func (e *SecretsError) Error() string -func (e *SecretsError) Diagnostics() []Diagnostic -type SecretFileResult struct { - Name, Path string - Status SecretFileStatus - ExistingEmpty bool -} -type SecretFileStatus string -// Constants use values "loaded", "skipped_existing", "not_applied", "failed": -// SecretFileLoaded, SecretFileSkippedExisting, SecretFileNotApplied, SecretFileFailed. -``` - -Extract applicable code and tests directly from WeatherReporter's -[secrets loader](../../../weatherreporter/internal/config/secrets.go), inspected -at revision `5d306a202471e10fe98923a1df27d0b0fce5c666`. Reuse its filename checks, -file-type rejection, single terminal LF/CRLF removal, and separation of `loadSecrets` -from `applySecrets`. WeatherReporter now preloads file contents, validates its -configuration, then applies environment writes with attempted rollback. ConfigKit -preserves existing variables, reports partial apply failure without rollback, and -keeps bootstrap separate from configuration loading. Record extraction symbols, -revision and adaptations during implementation. - -Narratio's [secrets loader](../../../narratio/internal/app/secrets_env.go) at the -revision recorded above is a complementary foundation: `loadSecretsFromConfigWithDependencies` -preserves existing variables including empty ones, and `readAPIKeyFile` uses -[bounded reads](../../../narratio/internal/fileops/read_bounded.go). Its -[tests](../../../narratio/internal/app/secrets_env_test.go) provide reusable failure -and environment-preservation cases. Adapt its immediate writes and aggregate/log -reporting to ConfigKit's preflight and per-file structured report. Narratio skips -invalid names, removes all trailing CR/LF characters, rejects a symlink at the -selected directory path, and enforces restrictive permission bits. Those policies -are not inherited: use ConfigKit's name/type/newline/directory rules and fixed -bounds. ConfigKit does not add a permission-mode gate in the initial API; -applications own deployment permissions. Extract bounded-read mechanisms without -copying Narratio's application dependency framework or logger. - -Process only immediate entries, sorted by filename. Reject invalid names, -symlinks, directories, and nonregular files, including entries whose names are -already set. Allow a symlink in the explicitly selected directory path by resolving -it to a canonical directory first. Validate all entry metadata before mutation. -`os.LookupEnv` presence wins, including an empty existing variable: skip reading -that file's contents and report `SkippedExisting` with `ExistingEmpty` set -accurately. Read eligible files with bounds, remove exactly one terminal CRLF or -LF, preserve all other whitespace, and reject NUL bytes. Empty file values are -allowed at bootstrap, but fail if subsequently used by a required reference. -Preflight all eligible reads before any `os.Setenv`. No writes occur on preflight -failure. At apply time recheck presence before each write and skip newly set names. -Concurrent mutation is unsupported; this is not an atomic compare-and-set facility. - -Return a non-nil report even on failure. Successful writes are `Loaded`, blocked -files are `SkippedExisting`, the failed entry is `Failed`, and eligible but unwritten -entries are `NotApplied`. Directory-level failures have a safe error without a -fabricated entry. Stop on the first deterministic failure; reports include all -entries enumerated so far, with unprocessed entries `NotApplied`. An apply failure -preserves earlier successful writes and reports partial progress; do not attempt -rollback that could overwrite unrelated process changes. Errors are `*SecretsError` -with safe `secrets.*` diagnostic codes and no raw unwrap chain. Errors and report -rendering contain names, paths, statuses, and safe classifications, never contents. - -Report entries and diagnostic slices are defensive copies. Bootstrap diagnostics -include safely quoted canonical paths in messages with no invented configuration -source ID or source location. -Report formatting escapes control characters and propagates short writes/errors. - -The report is the authoritative bootstrap provenance: a blocked file explicitly -identifies the existing process variable as the winner, even when empty. Keep it -alongside the load result and render it before configuration explanation in an -application command. Secret-resolution metadata records the variable name and -`resolved`, `unset`, or `empty` lookup outcome (failures in diagnostics). It does -not claim which external actor populated the process environment. Reports are -separate snapshots, not accepted as evidence of the environment's later state; -there is no implicit global report or stale filename attribution in `Load`. - -## CLI overrides - -Precedence is **application defaults < composed base < selected profile < explicit -CLI overrides**. Domain normalization follows those inputs and is recorded. - -```go -type Override struct { - Path Path - Value any - Flag string -} -// Options[T].Overrides is []Override. -``` - -Applications provide records only for flags explicitly set by the caller; parser -defaults are not overrides. `Flag` is a required, non-secret display name, such as -`--workers`, never the raw argument or value. ConfigKit has no CLI parser dependency. -Values are detached and converted to ConfigKit-owned nodes before composition: -accept built-in string, bool, integer and unsigned integer widths, finite floats, -`nil`, `[]any`, and `map[string]any` recursively. Strings remain strings; do not -parse YAML fragments or coerce textual booleans/numbers. Durations and secret -references use strings. Reject other types, cycles, nonfinite numbers, and limits -violations with safe `override.*` diagnostics. - -Validate every path/record before applying any. Reject root paths, sequence-index -traversal, duplicate paths, and ancestor/descendant overlaps within the override -list. Numeric tokens are mapping keys only here. Mapping-key paths may create -missing mapping ancestors, but cannot traverse an existing scalar/null/sequence. -The target path overlays with the same rules as profiles: mappings recursively -merge; sequences and all other replacements are atomic. Use a whole sequence -value to replace a list. Preserve supplied list order; mapping entries use canonical -key order. Unknown struct fields and effective type mismatches fail normal strict -decoding. Never allow `_configkit` directives or writes to the configured schema -version path, including ancestor replacement. Overrides also work with `NoSource()`. - -Each record contributes a synthetic CLI source after file/profile sources, in -record order, labeled with `Flag`, with no file coordinates. Explicit false, zero, -empty string, null, and empty list remain present. Snapshot caller-owned values -before applying them. History includes replaced inputs even for equal-value writes. -Neither callers nor hooks may mutate supplied data concurrently with loading. - -## Resolution lifecycle - -```text -application selects source and loading options - → parse and enforce YAML policy for every source - → discover explicit imports and strictly compose the base - → application schema-version check, when configured - → apply the selected profile - → apply explicit CLI overrides to the node tree - → apply static defaults and strictly decode - → apply recorded application resolution and normalization - → resolve surviving secret references from the environment - → collect application semantic validation diagnostics - → return independently owned configuration and metadata -``` - -Value precedence is defaults < composed base < selected profile < CLI overrides. Resolution is -an application transformation after those layers, not another user override -source. Future migration support belongs after base composition/version -inspection and before profile application and typed decoding; migrations are -not part of the initial release. - -YAML policy applies to every document even when a later layer would replace its -values. Schema/type checks apply to the effective input tree; application -validation runs only after successful decoding and resolution. An invalid -configuration is never returned as a successful, usable result. - -## YAML and scalar policy - -Source ownership, the supported YAML subset, scalar grammar, fixed input limits, -and safe failure behavior are implemented. The [current configuration input -contract](../config.md) now owns those definitions, including typed scalar -conversion through the current loading lifecycle. - -## Paths and presence - -Semantic path construction, JSON Pointer parsing, and layered metadata population -are implemented. Their current contracts are owned by the Go -documentation in [`path.go`](../../path.go), [`metadata.go`](../../metadata.go), -and the [current metadata contract](../config.md#layered-metadata), including -presence, composite origins, parent replacements, removed-path history, and -synthetic resolution operations. - -## Composition and overlay - -Strict base composition and selected-profile overlay are implemented and owned by -the [current composition contract](../config.md#strict-document-composition) and -[profile contract](../config.md#selected-profiles). - -## Explicit imports and source identity - -Explicit path and glob imports, recursive traversal, source identity, containment, -filesystem context, and budgets are implemented and now owned by the -[current configuration contract](../config.md#explicit-imports). - -## Profiles - -Selected profiles are implemented and owned by the -[current profile contract](../config.md#selected-profiles). Applications continue -to own the flags and commands that choose a profile name. - -## Static defaults and null - -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 - -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. Secret classification and effective rendering are owned by the -[current redaction contract](../config.md#secret-classification-and-effective-rendering). - -## Recorded resolution and validation - -Application-owned recorded resolution is implemented. The -[current resolution contract](../config.md#recorded-application-resolution) owns -explicitness queries, helpers and manual records, structural mutation auditing, -coalescing, diagnostics, origins, history, and snapshot ownership. Layered input -history is owned by the [current metadata contract](../config.md#layered-metadata); -redacted historical-value explanation remains target work. - -Canonical typed projection, detached cloning, structural comparison, and publishing -independently captured resolved results are implemented. The -[current ownership contract](../config.md#typed-projection-and-ownership) owns those -mechanics. - -Application semantic validation is implemented. The -[current semantic-validation contract](../config.md#semantic-validation) owns -schema-aware reporting, nearest locations, warning/error behavior, mutation audit, -callback lifetime, and application/runtime responsibility boundaries. - -## Provenance, diagnostics, and redaction - -Safe diagnostic/provenance value records, canonical diagnostic rendering, and -`LoadError` access are implemented. Their current contracts are owned by -[`records.go`](../../records.go) and [`diagnostic.go`](../../diagnostic.go). -Default, base, profile, and resolution records are attached to the loading lifecycle; -their source ordering, composite/synthetic origins, replacement history, and detached -queries are owned by the [current metadata contract](../config.md#layered-metadata). -Secret classification and effective rendering are owned by the -[current redaction contract](../config.md#secret-classification-and-effective-rendering). -Safe historical values and explanation remain target work below. - -Diagnostics have stable codes, severity, semantic path when available, a safe -message, primary location, and related locations. Sort deterministically by -source traversal order, location, path, and code. Expose structured errors for -programmatic inspection; human message wording need not be a compatibility -contract. Fatal parse/graph failures may stop dependent work; collect independent -schema and semantic errors where continuing is meaningful. - -On any error, loading returns no usable configuration. A structured error may -carry safe diagnostics and sources discovered so far. Successful results carry -configuration, metadata, sources, and nonfatal diagnostics. Avoid ambiguous -partial-success results or two competing authoritative error lists. - -Redaction is part of the initial lifecycle, including parse and decode failures. -Never blindly forward parser/custom-decoder error text or source snippets that -may contain values. Report safe expected-type and location information instead. -Applications likewise must supply value-free validation messages and resolution -reasons. ConfigKit cannot sanitize secrets embedded in arbitrary application -prose reliably. - -Provide full safe historical-value explanation in the initial release, as defined -[below](#historical-value-explanation). Defer config diffs and fingerprints. A future fingerprint design -must specify canonicalization, secret handling, and disclosure risks before -claiming identity or reproducibility guarantees. - -## Application schema versions - -Applications may declare a schema-version path (for example `/schema_version`) -and a pre-decode checker. The main document owns this value; fragments omit it, -and profiles/CLI overrides cannot supply or change it. Check fragment placement before -composition so a version supplied only by an import is also rejected. ConfigKit enforces that placement when -the application enables the mechanism. The application decides whether a version -is required and which values it accepts. ConfigKit does not require one universal -version field or interpret an application's version as the library version. -Automatic migration is deferred. - -## Consumer API design - -The core declarations, callback lifecycle, strict composition, explicit imports, -and selected profiles are implemented; their current contract is owned by -[configuration input](../config.md) and Go reference comments. Remaining declarations -and behavior in this section are target design, not implemented Go reference -documentation or a frozen naming commitment. Declarations omit method bodies and -private fields. -Consumer snippets assume the indicated standard-library imports and a `configkit` -import using the module path selected in the [implementation plan](implementation.md#implementation-decisions). -They are illustrative target examples, not currently runnable examples. The -implementation plan owns their conversion into maintained executable examples -and verification against consumer models. - -### Entry point and reusable application definition - -`Load`, `Definition`, `Options`, `FilesystemContext`, `Override`, `VersionRule`, -`Version`, `Validation`, and `Result` are declared. The current loader supports the -implemented boundary described by the [current contract](../config.md). Explicit -overrides, secret references/bootstrap, and historical-value explanation remain -target work; unavailable options fail explicitly rather than being ignored. - -```go -func Load[T any](source Source, options ...Options[T]) (*Result[T], error) - -type Definition[T any] struct { - Defaults func() T - Resolve func(*T, *Resolution) error - Validate func(*T, *Validation) - Version *VersionRule - Secret func(Path) bool -} - -type Options[T any] struct { - Definition Definition[T] - Imports bool - Profile string - Filesystem *FilesystemContext - Overrides []Override -} - -type FilesystemContext struct { - BaseDir string - Root string -} -``` - -`Load` accepts zero or one options value; multiple values are an options error, -not a precedence mechanism. The optional argument preserves the shortest load -without requiring a builder or an option function per setting. With no options, -`T` starts at its zero value, hooks are absent, and imports/profiles are disabled. -Strict YAML policy, decoding, presence, and safe diagnostics are always active; -there is no permissive-mode switch. - -`Definition[T]` contains reusable application rules; `Options[T]` contains the -per-load source environment and selection. Applications may reuse a definition -for many loads. Treat definitions as read-only after sharing them. Each load -invokes the defaults factory afresh, creates new callback contexts, and owns its -result. Callbacks must not retain these contexts or configuration pointers; shared -closures must be safe for concurrent calls if the application loads concurrently. -ConfigKit does not synchronize application-owned captured state. - -`Source`, its constructors, and import filesystem context are implemented; -[`source.go`](../../source.go) and the [configuration input contract](../config.md#sources) -own their current behavior. Applications must use non-secret source labels because -labels appear in diagnostics and source reports. Filesystem options do not change -application field path meanings. - -`NoSource()` explicitly selects defaults-only loading. It contributes no document -or file source entry and marks no fields explicit by itself; CLI overrides still -contribute synthetic sources and explicit values; it still runs schema checking with -an absent version, defaults, resolution, validation, and snapshot/inspection. -An application requiring an input version can therefore reject this mode. -Imports, an active profile, or filesystem context with `NoSource()` are options -errors. A zero `Source` is invalid and is not an implicit defaults-only request. -Empty files and empty byte/reader inputs remain invalid YAML inputs. - -The application decides whether its default path may be absent and explicitly -selects `NoSource()` in that case. ConfigKit never suppresses missing-file errors -on `File`; an explicitly selected missing file must still fail. Applications must -not treat permission or parse failures as evidence that no file was provided. - -A minimal application needs only its type and one load: - -```go -type Config struct { - Enabled bool `yaml:"enabled"` - Workers int `yaml:"workers"` -} - -func ReadConfig(path string) (Config, error) { - result, err := configkit.Load[Config](configkit.File(path)) - if err != nil { - return Config{}, err - } - return result.Config, nil -} -``` - -Adding defaults and validation remains ordinary application code: - -```go -var workerRules = configkit.Definition[Config]{ - Defaults: func() Config { return Config{Enabled: true, Workers: 4} }, - Validate: func(cfg *Config, v *configkit.Validation) { - if cfg.Workers < 1 { - v.Error(configkit.Keys("workers"), "must be at least one") - } - }, -} - -func ReadWorkerConfig(path string) (*configkit.Result[Config], error) { - return configkit.Load(configkit.File(path), configkit.Options[Config]{ - Definition: workerRules, - }) -} -``` - -The same application rules support defaults-only operation: - -```go -func ReadWorkerDefaults() (*configkit.Result[Config], error) { - return configkit.Load(configkit.NoSource(), configkit.Options[Config]{ - Definition: workerRules, - }) -} -``` - -### Paths and metadata - -The path API, detached metadata records, empty/unknown query behavior, and layered -population are implemented; [`path.go`](../../path.go), -[`records.go`](../../records.go), [`metadata.go`](../../metadata.go), and the -[metadata configuration contract](../config.md#layered-metadata) own current -behavior. Safe historical values remain target behavior for explanation. - -### Resolution and validation callbacks - -The resolution and semantic-validation declarations and behavior below are -implemented. The [current recorded-resolution contract](../config.md#recorded-application-resolution) -and [semantic-validation contract](../config.md#semantic-validation) are -authoritative. - -```go -func (r *Resolution) IsExplicit(path Path) bool -func (r *Resolution) Record(path Path, kind ChangeKind, reason string, dependencies ...Path) - -func Default[V any](r *Resolution, path Path, target *V, value V, reason string, dependencies ...Path) -func Normalize[V any](r *Resolution, path Path, target *V, value V, reason string, dependencies ...Path) - -func (r *Resolution) Error(path Path, message string, related ...Path) -func (r *Resolution) Warning(path Path, message string, related ...Path) -func (v *Validation) Error(path Path, message string, related ...Path) -func (v *Validation) Warning(path Path, message string, related ...Path) -``` - -`ChangeKind` distinguishes derived defaults and normalization. `Default` assigns -and records only when the effective input at the path is not explicit; thus it -may replace a static baseline. `Normalize` assigns and records a changed value -whether the input was explicit or defaulted. Both helpers accumulate invalid-path -or record errors in the resolution context, checked before validation. They do -not require applications to construct source locations or duplicate value history. -Targets must correspond to the declared path in the final configuration; ConfigKit -checks the projected path value against the helper's assigned value and audits -all net changes for coverage. This is consistency validation, not proof of the -identity of an arbitrary Go pointer supplied by application code. - -For a multi-field transformation, the application may mutate normally and call -`Record` for affected paths or a replaced container. The before/final comparison -requires coverage of net changes; it does not infer reasons or promise all -intermediate mutations. The helper/record design must be exercised on consumer -code before stabilization; do not expand bookkeeping merely to make an internal -audit easier to implement. - -Validation receives the resolved configuration by pointer for ergonomic reuse of -application code but must not mutate it, including nested maps or slices. Its -context collects independent messages; ConfigKit supplies diagnostic codes and -available primary/related locations. A missing field uses the nearest relevant -container/source location, or a synthetic location when no input source applies. -Application-provided messages and reasons must be safe, value-free prose. - -Resolvers and validators can report safe domain messages through `Error` and -`Warning`, specifying one primary path and optional related paths. ConfigKit -supplies phase-specific codes and attaches available locations to all paths. -Related paths are useful for duplicates and cross-field constraints; no caller -must construct file coordinates. Reported errors fail the load even when a -resolver returns nil. Warnings alone do not fail it. Stop before the next phase -when a hook has reported errors. - -A non-nil resolver error also aborts the load. If the context already contains -errors, retain those without adding a redundant generic failure; otherwise emit -a safe hook-failure diagnostic. Never forward arbitrary error text by default. -Only the explicit reporting methods opt into human-visible application messages; -callers must not interpolate secrets or attach raw underlying errors as messages. -Validators need no multi-error wrapper. -Callback panics are programming failures and are not converted to configuration -validation errors. Contexts are valid only during their callback and are not safe -for concurrent use within a callback. - -A Narratio-like application can retain typed domain logic, including defaults for -new map entries. This illustrative example uses `strings` and `time`: - -```go -type Artifact struct { - Output string `yaml:"output"` - Timeout time.Duration `yaml:"timeout"` -} - -type PipelineConfig struct { - PollInterval time.Duration `yaml:"poll_interval"` - Timeout time.Duration `yaml:"timeout"` - Artifacts map[string]Artifact `yaml:"artifacts"` - APIKey configkit.Secret `yaml:"api_key"` -} - -func resolvePipeline(cfg *PipelineConfig, r *configkit.Resolution) error { - configkit.Default(r, configkit.Keys("timeout"), &cfg.Timeout, - cfg.PollInterval/10, "derived from poll interval", - configkit.Keys("poll_interval")) - - for name, artifact := range cfg.Artifacts { - path := configkit.Keys("artifacts", name) - configkit.Default(r, path.Key("timeout"), &artifact.Timeout, - cfg.Timeout, "inherited pipeline timeout", configkit.Keys("timeout")) - configkit.Normalize(r, path.Key("output"), &artifact.Output, - strings.TrimSpace(artifact.Output), "trimmed surrounding whitespace") - cfg.Artifacts[name] = artifact - } - return nil -} - -func validatePipeline(cfg *PipelineConfig, v *configkit.Validation) { - if cfg.PollInterval <= 0 { - v.Error(configkit.Keys("poll_interval"), "must be positive") - } - if cfg.Timeout <= 0 { - v.Error(configkit.Keys("timeout"), "must be positive") - } - for name, artifact := range cfg.Artifacts { - path := configkit.Keys("artifacts", name) - if artifact.Timeout <= 0 { - v.Error(path.Key("timeout"), "must be positive") - } - if artifact.Output == "" { - v.Error(path.Key("output"), "must be nonempty") - } - } -} - -var pipelineRules = configkit.Definition[PipelineConfig]{ - Defaults: func() PipelineConfig { - return PipelineConfig{PollInterval: time.Minute} - }, - Resolve: resolvePipeline, - Validate: validatePipeline, -} - -func ReadPipeline(path, profile string) (*configkit.Result[PipelineConfig], error) { - return configkit.Load(configkit.File(path), configkit.Options[PipelineConfig]{ - Definition: pipelineRules, - Imports: true, - Profile: profile, - }) -} - -func ReadPipelineBytes(label string, data []byte) (*configkit.Result[PipelineConfig], error) { - return configkit.Load(configkit.Bytes(label, data), configkit.Options[PipelineConfig]{ - Definition: pipelineRules, - }) -} -``` - -Map entries are copied out and written back using ordinary Go code; helpers audit -the final projection. Independent map traversal order must not affect sorted -records or diagnostics. Interdependent application transformations must use an -application-defined deterministic order. Campaign/session fallback can similarly -remain application code; this example does not turn it into ConfigKit composition. - -A validator can report both sides of a duplicate without constructing source -locations. This example uses `sort` to make primary-path selection deterministic: - -```go -func validateUniqueOutputs(cfg *PipelineConfig, v *configkit.Validation) { - names := make([]string, 0, len(cfg.Artifacts)) - for name := range cfg.Artifacts { - names = append(names, name) - } - sort.Strings(names) - seen := make(map[string]configkit.Path) - for _, name := range names { - output := cfg.Artifacts[name].Output - if output == "" { - continue // The ordinary field validator reports this separately. - } - path := configkit.Keys("artifacts", name, "output") - if first, exists := seen[output]; exists { - v.Error(path, "output must be unique", first) - } else { - seen[output] = path - } - } -} -``` - -Call this helper from the application's validator when output uniqueness is a -domain requirement. The message identifies the relationship without printing -potentially sensitive configured values. - -### Schema checking and secret classification - -`VersionRule`, `Version`, and the version-checking use of `Validation` are -implemented. The [current version contract](../config.md#application-schema-versions) -owns absent/null/string handling, report locations, warnings, failure behavior, and -raw-error suppression. The -[current redaction contract](../config.md#secret-classification-and-effective-rendering) -owns secret classification. - -```go -type VersionRule struct { - Path Path - Check func(Version, *Validation) error -} - -type Version struct { - Present bool - Null bool - Text string -} -``` - -The version checker receives the main document's scalar at the declared path, -with absent/null states preserved. Non-string, non-null values are schema-version -errors before the hook; the application decides whether absent/null or a string -is acceptable. The checker reports actionable safe messages through its validation -context, using the same primary/related-path API as other hooks. At this pre-decode -phase, locations resolve against the composed input rather than a typed schema; -a missing path falls back to the nearest input container. No parser tree is -exposed. Reported errors or a non-nil return fail the load. A non-nil return with -no reported error produces a generic safe version-check failure at the declared -version path; raw error text is not printed. Placement and timing follow -[schema versioning](#application-schema-versions). A rule with no checker is an -options error. - -For example, an application can preserve useful migration guidance without -embedding input values in its error text: - -```go -var schemaVersionPath = configkit.Keys("schema_version") - -func checkVersion(version configkit.Version, v *configkit.Validation) error { - if !version.Present || version.Null { - v.Error(schemaVersionPath, "schema_version is required") - } else if version.Text != "example.v1" { - v.Error(schemaVersionPath, "unsupported schema version; migrate using the application configuration guide") - } - return nil -} - -var versionRule = configkit.VersionRule{ - Path: schemaVersionPath, - Check: checkVersion, -} -``` - -Assign `&versionRule` to a definition's `Version` field to enable it. Schema -versions remain strings under ConfigKit's convention; adopting consumers with -integer versions must migrate their syntax rather than enable a second scalar -policy. - -`Definition.Secret` and `configkit:"secret"` behavior are implemented and owned by -the [current redaction contract](../config.md#secret-classification-and-effective-rendering). - -### Results, errors, and inspection - -`Result.Config`, `Metadata`, `Sources`, `Diagnostics`, and `WriteEffective`, together -with `LoadError` and `WriteDiagnostics`, are implemented at the current loading boundary. -The [current loading contract](../config.md#single-source-loading) and Go reference -comments own their behavior. Historical-value explanation is also implemented and -owned by the -[current explanation contract](../config.md#historical-value-explanation). - -```go -type Result[T any] struct { - Config T - // Private snapshot and metadata storage. -} - -func (r *Result[T]) Metadata() Metadata -func (r *Result[T]) Sources() []SourceInfo -func (r *Result[T]) Diagnostics() []Diagnostic -func (r *Result[T]) WriteEffective(output io.Writer) error - -type LoadError struct { - // Private diagnostic and source storage. -} - -func (e *LoadError) Error() string -func (e *LoadError) Diagnostics() []Diagnostic -func (e *LoadError) Sources() []SourceInfo -func WriteDiagnostics(output io.Writer, diagnostics []Diagnostic) error -``` - -Every configuration/source/options/hook failure returns `(nil, *LoadError)`. -Success returns a non-nil result and nil error; its diagnostics contain only -nonfatal warnings. Source lists use stable source identifiers and safe labels; -diagnostics expose code, severity, path, message, primary location, and related -locations as described [above](#provenance-diagnostics-and-redaction). No raw -parser error or unrestricted secret-bearing history is exposed. No automatic -logging occurs. Inspection methods returning slices provide defensive copies. - -Caller-owned configuration, the frozen result snapshot, secret classification, and -`WriteEffective` behavior are owned by the -[current redaction contract](../config.md#secret-classification-and-effective-rendering). - -An application can report errors and inspect a successful load without rebuilding -diagnostic formatting. This example uses `errors` and `io` and the pipeline -loader above: - -```go -func InspectPipeline(path, profile string, output io.Writer) error { - result, err := ReadPipeline(path, profile) - if err != nil { - var loadErr *configkit.LoadError - if errors.As(err, &loadErr) { - if writeErr := configkit.WriteDiagnostics(output, loadErr.Diagnostics()); writeErr != nil { - return writeErr - } - } - return err - } - if err := configkit.WriteDiagnostics(output, result.Diagnostics()); err != nil { - return err - } - return result.WriteEffective(output) -} -``` - -Applications can use `Metadata().Lookup`, `Metadata().History`, and `Sources()` -for structured inspection without implementing value rendering or importing a -parser. The implemented explanation API supplies safe values and formatting for -application-owned `config explain` commands. - -### Historical-value explanation - -Historical explanation is implemented. The -[current explanation contract](../config.md#historical-value-explanation) owns its -public records, replay ordering, redaction, rendering, and allocation bounds. - -### Module-specific options - -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 - -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. - -Canonical projection, detached copying, cycle and finite-value enforcement, custom -scalar reconstruction, and structural comparison are implemented and owned by the -[current ownership contract](../config.md#typed-projection-and-ownership). The same -field-binding model must still supply defaults, decoding, change auditing, and -rendering. Application text methods must remain side-effect-free with respect to -shared state; this is a bounded scalar seam, not a custom structural decoder. - -## Internal organization - -Keep document composition, overlays, and parser machinery internal initially. -Applications use the typed lifecycle; application-specific resolution alone does -not justify exposing a document-processing toolkit. - -Internally separate source access, policy-validated documents, composition, -overlay, field binding/decoding, metadata, resolution, and safe rendering. -These are responsibilities, not a mandate for public interfaces or a file per -concept. Start with one public package and a private YAML adapter; add package -boundaries only when dependencies and ownership warrant them. - -Use standard-library filesystem, I/O, scalar conversion, reflection, ordering, -and synchronization primitives. ConfigKit owns the policy that coordinates them: -field binding, presence/default behavior, strict composition, overlays, provenance, -resolution auditing, and secret propagation. One descriptor graph and document -model support those operations; do not add competing binding or copying models. -Convenience functions must not broaden accepted scalar syntax, silently truncate -input, lose nested ownership, or substitute Go field promotion for YAML inlining. - -The [implementation plan](implementation.md#standard-library-mechanisms-and-custom-policy) -owns the selected primitives and their integration checks. Test comparison uses -`go-cmp` only in tests, never as the runtime resolution comparator. Additional -dependencies remain case-by-case decisions based on code saved after accounting -for adaptation, validation, and recurring maintenance. - -### YAML dependency and public API isolation - -Use the YAML organization's `go.yaml.in/yaml/v4` package behind `internal/yaml`. -V4 is upstream's active development line; v3 receives security fixes only. -This choice aligns a new library with ongoing upstream maintenance while keeping -ConfigKit responsible for its own configuration model. -See [upstream's version policy](https://github.com/yaml/go-yaml#version-intentions). -The [implementation decisions](implementation.md#implementation-decisions) own -the exact selected release-candidate pin and upgrade validation requirements. - -The adapter owns all direct upstream imports, including parser plugins, loading, -and emission. Its boundary uses ConfigKit-owned document and diagnostic records. -No upstream nodes, type aliases, options, plugin interfaces, error types, or -generic constraints form part of ConfigKit's public declarations or callbacks. -ConfigKit must not return parser-created values hidden inside `any`, metadata, -or an error's unwrap chain. Applications use ordinary Go types, ConfigKit types, -and the supported standard-library text interfaces; they need no YAML-package -imports or upstream version presets to use ConfigKit. - -The existing `yaml` field-tag spelling is a ConfigKit-owned binding contract, -not delegation to upstream reflection or marshal/unmarshal hooks. ConfigKit also -owns scalar interpretation, duplicate detection, document count, source-coordinate -conventions, diagnostic classifications, and effective-output formatting. Upstream -options may help enforce these contracts internally but cannot redefine them. - -An upstream breaking API change must be absorbed in the adapter or other private -implementation, preserving downstream Go source compatibility and documented -configuration behavior. Parser upgrades do not independently authorize changes -to ConfigKit's public contracts. If an upstream version cannot preserve those -contracts, retain the compatible dependency or adapt the implementation; any -intentional ConfigKit contract change follows ConfigKit's own release policy. - -This boundary isolates API coupling, not every dependency risk: the YAML module -remains a transitive build dependency, and parser defects or behavior changes -still require ConfigKit regression checks. It does not require a public parser -interface, configurable backends, or simultaneous v3/v4 implementations. - -## Explicit non-goals - -General environment-variable configuration overlays are not on the roadmap. -Secret references and explicit bootstrap are the supported environment convention. -Literal-secret configuration and implicit secret-directory discovery are unsupported. - -## Deferred scope - -The initial target excludes: +| Sources, lifecycle, limits, YAML/scalars, decoding, and ownership | [Configuration contract](../config.md) and Go reference comments | +| Paths, presence, origins, sources, and definition history | [Layered metadata](../config.md#layered-metadata), [`path.go`](../../path.go), and [`metadata.go`](../../metadata.go) | +| Strict composition, imports, globs, source identity, and profiles | [Strict composition](../config.md#strict-document-composition), [explicit imports](../config.md#explicit-imports), and [selected profiles](../config.md#selected-profiles) | +| Static defaults, null, types, projection, and custom text scalars | [Defaults and typed decoding](../config.md#defaults-and-typed-decoding), [schema binding](../config.md#application-schema-binding), and [typed projection](../config.md#typed-projection-and-ownership) | +| Schema versions, recorded resolution, and semantic validation | [Version contract](../config.md#application-schema-versions), [resolution](../config.md#recorded-application-resolution), and [validation](../config.md#semantic-validation) | +| Secret classification, typed references, and directory bootstrap | [Redaction](../config.md#secret-classification-and-effective-rendering), [references](../config.md#typed-secret-references), and [bootstrap](../config.md#explicit-secrets-directory-bootstrap) | +| Explicit CLI overrides and complete precedence | [CLI override contract](../config.md#explicit-cli-overrides) | +| Effective rendering and safe historical explanation | [Effective rendering](../config.md#secret-classification-and-effective-rendering) and [historical explanation](../config.md#historical-value-explanation) | +| Canonical module options and downstream adoption | [Module options](../config.md#canonical-module-options) and [consumer guide](../consumers/README.md) | +| Diagnostics and structured errors | [`records.go`](../../records.go), [`diagnostic.go`](../../diagnostic.go), and relevant configuration-contract sections | +| Internal components and dependency placement | [Component overview](../internal/overview.md) and [architecture policy](../policy/architecture.md) | + +## Explicit non-goals and deferred scope + +General environment-variable configuration overlays are not supported. Typed secret +references and explicit directory bootstrap are the environment convention. Literal +credential configuration and implicit secret-directory discovery are unsupported. + +The initial release deliberately excludes: - automatic `conf.d` discovery; - remote imports, arbitrary providers, and additional configuration formats; - custom merge strategies, sequence merging, reset operators, and profile inheritance; -- parser-specific or custom structural decoding extension APIs; +- parser-specific or custom structural-decoding extension APIs; - generic dynamic-subtree conversion and module-registry APIs; - automatic schema migrations; - hot reload, runtime resource replacement, and remote resource validation; -- a bundled CLI framework or commands; -- config diffs and fingerprints. +- a bundled CLI framework or application commands; and +- configuration diffs and fingerprints. -Applications can build their own validate/effective/sources commands on the -library. Broadening these boundaries requires concrete consumer evidence and -an explicit design change. +Applications may build their own validate, effective-value, source, bootstrap, and +explanation commands around the library. Broadening these boundaries requires +concrete consumer evidence, compatibility review, and an explicit design change. diff --git a/docs/roadmap/release-readiness.md b/docs/roadmap/release-readiness.md index 39c8907..2eed82f 100644 --- a/docs/roadmap/release-readiness.md +++ b/docs/roadmap/release-readiness.md @@ -2,24 +2,36 @@ ## Status and ownership -ConfigKit is not release-ready. This document tracks outstanding release -prerequisites under the [temporary bootstrap exception](../policy/release.md#temporary-bootstrap-deferral). -It is not the library implementation plan and does not assign implementation -stages. The maintainer preparing the first release owns closure. +ConfigKit's implementation is prepared for release-candidate validation, but no +release candidate has been selected or publication authorized. The maintainer +preparing the first release owns the remaining gates under the +[temporary bootstrap exception](../policy/release.md#temporary-bootstrap-deferral). +This evidence record remains until candidate preparation retires completed roadmap +material in favor of the operational [release procedure](../release.md) and normal +release notes. -## Outstanding prerequisites +## Preparation evidence -| Gap | Evidence required to close | +| Preparation item | Status and evidence | | --- | --- | -| No Go module or declared support matrix | Established module path, supported Go versions and platform targets documented in the release procedure | -| No release runbook | `docs/release.md` exists with the prerequisites, commands, publication guards, and verification required by release policy | -| No complete release checker | A documented command exists and exercises every applicable automated candidate gate; manual gates and inapplicable checks are explicit | -| No consumer verification procedure | Runbook verifies retrieval of the exact released module version and a representative consumer build without local replacement directives | -| No verified secret/precedence/explanation contracts | Current contracts and passing bootstrap skip/partial-failure, secret isolation, CLI precedence, and safe historical-value regressions; representative consumer examples | -| No validated release candidate | Version-matched release note, current contracts/examples, and complete checks against the exact committed candidate | +| Module and support matrix | Closed: the runbook records module `gitea.maximumdirect.net/eric/ConfigKit`, minimum Go 1.25 with a patched 1.25 plus newer supported toolchain, and Linux/macOS on amd64/arm64. | +| Maintainer release procedure | Closed: `docs/release.md` owns access prerequisites, candidate recording, complete validation, exact remote/commit/tag guards, source-only publication, consumer retrieval, and correction. | +| Automated source checker | Closed for current-source preparation: `scripts/release-check.sh` passed on 2026-09-06 with Go 1.25.5 and Go 1.26.7 on linux/amd64. It performs disposable-copy module hygiene, cached/offline tests, race, vet, native and four-target cross-builds, formatting/whitespace, examples, import isolation, and local-link gates. The exact candidate must rerun it. | +| Consumer verification procedure | Closed as procedure: the runbook retrieves an exact published version into a clean temporary module with no local replacement and tests/builds it. Actual retrieval remains a post-publication gate. | +| Secret/precedence/explanation evidence | Closed locally: current contracts and deterministic tests cover bootstrap skip/partial failure, credential isolation, defaults/base/profile/CLI precedence, historical redaction, and maintained consumer startup examples. | +| Parser dependency reassessment | Closed for preparation: on 2026-09-06, `go list -m -versions go.yaml.in/yaml/v4` and `go list -m -json go.yaml.in/yaml/v4@latest` both showed `v4.0.0-rc.6` as the newest available release. The explicit pin is retained; a candidate compatibility review must reassess and record it again. | -No release can proceed while these prerequisites remain open. Close items only -on evidence from actual files and checks, not proposed commands. Record closure -evidence here while this roadmap remains active. Once all gaps are closed, -remove the bootstrap exception and retire this document in favor of the -operational runbook and normal release records. +## Outstanding release gates + +| Gate | Owner and closure evidence | +| --- | --- | +| Exact candidate and release note | Maintainer selects an unused stable version, writes `docs/releases/.md`, commits all release changes, runs the checker against that exact clean commit, pushes it to `origin/main`, and records the commit. | +| Native platform execution | Maintainer records applicable checker, race, symlink, and permission-fixture results on native Linux and macOS. Current local evidence is Linux/amd64 only; cross-builds do not close macOS execution. | +| Compatibility, migration, and content review | Maintainer records public API/configuration/diagnostic review, downstream migration impact, release-note approval, and inspection for credentials, private data, generated output, unintended files, and unrelated changes. | +| Roadmap retirement | Maintainer removes obsolete completed files under `docs/roadmap/`, repairs inbound links, and reruns the checker as part of the candidate. This record and the implementation plan intentionally remain while no candidate exists. | +| Remote publication guards | Maintainer fetches authoritative `origin/main` and tags immediately before publication and proves branch, clean worktree/index, candidate commit, remote commit, release note, approvals, and unused version all agree. | +| Tag and retrieval verification | After authorization, maintainer pushes only the exact immutable source tag, verifies its peeled remote commit, and retrieves/tests/builds the exact version in a clean consumer module without `replace`. | + +No tag, push, downstream migration, release note, or release publication was +performed during implementation. A green local checker does not close the exact +candidate, native macOS, remote, or post-publication gates. diff --git a/documentation_test.go b/documentation_test.go new file mode 100644 index 0000000..7549a92 --- /dev/null +++ b/documentation_test.go @@ -0,0 +1,151 @@ +package configkit + +import ( + "bufio" + "fmt" + "net/url" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + "unicode" +) + +var markdownLinkPattern = regexp.MustCompile(`(?m)(?:^|[[:space:]])\[[^\]]*\]\(([^)[:space:]]+)`) + +func TestLocalDocumentationLinks(t *testing.T) { + repository, err := filepath.Abs(".") + if err != nil { + t.Fatal(err) + } + var failures []string + err = filepath.WalkDir(repository, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + if entry.Name() == ".git" { + return filepath.SkipDir + } + return nil + } + if filepath.Ext(path) != ".md" { + return nil + } + content, readErr := os.ReadFile(path) + if readErr != nil { + return readErr + } + for _, match := range markdownLinkPattern.FindAllStringSubmatch(string(content), -1) { + target := strings.Trim(match[1], "<>") + if strings.Contains(target, "://") || strings.HasPrefix(target, "mailto:") { + continue + } + filePart, fragment, _ := strings.Cut(target, "#") + decoded, decodeErr := url.PathUnescape(filePart) + if decodeErr != nil { + failures = append(failures, fmt.Sprintf("%s: invalid escaped link %q", relativePath(repository, path), target)) + continue + } + linked := path + if decoded != "" { + linked = filepath.Clean(filepath.Join(filepath.Dir(path), filepath.FromSlash(decoded))) + } + _, statErr := os.Stat(linked) + outside := linked != repository && !strings.HasPrefix(linked, repository+string(filepath.Separator)) + if os.IsNotExist(statErr) && outside && optionalSiblingSource(repository, linked) { + continue // Recorded sibling-source links are optional in isolated clones. + } + if statErr != nil { + failures = append(failures, fmt.Sprintf("%s: missing local link %q", relativePath(repository, path), target)) + continue + } + if fragment != "" && filepath.Ext(linked) == ".md" && !markdownHasFragment(linked, fragment) { + failures = append(failures, fmt.Sprintf("%s: missing fragment in %q", relativePath(repository, path), target)) + } + } + return nil + }) + if err != nil { + t.Fatal(err) + } + if len(failures) != 0 { + t.Fatalf("documentation link failures:\n%s", strings.Join(failures, "\n")) + } +} + +func optionalSiblingSource(repository, linked string) bool { + relative, err := filepath.Rel(filepath.Dir(repository), linked) + if err != nil || relative == "." || filepath.IsAbs(relative) { + return false + } + parts := strings.Split(filepath.ToSlash(relative), "/") + if len(parts) == 0 { + return false + } + switch strings.ToLower(parts[0]) { + case "narratio", "notarius", "weatherreporter", "seriatim": + return true + default: + return false + } +} + +func markdownHasFragment(path, wanted string) bool { + file, err := os.Open(path) + if err != nil { + return false + } + defer file.Close() + wanted, err = url.PathUnescape(wanted) + if err != nil { + return false + } + seen := make(map[string]int) + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if !strings.HasPrefix(line, "#") { + continue + } + heading := strings.TrimSpace(strings.TrimLeft(line, "#")) + base := markdownFragment(heading) + count := seen[base] + seen[base]++ + fragment := base + if count > 0 { + fragment = fmt.Sprintf("%s-%d", base, count) + } + if fragment == wanted { + return true + } + } + return false +} + +func markdownFragment(heading string) string { + var result strings.Builder + previousSpace := false + for _, character := range strings.ToLower(heading) { + switch { + case unicode.IsLetter(character), unicode.IsDigit(character), character == '_', character == '-': + result.WriteRune(character) + previousSpace = false + case unicode.IsSpace(character): + if result.Len() > 0 && !previousSpace { + result.WriteByte('-') + previousSpace = true + } + } + } + return strings.TrimRight(result.String(), "-") +} + +func relativePath(root, path string) string { + relative, err := filepath.Rel(root, path) + if err != nil { + return path + } + return filepath.ToSlash(relative) +} diff --git a/scripts/release-check.sh b/scripts/release-check.sh new file mode 100755 index 0000000..a4f56bb --- /dev/null +++ b/scripts/release-check.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +repository_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd) +cd "$repository_dir" + +baseline_toolchain=${CONFIGKIT_BASELINE_TOOLCHAIN:?set CONFIGKIT_BASELINE_TOOLCHAIN to a cached supported Go 1.25 patch release} +newer_toolchain=${CONFIGKIT_NEWER_TOOLCHAIN:?set CONFIGKIT_NEWER_TOOLCHAIN to a cached newer supported Go toolchain} + +run_go() { + local toolchain=$1 + shift + GOTOOLCHAIN="$toolchain" GOPROXY=off go "$@" +} + +check_toolchain() { + local toolchain=$1 + GOTOOLCHAIN="$toolchain" GOPROXY=off go version >/dev/null + run_go "$toolchain" mod download +} + +check_toolchain "$baseline_toolchain" +check_toolchain "$newer_toolchain" +bash -n scripts/release-check.sh + +temporary_dir=$(mktemp -d) +trap 'rm -rf -- "$temporary_dir"' EXIT +git ls-files --cached --others --exclude-standard -z | + tar --null --files-from=- --create --file=- | + tar --extract --file=- --directory="$temporary_dir" +( + cd "$temporary_dir" + GOTOOLCHAIN="$baseline_toolchain" GOPROXY=off go mod tidy +) +diff -u go.mod "$temporary_dir/go.mod" +diff -u go.sum "$temporary_dir/go.sum" + +mapfile -d '' go_files < <(git ls-files --cached --others --exclude-standard -z -- '*.go') +if ((${#go_files[@]})); then + unformatted=$(gofmt -l -- "${go_files[@]}") + if [[ -n "$unformatted" ]]; then + printf 'gofmt required for:\n%s\n' "$unformatted" >&2 + exit 1 + fi +fi + +git diff --check HEAD -- +while IFS= read -r -d '' candidate_file; do + case "$candidate_file" in + *.go|*.md|*.sh|go.mod|go.sum|LICENSE|.gitignore|AGENTS.md) + if LC_ALL=C grep -nE '[[:blank:]]+$' -- "$candidate_file"; then + printf 'trailing whitespace found in %s\n' "$candidate_file" >&2 + exit 1 + fi + ;; + esac +done < <(git ls-files --cached --others --exclude-standard -z) + +for toolchain in "$baseline_toolchain" "$newer_toolchain"; do + run_go "$toolchain" test ./... + run_go "$toolchain" vet ./... + run_go "$toolchain" build ./... +done + +run_go "$newer_toolchain" test -race ./... +run_go "$newer_toolchain" test . -run '^Example' +run_go "$newer_toolchain" test ./examples/consumers +run_go "$newer_toolchain" test . -run '^TestLocalDocumentationLinks$' +run_go "$newer_toolchain" test . -run '^TestDependencyImportBoundary$' + +for target in linux/amd64 linux/arm64 darwin/amd64 darwin/arm64; do + target_os=${target%/*} + target_arch=${target#*/} + GOTOOLCHAIN="$newer_toolchain" GOPROXY=off CGO_ENABLED=0 GOOS="$target_os" GOARCH="$target_arch" go build ./... +done