Closes the schema-versioning workstream of #90. Sets up the machinery
for "we will be updating a lot" (the user's framing) without forcing
the first real schema bump to discover semantics under deadline
pressure. Today every template is at v1; this PR adds the framework,
ships zero behavior change for v1 templates, and reserves v2+ for
when there's a concrete reason to bump.
Validator changes:
- `KNOWN_SCHEMA_VERSIONS = {1}` — the set the validator currently
accepts. Future bumps add to this set.
- `DEPRECATED_SCHEMA_VERSIONS: set[int] = set()` — versions accepted
with warning during a deprecation window.
- Per-version contract: `_check_schema_v1(config)` enforces the v1
REQUIRED_KEYS / OPTIONAL_KEYS / KNOWN_RUNTIMES contract — exactly
what the previous monolithic check_config_yaml did.
- Dispatch table: `SCHEMA_CHECKS = {1: _check_schema_v1}`. Versions
that aren't in the table hard-error.
- check_config_yaml() now: reads template_schema_version → emits
deprecation warning if applicable → dispatches to the right
SCHEMA_CHECKS entry → unknown versions hard-error with actionable
instructions ("add a SCHEMA_V<N> block").
- Schema versions are FROZEN once shipped: never edit a SCHEMA_V<N>
constant in place. To bump, ADD v<N+1> alongside, deprecate v<N>,
migrate consumers, drop v<N> next cycle. Header comment documents
the discipline.
New script `migrate-template.py`:
- `MIGRATIONS: dict[int, Callable[[dict], dict]]` registry — each
entry maps a SOURCE version to the function that produces the
next version's dict. Empty today.
- `migrate_config(config, from, to)` chains migrations sequentially.
Forward-only (errors on backward), errors on missing intermediate
steps (never silently skip), asserts every migration stamps its
output's template_schema_version.
- CLI: `migrate-template.py [--from N] [--to M] [--dry-run] DIR`.
Defaults: --from = whatever config.yaml declares, --to = highest
reachable from MIGRATIONS (currently 1, so a no-op).
Behavior change to the existing
test_missing_required_keys_errors test:
Previously the validator emitted 3 "missing required key" errors
when name/runtime/template_schema_version were all missing. Now it
short-circuits on missing version with a single actionable error —
listing downstream missing keys is noise on top of the real
problem (no version means we can't pick a contract). The test was
updated to pin the new behavior; a new sibling test
(test_missing_required_keys_under_v1_dispatch_errors) pins that v1
still lists name/runtime/etc. when present-with-v1.
Verification:
- 42/42 tests pass (20 prior + 9 new schema-dispatch tests in
test_validate_workspace_template.py + 17 new migrator tests in
test_migrate_template.py).
- Real langgraph template runs through the full updated validator
end-to-end with 0 warnings / 0 errors.
This + #17 means #90 is done end-to-end:
- Phase 2: validator green on all 8 templates as a required check (already shipped)
- Phase 2.5: adapter.py runtime-load contract (#17)
- Phase 3: schema versioning + migration framework (this PR)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>