feat(plan): harden persistence with schema_version + loaded-object validation (#18)#25
Conversation
…lidation (closes #18) Bring the plan engine's persistence contract up to the frame engine's level (the #5 peer): plans now carry an integer `schema_version` (PLAN_SCHEMA_VERSION = 1), written on save and checked on load. `plan_store.load` fails closed with a clean DevagueError (exit 1 + upgrade hint) on a newer unsupported schema; a pre-0.7.0 plan with no `schema_version` loads silently as the current schema. Loaded `Task.origin` / `Task.status` and `PlanRisk.kind` are validated at construction via `__post_init__`, so a hand-edited/corrupted plan surfaces an actionable "malformed plan" error instead of a traceback. `resolve_plan` now distinguishes invalid slug / newer schema / missing / malformed, each with its own remediation (mirroring frame `resolve`). Deliberately out of scope: task/dep/cover id cross-reference validation at load — coverage and acyclic-dependency checks already run against the live frame in `plan converge`; validating at load would duplicate that and could reject valid in-progress plans. Tests mirror the frame suites (round-trip, newer-schema rejection, legacy compat, malformed-value rejection, CLI clean errors). Docs: spec-contract.md plan-peer section. Version 0.6.1 -> 0.7.0. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review Summary by QodoHarden plan persistence with schema versioning and loaded-object validation
WalkthroughsDescription• Add schema_version field to Plan with validation on load/save • Implement fail-closed schema compatibility checking with upgrade hints • Validate loaded Task and PlanRisk enum values via __post_init__ • Enhance resolve_plan to distinguish schema, malformed, missing errors • Bump version 0.6.1 → 0.7.0 with comprehensive test coverage Diagramflowchart LR
A["Plan Model"] -->|"add schema_version field"| B["Plan with Version"]
B -->|"write on save"| C["plan_store.save"]
C -->|"JSON with schema_version"| D["Persisted Plan"]
D -->|"check on load"| E["plan_store.load"]
E -->|"version > current?"| F["IncompatiblePlanSchemaError"]
E -->|"validate enums"| G["Task/PlanRisk __post_init__"]
G -->|"invalid value?"| H["ValueError"]
F -->|"caught by"| I["resolve_plan"]
H -->|"caught by"| I
I -->|"clean DevagueError"| J["User-friendly exit 1"]
File Changes1. devague/plan.py
|
Code Review by Qodo
1.
|
… both engines Addresses the two Qodo bug findings on PR #25 (review comment ids 3293166937 and the "Schema version coerced loosely" finding): 1. Slug mismatch (Reliability). store.load / plan_store.load now reject a file whose embedded slug differs from the requested slug. Previously a tampered demo.json containing {"slug": "other"} would load, and a later save() would be redirected onto other.json / flip the current pointer — a silent-overwrite hole. validate_slug only blocked path-escape, not a mismatched valid slug. 2. Loose int() coercion (Correctness). schema_version is now parsed strictly via the shared frame.parse_schema_version helper: missing -> default (back-compat), present must be a real int. int() silently accepted True->1 and truncated 1.9->1, letting a malformed version load as current instead of being rejected. Both guards applied symmetrically to the frame engine (the persistence twin) so they don't diverge — the identical code shipped in #5. Tests mirror across all four suites (parametrized non-int schema_version on frame+plan; slug-mismatch on store+plan_store). Docs + CHANGELOG updated. Still 0.7.0 (unreleased). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
|
Re: the second Qodo finding, "Schema version coerced loosely" (no inline thread for it) — also fixed in 14183e6.
|



What
Hardens the plan engine's persistence contract to match the frame engine (the #5 peer). Plans are durable artifacts, but
plan_store.load()previously trusted whatever JSON it read — no version check, no validation of loaded enum values.Closes #18.
Changes
PLAN_SCHEMA_VERSION = 1added todevague/plan.py;Plancarries aschema_versionfield, written on save (devague/plan_store.py).plan_store.load()raisesIncompatiblePlanSchemaErrorwhen a plan declares aschema_versionnewer than this devague supports → surfaced as a cleanDevagueError(exit 1) with an upgrade hint, no traceback.schema_versionkey loads silently as the current schema (int(d.get("schema_version", PLAN_SCHEMA_VERSION))infrom_dict).Task.origin/Task.statusandPlanRisk.kindare validated at construction via__post_init__; a hand-edited/corrupted plan surfaces an actionable "malformed plan" error instead of a traceback.resolve_plan(devague/cli/_plans.py) now distinguishes invalid slug / newer schema / missing / malformed — each with its own remediation, mirroring frameresolve.Deliberately out of scope (the issue's "consider … ids" item)
Task/dep/cover id cross-reference validation at load is not added: coverage and acyclic-dependency checks already run against the live frame in
plan converge; validating at load would duplicate that logic and could reject valid in-progress plans. The frame engine likewise doesn't validate id cross-refs at load. Per-issue non-goals (no model redesign, no TDD entities, no orchestration) respected.Tests & verification
tests/test_plan.py,tests/test_plan_store.py,tests/test_cli_plan.py)."schema_version": 1; tampering to99→ upgrade-hint error (exit 1); badorigin→ malformed error (exit 1); removing the key → loads fine.Docs & version
docs/spec-contract.md"The plan peer" documents the plan schema/version + validation behavior.Version bumped 0.6.1 → 0.7.0 with a CHANGELOG entry.
devague (Claude)