1 line
8.9 KiB
JSON
1 line
8.9 KiB
JSON
{"body": "[Claude #5] Stage 3 code-edit IMP-46 — u5 auto-cache CLI flag + 2^3 gate truth table\n\nExecuting unit: u5\n\n■ unit_executed\n- id: u5\n- summary: Wire IMP-46 u5 auto-cache opt-in across three layers — (1) add `ai_fallback_auto_cache: bool = False` to `src/config.py` Settings; (2) extend `src/phase_z2_ai_fallback/cache.py::save_proposal` with a keyword-only `auto_cache: bool = False` parameter that bypasses ONLY the `user_approved` gate (`visual_check_passed` remains mandatory and non-bypassable); (3) add `--auto-cache` CLI flag in `src/phase_z2_pipeline.py` that mutates `settings.ai_fallback_auto_cache=True` at parse time. The 2^3 gate truth table over `(visual_check_passed, user_approved, auto_cache)` is now exhaustively enumerated in `tests/phase_z2_ai_fallback/test_cache.py` with exactly three persisting rows: `(T,T,F)`, `(T,T,T)`, `(T,F,T)`. Every other row raises `AiFallbackCacheGateError` before any filesystem touch.\n\n■ files_changed\n- src/config.py (+8 lines — `ai_fallback_auto_cache: bool = False` Settings field + docstring)\n- src/phase_z2_ai_fallback/cache.py (+~30/-10 lines — `save_proposal(auto_cache=False)` keyword-only param, docstring expanded with u5 contract section, module docstring `u2 + u3` → `u2 + u3 + u5`)\n- src/phase_z2_pipeline.py (+~20 lines — `--auto-cache` argparse flag + in-process `settings.ai_fallback_auto_cache=True` mutation)\n- tests/phase_z2_ai_fallback/test_cache.py (+~110 lines — 8-row parametrised truth-table test + 5 targeted u5 cases)\n- tests/test_phase_z2_ai_fallback_config.py (+~12 lines — `test_ai_fallback_auto_cache_default_off`)\n\n■ diff_summary\n- `src/config.py`\n - New field `ai_fallback_auto_cache: bool = False` placed alongside the existing IMP-33 u1 ai_fallback_* block. Docstring explains the gate semantics (\"bypasses `user_approved` only; `visual_check_passed` never bypassable\") and locks the default OFF + CLI mutation contract.\n- `src/phase_z2_ai_fallback/cache.py`\n - Module docstring header: `IMP-46 u2 + u3` → `IMP-46 u2 + u3 + u5`. New \"u5 auto-cache contract\" section enumerates the truth table (3 cells succeed, 5 raise) and pins the keyword-only / default-False shape.\n - Guardrails block updated: \"Both write gates preserved — `visual_check_passed=False` always raises … `user_approved=False` also raises by default; the IMP-46 u5 `auto_cache=True` override bypasses ONLY the `user_approved` gate (`visual_check_passed` is never bypassed). Gate violation never silently no-ops.\"\n - `save_proposal` signature gains keyword-only `auto_cache: bool = False` at the end of the kwargs block (after `fingerprints`). Position-only callers cannot accidentally pass it.\n - New type-check: `if not isinstance(auto_cache, bool): raise TypeError(\"auto_cache must be a bool\")`. Symmetric with `slide_css` / `fingerprints` validation.\n - Gate logic change — single line:\n - Before: `if not user_approved: raise AiFallbackCacheGateError(...)`\n - After: `if not user_approved and not auto_cache: raise AiFallbackCacheGateError(...)`\n - The `visual_check_passed` gate is unchanged — `auto_cache` does NOT appear in that branch. Error messages updated to reflect the new bypass surface (\"auto_cache cannot bypass this gate\" on visual-check failure; \"Pass auto_cache=True (or --auto-cache on the CLI) to bypass\" on user-approved failure).\n - No new imports. No `src.config` import inside cache.py — the cache module remains a pure comparator/storage layer that takes the resolved boolean from the caller (preserves AI isolation contract `tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69`).\n- `src/phase_z2_pipeline.py`\n - New `--auto-cache` flag added to the existing `if __name__ == \"__main__\"` argparse block (after `--override-section-assignment`, before `args = parser.parse_args()`). `action=\"store_true\"`, `default=False`. Help text states the gate semantics + setting wiring.\n - After `parse_args()`: a 3-line conditional imports `settings` from `src.config` and sets `ai_fallback_auto_cache=True` when the flag is present. The deferred import (inside `if args.auto_cache:`) keeps `src.config` off the unconditional pipeline import surface — only operators who opt in pull it in.\n - The mutation is in-process only — no .env write, no side effects between runs. A subsequent pipeline run without `--auto-cache` starts back at the Settings default (`False`).\n- `tests/phase_z2_ai_fallback/test_cache.py`\n - New section header \"IMP-46 u5: auto_cache gate (2^3 truth table)\".\n - `_GATE_TRUTH_TABLE` constant — 8 rows, exhaustive Cartesian product of `(V, U, A)` × `expect_persist`. Hand-locked, NOT generated from the implementation, so a regression in the gate logic surfaces as a row failure (3 persist + 5 raise).\n - `test_save_gate_truth_table` parametrised — for each row, either calls `save_proposal` and asserts the file exists (persist row), or asserts `AiFallbackCacheGateError` raises AND the frame_id directory is absent (gate row — symmetric with the existing `test_save_gate_violation_does_not_touch_filesystem`).\n - `test_auto_cache_default_off_preserves_dual_gate_semantics` — calling without the `auto_cache` kwarg keeps the IMP-46 u2 behaviour (`user_approved=False` raises with `\"user_approved\"` in the message; no directory created).\n - `test_auto_cache_cannot_bypass_visual_check` — `(V=False, U=True, A=True)` row promoted to its own assertion that also checks the error message mentions `visual_check_passed` (regression sentinel against accidentally moving `auto_cache` into the visual-check branch).\n - `test_auto_cache_bypass_user_approved_persists` — `(V=True, U=False, A=True)` round-trip: persists, file exists, `read_proposal` returns the original payload.\n - `test_auto_cache_rejects_non_bool` — passing `auto_cache=\"yes\"` raises `TypeError` (symmetric with the existing non-string / non-dict guards for `slide_css` and `fingerprints`).\n - `test_auto_cache_is_keyword_only` — `inspect.signature(save_proposal).parameters[\"auto_cache\"]` is `KEYWORD_ONLY` and default `False`. Locks the public surface so a future refactor can't accidentally make it positional or default-True.\n- `tests/test_phase_z2_ai_fallback_config.py`\n - New section explaining the u5 CLI / settings contract.\n - `test_ai_fallback_auto_cache_default_off` — `Settings().ai_fallback_auto_cache is False`. The CLI flag mutates the in-process settings instance; the default must stay OFF so the dual-gate contract survives without an operator opt-in.\n\n■ validation\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py -q` → 51 passed in 0.22s\n- `python -m pytest tests/phase_z2_ai_fallback/ -q` → 201 passed in 0.40s (u1-u4 regression incl. test_ast_isolation, test_signature, test_cache_invalidation, test_step12 all green)\n- `python -m pytest tests/test_family_contract_baseline.py -q` → 4 passed in 0.59s\n- `python -m src.phase_z2_pipeline --help` → `--auto-cache` flag surfaced with the documented help text\n- AST isolation guard re-confirmed: cache.py imports unchanged (`json`, `pathlib`, `src.phase_z2_ai_fallback.schema`); no `src.config` import inside `src/phase_z2_ai_fallback/` (the setting is read by the CLI entry point and the resolved bool is passed in by callers, preserving the AI-isolation contract `tests/phase_z2_ai_fallback/test_ast_isolation.py:48-69`)\n\n■ scope-lock verification\n- ✅ u5 declared files only (config.py / cache.py / phase_z2_pipeline.py / test_cache.py / test_phase_z2_ai_fallback_config.py).\n- ✅ No `route_ai_fallback` call site changes — IMP-33 AI hook untouched.\n- ✅ No `read_proposal` signature change — u3 fingerprint comparator surface preserved.\n- ✅ No `save_proposal` call site changes outside tests — there is no production `save_proposal` invocation yet (verified by `rg \"save_proposal\\\\(\" src` — only `cache.py` definition + docstring references in `router.py`).\n- ✅ `visual_check_passed` gate is non-bypassable — `auto_cache` only relaxes `user_approved`. Truth-table row `(V=False, U=True, A=True)` raises, locked by `test_auto_cache_cannot_bypass_visual_check`.\n- ✅ Settings default OFF — `Settings().ai_fallback_auto_cache is False` (`test_ai_fallback_auto_cache_default_off`). Honours `feedback_ai_isolation_contract` (auto-cache is an explicit operator opt-in, not silent).\n- ✅ No-hardcoding lock — no sample / MDX / template_id specific literal added. `auto_cache` is a pure axis of the cache write gate.\n\n■ remaining_units\n- [u6] — Track `data/frame_cache/.gitkeep` via `.gitignore` exception under the existing `data/` ignore, add repo-layout coverage (`tests/phase_z2_ai_fallback/test_cache_repo_layout.py`), and refresh `docs/architecture/IMP-17-CARVE-OUT.md` cache row from stub status → persistent backend status.\n\n■ follow_up_issue_candidates\n- None. u5 scope-lock held; no axis discovered outside `(V, U, A)` × cache write surface.\n\nFINAL_CONSENSUS_TARGET: continue_same (u6 remains)\n"} |