Support integer schema versions through typed decoding
This commit is contained in:
142
docs/roadmap/integer-schema-versions.md
Normal file
142
docs/roadmap/integer-schema-versions.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 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).
|
||||
|
||||
## Motivation and scope
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user