Implementation Plan: Revive the compat-planner.json contract check (CI-portable) and fix the drift it surfaces
Branch: fix/compat-planner-contract-ci-portable | Issue: #2419 | Spec: spec.md
Summary
Resurrect the dead compat-planner.json contract-conformance check in tests/specify_cli/cli/commands/test_upgrade_command.py. The shared helper _validate_json_contract resolves the contract via a worktree-relative path that only exists in a maintainer landing-worktree, so in CI _CONTRACT is None and jsonschema.validate early-returns at every call site — validating nothing. Fix: (1) anchor the path on the already-correct repo root and drop the silent .exists()/suppress fallback so a missing contract fails hard (FR-001/002/003, NFR-001); (2) because the revived check immediately catches one real pre-existing drift — UnifiedBundleMigration.description at 283 chars vs the contract's maxLength: 256 — trim that description to ≤256 preserving meaning so the suite is green with validation genuinely live (FR-005), never by re-suppressing; (3) prove the check is non-vacuous (a schema-violating payload fails) and the full file is green (FR-004, SC-001–004). The post-spec squad empirically confirmed exactly one violation (48 pass / 1 fail). The post-plan squad surfaced a second dead enforcer of the same contract — tests/specify_cli/compat/test_messages.py (parents[4] overshoot, 5 render_json call sites) — so the mission closes the defect class by reviving it too, with the correct parents[3] anchor (empirically green, 28 pass) (FR-006, SC-005).
Technical Context
Language/Version: Python 3.11 (repo pinned) Primary Dependencies: pytest, jsonschema (hard dep, pyproject.toml), typer.testing.CliRunner; module-under-test is the spec-kitty upgrade CLI surface Storage: n/a (reads the committed JSON contract file) Testing: tests/specify_cli/cli/commands/test_upgrade_command.py (the ~12 _validate_json_contract call sites); target test test_project_migration_needed_project_dry_run_json_contract Target Platform: CI (GitHub Actions /home/runner/work/spec-kitty/spec-kitty) + local + maintainer .worktrees/<name>/ Project Type: single project (test-infra fix + one production-string trim) Performance Goals: n/a Constraints: reuse _WORKTREE_ROOT (Path(__file__).parents[4]); no new # type: ignore/# noqa/silent suppress; contract file + upgrade runtime behavior unchanged; mypy --strict + ruff clean Scale/Scope: 2 files — test_upgrade_command.py (path + fail-hard) and m_3_2_0rc35_unified_bundle.py (trim one description)
Charter Check
GATE: must pass before task decomposition.
- Evidence-first / non-vacuous (red-first) — the fix is proven by a payload that FAILS validation pre-fix and passes post-fix; the revival itself is shown to catch the 283-char drift before FR-005 trims it. ✅
- Never retry-to-green / no re-suppression — green is reached by trimming the real drifted string, not by neutering the check, skipping, or gaming the fixture. ✅
- Canonical sources — anchor on the already-correct
_WORKTREE_ROOT; do not invent a new root-finder; do not touch the stable contract. ✅ - Draft-PR-first / operator decides — lands as a cross-fork draft; operator merges. ✅
- Quality gates —
ruff+mypy --strictclean, no new suppressions. ✅
No violations → Complexity Tracking not required.
Project Structure
Documentation (this mission)
kitty-specs/compat-planner-contract-ci-portable-01KWVKYH/
├── spec.md · plan.md · tasks.md
Source / deliverables (repository root)
tests/specify_cli/cli/commands/test_upgrade_command.py # repo-root-anchored path + hard-fail (no skip); FR-001/002/003
tests/specify_cli/compat/test_messages.py # parents[3] anchor + hard-fail; activate 5 render_json checks; FR-006
src/specify_cli/upgrade/migrations/m_3_2_0rc35_unified_bundle.py # trim UnifiedBundleMigration.description to <=256; FR-005
Structure Decision: single project; the fix spans two dead-check test files and one production description string.
Implementation Concern Map
IC-01 — CI-portable contract resolution + always-validate + fail-hard
- Purpose: make the shared
_validate_json_contracthelper load the real contract in any checkout and validate unconditionally; a missing/unreadable contract fails hard. - Relevant requirements: FR-001, FR-002, FR-003; NFR-001; SC-001, SC-003.
- Affected surfaces:
test_upgrade_command.pymodule-level_CONTRACT_PATH/_CONTRACT(lines ~61-75) and the helper_validate_json_contract(lines ~107-124). Resolve_CONTRACT_PATH = _WORKTREE_ROOT / "kitty-specs" / "cli-upgrade-nag-lazy-project-migrations-01KQ6YDN" / "contracts" / "compat-planner.json"; load the contract at import (or first use) and let a load failure raise with a clear message (drop.exists()+contextlib.suppress); remove theif _CONTRACT is not None:guard so validation always runs. - Sequencing/depends-on: none (root change).
- Risks: activating validation at ~12 call sites surfaces real non-conformances — expected; the one known is handled in IC-02, any other is a real violation to triage (not re-suppress).
IC-02 — Remediate the drift the revived check surfaces
- Purpose: bring the one over-cap description into compliance so the live check is green.
- Relevant requirements: FR-005; SC-002, SC-004.
- Affected surfaces:
m_3_2_0rc35_unified_bundle.pyUnifiedBundleMigration.description— trim from 283 to ≤256 chars preserving meaning (e.g. condense the trailing "Does NOT scan worktrees, remove symlinks, or reconcile .gitignore." clause). Do NOT change the contract'smaxLength: 256. - Sequencing/depends-on: IC-01 (the check must be live to witness the fix).
- Risks: over-trimming loses meaning — keep the substantive "validate bundle / refresh derivatives / structured report / idempotent" content.
IC-03 — Non-vacuous witness + full-file green
- Purpose: prove the check genuinely fires and the whole file is green with validation live.
- Relevant requirements: FR-004; SC-001, SC-002, SC-004.
- Affected surfaces: a focused assertion that a deliberately schema-violating payload raises
jsonschema.ValidationError(the non-vacuous witness), plus a simulated-CI-layout check that_CONTRACTloads; run the fulltest_upgrade_command.pygreen after IC-01+IC-02. - Sequencing/depends-on: IC-01, IC-02.
- Risks: the witness must assert on the real helper/contract, not a stubbed schema, or it proves nothing.
IC-04 — Close the class: revive the sibling render_json check
- Purpose: revive the second dead enforcer of the same contract so no vacuous-green check survives the mission.
- Relevant requirements: FR-006; SC-004, SC-005.
- Affected surfaces:
test_messages.py_CONTRACT_PATH(lines ~44-50) → anchor onPath(__file__).parents[3](this file is 3-deep, so parents[3] is the repo root — NOT parents[4]);_get_contract/_validate_against_schema(lines ~279-292) → drop the.exists()→None+except Exception: return None+if contract is None: returnsilent-skip so a missing/unreadable contract fails hard; the 5render_jsoncall sites (311,325,340,353,366) then validate for real (empirically green, 28 pass). - Sequencing/depends-on: none (independent of IC-01/02/03; same class, different file).
- Risks: depth differs from the sibling — use
parents[3]here vsparents[4]intest_upgrade_command.py; do not copy the sibling's index. Keep thejsonschema-absent fallback branch honest (fail-hard, not silent-return).