Files
ConfigKit/docs/roadmap/integer-schema-versions.md

7.9 KiB

String and integer schema versions

Status: planned; not implemented. This document owns the intended end state. The implementation plan owns sequencing and progress. Current behavior remains defined by the configuration contract.

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; 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.