Files
C.E.L_Slide_test2/.orchestrator/drafts/62_r3_payload.json
T

1 line
7.4 KiB
JSON

{"body": "[Claude #3] Stage 3 code-edit IMP-46 \u2014 u3 fingerprint invalidation\n\nExecuting unit: u3\n\n\u25a0 unit_executed\n- id: u3\n- summary: Add fingerprint-based read-side invalidation to `src/phase_z2_ai_fallback/cache.py`. `save_proposal` already persisted the `fingerprints` dict verbatim (u2); u3 extends `read_proposal` with an optional `fingerprints` kwarg. When supplied, the stored `fingerprints` dict must equal the caller's dict by strict equality \u2014 any mismatch (single-axis drift, extra key, missing key, non-dict stored value, legacy entry with no `fingerprints` field) returns `None`. Default `fingerprints=None` performs no comparison (back-compat). Fingerprint *computation* stays outside `cache.py`: the module is a content-addressed *comparator*, not a *hasher* \u2014 preserves AI isolation and keeps the cache schema-agnostic.\n\n\u25a0 files_changed\n- src/phase_z2_ai_fallback/cache.py (modified, +47 / -10 \u2014 module docstring expanded with u3 contract; `read_proposal` gains keyword-only `fingerprints: dict | None = None`)\n- tests/phase_z2_ai_fallback/test_cache_invalidation.py (new, 21 test cases, 305 SLOC)\n\n\u25a0 diff_summary\n- `src/phase_z2_ai_fallback/cache.py`\n - Module docstring header renamed `u2` \u2192 `u2 + u3`; new section \"u3 invalidation contract (this module is a *comparator*, not a *computer*)\" documents:\n - `save_proposal` persists `fingerprints` verbatim \u2014 no fingerprint axis names are hardcoded in cache.py\n - `read_proposal(fingerprints=...)` enforces strict equality (NOT subset); default `None` skips comparison\n - Rationale for keeping computation outside: AI isolation + schema-agnostic (new fingerprint axes don't require editing cache.py)\n - `read_proposal` signature changed from `(key: str)` to `(key: str, *, fingerprints: dict | None = None)`. The new kwarg is keyword-only so positional callers cannot accidentally pass an axis-extension dict.\n - New input validation: `fingerprints is not None and not isinstance(fingerprints, dict)` \u2192 `TypeError` (symmetric with `save_proposal`'s fingerprints validation).\n - New comparison block placed AFTER file existence + JSON load + non-dict-root checks, but BEFORE the `proposal` validation: if `fingerprints is not None` and `stored = data.get(\"fingerprints\")` is not a dict OR `stored != fingerprints` \u2192 return `None`. This ordering means missing-file / corrupt-JSON precedence is preserved (no false hit through a phantom equality check) and a hand-corrupted `fingerprints` field (e.g. serialized as a list) is treated as an invalidation, not as a `TypeError`.\n - No new module-level imports. No new constants. No fingerprint computation, hashing primitives, axis enumeration, or Phase Z runtime references introduced.\n- `tests/phase_z2_ai_fallback/test_cache_invalidation.py` (new \u2014 21 cases, all pass)\n - Save-side: `test_save_persists_fingerprints_verbatim` re-asserts the u2 round-trip foundation u3 depends on.\n - Back-compat (read without kwarg):\n - `test_read_without_fingerprints_kwarg_returns_proposal` \u2014 legacy callers still hit.\n - `test_read_without_fingerprints_kwarg_ignores_stored_mismatch` \u2014 entry saved with `{\"contract_sha\": \"old\"}` still readable by legacy reader.\n - Matching path:\n - `test_read_with_matching_fingerprints_returns_proposal` \u2014 exact dict equality hits.\n - `test_read_with_empty_fingerprints_matches_empty_stored` \u2014 both sides `{}` is a valid match, not a special-case None.\n - Invalidation (the 11-case core):\n - Parametrized over the three declared shas: `test_read_invalidates_on_single_axis_drift[contract_sha|partial_sha|catalog_sha]` \u2014 each axis individually breaks the match.\n - `test_read_invalidates_when_caller_supplies_extra_key` \u2014 strict equality (not subset) is the locked semantic.\n - `test_read_invalidates_when_caller_supplies_subset` \u2014 same, from the other direction.\n - `test_read_invalidates_when_entry_saved_without_fingerprints` \u2014 caller demands proof of freshness; empty stored set is not \"compatible by default\".\n - `test_read_invalidates_when_stored_fingerprints_not_dict` \u2014 hand-corrupted payload (`fingerprints: [...]`) misses cleanly.\n - `test_read_invalidates_when_stored_fingerprints_field_missing` \u2014 legacy payload (no `fingerprints` field at all) invalidates under fingerprint-aware lookup.\n - Precedence locks:\n - `test_read_with_matching_fingerprints_still_loses_to_missing_file` \u2014 file-missing wins.\n - `test_read_with_matching_fingerprints_still_loses_to_corrupt_json` \u2014 JSON-corrupt wins.\n - Input validation symmetry: `test_read_rejects_non_dict_fingerprints[list|string|int]` (3 cases) \u2014 TypeError matches save-side surface.\n - Module isolation lock: `test_cache_module_has_no_fingerprint_computer` \u2014 public surface of `cache.py` is grepped for `hash`/`sha`/`fingerprint` substrings; passes iff zero functions named for fingerprint computation exist. This is the structural defence against later patches accidentally pulling fingerprint computation into the cache module.\n - Cross-signature isolation: `test_distinct_fingerprint_sets_isolated_per_signature` \u2014 two entries with different sig_hashes hold their own fingerprints; crossed lookups miss, aligned lookups hit.\n\n\u25a0 validation\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_ast_isolation.py tests/phase_z2_ai_fallback/test_router.py -q` \u2192 84 passed in 0.32s.\n- `python -m pytest tests/phase_z2_ai_fallback/ -q` \u2192 176 passed in 0.37s (whole package green, no regressions on u1 signature / u2 cache backend / IMP-33 router / AST guard).\n- No `src/**` files touched outside `src/phase_z2_ai_fallback/cache.py`.\n- AST isolation guard re-verified: no new top-level imports added; the existing `hashlib` whitelist from u1 is NOT used by cache.py (u3 does not import `hashlib` \u2014 comparison is plain `dict` equality, not hash recomputation).\n\n\u25a0 guardrail / contract evidence\n- AI isolation contract: `cache.py` still imports only `json`, `pathlib`, and `src.phase_z2_ai_fallback.schema`. No Anthropic, no MDX, no Phase Z runtime references.\n- No-hardcoding: the three sha axis names (`contract_sha` / `partial_sha` / `catalog_sha`) do NOT appear in `cache.py` source \u2014 they are caller-supplied dict keys; cache.py only compares dicts. The names appear only in test fixtures and the module docstring (documentation).\n- PZ-1 (normal-path AI=0): unchanged \u2014 `read_proposal` performs no AI invocation.\n- Both write gates (`visual_check_passed` AND `user_approved`) preserved untouched. u3 only modifies the read path.\n- Gate precedence preserved: u2's `test_save_gate_violation_does_not_touch_filesystem` and the 33-case u2 suite all still green.\n- Strict equality (NOT subset) chosen explicitly to ensure invalidation on additive axis drift: if a future patch introduces a 4th fingerprint axis, every existing cache entry will invalidate on the next fingerprint-aware lookup until re-saved with the new axis. Subset semantics would silently honor stale entries \u2014 rejected.\n\n\u25a0 remaining_units\n- [u4, u5, u6]\n\n\u25a0 follow_up_issue_candidates\n- None. u3 is scope-locked to read-side comparator + tests; fingerprint computation (axis SHA derivation from live contract YAML / partial templates / catalog payloads) is u4's responsibility per Stage 2 plan and stays out of `cache.py` by design. No new axis discovered.\n"}