Contracts
freshness-cache-contract.md
Contract: Charter Freshness Cache (WP-B)
Governs NFR-002 (no stale governance verdict), C-005 (fail-closed, content-keyed).
Key inputs (all three; a missing one = fail-closed miss)
The freshness verdict depends on THREE off-BUNDLE_CONTENT_HASH_FILES inputs that _compute_synthesized_drg (computer.py:648-669) reads. The key MUST fold in all three: 1. the charter bundle — compute_bundle_content_hash(repo_root) (covers charter.yaml); 2. the synthesized-DRG graph file (graph.yaml, via _doctrine_graph_path); 3. the synthesis-manifest file (.kittify/charter/synthesis-manifest.yaml, MANIFEST_PATH) — the verdict reads manifest.built_in_only (:655) and manifest.bundle_content_hash (:761-767), so a (charter, graph)-only key serves a stale verdict when the manifest drifts.
Key = sha256(bundle_hash + ":" + graph_hash + ":" + manifest_hash).
Behavioral guarantees (each is an in-diff test)
1. Hit correctness: given an unchanged charter bundle, graph file, AND manifest file, a second next returns a verdict equal to the freshly-computed one, without calling _safe_load_yaml/compute_freshness (proven by a spy/patch asserting the parse is skipped). 2. Bundle invalidation: mutate any file in BUNDLE_CONTENT_HASH_FILES (e.g. charter.yaml) → the next lookup MISSES → the served verdict reflects the change. 3. DRG-graph invalidation: mutate the graph file only (bundle + manifest untouched) → the next lookup MISSES → the served verdict reflects the change. 4. Manifest invalidation: mutate synthesis-manifest.yaml only (bundle + graph untouched) — e.g. flip built_in_only or change bundle_content_hash → the next lookup MISSES → the served verdict reflects the change. (The B1 stale-"fresh" guard the post-tasks squad added.) 5. Fail-closed: if any of the three hashes cannot be computed (missing/unreadable), the read is treated as a miss and compute_freshness runs; no poisoned entry is written. 6. Content-only key: touching a file's mtime without changing its content does NOT invalidate (proves the key is content-based, not mtime-based). 7. Schema-version invalidation: bumping schema_version invalidates all prior entries.
Non-goals
- Does not change charter-freshness semantics — the verdict a cache-miss computes is byte-for-byte the pre-mission verdict.
- Does not cache anything beyond the freshness verdict on the
nextpreflight path.
next-output-preservation-contract.md
Contract: next Output Preservation (NFR-004) — spans WP-A + WP-B
Both durable-fix levers MUST be behavior-preserving. The oracle is a real black-box subprocess diff.
Guarantee
For identical mission inputs, spec-kitty next --agent <a> --mission <m> --json produces byte-identical stdout JSON whether:
- imports are trimmed (WP-A) or not,
- the charter freshness verdict is served from cache (warm) or freshly computed (cold, WP-B),
excluding the intrinsically per-call timestamp field (generated at emit, never part of the projection or the freshness verdict).
Test shape
- Run the CLI twice via
subprocess.run([sys.executable, "-m", "specify_cli", "next", …, "--json"])(cold then warm), capture stdout, JSON-load, delete/normalize onlytimestamp, assert equality. - Do NOT reuse the masked
canonical()helper fromtests/runtime/test_bridge_parity.py— its masking of ULID/timestamp/path noise would silently accept a real regression that lands in a masked field. NFR-004 wants literal byte-identity (minus the one documentedtimestamp). - Cover both the no-charter fixture (WP-A path) and a charter-bearing fixture (WP-B path).
Rationale
Deferred (lazy) imports must resolve identically when first used; a cached freshness verdict must deserialize to the same object the parse would have produced. This contract is the safety net that lets both optimizations ship without changing observable behavior.