From fd8a68d2683e121359e0f5095505f94ef755e833 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 12 Sep 2026 00:33:49 +0000 Subject: [PATCH] Complete integer schema version consumer readiness --- docs/config.md | 15 ++ docs/consumers/README.md | 5 + docs/development.md | 8 +- docs/roadmap/implementation.md | 27 +++- docs/roadmap/integer-schema-versions.md | 151 ++---------------- examples/consumers/consumers.go | 4 +- examples/consumers/consumers_test.go | 21 ++- .../consumers/testdata/notarius/config.yml | 2 +- .../testdata/notarius/integer-version.yml | 8 +- .../testdata/notarius/noninteger-version.yml | 2 + .../testdata/notarius/unsupported-version.yml | 2 + 11 files changed, 93 insertions(+), 152 deletions(-) create mode 100644 examples/consumers/testdata/notarius/noninteger-version.yml create mode 100644 examples/consumers/testdata/notarius/unsupported-version.yml diff --git a/docs/config.md b/docs/config.md index 20185ee..629e9a4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -81,6 +81,21 @@ exponent numbers, booleans, mappings, and sequences produce `version.type`, and checker does not run. ConfigKit does not interpret the text or impose a universal version path. +Integer versions use the strict grammar `^-?(0|[1-9][0-9]*)$`. Their spelling is +preserved exactly, including `-0`, and application policy decides which identifiers +are supported. A numeric version has no built-in `int64` range limit. Existing parser +policy still rejects leading zeroes, leading plus signs, hexadecimal notation, and +other non-JSON scalar forms before version checking. + +At exactly the configured semantic path, accepted integer input can decode into a +plain Go string or named string without a text codec, including pointer wrappers. +This is a load-local exception: siblings and unrelated fields, dynamic values, +integer and float destinations, durations, `Secret`, and text codecs retain ordinary +strict decoding. The retained source and history stay numeric while typed projection +and effective YAML contain the resulting string; explanation records this through +the normal decoding reconciliation event. Fragment, profile, and CLI ownership +protections apply before this conversion. + The checker reports safe, value-free errors and warnings through `Validation.Error` and `Validation.Warning`; ConfigKit resolves primary and related paths to exact or nearest input locations. Reported errors fail loading. If the checker returns an diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 8ebbadc..101be95 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -20,6 +20,11 @@ The YAML under [`testdata`](../../examples/consumers/testdata) is loaded by the executable examples and ordinary tests. It contains no private application data, credentials, network calls, or dependency on a sibling checkout. +The Notarius-shaped fixtures deliberately load both `version: 4` and +`version: "4"` into the same string-backed application field. Its version checker +still owns the supported identifier; fractional numeric input and unsupported +integer identifiers remain negative examples. + ## Happy path An application defines ordinary types and passes one explicit source. Optional diff --git a/docs/development.md b/docs/development.md index 3b41c5c..66ecb5c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -42,9 +42,11 @@ git show cb5b435:docs/roadmap/remediation.md Release-candidate, publication, and verification work follows the operational [release procedure](release.md). -Planned feature work: [string and integer schema versions](roadmap/integer-schema-versions.md) -and its [sequential implementation plan](roadmap/implementation.md). -These describe future behavior, not the current configuration contract. +The completed string-and-integer schema-version work remains recorded in its +[feature roadmap](roadmap/integer-schema-versions.md) and +[sequential implementation plan](roadmap/implementation.md) until release +preparation retires them. The [configuration contract](config.md) owns current +behavior. ## Baseline validation diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d543799..97285a7 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,7 +1,9 @@ # Integer schema versions: implementation plan -Status: in progress. The [feature roadmap](integer-schema-versions.md) owns the -intended behavior and architecture; this document owns execution and progress. +Status: implementation complete (2026-09-12). The +[feature roadmap](integer-schema-versions.md) records the feature scope, the +[configuration contract](../config.md) owns implemented behavior, and this document +owns execution and progress. ## Execution rules @@ -127,7 +129,7 @@ and release-readiness validation without publishing a release. ## Stage 4 — Complete consumer evidence and release readiness -Status: not started. Depends on: Stage 3. +Status: complete (2026-09-12). Depends on: Stage 3. Scope: maintained Notarius-shaped examples/fixtures, consumer documentation, documentation consistency review, and validation handoff. No downstream app edits. @@ -167,3 +169,22 @@ CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7 \ Rerun exact-commit release checks after any later candidate or roadmap-retirement changes. + +Completion record: maintained Notarius fixtures now accept integer `4` and quoted +`"4"` with the same string-backed result. Unsupported integer `5` reaches domain +policy, fractional `4.0` remains `version.type`, and dynamic option integers remain +`int64`. Current documentation and Go examples were audited; canonical behavior is +owned by the configuration contract and consumer guide, while historical release, +extraction, and ADR records were left unchanged. The focused consumer/example +commands, `./scripts/check.sh`, and the full release checker pass on Linux with +Go 1.25.5 and Go 1.26.7. + +Compatibility handoff: exported API shapes are unchanged, but integer input at a +configured version path intentionally changes from structural rejection to invoking +application policy and, for plain string destinations, successful decoding. The +conservative release choice is therefore a new pre-v1 minor version, subject to +maintainer selection and unused-version checks. No version, note, candidate, or tag +was created. Release preparation must still select the version, create and approve +matching notes, retire these completed roadmap files, rerun Linux and native macOS +checks on the exact candidate commit, push and guard that commit, publish the exact +tag, and verify clean consumer retrieval. diff --git a/docs/roadmap/integer-schema-versions.md b/docs/roadmap/integer-schema-versions.md index f12af47..d66064c 100644 --- a/docs/roadmap/integer-schema-versions.md +++ b/docs/roadmap/integer-schema-versions.md @@ -1,142 +1,17 @@ # String and integer schema versions -Status: planned; not implemented. This document owns the intended end state. -The [implementation plan](implementation.md) owns sequencing and -progress. Current behavior remains defined by the [configuration contract](../config.md). +Status: implemented on 2026-09-12; retained until release preparation. The +[implementation plan](implementation.md) records validation and stage handoffs. -## Motivation and scope +ConfigKit now accepts strings and strict decimal-integer syntax at the exact path +configured by `Definition.Version`. The application checker and a plain +string-backed destination receive the preserved text, while all unrelated decoding +remains strict. Composition ownership, source history, explanation reconciliation, +and secret redaction retain their existing behavior. -Notarius historically declares `version: 4`. ConfigKit currently rejects this at -the version-check boundary and independently rejects assigning an integer to a Go -string during decoding. Consumers should be able to retain an integer version -declaration while using a string for application version comparison and storage. - -Support this through the existing `VersionRule`, not a new parser mode, global -coercion option, public union type, or application-side YAML rewrite. Applications -continue to decide which version identifiers are supported. No downstream -repository changes or automatic schema migrations are included. - -## Target contract - -The following is intended future behavior, not a description of today's release. - -### Version checking - -Only the semantic path designated by `Definition.Version.Path` receives version -handling. There is no special behavior based on a field being named `version`. -An absent rule leaves all existing decoding behavior unchanged. - -| Input at the configured path | Checker input | -| --- | --- | -| Absent | `Version{Present: false}` | -| Explicit null | `Version{Present: true, Null: true}` | -| String, including empty string | Present, non-null; `Text` preserves the string | -| Number with integer syntax | Present, non-null; `Text` preserves the validated decimal spelling | -| Fractional/exponent number, boolean, mapping, or sequence | `CodeVersionType`; checker not called | - -Integer syntax means the existing strict decimal integer grammar -`^-?(0|[1-9][0-9]*)$`. Thus `4` and `"4"` both supply text `"4"`. -`-0` supplies `"-0"`; do not silently canonicalize it to `"0"`. Negative and zero -identifiers are structurally accepted; application policy may reject them. -`4.0` and `4e0` are not integer syntax, even though their mathematical values are -integral. Quoted versions such as `"4.0"` remain strings and remain accepted. -The parser's existing rejection of leading zeroes, hexadecimal, leading plus, -and other unsupported scalar spellings stays unchanged. - -Use the validated number text directly, without conversion through `int`, `int64`, -or floating point. Textual versions need no machine-integer range limit beyond -existing input resource limits. Numeric Go destinations retain their normal range -checks. Preserve the public `Version` and `VersionRule` shapes: no new kind flag -or opt-in field is needed. - -Checking remains before selected-profile application, defaults, typed decoding, -resolution, and secret lookup. Callback warnings, errors, absent/null handling, -safe diagnostics, and source attribution retain their current behavior. - -### Typed decoding - -At exactly the configured version path, an integer node may decode into a plain -Go string descriptor, including a named string without custom text decoding and -pointer wrappers around those types. The resulting string is the same text the -version checker receives. This permits both integer and quoted version YAML when -the application stores its version as a string. - -Other destination types retain ordinary decoding: an integer field can still -decode integer YAML, but quoted strings are not coerced into integers. Dynamic -`any` retains its usual scalar types and numeric bounds. This feature does not -force every destination to become a string. Custom text codecs, durations, and -secret-reference types gain no integer conversion, even if their path is the -configured version path. Missing/null values and strict unknown-field validation -also retain normal typed semantics; passing the checker does not bypass decoding. - -Unrelated fields, sibling fields with the same Go type, ancestors, descendants, -and fields merely named `version` receive no coercion. Semantic path matching must -respect escaped keys and contextual numeric tokens using existing path machinery. -Do not alter pointer-versus-key/index distinctions by comparing field names alone. - -### Composition, inspection, and secrets - -Version ownership remains unchanged: imported fragments cannot supply the -configured version, profiles cannot replace it or a covering ancestor, and CLI -overrides cannot overlap it. No conversion may hide a composition conflict or -weaken those checks. - -Keep the parsed input and retained history numeric when integer syntax was used. -For a Go string destination, typed projection and effective YAML show a string; -historical explanation should expose the existing decoding reconciliation event -between numeric input and string output. Presence, source locations, origins, -historical paths, and detached ownership remain intact. Do not represent this -conversion as application resolution or invent a synthetic source. - -Existing sensitivity classification and redaction apply to both original and -decoded snapshots. Conversion performs no environment access, invokes no codecs -that previously required strings, and introduces no raw-value diagnostic text. - -## Intended architecture - -The current entry points are `checkVersion` in `validation.go`, -`decodeLoadInput` in `load_result.go`, and `decodeConfiguration`/`decodeValue` in -`decode.go`. The lifecycle in `load.go` already supplies the definition to both -phases; retain its ordering. - -1. Share a small private integer-version text predicate/extractor between checking - and decoding, reusing the existing integer grammar. Do not extend the YAML - adapter or expose its node types. -2. Carry an optional, per-load version-path capability into typed decoding through - private decode context. Follow it through recursive decoding, including - pointer, map, and dynamic-structure paths. Default private decoder callers - should remain strict unless they explicitly receive that capability. -3. Apply the exception only when the current semantic path matches the configured - path, the source is an integer number, and the destination descriptor is a - plain string. All other cases use existing dispatch and errors. -4. Let normal typed projection produce the final string representation. Retain - original input for provenance and explanation; avoid rewriting an assembled - document or cloning the entire tree solely to convert one scalar. - -Do not attach path-specific conversion to a shared type descriptor: descriptors -can be reused by unrelated fields of the same Go type. Keep the capability local -to one decoding operation, not global state or a public schema extension. Exact -private context names/signatures are implementation choices, not API commitments. - -## Compatibility and completion - -Existing accepted strings keep their meaning. Newly accepted integers intentionally -change the previous `version.type` rejection contract and cause application version -checkers to run on inputs they previously never saw. Release review must account -for that, rather than describing the change as entirely behavior-neutral. A -pre-v1 minor release is the conservative choice under the -[release policy](../policy/release.md); this roadmap chooses no tag and grants no -patch-policy exception or publication authority. - -Completion requires end-to-end integer/string equivalence for a string-backed -Notarius-shaped configuration, strict rejection outside the version exception, -correct original/effective inspection, and retained version-ownership and secret -safety guarantees. Update Go reference comments, the configuration contract, and -consumer migration guidance as their behavior lands. Historical ADR rationale and -published release notes remain unchanged. If an accepted ADR actually conflicts -with the implementation decision, supersede it using the documentation policy; -do not rewrite its accepted rationale. - -Once implemented, move target contracts to their canonical owners and replace -duplicated roadmap text with links. Retire completed roadmap/plan documents during -release preparation, preserving the execution record in Git history. +The [configuration contract](../config.md#application-schema-versions) is the +canonical definition of the implemented behavior. The +[consumer adoption guide](../consumers/README.md) links maintained Notarius-shaped +string and integer examples. This roadmap chooses no release version and grants no +publication authority; compatibility review and roadmap retirement follow the +[release procedure](../release.md) when separately authorized. diff --git a/examples/consumers/consumers.go b/examples/consumers/consumers.go index 3d5c28b..97d5488 100644 --- a/examples/consumers/consumers.go +++ b/examples/consumers/consumers.go @@ -83,7 +83,7 @@ type ObjectBinding struct { Options map[string]any `yaml:"options"` } -// NotariusConfig demonstrates string versions and dynamic catalog options. +// NotariusConfig demonstrates string-backed versions and dynamic catalog options. type NotariusConfig struct { Version string `yaml:"version"` Bindings []ObjectBinding `yaml:"bindings"` @@ -93,7 +93,7 @@ type NotariusConfig struct { func LoadNotarius(path string) (*configkit.Result[NotariusConfig], error) { return configkit.Load(configkit.File(path), configkit.Options[NotariusConfig]{ Definition: configkit.Definition[NotariusConfig]{ - Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: requireVersion("v1")}, + Version: &configkit.VersionRule{Path: configkit.Keys("version"), Check: requireVersion("4")}, Validate: validateNotarius, }, }) diff --git a/examples/consumers/consumers_test.go b/examples/consumers/consumers_test.go index 3f1e164..af6ec00 100644 --- a/examples/consumers/consumers_test.go +++ b/examples/consumers/consumers_test.go @@ -26,7 +26,7 @@ func Example_notarius() { result, err := consumers.LoadNotarius(fixture("notarius", "config.yml")) window, ok := consumers.NotariusWindow(result.Config, 0) fmt.Println(result.Config.Version, result.Config.Bindings[0].Object, window, ok, err) - // Output: v1 summary 12 true + // Output: 4 summary 12 true } func Example_weatherReporter() { @@ -100,6 +100,10 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) { if _, ok := notarius.Config.Bindings[0].Options["window_units"].(int64); !ok { t.Fatalf("dynamic integer type = %T", notarius.Config.Bindings[0].Options["window_units"]) } + integerNotarius, err := consumers.LoadNotarius(fixture("notarius", "integer-version.yml")) + if err != nil || integerNotarius.Config.Version != notarius.Config.Version || integerNotarius.Config.Version != "4" { + t.Fatalf("Notarius version forms = %#v / %#v, %v", notarius.Config, integerNotarius, err) + } weather, err := consumers.LoadWeather(configkit.File(fixture("weather", "config.yml"))) if err != nil || weather.Config.Reports["daily"].Modules[1].Options.Sections[0] != "short_term" { @@ -124,10 +128,21 @@ func TestMigrationFixturesFailWithStableCodesAndPaths(t *testing.T) { path string }{ { - name: "unsupported integer schema version", - load: func() error { _, err := consumers.LoadNotarius(fixture("notarius", "integer-version.yml")); return err }, + name: "unsupported application version", + load: func() error { + _, err := consumers.LoadNotarius(fixture("notarius", "unsupported-version.yml")) + return err + }, code: configkit.CodeVersionError, path: "/version", }, + { + name: "non-integer numeric version", + load: func() error { + _, err := consumers.LoadNotarius(fixture("notarius", "noninteger-version.yml")) + return err + }, + code: configkit.CodeVersionType, path: "/version", + }, { name: "module shorthand", load: func() error { diff --git a/examples/consumers/testdata/notarius/config.yml b/examples/consumers/testdata/notarius/config.yml index ec3f86a..dda5c3f 100644 --- a/examples/consumers/testdata/notarius/config.yml +++ b/examples/consumers/testdata/notarius/config.yml @@ -1,4 +1,4 @@ -version: v1 +version: "4" bindings: - object: summary options: diff --git a/examples/consumers/testdata/notarius/integer-version.yml b/examples/consumers/testdata/notarius/integer-version.yml index a0ce75a..dca2dd0 100644 --- a/examples/consumers/testdata/notarius/integer-version.yml +++ b/examples/consumers/testdata/notarius/integer-version.yml @@ -1,2 +1,6 @@ -version: 1 -bindings: [] +version: 4 +bindings: + - object: summary + options: + window_units: 12 + format: markdown diff --git a/examples/consumers/testdata/notarius/noninteger-version.yml b/examples/consumers/testdata/notarius/noninteger-version.yml new file mode 100644 index 0000000..33bc7de --- /dev/null +++ b/examples/consumers/testdata/notarius/noninteger-version.yml @@ -0,0 +1,2 @@ +version: 4.0 +bindings: [] diff --git a/examples/consumers/testdata/notarius/unsupported-version.yml b/examples/consumers/testdata/notarius/unsupported-version.yml new file mode 100644 index 0000000..095a30d --- /dev/null +++ b/examples/consumers/testdata/notarius/unsupported-version.yml @@ -0,0 +1,2 @@ +version: 5 +bindings: []