Complete integer schema version consumer readiness

This commit is contained in:
2026-09-12 00:33:49 +00:00
parent 8702121fa4
commit fd8a68d268
11 changed files with 93 additions and 152 deletions

View File

@@ -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 checker does not run. ConfigKit does not interpret the text or impose a universal
version path. 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` The checker reports safe, value-free errors and warnings through `Validation.Error`
and `Validation.Warning`; ConfigKit resolves primary and related paths to exact or and `Validation.Warning`; ConfigKit resolves primary and related paths to exact or
nearest input locations. Reported errors fail loading. If the checker returns an nearest input locations. Reported errors fail loading. If the checker returns an

View File

@@ -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, executable examples and ordinary tests. It contains no private application data,
credentials, network calls, or dependency on a sibling checkout. 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 ## Happy path
An application defines ordinary types and passes one explicit source. Optional An application defines ordinary types and passes one explicit source. Optional

View File

@@ -42,9 +42,11 @@ git show cb5b435:docs/roadmap/remediation.md
Release-candidate, publication, and verification work follows the operational Release-candidate, publication, and verification work follows the operational
[release procedure](release.md). [release procedure](release.md).
Planned feature work: [string and integer schema versions](roadmap/integer-schema-versions.md) The completed string-and-integer schema-version work remains recorded in its
and its [sequential implementation plan](roadmap/implementation.md). [feature roadmap](roadmap/integer-schema-versions.md) and
These describe future behavior, not the current configuration contract. [sequential implementation plan](roadmap/implementation.md) until release
preparation retires them. The [configuration contract](config.md) owns current
behavior.
## Baseline validation ## Baseline validation

View File

@@ -1,7 +1,9 @@
# Integer schema versions: implementation plan # Integer schema versions: implementation plan
Status: in progress. The [feature roadmap](integer-schema-versions.md) owns the Status: implementation complete (2026-09-12). The
intended behavior and architecture; this document owns execution and progress. [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 ## Execution rules
@@ -127,7 +129,7 @@ and release-readiness validation without publishing a release.
## Stage 4 — Complete consumer evidence and release readiness ## 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, Scope: maintained Notarius-shaped examples/fixtures, consumer documentation,
documentation consistency review, and validation handoff. No downstream app edits. 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 Rerun exact-commit release checks after
any later candidate or roadmap-retirement changes. 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.

View File

@@ -1,142 +1,17 @@
# String and integer schema versions # String and integer schema versions
Status: planned; not implemented. This document owns the intended end state. Status: implemented on 2026-09-12; retained until release preparation. The
The [implementation plan](implementation.md) owns sequencing and [implementation plan](implementation.md) records validation and stage handoffs.
progress. Current behavior remains defined by the [configuration contract](../config.md).
## 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 [configuration contract](../config.md#application-schema-versions) is the
the version-check boundary and independently rejects assigning an integer to a Go canonical definition of the implemented behavior. The
string during decoding. Consumers should be able to retain an integer version [consumer adoption guide](../consumers/README.md) links maintained Notarius-shaped
declaration while using a string for application version comparison and storage. string and integer examples. This roadmap chooses no release version and grants no
publication authority; compatibility review and roadmap retirement follow the
Support this through the existing `VersionRule`, not a new parser mode, global [release procedure](../release.md) when separately authorized.
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.

View File

@@ -83,7 +83,7 @@ type ObjectBinding struct {
Options map[string]any `yaml:"options"` 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 { type NotariusConfig struct {
Version string `yaml:"version"` Version string `yaml:"version"`
Bindings []ObjectBinding `yaml:"bindings"` Bindings []ObjectBinding `yaml:"bindings"`
@@ -93,7 +93,7 @@ type NotariusConfig struct {
func LoadNotarius(path string) (*configkit.Result[NotariusConfig], error) { func LoadNotarius(path string) (*configkit.Result[NotariusConfig], error) {
return configkit.Load(configkit.File(path), configkit.Options[NotariusConfig]{ return configkit.Load(configkit.File(path), configkit.Options[NotariusConfig]{
Definition: configkit.Definition[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, Validate: validateNotarius,
}, },
}) })

View File

@@ -26,7 +26,7 @@ func Example_notarius() {
result, err := consumers.LoadNotarius(fixture("notarius", "config.yml")) result, err := consumers.LoadNotarius(fixture("notarius", "config.yml"))
window, ok := consumers.NotariusWindow(result.Config, 0) window, ok := consumers.NotariusWindow(result.Config, 0)
fmt.Println(result.Config.Version, result.Config.Bindings[0].Object, window, ok, err) fmt.Println(result.Config.Version, result.Config.Bindings[0].Object, window, ok, err)
// Output: v1 summary 12 true <nil> // Output: 4 summary 12 true <nil>
} }
func Example_weatherReporter() { func Example_weatherReporter() {
@@ -100,6 +100,10 @@ func TestMaintainedConsumerFixturesPreserveDomainOutcomes(t *testing.T) {
if _, ok := notarius.Config.Bindings[0].Options["window_units"].(int64); !ok { if _, ok := notarius.Config.Bindings[0].Options["window_units"].(int64); !ok {
t.Fatalf("dynamic integer type = %T", notarius.Config.Bindings[0].Options["window_units"]) 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"))) weather, err := consumers.LoadWeather(configkit.File(fixture("weather", "config.yml")))
if err != nil || weather.Config.Reports["daily"].Modules[1].Options.Sections[0] != "short_term" { 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 path string
}{ }{
{ {
name: "unsupported integer schema version", name: "unsupported application version",
load: func() error { _, err := consumers.LoadNotarius(fixture("notarius", "integer-version.yml")); return err }, load: func() error {
_, err := consumers.LoadNotarius(fixture("notarius", "unsupported-version.yml"))
return err
},
code: configkit.CodeVersionError, path: "/version", 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", name: "module shorthand",
load: func() error { load: func() error {

View File

@@ -1,4 +1,4 @@
version: v1 version: "4"
bindings: bindings:
- object: summary - object: summary
options: options:

View File

@@ -1,2 +1,6 @@
version: 1 version: 4
bindings: [] bindings:
- object: summary
options:
window_units: 12
format: markdown

View File

@@ -0,0 +1,2 @@
version: 4.0
bindings: []

View File

@@ -0,0 +1,2 @@
version: 5
bindings: []