Files
C.E.L_Slide_test2/.orchestrator/issues/62_comments_cache.json
T

1 line
138 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
[{"id": 22690, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22690", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #1] Stage 1 problem-review — IMP-46\n\n## 1. Source re-read + pair context\n\n- source axis = #62 (frame transformation cache layer — content-addressed cache; lookup before AI call + save after `visual_check PASS + 사용자 OK` or `--auto-cache`).\n- pair = #61 IMP-33 (closed at `c864fe0`, AI fallback scaffolding u1~u11, flag default OFF). IMP-33 already shipped a **u6 cache stub** + a **u8 step12 cache_key build site** that is sample-specific. IMP-46 = replace the stub backend AND fix the key.\n- depend = IMP-33 AI hook (now landed); declared consumer = every `route_ai_fallback` call site (currently only `gather_step12_ai_repair_proposals`).\n- out-of-scope (explicit) = AI call itself (IMP-33), cache → catalog promotion (R4).\n- roadmap axis = R3 (AI 보정/재구성 보조), wave 1 (실질 구동 필수).\n\n## 2. Root cause — what IMP-33 stub left unfinished (3-axis)\n\n### Axis A — u6 cache backend = NotImplementedError marker\n\n`src/phase_z2_ai_fallback/cache.py:79-82` (verified):\n\n```python\nraise NotImplementedError(\n \"IMP-46 persistent cache storage is not implemented yet; \"\n \"this is the IMP-33 u6 stub marker.\"\n)\n```\n\n`read_proposal` returns `None` for any key (`cache.py:36-45`); `save_proposal` enforces both gates then raises NotImplementedError (`cache.py:48-82`). IMP-46 = replace the marker with a real backend (read + write JSON under `data/frame_cache/{frame_id}/{signature_hash}.json` per issue spec). Gate semantics (visual_check_passed AND user_approved → write path) are already in place and must be preserved.\n\n### Axis B — u8 cache_key is sample-specific (the critical defect)\n\n`src/phase_z2_ai_fallback/step12.py:109-111` (verified):\n\n```python\ncache_key = \"::\".join(\n [template_id, \",\".join(sorted(record[\"source_section_ids\"]))]\n)\n```\n\n`source_section_ids` is the MDX section identifier (e.g., `\"02.mdx::sec-0\"`). This means:\n- a fresh MDX with the same structural shape → guaranteed cache MISS;\n- two MDX with different identifiers but identical signature → never hit;\n- adding/renaming a sample → orphans the cache entry.\n\nThis defeats the cache's reason to exist. The whole point of `feedback_no_hardcoding` (\"signature hash 가 sample-specific case 만들지 않게\") is to make the key **structural**, not source-identifier-based.\n\nIssue spec's required signature axes (verified against catalog + V4 evidence schema):\n\n| signature axis | source at HEAD | structural? |\n|---|---|---|\n| `frame_id` | `frame_contracts.yaml` per-template `frame_id:` (e.g. `1171281190`) — `templates/phase_z2/catalog/frame_contracts.yaml:23` | yes |\n| `v4_label` | V4 result `label` (`light_edit` / `restructure`) — passed as `record[\"label\"]` | yes |\n| `cardinality` | frame contract `cardinality.strict` OR V4 result `cardinality_signature` — `frame_contracts.yaml:27-29` | yes |\n| `source_shape` | `frame_contract.source_shape` (`top_bullets` / `paragraph` / `table`) — `frame_contracts.yaml:26` | yes |\n| `h3_count` | source MDX subsection count (derivable from `unit.raw_content`) | yes |\n| `char_count_bucket` | discretized text length (e.g., `<100`, `100-300`, `300-700`, `700+`) | yes |\n| `layout_preset` | `sidebar-right` / `two-column` / `hero-detail` / `single-column` — Phase Z layout choice | yes |\n| `zone_position` | `top` / `bottom_l` / `bottom_r` — zone topology position | yes |\n\nNone of these axes carry sample-specific identifiers. All are derivable from V4 result + frame contract + the unit's shape (not its source path). Hash → short hex digest.\n\n### Axis C — invalidation surface (3 sources)\n\nIssue spec: `frame contract 변경 / partial template 변경 / catalog 업데이트 → 해당 frame cache 폐기`. Verified sources at HEAD:\n\n| source | path | scope |\n|---|---|---|\n| frame contract | `templates/phase_z2/catalog/frame_contracts.yaml` (loaded by `src/phase_z2_mapper.py:49 load_frame_contracts` with `_CATALOG_CACHE` global) | per-template subtree |\n| partial template (family) | `templates/phase_z2/families/*.html` (12 files at HEAD) | per-template file |\n| partial template (frame) | `templates/phase_z2/frames/*.html` (2 files at HEAD: `process_product_two_way.html`, `three_parallel_requirements.html`) | per-template file |\n| catalog update | structural changes to `frame_contracts.yaml` (added `sub_zones`, `accepted_content_types`, etc.) | global if structural |\n\nTwo strategies under consideration (Stage 2 lock target):\n- **(I1) per-entry embedded fingerprint** — each cache entry stores SHA-256 of: (a) its template subtree of `frame_contracts.yaml`, (b) the family/frame partial HTML it references. On read, recompute and compare; mismatch → miss + log; never serve stale.\n- **(I2) global manifest version** — single counter bumped on any catalog/partial change. Simpler but coarse (one frame change invalidates everything).\n\nClaude #1 preference = **I1** — surgical, audit-clear, no manual counter maintenance, aligns with `feedback_no_hardcoding` (no global state coupling).\n\n### Axis D — cache value semantics (ambiguity in issue spec)\n\nIssue spec cache value = `builder_options + partial_overrides + slot mapping (+ slide-level CSS)`. Mapped to IMP-33 u2 schema (`src/phase_z2_ai_fallback/schema.py:22-25`):\n\n| issue spec field | u2 ProposalKind | reusability across samples |\n|---|---|---|\n| `builder_options` | `BUILDER_OPTIONS_PATCH` | high — structural (parser switch, knob value) |\n| `partial_overrides` | `PARTIAL_OVERRIDES` | **ambiguous** — payload `slots` may contain text content |\n| `slot mapping` | `SLOT_MAPPING_PROPOSAL` | high — content-unit → slot decision (no raw text) |\n| `slide-level CSS` | NOT in IMP-33 u2 whitelist | **out-of-scope under FORBIDDEN_KINDS (`raw_css`)** — `schema.py:28-30` |\n\n**The slide-level CSS branch is forbidden by IMP-33 schema** (`raw_css` in `FORBIDDEN_KINDS`). Issue spec's parenthetical \"(+ slide-level CSS 있으면)\" cannot be honored without expanding the u2 forbidden list, which would re-open IMP-17 carve-out. Scope-lock: **drop \"slide-level CSS\" from cache value**.\n\nThe `PARTIAL_OVERRIDES` payload may contain text. Two interpretations:\n- **(D1) verbatim cache** — store the AI proposal as-is (text included). Then signature MUST include a content fingerprint of the MDX text to avoid wrong-text false hits. This collapses cache hits to \"same MDX\" → no cross-sample generalization → cache hit rate ≈ re-render same file.\n- **(D2) structural cache** — store only structural decisions (builder_options + slot-mapping structure). Text content NOT cached; on hit, AI is not re-called, but text re-flows through the deterministic pipeline using the cached structural decisions. Cross-sample generalization works.\n\nClaude #1 reading: issue spec's \"결정론적 재사용\" + \"signature hash 가 sample-specific case 만들지 않게\" guardrails point to **D2**. Stage 2 must lock; Codex review needed.\n\n## 3. Scope-lock proposal (binding boundaries — Stage 2 will refine)\n\n### (a) Behavior delta — what changes, what does NOT\n\n| axis | today (HEAD `c864fe0`) | after IMP-46 |\n|---|---|---|\n| `read_proposal(key)` | returns `None` for any key (`cache.py:42-45`) | reads `data/frame_cache/{frame_id}/{signature_hash}.json` if present + fingerprint valid; else `None` |\n| `save_proposal(key, proposal, *, visual_check_passed, user_approved)` | both gates → `NotImplementedError`; either gate False → `AiFallbackCacheGateError` | both gates True → JSON write at canonical path; gate semantics **unchanged** |\n| step12 cache key | `template_id::source_section_ids` (sample-specific) | structural signature hash (frame_id + v4_label + cardinality + source_shape + h3_count + char_bucket + layout_preset + zone_position) |\n| AiFallbackProposal schema (u2) | 3 kinds (`BUILDER_OPTIONS_PATCH` / `PARTIAL_OVERRIDES` / `SLOT_MAPPING_PROPOSAL`); FORBIDDEN_KINDS includes `raw_css` | **unchanged** — IMP-46 does NOT expand the whitelist. Slide-level CSS dropped from cache value |\n| `route_ai_fallback` flow (u7) | flag-off OR route-mismatch → None; else cache_read → prompt → client → validate | **unchanged** call shape. Cache read step now backed by real storage; cache hit → return validated proposal without API call |\n| normal-path AI call count | 0 (PZ-1 lock) | **0 (locked)**. Cache only activates inside fallback path; flag default still OFF |\n| user_approved signal source | placeholder kwarg in `save_proposal` (no producer yet) | new producer: pipeline `slide_status.user_approved` field OR `--auto-cache` CLI flag (single-shot per run) → propagated to caller |\n| `data/frame_cache/` directory | does NOT exist | created on first write; per-frame subdirectory (`{frame_id}/`) |\n| AST isolation guard | `tests/phase_z2_ai_fallback/test_ast_isolation.py` (IMP-33 u10) — package may NOT import Phase Q / Kei / pipeline runtime symbols | **unchanged** — IMP-46 cache backend imports nothing new from Phase Q / Kei; only stdlib (`json`, `hashlib`, `pathlib`) |\n| existing IMP-33 tests | 9 tests in `tests/phase_z2_ai_fallback/test_cache.py` (gate enforcement, NotImplementedError marker) | gate tests **preserved verbatim**; NotImplementedError test deleted and replaced with persistent-storage tests (signature determinism, hit/miss, invalidation, write-path) |\n\n### (b) Signature (the key novel surface)\n\n```python\ndef build_cache_signature(\n *,\n frame_id: str, # frame_contract.frame_id (str)\n v4_label: str, # \"light_edit\" | \"restructure\"\n cardinality: int,\n source_shape: str, # \"top_bullets\" | \"paragraph\" | \"table\"\n h3_count: int,\n char_count_bucket: str, # discretized: \"<100\" | \"100-300\" | \"300-700\" | \"700+\"\n layout_preset: str, # \"sidebar-right\" | \"two-column\" | \"hero-detail\" | \"single-column\"\n zone_position: str, # \"top\" | \"bottom_l\" | \"bottom_r\"\n) -> str:\n \"\"\"Return short SHA-256 hex digest (16 chars) of the canonical-form tuple.\"\"\"\n```\n\nBucket boundaries MUST be declared in `src/config.py` (no inline literals) so future widening is anchor-driven. Stage 2 lock target.\n\n### (c) Cache entry schema (the persisted JSON)\n\n```jsonc\n{\n \"schema_version\": 1,\n \"signature\": \"<16-char hex>\",\n \"signature_axes\": {\n \"frame_id\": \"1171281190\",\n \"v4_label\": \"light_edit\",\n \"cardinality\": 3,\n \"source_shape\": \"top_bullets\",\n \"h3_count\": 3,\n \"char_count_bucket\": \"100-300\",\n \"layout_preset\": \"two-column\",\n \"zone_position\": \"top\"\n },\n \"fingerprints\": {\n \"frame_contract_subtree_sha256\": \"...\",\n \"partial_template_sha256\": \"...\" // null if no partial referenced\n },\n \"proposal\": { \"proposal_kind\": \"...\", \"payload\": { ... }, \"rationale\": \"...\" },\n \"created_at_utc\": \"2026-05-21T07:23:11Z\",\n \"visual_check_passed\": true,\n \"user_approved\": true,\n \"auto_cache_flag\": false\n}\n```\n\nStored at `data/frame_cache/{frame_id}/{signature}.json`. On read: load → verify `fingerprints.frame_contract_subtree_sha256` matches current catalog subtree → verify `fingerprints.partial_template_sha256` matches current partial template content → if mismatch, return `None` and log invalidation reason. Pydantic schema validation on parse.\n\n### (d) Save trigger flow\n\nCurrently no site calls `save_proposal`. IMP-46 introduces the producer:\n\n```\npipeline render → visual_check (existing, src/phase_z2_classifier.py:495)\n → user approval gate (new; settings.ai_fallback_auto_cache OR explicit slide_status.user_approved)\n → if both True AND proposal exists: save_proposal(signature, proposal, ...)\n```\n\nSave site = a new orchestration helper called from the pipeline AFTER `slide_status.visual_check_passed` and AFTER the (existing) user-OK signal. Stage 2 must lock the exact wiring point. Candidate site: end of `phase_z2_pipeline.run_pipeline` post-visual-check loop.\n\n### (e) `--auto-cache` flag\n\nIssue spec mentions `--auto-cache` as user_approved bypass. Plumbing: new `src/config.py` Settings field `ai_fallback_auto_cache: bool = False`, env-overridable. Effective user_approved = `(slide_status.user_approved OR settings.ai_fallback_auto_cache)`.\n\nDefault OFF — preserves the gate's safety meaning. CI runs with default OFF → no cache writes happen on CI.\n\n### (f) Invalidation triggers (Stage 2 lock target)\n\n- **on-read** (mandatory) — fingerprint comparison; mismatch → miss.\n- **on-write** (mandatory) — embed current fingerprints in entry.\n- **on-catalog-change** (deferred to R4) — explicit purge command; out-of-scope for IMP-46.\n\n## 4. Guardrails (Stage 2 binding)\n\n| # | guardrail | source |\n|---|---|---|\n| G1 | normal-path AI call count = **0**. Cache only operates inside fallback path; cache miss + flag-OFF route = no AI call | `feedback_ai_isolation_contract`, PZ-1, `IMP-17-CARVE-OUT.md` |\n| G2 | signature MUST be structural only — NO `source_section_ids` / NO MDX file path / NO sample identifier in signature input. Verified via unit test that 2 samples with different identifiers but identical structural axes produce the SAME signature | `feedback_no_hardcoding`, issue spec \"signature hash 가 sample-specific case 만들지 않게\" |\n| G3 | cache hit = deterministic — same signature MUST return byte-identical proposal across runs (test: read same entry twice + `json.loads().model_dump() == ...`) | issue spec \"cache hit 결과 = 결정론적\" |\n| G4 | save gate = visual_check_passed AND (user_approved OR auto_cache_flag). Existing `AiFallbackCacheGateError` semantics **preserved** | `cache.py:69-78`, issue spec |\n| G5 | invalidation on partial / contract change — read-time fingerprint comparison; on mismatch, return None and log reason. No serving stale entries | issue spec \"contract/partial 변경 시 invalidate\" |\n| G6 | slide-level CSS **out** of cache value — `raw_css` is in IMP-33 u2 `FORBIDDEN_KINDS` (`schema.py:28-30`); IMP-46 does NOT expand the whitelist | `feedback_ai_isolation_contract`, IMP-17 carve-out |\n| G7 | AST isolation **preserved** — IMP-46 cache backend imports ONLY stdlib (`json`, `hashlib`, `pathlib`) + IMP-33 u2 schema. NO Phase Q / Kei / pipeline runtime imports. Verified via `test_ast_isolation.py` rerun | IMP-33 u10 contract |\n| G8 | `data/frame_cache/` MUST be gitignored or under explicit `.gitignore` rule (cache is runtime artifact, not source-of-truth). Catalog promotion (cache → committed) deferred to R4 | issue spec \"cache → catalog promote = R4\" |\n| G9 | no-hardcoding — bucket boundaries, signature axis list, fingerprint sources ALL in `src/config.py` or catalog. No sample-specific (mdx 03/04/05) branches in cache module | RULE 0, `feedback_no_hardcoding` |\n| G10 | u8 step12 cache_key build site **MUST** be updated to call the new `build_cache_signature` helper. Leaving `template_id::source_section_ids` would render IMP-46 inert | Axis B above |\n| G11 | RULE 0 — signature axes evaluated against ALL 32 frames + ALL aligned MDX shapes. No frame-specific branching in signature builder | RULE 0 PIPELINE-CONSTRUCTION |\n| G12 | `auto_cache_flag` default **False** at HEAD merge. CI default preserves zero-write behavior | safety + IMP-33 default-OFF symmetry |\n| G13 | docstring + IMP-17/IMP-31 doc sync — `cache.py` module docstring updated (NotImplementedError marker removed); `IMP-17-CARVE-OUT.md:54` IMP-46 row updated from \"stub\" to \"active backend, gate semantics preserved\" | doc-sync, anchor sync |\n| G14 | `feedback_auto_pipeline_first` — no `review_required` injection between AI call and cache save. Save gate decision is auto (visual_check + user_approved/auto_cache); failure paths emit clear reason strings, not queues | `feedback_auto_pipeline_first` |\n| G15 | post-IMP-46 backlog row in `PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md` added with status close + reference to commit. Currently stale (IMP-33/IMP-46 rows missing — already flagged in #61 Stage 1) | anchor sync |\n\n## 5. Implementation slicing sketch (Stage 2 input — NOT binding)\n\nSuggested wave-1 ordering:\n\n1. **U1 — Config plumbing (zero behavior change)**\n - `src/config.py` Settings : `ai_fallback_auto_cache: bool = False`, `ai_fallback_cache_bucket_boundaries: tuple = (100, 300, 700)` (declared values, env-overridable).\n - .env.example update; no committed .env.\n\n2. **U2 — Signature builder + axes**\n - New module `src/phase_z2_ai_fallback/cache_signature.py` (stdlib only).\n - `build_cache_signature(...)` → 16-char SHA-256 hex.\n - `bucket_char_count(n: int, boundaries: tuple[int, ...]) -> str` helper.\n - Tests: determinism (same input → same hash), axis sensitivity (each axis change → different hash), cross-sample collision (2 different `source_section_ids` with same structural axes → same hash).\n\n3. **U3 — Fingerprint helpers**\n - `src/phase_z2_ai_fallback/cache_fingerprint.py` — `fingerprint_frame_contract_subtree(template_id) -> str`, `fingerprint_partial_template(template_id) -> str | None`.\n - Tests: stable across reads; sensitive to catalog edit; None when no partial.\n\n4. **U4 — Persistent backend (replaces NotImplementedError)**\n - Update `src/phase_z2_ai_fallback/cache.py` — `read_proposal` reads JSON from `data/frame_cache/{frame_id}/{signature}.json`; `save_proposal` writes JSON (still gated by the existing `visual_check_passed` AND `user_approved` conditions).\n - Path resolver respects `frame_id` axis (subdirectory).\n - Tests: round-trip read/write, invalidation on fingerprint mismatch, gate enforcement preserved (existing 6 gate tests must still pass).\n\n5. **U5 — Step12 integration**\n - `src/phase_z2_ai_fallback/step12.py:109-111` — replace sample-specific `cache_key` with `build_cache_signature(...)` using V4 result + frame contract + unit shape.\n - Add axis derivation helpers (h3 count, char count bucket, layout preset, zone position).\n - Tests: 2 samples × same shape × different identifiers → same signature → cache hit on 2nd run.\n\n6. **U6 — Save site**\n - New helper `apply_fallback_save_decision(slide_status, proposal, ...) -> None` in `src/phase_z2_ai_fallback/save_site.py` (or pipeline-side module).\n - Called from pipeline after visual_check + user_approval.\n - Honors `--auto-cache` flag via config.\n - Tests: visual_check False → no write; user_approved False AND auto_cache False → no write; both True (or auto_cache True) → JSON written at canonical path.\n\n7. **U7 — `.gitignore` + invalidation log**\n - Add `data/frame_cache/` to `.gitignore`.\n - Optional: emit invalidation reason to `debug.json` (`cache_invalidation_log` additive field).\n\n8. **U8 — Tests + AST isolation rerun**\n - Full `tests/phase_z2_ai_fallback/` rerun (existing 116 tests must pass).\n - New tests under `tests/phase_z2_ai_fallback/test_cache_persistent.py`.\n - AST isolation guard rerun against new modules.\n\n9. **U9 — Docs sync**\n - `cache.py` module docstring update.\n - `IMP-17-CARVE-OUT.md:54` IMP-46 row update.\n - `IMP-31-GATE-AUDIT.md` (if any cache reference).\n - `PHASE-Z-PIPELINE-STATUS-BOARD.md` (IMP-46 line).\n - `PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md` row add.\n\n## 6. Open questions (Codex round 1차 review)\n\n- **Q1 — Cache value semantics: D1 (verbatim, text included) vs D2 (structural only, text re-flowed)?** Claude #1 preference = D2 (cross-sample generalization, no hidden text leak). Codex view?\n- **Q2 — Invalidation strategy: I1 (per-entry embedded fingerprint) vs I2 (global manifest counter)?** Claude #1 preference = I1 (surgical, audit-clear).\n- **Q3 — `--auto-cache` flag plumbing: settings (env / .env) vs CLI argument vs both?** Claude #1 strawman = settings only (env-overridable). CLI-only adds new arg-parser surface.\n- **Q4 — Signature axis list completeness: are 8 axes (frame_id / v4_label / cardinality / source_shape / h3_count / char_count_bucket / layout_preset / zone_position) sufficient?** Should `internal_region_id` be added (Layer A SPEC v1 surface)? Claude #1 view: leave for v2; today Internal Region is trace-only.\n- **Q5 — Save site placement: end of pipeline `run_pipeline` post-visual-check vs separate explicit `save_fallback_proposals(...)` helper invoked by caller?** Claude #1 preference = explicit helper (clearer audit + composability with `--auto-cache`).\n- **Q6 — Char count bucket boundaries: (100, 300, 700) vs other?** Stage 2 lock target. Claude #1 strawman based on typical Korean text density at 16px / 600px-wide zones.\n- **Q7 — Hash truncation: 16-char SHA-256 hex (default) vs longer?** Collision probability at 16 chars ≈ 2^-64; for 1k frames × 10 v4_labels × 8 cardinalities ≈ 80k entries → negligible. Claude #1 preference = 16.\n- **Q8 — Should IMP-46 also fix the symbolic `cache_key` parameter type at `route_ai_fallback(cache_key: str, ...)` in `src/phase_z2_ai_fallback/router.py:45`?** Today the router treats it as opaque string; with structural signature, type stays `str` (no shape change). Claude #1 view: no router-side change required; signature build happens caller-side (step12) and passes the result.\n- **Q9 — Pair atomicity: IMP-46 in single PR vs split (signature first, backend second)?** Claude #1 preference = single PR with u1~u9 (atomic; gates preserved).\n\n=== EVIDENCE ===\n\nFiles read (path:line):\n\n- `src/phase_z2_ai_fallback/cache.py` — full file (`read_proposal` returns None; `save_proposal` gates → NotImplementedError marker).\n- `src/phase_z2_ai_fallback/step12.py` — full file (`cache_key` build at L109-111 confirmed sample-specific).\n- `src/phase_z2_ai_fallback/router.py` — full file (cache_read at L66; no save call).\n- `src/phase_z2_ai_fallback/schema.py` — full file (3 ProposalKind values; `FORBIDDEN_KINDS` includes `raw_css`).\n- `src/phase_z2_ai_fallback/prompts.py` — full file (`V4_ROUTE_AI_ADAPTATION = \"ai_adaptation_required\"`; user payload structure).\n- `src/phase_z2_ai_fallback/validate.py` — full file (4 guards; payload may contain slot text under `PARTIAL_OVERRIDES`).\n- `src/phase_z2_ai_fallback/step17.py` L1-80 (overflow cascade order; AI_REPAIR stage definition).\n- `src/phase_z2_ai_fallback/__init__.py` — full file (exports = schema only; cache NOT re-exported).\n- `src/config.py` — full file (existing `ai_fallback_*` 8 fields; no `ai_fallback_auto_cache` yet).\n- `src/phase_z2_mapper.py` L1-80 (catalog loader at L49; `_CATALOG_CACHE` global; `CATALOG_PATH` at L34).\n- `templates/phase_z2/catalog/frame_contracts.yaml` L1-60 (single yaml; per-template `frame_id` + `source_shape` + `cardinality.strict`).\n- `tests/phase_z2_ai_fallback/test_cache.py` — full file (9 tests covering gate + NotImplementedError marker; 1 test must be replaced when backend lands).\n\nGrep / count results:\n\n- `grep -rn \"frame_cache\\|IMP-46\\|cache_key\" src/phase_z2_ai_fallback/` = 5 hits across `cache.py`, `router.py`, `step12.py`, `step17.py` (all consistent with stub status).\n- `grep -rn \"visual_check_passed\" src/` = 4 sites (`cache.py` × 4, `phase_z2_classifier.py:495` producer, `phase_z2_pipeline.py:2635/5086` consumer). Producer of `user_approved` = none today (new surface).\n- `grep -n \"load_frame_contracts\" src/phase_z2_mapper.py` = L49 (loader); `get_contract` at L56 (per-template subtree access).\n- `ls templates/phase_z2/frames/` = 2 files (`process_product_two_way.html`, `three_parallel_requirements.html`).\n- `ls templates/phase_z2/families/` = 12 family partials.\n- `ls data/` = no `frame_cache/` directory at HEAD (confirmed via `ls data/`).\n- `grep -rln \"auto_cache\\|--auto-cache\" src/` = 0 (new surface).\n\nCommits cross-checked:\n\n- `c864fe0` (IMP-33 u1~u11) — current HEAD; cache stub landed.\n- `1efbf67` (IMP-30 first-render invariant) — provisional zone synthesis upstream of step12 cache_key build.\n- Pair issue #62 body (current task input) — signature axes + value scope + invalidation rules verified.\n\nGitea API:\n\n- #61 (IMP-33) **closed**; #62 (IMP-46) **open** — current Stage 1.\n- `.orchestrator/drafts/61_stage_problem-review_claude_r1.md` re-read as reference (paired axis context, especially Axis E G11 about cache visual_check + 사용자 OK gating).\n- No prior `.orchestrator/drafts/62_*` — confirmed first Stage 1 round.\n\nNo code change made — Stage 1 problem-review only.\n\nrewind_target: 해당 없음 (YES 제안)\n\nFINAL_CONSENSUS: YES\n", "assets": [], "created_at": "2026-05-21T13:03:18+09:00", "updated_at": "2026-05-21T13:03:18+09:00"}, {"id": 22693, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22693", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #1] Stage 1 problem-review - IMP-46\n\nStatus 3-axis:\n\n1. Backend axis: verified. `src/phase_z2_ai_fallback/cache.py:36` returns `None` for every valid key, and `src/phase_z2_ai_fallback/cache.py:48` preserves the two write gates before raising the IMP-46 `NotImplementedError` at `src/phase_z2_ai_fallback/cache.py:79`. IMP-46 must replace this marker with persistent JSON read/write while preserving the visual_check and user_approved gates.\n\n2. Signature axis: verified, with a scope correction. `src/phase_z2_ai_fallback/step12.py:109` builds `cache_key` from `template_id` plus sorted `source_section_ids`; those IDs are source/sample identifiers, so structurally identical content with different MDX section IDs will miss. This violates the issue's no-hardcoding/content-addressed intent. The fix must introduce a deterministic structural signature hash using frame_id, v4 label, cardinality, source shape, h3 count, character-count bucket, layout preset, and zone position.\n\n3. Integration/scope axis: scope is larger than a backend swap. `src/phase_z2_ai_fallback/step12.py:32` does not accept `layout_preset` or `zone_position`, but both are required signature axes. `src/phase_z2_ai_fallback/step12.py:113` also passes `v4_result[\"cardinality\"] = None`, while `src/phase_z2_ai_fallback/prompts.py:66` only reads cardinality/cardinality_signature if present. Stage 2 must choose a call/integration point where unit raw_content, frame contract, resolved layout preset, and zone position are all available, or extend the Step 12 helper contract explicitly.\n\nMissing files / wrong assumptions flagged:\n\n- No existing persistent cache was found: no `data/frame_cache` directory and no `frame_cache`/`signature_hash` implementation in `src`, `templates`, `tests`, or `data`.\n- `route_ai_fallback` has one source call site, `src/phase_z2_ai_fallback/step12.py:120`; tests call it directly, but I found no production call to `gather_step12_ai_repair_proposals` outside tests. Do not assume the pipeline already invokes the Step 12 AI helper.\n- Current frame contracts contain 32 frames and expose structural axes such as `frame_id`, `source_shape`, and `cardinality` in `templates/phase_z2/catalog/frame_contracts.yaml`, but `source_shape` values are currently repo terms such as `top_bullets` and `h3_subsections`, not only the issue text's `table/bullet/paragraph`. The signature builder should canonicalize from existing repo data rather than hardcoding sample-specific or issue-wording-specific labels.\n- `save_proposal` is only called by tests today; the production write path after visual_check PASS plus user OK/`--auto-cache` still needs a concrete caller-owned integration point. The router intentionally imports only `read_proposal`, so write-after-approval must remain outside `route_ai_fallback`.\n- Cache value scope should stay within the existing `AiFallbackProposal` contract (`builder_options_patch`, `partial_overrides`, `slot_mapping_proposal`) plus any slide-level CSS metadata only if represented as a validated/cache-owned extension. Raw HTML/CSS or frame changes remain forbidden by `src/phase_z2_ai_fallback/schema.py` and validation contracts.\n\nScope-lock recommendation:\n\nProceed to Stage 2 only if the plan includes: a content-addressed signature builder, persistent JSON storage at `data/frame_cache/{frame_id}/{signature_hash}.json`, exact lookup first and optional fuzzy lookup as a separate deterministic layer, guarded save semantics, invalidation tied to frame contracts/partials/catalog changes, and focused tests for deterministic key stability across different source_section_ids. No sample-specific MDX 03/04/05 keys should survive.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath 'D:\\ad-hoc\\kei\\design_agent\\.orchestrator\\tmp\\codex_prompt.txt'`\n- `rg -n \"NotImplementedError|read_proposal|save_proposal|FrameCache|cache_key\" src/phase_z2_ai_fallback`\n- `rg -n \"frame_id:|source_shape:|cardinality:|strict:|partial|template\" templates/phase_z2/catalog/frame_contracts.yaml templates/phase_z2/catalog -g \"*.yaml\"`\n- `rg -n \"route_ai_fallback|gather_step12_ai_repair_proposals|visual_check|auto-cache|auto_cache|user_approved|builder_options|partial_overrides|slot\" src tests`\n- `Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\cache.py' | Select-Object -First 120`\n- `Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\step12.py' | Select-Object -First 170`\n- `Get-Content -LiteralPath 'src\\phase_z2_ai_fallback\\router.py' | Select-Object -First 140`\n- `rg -n \"frame_cache|signature_hash|signature|data/frame_cache|source_section_ids|cardinality_signature|layout_preset|zone_position\" src templates tests data -g \"*.py\" -g \"*.yaml\" -g \"*.md\"`\n- `(rg -c \"^ template_id:\" templates/phase_z2/catalog/frame_contracts.yaml)`\n- `rg -n \"route_ai_fallback\\(\" src tests -g \"*.py\"`\n- `rg -n \"save_proposal\\(\" src tests -g \"*.py\"`\n- `rg -n \"gather_step12_ai_repair_proposals\\(\" src tests -g \"*.py\"`\n- `git status --short`\nFiles checked:\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\cache.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\step12.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\router.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\schema.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\prompts.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_composition.py`\n- `D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_pipeline.py`\n- `D:\\ad-hoc\\kei\\design_agent\\templates\\phase_z2\\catalog\\frame_contracts.yaml`\nTest results:\n- Not run. Current stage explicitly says NO pytest.\nVerified facts:\n- `frame_contracts.yaml` currently has 32 `template_id` entries.\n- `read_proposal` is a miss-only stub and `save_proposal` raises the IMP-46 storage marker only after both gates pass.\n- Current Step 12 cache key is source-section-ID-based, not structural/content-addressed.\n- No persistent `data/frame_cache` implementation was found.\n- The current source tree has no production caller for `gather_step12_ai_repair_proposals`.\nFINAL_CONSENSUS: YES", "assets": [], "created_at": "2026-05-21T13:05:35+09:00", "updated_at": "2026-05-21T13:05:35+09:00"}, {"id": 22696, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22696", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #2] Stage 2 simulation-plan IMP-46 frame transformation cache layer\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: Add signature module — deterministic hash over 8 structural axes; char_count bin + source_shape enum helpers; no sample-id leakage\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json (schema_version=1); read None on missing/corrupt JSON\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: Add cache invalidation — fingerprint of frame_contract + partial template + catalog version stored in cache value; mismatch on read returns None\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: Extend Step 12 helper — accept layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; build cache_key via u1 signature; remove section_id leakage\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: Add --auto-cache override — settings.ai_fallback_auto_cache (default False); save_proposal honours flag in place of user_approved; visual_check_passed gate preserved\n files:\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 30\n- id: u6\n summary: .gitignore data/frame_cache + .gitkeep marker + IMP-17-CARVE-OUT.md cache-row refresh (stub → persistent JSON backend)\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests: []\n estimate_lines: 20\n\nPer-unit rationale:\n- u1: pure deterministic builder. q2 = fixed bins {0-50, 51-150, 151-400, 401-1000, 1001+}. q3 = enum {bullet, paragraph, table, mixed}. No section_id input — RULE 7 lock.\n- u2: lands backend at IMP-33 u6 stub; preserves both gates; schema_version=1 (q4); cache_key stays `{frame_id}::{signature_hash}` so router signature untouched. Replaces test_save_raises_not_implemented with round-trip.\n- u3: fingerprint = sha256 over {contract_sha, partial_sha, catalog_sha} (q5); read recomputes + treats mismatch as miss. No destructive sweep.\n- u4: drops sample-specific cache_key; new kwargs propagate signature inputs + cardinality into v4_result (prompts.py:55-66 already optional-reads cardinality). Replaces test_cache_key_includes_template_and_section_ids with structural signature assertion.\n- u5: flag-driven write-gate override; visual_check_passed remains mandatory. Default False keeps PZ-1 + AI-isolation intact.\n- u6: data/frame_cache git-ignored with .gitkeep (q6); IMP-17 carve-out table refreshed to match landed behaviour.\n\nOut of scope:\n- AI call itself (IMP-33 c864fe0)\n- Fuzzy match lookup (옵션, deferred)\n- cache → catalog promotion (R4)\n- New route_ai_fallback call sites beyond step12.py:120\n- Stage 0 mdx_normalizer integration\n- AI prompt redesign in prompts.py beyond cardinality propagation\n\nRollback: each unit single-commit revertable; u2/u3/u5 share cache.py → revert reverse order (u5→u3→u2); u1 + u6 additive and safely deletable.\n\n=== EVIDENCE ===\n- src/phase_z2_ai_fallback/cache.py:36-82 (read None at 45; save NotImplementedError at 79-82; gates at 69-78)\n- src/phase_z2_ai_fallback/step12.py:32-41 (helper lacks layout_preset/zone_position/source_shape/h3_count/char_count/cardinality)\n- src/phase_z2_ai_fallback/step12.py:109-118 (sample-specific cache_key; cardinality=None)\n- src/phase_z2_ai_fallback/prompts.py:55-66 (cardinality optional-read — u4 propagation prompt-safe)\n- src/phase_z2_ai_fallback/router.py:43-89 (router only reads — no new call site needed)\n- tests/phase_z2_ai_fallback/test_cache.py:60-66 (NotImplementedError assertion — u2 replaces)\n- tests/phase_z2_ai_fallback/test_step12.py:163-175 (cache_key assertion — u4 replaces)\n- docs/architecture/IMP-17-CARVE-OUT.md:54 (cache row — u6 refreshes)\n- data/frame_cache verified absent\n- IMP-33 base commit c864fe0\n\nFINAL_CONSENSUS: YES\n", "assets": [], "created_at": "2026-05-21T13:10:51+09:00", "updated_at": "2026-05-21T13:10:51+09:00"}, {"id": 22698, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22698", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #2] Stage 2 simulation-plan review IMP-46\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: Add deterministic structural signature module over the 8 declared axes, with char-count buckets and source-shape encoding.\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: Replace cache.py NotImplementedError stub with persistent JSON backend under data/frame_cache/{frame_id}/{signature_hash}.json.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: Add invalidation check for frame contract, partial template, and catalog fingerprints.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: Extend Step 12 helper inputs and replace template/section-id cache key with structural signature.\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: Add auto-cache override setting while preserving visual_check_passed gate.\n files:\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 30\n- id: u6\n summary: Ignore generated frame cache data and refresh IMP-17 carve-out documentation.\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests: []\n estimate_lines: 20\n\nPer-unit rationale:\n- u1: Covers the 8-axis exact structural signature and removes section/sample id leakage. Needs one explicit same-structure/different-section-id determinism test in Stage 3.\n- u2: Backend path, schema_version, corrupt/missing miss behavior, and write gate are in scope. Plan is missing explicit cache value shape coverage for builder_options, partial_overrides, slot mapping, and optional slide-level CSS.\n- u3: Invalidation axis is named, but not implementation-ready: the plan does not say where contract_sha, partial_sha, and catalog_sha are computed or how read_proposal receives/recomputes them. Unit files only cover cache.py tests, so integration with Step 12/router inputs is under-specified.\n- u4: Structural key replacement and cardinality/layout/zone propagation are in scope. Plan does not cover exact-first lookup versus optional fuzzy lookup; fuzzy may be deferred, but the plan must say so and test exact deterministic hit behavior.\n- u5: This does not satisfy the issue body as written. The issue asks for a --auto-cache flag; the plan only adds settings.ai_fallback_auto_cache and names no CLI/entrypoint file or existing flag surface.\n- u6: Size/file-count rule passes. The .gitignore/.gitkeep choice is acceptable if generated JSON stays untracked and the directory anchor is tracked.\n\nOut-of-scope notes:\n- AI invocation internals, cache-to-catalog promotion, new route_ai_fallback call sites, and Stage 0 mdx_normalizer remain out of scope.\n- Fuzzy lookup can be out of scope only if explicitly deferred as optional; current plan neither implements nor defers it.\n\nRollback strategy:\n- Revert u1-u6 touched files and delete generated data/frame_cache JSON. No destructive sweep is required because cache misses fall back to the existing AI path.\n\n=== EVIDENCE ===\nCommands run:\n- Get-Content D:\\ad-hoc\\kei\\design_agent\\.orchestrator\\tmp\\codex_prompt.txt\n- git status --short\n- rg over src/tests/docs for cache, Step 12, signature, and config surfaces\n- Get-Content src/phase_z2_ai_fallback/cache.py\n- Get-Content src/phase_z2_ai_fallback/step12.py\n- Get-Content src/phase_z2_ai_fallback/prompts.py\n- Get-Content src/phase_z2_ai_fallback/router.py\n- Get-Content src/phase_z2_ai_fallback/schema.py\n- Get-Content src/config.py\n- Get-Content tests/phase_z2_ai_fallback/test_cache.py\n- Get-Content tests/phase_z2_ai_fallback/test_step12.py\n- Get-Content tests/phase_z2_ai_fallback/test_router.py\n- Select-String docs/architecture/IMP-17-CARVE-OUT.md for cache/fallback anchors\n- pytest -q tests\nFiles checked:\n- D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\cache.py\n- D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\step12.py\n- D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\prompts.py\n- D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\router.py\n- D:\\ad-hoc\\kei\\design_agent\\src\\phase_z2_ai_fallback\\schema.py\n- D:\\ad-hoc\\kei\\design_agent\\src\\config.py\n- D:\\ad-hoc\\kei\\design_agent\\tests\\phase_z2_ai_fallback\\test_cache.py\n- D:\\ad-hoc\\kei\\design_agent\\tests\\phase_z2_ai_fallback\\test_step12.py\n- D:\\ad-hoc\\kei\\design_agent\\tests\\phase_z2_ai_fallback\\test_router.py\n- D:\\ad-hoc\\kei\\design_agent\\docs\\architecture\\IMP-17-CARVE-OUT.md\nTest results:\n- pytest -q tests: 526 passed in 41.83s\nVerified facts:\n- Current cache.py read_proposal always returns None and save_proposal still raises NotImplementedError after both gates pass.\n- Current step12.py builds cache_key from frame_template_id plus sorted source_section_ids, so the Stage 1 defect is real.\n- Current router.py reads cache before prompt/client and does not import save_proposal.\n- Current src/config.py has ai_fallback_enabled defaults but no ai_fallback_auto_cache field.\n- Existing tests assert the old sample-specific Step 12 cache key and the cache NotImplementedError marker, so Stage 3 must update those tests.\n- Claude r1 implementation_units block exists and every unit is <=50 estimated lines and <=3 files; size rule passes.\nrewind_target: stage_2_plan\nFINAL_CONSENSUS: NO\n", "assets": [], "created_at": "2026-05-21T13:14:29+09:00", "updated_at": "2026-05-21T13:14:29+09:00"}, {"id": 22701, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22701", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #2] Stage 2 simulation-plan IMP-46 frame transformation cache (rewind r1)\n\nAxis→unit: AI hook=u2/u5; path=u2/u6; 8-axis sig=u1; cache value=u2; exact lookup=u4; gate+--auto-cache=u5; invalidation=u3; guardrails=u1/u2/u5 tests.\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: signature.py — SHA256 over 8-axis JSON dict (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position); char_count bins {0-50,51-150,151-400,401-1000,1001+}; source_shape enum {bullet,paragraph,table,mixed}; schema_version=1.\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: cache.py — replace NotImplementedError marker with persistent JSON at data/frame_cache/{frame_id}/{sig}.json. Stored shape `{schema_version, proposal, slide_css, fingerprints}`. Missing/corrupt → None. Round-trip per ProposalKind + slide_css set/None.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: cache.py — extend read/save with fingerprints kwarg {contract_sha,partial_sha,catalog_sha}; save stores, read compares → mismatch=None. Compute at caller (u4), not cache module.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: step12.py — extend helper inputs (layout_preset, zone_position, source_shape, h3_count, char_count, cardinality); inline-compute 3 fingerprints; replace sample cache_key with u1 signature. Exact-match only; fuzzy explicitly deferred (Stage 6 follow-up).\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: --auto-cache CLI flag in src/phase_z2_pipeline.py (argparse line 5208, action='store_true'); wired to settings.ai_fallback_auto_cache (new, default False); save_proposal accepts auto_cache=bool, bypasses user_approved only (visual_check_passed mandatory).\n files:\n - src/phase_z2_pipeline.py\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n - tests/test_phase_z2_ai_fallback_config.py\n estimate_lines: 40\n- id: u6\n summary: Track data/frame_cache/.gitkeep (data/ gitignored line 11 → add `!data/frame_cache/.gitkeep` exception); refresh IMP-17-CARVE-OUT.md cache row stub→persistent.\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests: []\n estimate_lines: 20\n\nPer-unit rationale (Codex r1 gaps closed):\n- u1: determinism test — same 8 axes / different section_ids → same hash; one-axis change → different hash.\n- u2: cache value shape — proposal.payload holds all 3 ProposalKind variants; slide_css separate optional top-level. Miss+corrupt→None.\n- u3: compute site = u4 caller. `sha256(json.dumps(.,sort_keys=True))` for contract+partial, `sha256(catalog_yaml.read_bytes())` for catalog. Cache stores opaquely; mismatch=None.\n- u4: flow = sig→3 fp inline→read(sig,fp)→miss=router→visual+user_OK=save(sig,proposal,fp,gates). Deterministic-hit test added. Existing section-id assertion replaced.\n- u5: real CLI surface (not settings-only). Gate truth table 2³=8 cells tested. visual_check_passed=False ALWAYS rejects.\n- u6: `!` exception keeps .gitkeep tracked; JSON untracked.\n\nOut of scope:\n- AI body (IMP-33 c864fe0); fuzzy lookup (deferred); cache→catalog (R4); new router call sites; Stage 0 normalizer; prompts.py redesign beyond cardinality.\n\nRollback:\n- Per-unit revert. cache.py shared (u2/u3/u5) → reverse order. u1/u6 additive. Generated JSON gitignored — delete working tree only.\n\n=== EVIDENCE ===\n- cache.py:79-82 NotImplementedError marker; cache.py:36-45 read=None; cache.py:48-78 gates.\n- step12.py:32 helper missing axes; step12.py:109-111 sample cache_key; step12.py:117 cardinality=None.\n- prompts.py:65-66 cardinality optional-read (propagation safe).\n- router.py:43-89 read-only, no save_proposal import (sig unchanged).\n- config.py:19 no auto_cache field (u5 adds).\n- phase_z2_pipeline.py:5208-5263 argparse entrypoint confirmed.\n- test_cache.py:60-66 NotImplementedError assertion → u2 rewrites.\n- test_step12.py:163-175 section-id assertion → u4 rewrites.\n- .gitignore:11 `data/` → u6 adds `!data/frame_cache/.gitkeep`.\n- `ls data/` → frame_cache/ absent (u6 creates).\n- baseline pytest -q tests = 526 passed (Codex r1).\n\nVerified: all units ≤50 lines + ≤3 files; Codex r1 NO gaps closed (u1 determinism, u2 value shape, u3 fp compute site, u4 exact-match decl + fuzzy deferred, u5 real CLI); rejected tests rewritten not deleted; flag defaults OFF; PZ-1 AI=0 intact.\n\nFINAL_CONSENSUS: YES\n", "assets": [], "created_at": "2026-05-21T13:20:41+09:00", "updated_at": "2026-05-21T13:20:41+09:00"}, {"id": 22703, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22703", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #3] Stage 2 simulation-plan review - IMP-46\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: Add signature.py with deterministic SHA256 over the 8 declared structural axes: frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position. Use fixed char-count buckets 0-50, 51-150, 151-400, 401-1000, 1001+; source_shape enum bullet, paragraph, table, mixed; schema_version=1.\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json. Stored value includes schema_version, proposal, slide_css, fingerprints; missing/corrupt files return None; round-trip all ProposalKind values and slide_css None/set.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: Add invalidation by fingerprints: contract_sha, partial_sha, catalog_sha. save stores fingerprints, read compares supplied fingerprints and returns None on mismatch; fingerprint computation stays outside cache.py.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: Extend Step 12 helper inputs for layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; derive char_count_bucket via u1; compute the three fingerprints; replace template/section-id cache_key with structural signature. Exact-match lookup only; fuzzy is explicitly deferred as optional follow-up.\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: Add --auto-cache CLI flag in src/phase_z2_pipeline.py, wire settings.ai_fallback_auto_cache default False, and let save_proposal accept auto_cache. auto_cache bypasses user_approved only; visual_check_passed remains mandatory.\n files:\n - src/phase_z2_pipeline.py\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n - tests/test_phase_z2_ai_fallback_config.py\n estimate_lines: 40\n- id: u6\n summary: Track data/frame_cache/.gitkeep by adding a .gitignore exception under the existing data/ ignore, and refresh IMP-17-CARVE-OUT.md cache row from stub status to persistent backend status.\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests: []\n estimate_lines: 20\n\nPer-unit rationale:\nu1 covers the no-hardcoding structural signature lock and removes source_section_id/sample leakage. Tests must assert same 8 axes produce same hash and any axis change changes it.\nu2 covers the content-addressed persistent path and cache value shape. Tests must cover read miss, corrupt JSON miss, path creation, and ProposalKind round-trips.\nu3 covers contract/partial/catalog invalidation without coupling cache.py to loaders. Tests must prove matching fingerprints hit and each mismatch misses.\nu4 covers automatic lookup before the AI hook at the current production call site and supplies all missing signature inputs. Tests must prove router receives structural cache_key, not template plus section ids.\nu5 covers the save gate and auto-cache override while preserving default OFF governance. Tests must prove visual_check is never bypassed.\nu6 covers repo hygiene for the ignored data/ path and docs sync. No runtime test is needed; verify by git status and doc diff.\n\nOut-of-scope notes:\nAI invocation/client behavior remains IMP-33-owned and untouched; cache never invokes AI. Cache-to-catalog promotion remains R4. New route_ai_fallback call sites beyond Step 12 are excluded. Fuzzy lookup is optional and deferred; exact deterministic lookup is required for this stage.\n\nRollback strategy:\nRevert the six unit file sets; remove data/frame_cache/.gitkeep and restore the data/ ignore-only state. This returns cache.py to IMP-33 stub behavior and Step 12 to its existing cache_key path.\n\n=== EVIDENCE ===\nCommands run:\n- Get-Content .orchestrator/tmp/codex_prompt.txt\n- rg --files\n- git status --short\n- Get-Content src/phase_z2_ai_fallback/cache.py\n- Get-Content src/phase_z2_ai_fallback/step12.py\n- Get-Content src/phase_z2_ai_fallback/prompts.py\n- Get-Content src/phase_z2_ai_fallback/schema.py\n- Get-Content src/phase_z2_ai_fallback/router.py\n- Get-Content tests/phase_z2_ai_fallback/test_cache.py\n- Get-Content tests/phase_z2_ai_fallback/test_step12.py\n- rg -n \"ai_fallback|auto-cache|auto_cache|argparse|ArgumentParser\" src/phase_z2_pipeline.py src/config.py tests/test_phase_z2_ai_fallback_config.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_step12.py docs/architecture/IMP-17-CARVE-OUT.md .gitignore\n- rg -n \"def read_proposal|def save_proposal|NotImplementedError|cache_key|route_ai_fallback|cardinality|build_ai_fallback_prompt|data/|frame_cache|IMP-46|AI_FALLBACK|ai_fallback\" src/phase_z2_ai_fallback/cache.py src/phase_z2_ai_fallback/step12.py src/phase_z2_ai_fallback/prompts.py src/config.py .gitignore docs/architecture/IMP-17-CARVE-OUT.md\n- Test-Path checks for src/phase_z2_ai_fallback/signature.py, tests/phase_z2_ai_fallback/test_signature.py, tests/phase_z2_ai_fallback/test_cache_invalidation.py, data/frame_cache\n- pytest -q tests\nFiles checked:\n- src/phase_z2_ai_fallback/cache.py\n- src/phase_z2_ai_fallback/step12.py\n- src/phase_z2_ai_fallback/prompts.py\n- src/phase_z2_ai_fallback/schema.py\n- src/phase_z2_ai_fallback/router.py\n- src/config.py\n- src/phase_z2_pipeline.py\n- tests/phase_z2_ai_fallback/test_cache.py\n- tests/phase_z2_ai_fallback/test_step12.py\n- tests/test_phase_z2_ai_fallback_config.py\n- docs/architecture/IMP-17-CARVE-OUT.md\n- .gitignore\nTest results:\n- pytest -q tests: 526 passed in 42.03s\nVerified facts:\n- cache.py still returns None on read and raises NotImplementedError after both gates, matching the IMP-46 target.\n- step12.py still builds cache_key from template_id plus sorted source_section_ids and passes cardinality None, so u4 directly fixes the known defect.\n- prompts.py reads cardinality/cardinality_signature only if present, so Step 12 contract extension is required.\n- signature.py, test_signature.py, test_cache_invalidation.py, and data/frame_cache are absent now and correctly planned for Stage 3.\n- .gitignore currently ignores data/ at line 11, so u6 needs an exception for data/frame_cache/.gitkeep.\n- IMPLEMENTATION_UNITS exists; every unit is <= 50 estimated lines and <= 3 files.\n- Side effects before posting: no source edits made for implementation; only this required draft artifact is created.\nCommit SHA: n/a for Stage 2\nFINAL_CONSENSUS: YES\n", "assets": [], "created_at": "2026-05-21T13:24:17+09:00", "updated_at": "2026-05-21T13:24:17+09:00"}, {"id": 22705, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22705", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "⚠️ **[Orchestrator]** Stage 2 FINAL_CONSENSUS: YES rejected: unit with `tests: []` (forbidden — implementation + tests = same unit).\n\nRequirements (strict):\n- `=== IMPLEMENTATION_UNITS ===` block with at least one `- id: u<N>` entry\n- Each unit MUST include `tests: [<path>, ...]` (NOT `tests: []`)\n- Implementation + tests = same unit (no deferring tests to later units)\n\nExample:\n === IMPLEMENTATION_UNITS ===\n - id: u1\n summary: ...\n files: [...]\n tests: [tests/.../test_xxx.py]\n estimate_lines: <int>\n", "assets": [], "created_at": "2026-05-21T13:24:37+09:00", "updated_at": "2026-05-21T13:24:37+09:00"}, {"id": 22706, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22706", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #3] Stage 2 simulation-plan IMP-46 (r2 — u6 test gap closed)\n\nSingle delta vs r1: u6 had `tests: []` (orchestrator rejected). Now u6 ships `test_cache_repo_layout.py`. Other 5 units unchanged from r1 (Codex #3 YES).\n\nAxis→unit: AI hook=u2/u5; path=u2/u6; 8-axis sig=u1; cache value=u2; exact lookup=u4; gate+--auto-cache=u5; invalidation=u3; guardrails=u1/u2/u5/u6 tests.\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: signature.py — SHA256 over 8-axis JSON dict (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position); char_count bins {0-50,51-150,151-400,401-1000,1001+}; source_shape enum {bullet,paragraph,table,mixed}; schema_version=1.\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: cache.py — replace NotImplementedError marker with persistent JSON at data/frame_cache/{frame_id}/{sig}.json. Stored shape {schema_version, proposal, slide_css, fingerprints}. Missing/corrupt → None. Round-trip all ProposalKind values + slide_css None/set; mkdir parents on save.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: cache.py — extend read/save with fingerprints kwarg {contract_sha, partial_sha, catalog_sha}; save stores, read compares supplied fingerprints → mismatch=None. Fingerprint computation stays outside cache.py (caller computes at u4).\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: step12.py — extend helper inputs (layout_preset, zone_position, source_shape, h3_count, char_count, cardinality); derive char_count_bucket via u1; inline-compute 3 fingerprints (sha256 of contract dict + partial template bytes + catalog yaml bytes); replace sample cache_key with u1 signature. Exact-match lookup only; fuzzy explicitly deferred (Stage 6 follow-up candidate).\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: --auto-cache CLI flag in src/phase_z2_pipeline.py argparse, wired to settings.ai_fallback_auto_cache (new, default False); save_proposal accepts auto_cache=bool which bypasses user_approved only — visual_check_passed remains mandatory. Gate truth table 2^3=8 cells tested; visual_check_passed=False ALWAYS rejects.\n files:\n - src/phase_z2_pipeline.py\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n - tests/test_phase_z2_ai_fallback_config.py\n estimate_lines: 40\n- id: u6\n summary: Track data/frame_cache/.gitkeep via .gitignore exception (`!data/frame_cache/.gitkeep` under existing `data/` ignore at line 11); refresh IMP-17-CARVE-OUT.md cache row from stub→persistent backend. Test asserts .gitkeep exists as a tracked file path AND IMP-17-CARVE-OUT.md contains the persistent-backend marker string (e.g., \"data/frame_cache/{frame_id}/{signature_hash}.json\").\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests:\n - tests/phase_z2_ai_fallback/test_cache_repo_layout.py\n estimate_lines: 30\n\nPer-unit rationale (deltas from r1):\n- u1~u5: unchanged; Codex #3 approved.\n- u6: now ships `tests/phase_z2_ai_fallback/test_cache_repo_layout.py` — two invariants: (1) data/frame_cache/.gitkeep exists (repo layout); (2) IMP-17-CARVE-OUT.md contains persistent-backend marker substring (mirrors test_docs_sync.py pattern). estimate_lines 20→30.\n\nOut of scope:\n- AI body (IMP-33 c864fe0); fuzzy lookup (deferred); cache→catalog (R4); new router call sites; Stage 0 normalizer; prompts.py redesign beyond cardinality field exposure.\n\nRollback:\n- Per-unit revert. cache.py shared (u2/u3/u5) → reverse order u5→u3→u2. u1/u6 additive. Generated JSON under data/frame_cache/ stays gitignored — delete working tree only.\n\n=== EVIDENCE ===\nFiles verified present:\n- D:/ad-hoc/kei/design_agent/docs/architecture/IMP-17-CARVE-OUT.md (target for u6 doc refresh)\n- D:/ad-hoc/kei/design_agent/tests/phase_z2_ai_fallback/ (target dir for new test file; existing test_docs_sync.py provides docs-marker pattern)\nComments reviewed:\n- Codex #2 (Stage 2 r0 review — rejected for missing fuzzy/CLI/cache-value-shape detail)\n- Claude #2 (Stage 2 r1 plan — addressed Codex #2 gaps)\n- Codex #3 (Stage 2 r1 review — FINAL_CONSENSUS: YES)\n- Orchestrator post-Codex#3 rejection (u6 tests: [] forbidden — only gap requiring r2)\nScope-lock referenced from Stage 1 exit report (binding contract above).\n\nFINAL_CONSENSUS: YES\n", "assets": [], "created_at": "2026-05-21T13:26:45+09:00", "updated_at": "2026-05-21T13:26:45+09:00"}, {"id": 22709, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22709", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #4] Stage 2 simulation-plan review - IMP-46\n\n=== IMPLEMENTATION_UNITS ===\n- id: u1\n summary: Add signature.py with deterministic SHA256 over the 8 declared structural axes: frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position. Fixed char-count buckets 0-50, 51-150, 151-400, 401-1000, 1001+; source_shape enum bullet, paragraph, table, mixed; schema_version=1.\n files:\n - src/phase_z2_ai_fallback/signature.py\n - tests/phase_z2_ai_fallback/test_signature.py\n tests:\n - tests/phase_z2_ai_fallback/test_signature.py\n estimate_lines: 50\n- id: u2\n summary: Replace cache.py NotImplementedError marker with persistent JSON backend at data/frame_cache/{frame_id}/{signature_hash}.json. Stored value includes schema_version, proposal, slide_css, fingerprints; missing/corrupt files return None; mkdir parents on save; round-trip all ProposalKind values and slide_css None/set.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n estimate_lines: 50\n- id: u3\n summary: Add invalidation by fingerprints contract_sha, partial_sha, catalog_sha. save stores fingerprints, read compares supplied fingerprints and returns None on mismatch; fingerprint computation stays outside cache.py.\n files:\n - src/phase_z2_ai_fallback/cache.py\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache_invalidation.py\n estimate_lines: 45\n- id: u4\n summary: Extend Step 12 helper inputs for layout_preset, zone_position, source_shape, h3_count, char_count, cardinality; derive char_count_bucket via u1; compute the three fingerprints; replace template/section-id cache_key with structural signature. Exact-match lookup only; fuzzy is deferred.\n files:\n - src/phase_z2_ai_fallback/step12.py\n - tests/phase_z2_ai_fallback/test_step12.py\n tests:\n - tests/phase_z2_ai_fallback/test_step12.py\n estimate_lines: 50\n- id: u5\n summary: Add --auto-cache CLI flag in src/phase_z2_pipeline.py, wire settings.ai_fallback_auto_cache default False, and let save_proposal accept auto_cache. auto_cache bypasses user_approved only; visual_check_passed remains mandatory; test the 2^3 gate truth table.\n files:\n - src/phase_z2_pipeline.py\n - src/config.py\n - src/phase_z2_ai_fallback/cache.py\n tests:\n - tests/phase_z2_ai_fallback/test_cache.py\n - tests/test_phase_z2_ai_fallback_config.py\n estimate_lines: 40\n- id: u6\n summary: Track data/frame_cache/.gitkeep via .gitignore exception under the existing data/ ignore, add repo-layout coverage, and refresh IMP-17-CARVE-OUT.md cache row from stub status to persistent backend status.\n files:\n - .gitignore\n - data/frame_cache/.gitkeep\n - docs/architecture/IMP-17-CARVE-OUT.md\n tests:\n - tests/phase_z2_ai_fallback/test_cache_repo_layout.py\n estimate_lines: 20\n\nPer-unit rationale:\nu1 covers the no-hardcoding structural signature lock and removes sample/source_section_id leakage; tests must prove same 8 axes give same hash and any axis change changes it.\nu2 covers the required cache path and cache value shape while preserving read-path determinism and cache-miss fallback for missing/corrupt files.\nu3 covers contract/partial/catalog invalidation without making cache.py know how to compute project-specific hashes.\nu4 covers the sole production Step 12 integration point and supplies the missing layout/cardinality/source-shape inputs before signature build.\nu5 covers the issue write gate and auto-cache override while keeping the default flag off and visual_check_passed as a non-bypassable gate.\nu6 covers the previously missing repo-layout/test gap: data/ is ignored today, data/frame_cache is absent, so .gitkeep needs an explicit exception plus a layout test.\n\nOut-of-scope notes:\nAI invocation logic, prompt redesign, cache-to-catalog promotion, new route_ai_fallback call sites, fuzzy lookup implementation, and Stage 0 mdx_normalizer integration remain out of scope.\n\nRollback strategy:\nRevert the six unit filesets by unit if a unit regresses; cache files are content-addressed under data/frame_cache and can be removed without affecting AI hook behavior.\n\n=== EVIDENCE ===\nCommands run: git status --short; rg --files; Get-Content src/phase_z2_ai_fallback/cache.py; Get-Content src/phase_z2_ai_fallback/step12.py; rg -n ai_fallback|auto_cache|auto-cache|ArgumentParser|add_argument src/config.py src/phase_z2_pipeline.py tests/test_phase_z2_ai_fallback_config.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_step12.py; Get-Content .gitignore; Test-Path data/frame_cache; Get-Content tests/phase_z2_ai_fallback/test_cache.py; Get-Content tests/phase_z2_ai_fallback/test_step12.py; pytest -q tests\nFiles checked: src/phase_z2_ai_fallback/cache.py; src/phase_z2_ai_fallback/step12.py; src/config.py; src/phase_z2_pipeline.py; .gitignore; tests/phase_z2_ai_fallback/test_cache.py; tests/phase_z2_ai_fallback/test_step12.py; tests/test_phase_z2_ai_fallback_config.py\nTest results: pytest -q tests => 526 passed in 41.71s\nVerified facts: current cache.py still returns None on read and raises NotImplementedError after both gates; current step12.py cache_key still uses template_id plus sorted source_section_ids; no auto-cache setting or CLI flag exists; .gitignore ignores data/ and data/frame_cache is absent; revised units are all at or under 50 estimated lines and at or under 3 files; u6 now includes tests/phase_z2_ai_fallback/test_cache_repo_layout.py; issue axes map to u1 signature, u2 path/value/backend, u3 invalidation, u4 lookup/integration, u5 gate/auto-cache, u6 repo layout/docs.\nFINAL_CONSENSUS: YES", "assets": [], "created_at": "2026-05-21T13:29:03+09:00", "updated_at": "2026-05-21T13:29:03+09:00"}, {"id": 22712, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22712", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #1] Stage 3 code-edit IMP-46 — u1 signature builder\n\nExecuting unit: u1\n\n■ unit_executed\n- id: u1\n- summary: Add `signature.py` with deterministic SHA256 over the 8 declared structural axes (frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position). Fixed char-count buckets 0-50 / 51-150 / 151-400 / 401-1000 / 1001+; `source_shape` enum (bullet/paragraph/table/mixed); `SCHEMA_VERSION=1` embedded in hashed payload.\n\n■ files_changed\n- src/phase_z2_ai_fallback/signature.py (new, 76 SLOC)\n- tests/phase_z2_ai_fallback/test_signature.py (new, 20 cases)\n- tests/phase_z2_ai_fallback/test_ast_isolation.py (whitelist `hashlib`; stdlib peer of `json`/`pathlib`)\n\n■ diff_summary\n- New module `src/phase_z2_ai_fallback/signature.py`:\n - `SCHEMA_VERSION = 1` constant, embedded inside the hashed JSON payload so a future schema bump auto-invalidates digests.\n - `SourceShape(str, Enum)` with members `BULLET / PARAGRAPH / TABLE / MIXED`.\n - `CHAR_COUNT_BUCKET_LABELS = (\"0-50\", \"51-150\", \"151-400\", \"401-1000\", \"1001+\")` exposed as a public tuple for cross-module reuse.\n - `bucket_char_count(int) -> str`: maps a non-negative int char count to one of the 5 fixed buckets; rejects `bool`, non-int, or negative input.\n - `build_signature(*, frame_id, v4_label, cardinality, source_shape, h3_count, char_count_bucket, layout_preset, zone_position) -> str`: keyword-only API restricted to the 8 declared axes. Coerces `source_shape` from `str` via `SourceShape(...)`; validates `char_count_bucket ∈ CHAR_COUNT_BUCKET_LABELS`. Computes SHA256 over `json.dumps(payload, sort_keys=True, ensure_ascii=False)` → returns 64-char hex digest.\n- New tests `tests/phase_z2_ai_fallback/test_signature.py` (20 cases, all pass):\n - schema_version == 1; bucket label tuple matches spec.\n - Determinism: same inputs → same digest, length 64.\n - Axis-change sensitivity: parametrized over all 8 axes — flipping any single axis mutates the digest.\n - `source_shape` string ⇔ enum equivalence.\n - Rejection of unknown `source_shape` and unknown `char_count_bucket`.\n - `cardinality=None` yields a valid digest distinct from `cardinality=0`.\n - Public-surface lock: `inspect.signature(build_signature).parameters` is exactly the 8-axis set (no `template_id` / `source_section_ids` / `mdx_path` leakage).\n - Bucket boundaries: exhaustively asserted at each edge (0, 50, 51, 150, 151, 400, 401, 1000, 1001, 10_000).\n - Type rejection on `bucket_char_count(3.14)` and `bucket_char_count(True)`.\n - Stable-fixture lock: a known 8-tuple is rehashed inline to detect silent payload-shape drift.\n- AST isolation guard (`test_ast_isolation.py`): added `\"hashlib\"` to `_ALLOWED_TOP_LEVEL` (alphabetical placement between `enum` and `json`). Stdlib peer of already-whitelisted `json` / `pathlib` / `dataclasses`. No forbidden surface widened — Phase Q / Kei / phase_z2_* runtime imports still rejected.\n\n■ validation\n- `python -m pytest tests/phase_z2_ai_fallback/test_signature.py -q` → 20 passed.\n- `python -m pytest tests/phase_z2_ai_fallback/ tests/test_family_contract_baseline.py -q` → 136 passed (full fallback suite + baseline contract test green; AST isolation, schema, cache gate, step12, router, prompts, client mock all unaffected).\n\n■ guardrails\n- AI isolation contract: `signature.py` performs zero AI/network/MDX I/O. Pure stdlib (`hashlib`, `json`, `enum`).\n- No-hardcoding lock: the 8-axis surface accepts no sample/section/template identifiers. The public-surface test enforces this structurally.\n- Scope lock honored: u1 alone — no edits to `cache.py`, `step12.py`, pipeline, or config. Cache backend (u2), invalidation (u3), Step 12 wiring (u4), gate flag (u5), repo layout (u6) all untouched.\n- Schema versioning: `SCHEMA_VERSION` is part of the hashed payload, so any future axis rename / addition can bump it for deterministic invalidation (will be the invariant that u3 fingerprints layer on top of, not replace).\n\n■ remaining_units\n- u2 — cache.py persistent JSON backend at `data/frame_cache/{frame_id}/{signature_hash}.json`\n- u3 — fingerprint-based invalidation (contract_sha / partial_sha / catalog_sha)\n- u4 — Step 12 helper contract extension + structural cache_key replacement\n- u5 — `--auto-cache` CLI flag + `settings.ai_fallback_auto_cache` + 2^3 gate truth table\n- u6 — `.gitignore` exception + `data/frame_cache/.gitkeep` + IMP-17-CARVE-OUT.md refresh + repo-layout test\n\n■ follow_up_issue_candidates\n- (none) — `hashlib` whitelist addition is in-scope u1 plumbing (stdlib parity with `json`); not a scope-creep axis. No other off-scope surface discovered.\n\nstopping here per \"1 turn = 1 step\" rule; no commit, no push.\n", "assets": [], "created_at": "2026-05-21T13:32:54+09:00", "updated_at": "2026-05-21T13:32:54+09:00"}, {"id": 22714, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22714", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #1] Stage 3 code-edit review IMP-46 u1\n\nScope: verified only the single executed unit, u1.\n\nVerdict: u1 is correct. The new signature builder is deterministic, content-addressed over only the 8 declared structural axes, embeds schema version 1 in the hashed payload, exposes the fixed char-count buckets, rejects invalid bucket/source-shape inputs, and does not include template, section, MDX, or sample identifiers in the signature surface. The AST whitelist update is limited to the stdlib `hashlib` import needed by this unit.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath src/phase_z2_ai_fallback/signature.py`\n- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_signature.py`\n- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_ast_isolation.py`\n- `python -m pytest tests/phase_z2_ai_fallback/test_signature.py tests/phase_z2_ai_fallback/test_ast_isolation.py -q`\n- `rg -n \"template_id|source_section_ids|mdx_path|section_id|MDX 03|MDX 04|MDX 05\" src/phase_z2_ai_fallback/signature.py tests/phase_z2_ai_fallback/test_signature.py`\n- PowerShell Python introspection for `SCHEMA_VERSION`, `CHAR_COUNT_BUCKET_LABELS`, and `build_signature` parameters\nFiles checked:\n- `src/phase_z2_ai_fallback/signature.py`\n- `tests/phase_z2_ai_fallback/test_signature.py`\n- `tests/phase_z2_ai_fallback/test_ast_isolation.py`\nTest results:\n- `42 passed in 0.12s`\nVerified facts:\n- `SCHEMA_VERSION == 1`.\n- `CHAR_COUNT_BUCKET_LABELS == (\"0-50\", \"51-150\", \"151-400\", \"401-1000\", \"1001+\")`.\n- `build_signature` public parameters are exactly `frame_id`, `v4_label`, `cardinality`, `source_shape`, `h3_count`, `char_count_bucket`, `layout_preset`, `zone_position`.\n- `rg` found no `template_id`, `source_section_ids`, `mdx_path`, `section_id`, `MDX 03`, `MDX 04`, or `MDX 05` leakage in the u1 signature files.\n- `test_ast_isolation.py` only adds `hashlib` to the allowed top-level imports for this unit.\nRemaining units: [u2, u3, u4, u5, u6]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO", "assets": [], "created_at": "2026-05-21T13:34:25+09:00", "updated_at": "2026-05-21T13:34:25+09:00"}, {"id": 22716, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22716", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #2] Stage 3 code-edit IMP-46 — u2 persistent JSON cache backend\n\nExecuting unit: u2\n\n■ unit_executed\n- id: u2\n- summary: Replace the IMP-33 u6 `NotImplementedError` marker in `src/phase_z2_ai_fallback/cache.py` with a content-addressed JSON backend at `data/frame_cache/{frame_id}/{signature_hash}.json`. Stored payload = `{schema_version: 1, proposal, slide_css, fingerprints}`. Missing / corrupt files → `read_proposal` returns `None`; `mkdir(parents=True, exist_ok=True)` on save. Both write gates (`visual_check_passed` AND `user_approved`) preserved BEFORE any filesystem touch.\n\n■ files_changed\n- src/phase_z2_ai_fallback/cache.py (rewrite, 137 SLOC — was 82 SLOC stub)\n- tests/phase_z2_ai_fallback/test_cache.py (rewrite, ~290 SLOC — 31 test cases covering gates, round-trip, persistence layout, corruption tolerance)\n\n■ diff_summary\n- `src/phase_z2_ai_fallback/cache.py`\n - Module-level constants: `SCHEMA_VERSION = 1`, `KEY_DELIMITER = \"::\"`, `CACHE_ROOT: pathlib.Path = pathlib.Path(\"data/frame_cache\")` (mutable for monkeypatch-based test isolation; tests redirect to `tmp_path`).\n - `AiFallbackCacheGateError(RuntimeError)` — class preserved exactly (loud gate violation surface).\n - `_parse_key(key) -> tuple[str, str] | None` — splits `\"{frame_id}::{signature_hash}\"`; returns `None` if delimiter missing or component empty or extra `::` present. Centralizes the on-disk layout decision in one place.\n - `_cache_path(frame_id, signature_hash) -> pathlib.Path` — single source of truth for the file layout (`CACHE_ROOT / frame_id / f\"{signature_hash}.json\"`).\n - `read_proposal(key)`:\n - Empty / non-string `key` → `ValueError` (loud; matches prior contract).\n - Legacy key (no `::`) → silent `None`. This is router back-compat — `test_router.py:58` still passes `cache_key=\"frame:1171281190:cardinality:many\"` and must continue to miss safely until u4 replaces the cache_key at the step12 call site.\n - File missing → `None`. Corrupt JSON (`OSError` / `json.JSONDecodeError`) → `None`. Non-dict root, missing `proposal` key, non-dict `proposal` value, or `AiFallbackProposal.model_validate` failure (e.g. forbidden `proposal_kind` like `mdx_text` smuggled into a stale payload) → `None`. Read errors NEVER propagate — the cache is a hint, never a hard dependency.\n - `save_proposal(key, proposal, *, visual_check_passed, user_approved, slide_css=None, fingerprints=None)`:\n - Gate order preserved: empty key → `ValueError`; wrong type → `TypeError`; `visual_check_passed=False` → `AiFallbackCacheGateError`; `user_approved=False` → `AiFallbackCacheGateError`. Both gates raise BEFORE the parent directory is touched (verified by `test_save_gate_violation_does_not_touch_filesystem`).\n - `slide_css` validated (`str | None`), `fingerprints` validated (`dict | None`), then key parsed via `_parse_key`; legacy key format → `ValueError` (loud — writes must use the structural form).\n - On success: `path.parent.mkdir(parents=True, exist_ok=True)`, JSON payload written with `sort_keys=True, ensure_ascii=False, indent=2`. Returns the resolved `pathlib.Path`.\n - `fingerprints` is persisted but NOT compared in u2 — comparison / invalidation is u3's responsibility (explicit module docstring note).\n - Module docstring documents the on-disk layout, the legacy-key silent-miss back-compat, the u3 fingerprint deferral, and the AI-isolation invariant (no Anthropic / MDX / Phase Z runtime imports).\n- `tests/phase_z2_ai_fallback/test_cache.py` — 31 cases. `@pytest.fixture(autouse=True) _isolated_cache_root` redirects `cache_mod.CACHE_ROOT` to `tmp_path / \"frame_cache\"` via `monkeypatch.setattr` for every test, so the production directory is never touched. Coverage:\n - read: missing file → None; empty key → ValueError; non-string key → ValueError; legacy key format → None (router back-compat); corrupt JSON → None; non-dict root → None; payload missing `proposal` → None; forbidden `proposal_kind=\"mdx_text\"` → None (validator rejects).\n - write gates: visual_check=False rejects; user_approved=False rejects; both False rejects; gate violation does NOT create the `frame_id` directory; empty key → ValueError; non-proposal object → TypeError; legacy key format → ValueError; non-string `slide_css` → TypeError; non-dict `fingerprints` → TypeError; `AiFallbackCacheGateError` is not a `NotImplementedError` subclass.\n - persistence + round-trip: creates parent directories; returns resolved path; payload includes `schema_version=1`; payload includes `proposal.model_dump(mode=\"json\")`; default `slide_css=None` and default `fingerprints={}`; round-trip with `slide_css` set; round-trip with fingerprints dict; read returns the same proposal after save; **parametrized round-trip across all three `ProposalKind` values** (BUILDER_OPTIONS_PATCH, PARTIAL_OVERRIDES, SLOT_MAPPING_PROPOSAL); overwrite an existing entry; file layout uses `frame_id/` directory + `signature_hash.json` file; two distinct signature hashes under the same frame are isolated; triple-delimiter key rejected.\n- No changes to `tests/phase_z2_ai_fallback/test_ast_isolation.py` — `json` and `pathlib` were already in the `_ALLOWED_TOP_LEVEL` whitelist, so the new backend introduces no new top-level imports.\n\n■ validation\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_ast_isolation.py -q` → **55 passed in 0.26s**.\n- `python -m pytest tests/phase_z2_ai_fallback/ -q` → **156 passed in 0.27s** (full AI fallback regression — includes `test_router.py` which still calls `read_proposal(\"frame:1171281190:cardinality:many\")` and continues to get `None` because the legacy key format is now silently treated as a miss; PZ-1 unaffected).\n- `python -m pytest tests/test_family_contract_baseline.py -q` → **4 passed in 0.59s**.\n- Public-surface introspection:\n - `SCHEMA_VERSION == 1`, `KEY_DELIMITER == \"::\"`, `CACHE_ROOT == pathlib.Path(\"data/frame_cache\")`.\n - `read_proposal(key: str) -> AiFallbackProposal | None`.\n - `save_proposal(key: str, proposal: AiFallbackProposal, *, visual_check_passed: bool, user_approved: bool, slide_css: str | None = None, fingerprints: dict | None = None) -> pathlib.Path`.\n - `AiFallbackCacheGateError` MRO ends at `RuntimeError` (NOT a `NotImplementedError` — the IMP-33 marker is gone).\n\n■ scope_locks_honoured\n- u3 deferred: `fingerprints` field is persisted on save and the field is read back on disk, but `read_proposal` does NOT compare supplied fingerprints against the stored value. That mismatch-comparison logic is u3.\n- u4 deferred: `router.py`, `step12.py`, and `test_router.py` are NOT modified. The router still calls `read_proposal(cache_key)` with the IMP-33 u7 legacy string. u2's parser silently misses on legacy keys so the router's cache path keeps returning `None` (the prior stub behavior). u4 will replace the cache_key at the step12 call site with the structural `frame_id::signature_hash` form.\n- u5 deferred: `--auto-cache` CLI flag, `settings.ai_fallback_auto_cache`, and the `auto_cache` parameter on `save_proposal` are NOT introduced here. Both gates remain mandatory; neither can be bypassed in u2.\n- u6 deferred: `.gitignore` exception for `data/frame_cache/.gitkeep` and the `IMP-17-CARVE-OUT.md` row refresh are NOT performed here.\n- AI isolation contract preserved: no Anthropic, MDX, or Phase Z runtime imports; only stdlib (`json`, `pathlib`) + intra-package (`schema`).\n\n■ remaining_units\n- u3 (cache invalidation by fingerprints — `read_proposal` returns None on mismatch; new `tests/phase_z2_ai_fallback/test_cache_invalidation.py`).\n- u4 (Step 12 helper inputs + structural cache_key replacement — `src/phase_z2_ai_fallback/step12.py` + `tests/phase_z2_ai_fallback/test_step12.py`).\n- u5 (`--auto-cache` CLI flag + `settings.ai_fallback_auto_cache` + `auto_cache` kwarg on `save_proposal` + 2³ gate truth-table test).\n- u6 (`data/frame_cache/.gitkeep` + `.gitignore` exception + `IMP-17-CARVE-OUT.md` cache row refresh + `tests/phase_z2_ai_fallback/test_cache_repo_layout.py`).\n\n■ follow_up_issue_candidates\n- (none — Stage 1 scope_lock holds; no axis discovered beyond the unit decomposition).\n\n■ STOP\n- No commit, no push. The Gitea comment is the deliverable.\n", "assets": [], "created_at": "2026-05-21T13:41:21+09:00", "updated_at": "2026-05-21T13:41:21+09:00"}, {"id": 22719, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22719", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #2] Stage 3 code-edit review IMP-46 u2\n\nScope: verified only the single executed unit, u2.\n\nVerdict: u2 is correct. The cache stub has been replaced with a persistent JSON backend at `data/frame_cache/{frame_id}/{signature_hash}.json`, stores `schema_version`, `proposal`, `slide_css`, and `fingerprints`, returns `None` for missing/corrupt/unparseable cached reads, preserves the dual write gate before filesystem writes, and keeps fingerprint comparison out of u2. No AI invocation path or save call site was added.\n\nMinor wording note: the implementation returns the storage `Path`; it does not explicitly call `Path.resolve()` when `CACHE_ROOT` is relative. This does not violate the u2 contract.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath src\\phase_z2_ai_fallback\\cache.py`\n- `Get-Content -LiteralPath tests\\phase_z2_ai_fallback\\test_cache.py`\n- `git diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache.py`\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py -q`\n- `rg -n \"NotImplementedError|anthropic|Claude|MDX 03|MDX 04|MDX 05|source_section_ids|mdx_path|template_id\" src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache.py`\n- PowerShell Python introspection for `SCHEMA_VERSION`, `KEY_DELIMITER`, `CACHE_ROOT`, `save_proposal`, and `read_proposal`\n- `rg -n \"save_proposal\\(|read_proposal\\(|cache_key\\s*=|route_ai_fallback\\(\" src tests/phase_z2_ai_fallback/test_step12.py`\n- `Get-Content -LiteralPath src\\phase_z2_ai_fallback\\step12.py`\n- `Get-Content -LiteralPath src\\phase_z2_ai_fallback\\router.py`\n- `python -m pytest tests/phase_z2_ai_fallback/test_router.py -q`\n- `git status --short`\nFiles checked:\n- `src/phase_z2_ai_fallback/cache.py`\n- `tests/phase_z2_ai_fallback/test_cache.py`\n- `src/phase_z2_ai_fallback/router.py`\n- `src/phase_z2_ai_fallback/step12.py`\nTest results:\n- `tests/phase_z2_ai_fallback/test_cache.py`: 33 passed in 0.12s\n- `tests/phase_z2_ai_fallback/test_router.py`: 9 passed in 0.03s\nVerified facts:\n- `SCHEMA_VERSION == 1`.\n- `KEY_DELIMITER == \"::\"`.\n- `CACHE_ROOT == data/frame_cache`.\n- `save_proposal` accepts `slide_css` and `fingerprints`, writes JSON only after `visual_check_passed=True` and `user_approved=True`, and returns the target storage path.\n- `read_proposal` returns `None` for missing files, malformed legacy keys without `::`, corrupt JSON, non-dict roots, missing/non-dict proposal payloads, and proposal validation failures.\n- Saved payload includes `schema_version`, `proposal`, `slide_css`, and `fingerprints`.\n- Parent directories are created lazily on successful save.\n- Gate failures are `AiFallbackCacheGateError`, not `NotImplementedError`, and the targeted test verifies no frame directory is created on gate rejection.\n- Current router still imports only `read_proposal`; no `save_proposal` call site was introduced in `src`.\n- Fingerprint persistence is present, but read-side fingerprint comparison is not implemented in u2, matching the u3 boundary.\n- `rg` found no `anthropic`, `Claude`, `MDX 03`, `MDX 04`, `MDX 05`, `source_section_ids`, `mdx_path`, or `template_id` leakage in `cache.py`; `NotImplementedError` remains only in explanatory/test text.\nRemaining units: [u3, u4, u5, u6]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO\n", "assets": [], "created_at": "2026-05-21T13:44:19+09:00", "updated_at": "2026-05-21T13:44:19+09:00"}, {"id": 22722, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22722", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #3] Stage 3 code-edit IMP-46 — u3 fingerprint invalidation\n\nExecuting unit: u3\n\n■ 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 — 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* — preserves AI isolation and keeps the cache schema-agnostic.\n\n■ files_changed\n- src/phase_z2_ai_fallback/cache.py (modified, +47 / -10 — 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■ diff_summary\n- `src/phase_z2_ai_fallback/cache.py`\n - Module docstring header renamed `u2` → `u2 + u3`; new section \"u3 invalidation contract (this module is a *comparator*, not a *computer*)\" documents:\n - `save_proposal` persists `fingerprints` verbatim — 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)` → `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` → 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 — 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` — legacy callers still hit.\n - `test_read_without_fingerprints_kwarg_ignores_stored_mismatch` — entry saved with `{\"contract_sha\": \"old\"}` still readable by legacy reader.\n - Matching path:\n - `test_read_with_matching_fingerprints_returns_proposal` — exact dict equality hits.\n - `test_read_with_empty_fingerprints_matches_empty_stored` — 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]` — each axis individually breaks the match.\n - `test_read_invalidates_when_caller_supplies_extra_key` — strict equality (not subset) is the locked semantic.\n - `test_read_invalidates_when_caller_supplies_subset` — same, from the other direction.\n - `test_read_invalidates_when_entry_saved_without_fingerprints` — caller demands proof of freshness; empty stored set is not \"compatible by default\".\n - `test_read_invalidates_when_stored_fingerprints_not_dict` — hand-corrupted payload (`fingerprints: [...]`) misses cleanly.\n - `test_read_invalidates_when_stored_fingerprints_field_missing` — 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` — file-missing wins.\n - `test_read_with_matching_fingerprints_still_loses_to_corrupt_json` — JSON-corrupt wins.\n - Input validation symmetry: `test_read_rejects_non_dict_fingerprints[list|string|int]` (3 cases) — TypeError matches save-side surface.\n - Module isolation lock: `test_cache_module_has_no_fingerprint_computer` — 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` — two entries with different sig_hashes hold their own fingerprints; crossed lookups miss, aligned lookups hit.\n\n■ 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` → 84 passed in 0.32s.\n- `python -m pytest tests/phase_z2_ai_fallback/ -q` → 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` — comparison is plain `dict` equality, not hash recomputation).\n\n■ 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 — 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 — `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 — rejected.\n\n■ remaining_units\n- [u4, u5, u6]\n\n■ 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", "assets": [], "created_at": "2026-05-21T13:48:56+09:00", "updated_at": "2026-05-21T13:48:56+09:00"}, {"id": 22724, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22724", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #3] Stage 3 code-edit review IMP-46 u3\n\nScope: verified only the single executed unit, u3.\n\nVerdict: u3 is correct. `read_proposal` now accepts a keyword-only `fingerprints` comparator, strict equality invalidates mismatched cache entries, missing/corrupt/stale payloads still return `None`, and `save_proposal` continues to persist caller-supplied fingerprints without computing them inside `cache.py`. The cache module remains a storage/comparison layer and does not invoke AI or add Phase Z runtime coupling.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath src/phase_z2_ai_fallback/cache.py`\n- `Get-Content -LiteralPath tests/phase_z2_ai_fallback/test_cache_invalidation.py`\n- `git diff -- src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py`\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache_invalidation.py -q`\n- `rg -n \"hashlib|sha256|def .*fingerprint|compute_.*finger|contract_sha|partial_sha|catalog_sha|anthropic|Claude|MDX 03|MDX 04|MDX 05|source_section_ids|mdx_path|template_id\" src/phase_z2_ai_fallback/cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py`\n- PowerShell Python introspection for `read_proposal` and `save_proposal` signatures\n- `git status --short`\nFiles checked:\n- `src/phase_z2_ai_fallback/cache.py`\n- `tests/phase_z2_ai_fallback/test_cache_invalidation.py`\nTest results:\n- `tests/phase_z2_ai_fallback/test_cache_invalidation.py`: 20 passed in 0.09s\nVerified facts:\n- `read_proposal` signature is `(key: str, *, fingerprints: dict | None = None) -> AiFallbackProposal | None`; the new parameter is keyword-only.\n- `save_proposal` still accepts `fingerprints: dict | None = None` and stores `dict(fingerprints)` in the JSON payload.\n- When caller fingerprints are supplied, stored fingerprints must be a dict and must equal the caller dict exactly; mismatch, missing field, non-dict stored value, missing file, or corrupt JSON returns `None`.\n- `fingerprints=None` on read skips comparison for back-compat.\n- Non-dict caller fingerprints raise `TypeError`.\n- The targeted grep found no `hashlib`, `sha256`, fingerprint computation function, Anthropic/Claude import, MDX sample IDs, `source_section_ids`, `mdx_path`, or `template_id` leakage in `cache.py`; fingerprint axis names appear only as documented opaque keys/test fixtures.\n- The working tree contains many pre-existing unrelated changes; u3 verification was limited to `cache.py` and `test_cache_invalidation.py`.\nRemaining units: [u4, u5, u6]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO\n", "assets": [], "created_at": "2026-05-21T13:50:48+09:00", "updated_at": "2026-05-21T13:50:48+09:00"}, {"id": 22727, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22727", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Claude #4] Stage 3 code-edit IMP-46 — u4 structural cache key + fingerprints\n\nExecuting unit: u4\n\n■ unit_executed\n- id: u4\n- summary: Replace the sample-identity cache_key (`template_id::sorted(source_section_ids)`) in `src/phase_z2_ai_fallback/step12.py` with a structural signature key `\"{frame_id}::{signature_hash}\"` derived from the 8 IMP-46 axes via `build_signature` (u1). Extend Step 12 helper inputs (unit attributes) for `layout_preset`, `zone_position`, `source_shape`, `h3_count`, `char_count`, and `cardinality`; project `char_count` through `bucket_char_count` (u1). Compute the three invalidation fingerprints (`contract_sha`, `partial_sha`, `catalog_sha`) and attach them — with the new `cache_key` — to each AI-eligible record for downstream `save_proposal` use. Exact-match lookup only (no fuzzy fallback added). v4_result.cardinality now mirrors the unit's cardinality (was hardcoded `None`).\n\n■ files_changed\n- src/phase_z2_ai_fallback/step12.py (modified, +82 / -2 — adds `_sha256_of` helper, `catalog_sha_loader` kwarg, structural signature + fingerprint block, record fields `cache_key` + `fingerprints`)\n- tests/phase_z2_ai_fallback/test_step12.py (modified, +247 / -16 — replaces the legacy `test_cache_key_includes_template_and_section_ids` test with 14 new u4 cases; FakeUnit extended with 6 new signature-input fields)\n\n■ diff_summary\n- `src/phase_z2_ai_fallback/step12.py`\n - Module docstring expanded with an `IMP-46 u4 — structural cache key + fingerprints` section that names every signature axis read from unit attributes and explains why fingerprint *computation* lives here (cache.py is a comparator per u3 — keeps the cache module schema-agnostic).\n - New stdlib imports: `hashlib`, `json` (both already in the AST isolation whitelist — `test_ast_isolation.py:39-42`).\n - New intra-package import: `bucket_char_count`, `build_signature` from `src.phase_z2_ai_fallback.signature` (u1).\n - New module-level helper `_sha256_of(payload: Any) -> str`: deterministic SHA256 over `json.dumps(payload, sort_keys=True, ensure_ascii=False)`. Used only for `contract_sha` and `partial_sha`.\n - `gather_step12_ai_repair_proposals` signature gains one new keyword-only argument:\n - `catalog_sha_loader: Callable[[], str] | None = None` — called once per gather invocation (verified by `test_catalog_sha_loader_called_once_per_gather`). When `None`, `catalog_sha` defaults to `\"\"` (sentinel — always present, so `fingerprints` is always a 3-key dict).\n - Record schema gains two fields, both initialised to `None`:\n - `\"cache_key\": str | None` — populated only on the AI-eligible code path; the structural axes are not guaranteed for skipped units, so the field is left `None` for `not_provisional` / `design_reference_only_no_ai` / `route_not_ai_adaptation:*` records.\n - `\"fingerprints\": dict | None` — same population rule.\n - Inside the AI-eligible branch (after route gates pass):\n - Read signature inputs from unit attributes via `getattr` with safe defaults (so existing test fixtures and pre-IMP-46 units survive): `frame_id_value`, `cardinality`, `layout_preset` (default `\"\"`), `zone_position` (default `\"\"`), `source_shape` (default `\"paragraph\"` — valid `SourceShape` enum member), `h3_count` (default `0`), `char_count` (default `0`).\n - `char_count_bucket = bucket_char_count(char_count)` — u1 fixed-bin projection.\n - `signature_hash = build_signature(frame_id=..., v4_label=label or \"\", cardinality=..., source_shape=..., h3_count=..., char_count_bucket=..., layout_preset=..., zone_position=...)` — 8-axis SHA256.\n - `cache_key = f\"{frame_id_value}::{signature_hash}\"` — matches cache.py `_parse_key` format (`KEY_DELIMITER = \"::\"`); validated by `test_cache_key_is_compatible_with_cache_parse_key`.\n - `fingerprints = {\"contract_sha\": _sha256_of(frame_contract), \"partial_sha\": _sha256_of(figma_partial_json), \"catalog_sha\": catalog_sha}`.\n - `v4_result[\"cardinality\"]` now reads the unit's `cardinality` attribute instead of the hardcoded `None` from IMP-33 u8.\n - `route_ai_fallback(cache_key=cache_key, ...)` now receives the structural key (the router's existing read-side path is unchanged — `read_proposal(cache_key)` continues to perform exact-match lookup only, as required by the u4 contract).\n\n- `tests/phase_z2_ai_fallback/test_step12.py`\n - Module docstring updated to declare the IMP-46 u4 coverage axis alongside the IMP-33 gates.\n - `FakeUnit` dataclass extended with 6 new fields (all with safe defaults): `cardinality: int | None = None`, `layout_preset: str = \"\"`, `zone_position: str = \"\"`, `source_shape: str = \"paragraph\"`, `h3_count: int = 0`, `char_count: int = 0`. All pre-existing tests continue to construct `FakeUnit(label=..., provisional=...)` without modification.\n - New helper `_ai_unit(**overrides)`: builds an AI-eligible (`provisional=True`, `label=\"restructure\"`) `FakeUnit` with realistic signature axes — keeps the u4 test bodies readable without mutating the existing test surface.\n - Legacy `test_cache_key_includes_template_and_section_ids` REMOVED — it asserted the broken `template_id::sorted(section_ids)` format that u4 explicitly replaces. Removing it (rather than xfailing) is consistent with the no-hardcoding lock: that key shape is now a defect, not a contract.\n - Existing `test_record_shape_contract_is_stable` renamed to `test_record_shape_contract_is_stable_with_u4_fields` and updated to assert exactly 12 keys (the original 10 + `cache_key` + `fingerprints`).\n - 14 new u4 cases:\n - `test_cache_key_format_is_frame_id_plus_sha256` — `cache_key.startswith(\"fid_123::\")`, suffix is 64-char lowercase hex; asserts the legacy substrings `\"tmpl_x\"` and `\"02-1\"` are absent.\n - `test_cache_key_invariant_to_section_id_changes` — `source_section_ids=[\"02-1\"]` and `[\"05-2\",\"07-3\"]` produce the same `cache_key` (no sample leakage).\n - `test_cache_key_invariant_to_template_id_changes` — `frame_template_id` is NOT in the signature surface (only `frame_id` is).\n - `test_cache_key_changes_when_any_signature_axis_changes` — parametrised-style loop over `{frame_id, layout_preset, zone_position, source_shape, h3_count, char_count, cardinality}`; each single-axis flip mutates `cache_key`. `char_count=500` is chosen specifically to cross the `151-400` → `401-1000` bucket boundary (verifies bucketing, not raw count).\n - `test_char_count_bucket_collapses_within_bucket` — `char_count=160` and `char_count=399` both fall in `\"151-400\"` and produce identical keys; `char_count=401` differs.\n - `test_fingerprints_attached_to_ai_record` — fingerprints dict has exactly the 3 declared keys, all string values; `contract_sha` and `partial_sha` match an inline recomputation of `hashlib.sha256(json.dumps(..., sort_keys=True, ensure_ascii=False).encode())`. Stable-fixture lock against silent fingerprint-format drift.\n - `test_fingerprints_default_catalog_sha_is_empty_string` — no `catalog_sha_loader` → `catalog_sha == \"\"` AND the key still appears in the dict (3-key invariant).\n - `test_fingerprints_change_when_contract_changes` — `frame_contract={\"a\":1}` vs `{\"a\":2}` mutates `contract_sha`, leaves `partial_sha` unchanged.\n - `test_fingerprints_change_when_partial_changes` — symmetric for `figma_partial_json`.\n - `test_v4_result_cardinality_uses_unit_value` — unit `cardinality=7` flows into `v4_result[\"cardinality\"]=7`; `cardinality=None` stays `None`. Closes the IMP-33 u8 hardcoded-`None` gap noted in Stage 1.\n - `test_skipped_records_have_no_cache_key_or_fingerprints` — `not_provisional`, `reject`, and `light_edit` paths all keep `cache_key=None` and `fingerprints=None`.\n - `test_catalog_sha_loader_called_once_per_gather` — `MagicMock` loader is called exactly once across 3 AI-eligible units (not once per unit).\n - `test_record_shape_contract_is_stable_with_u4_fields` — record key set is exactly `{unit_index, source_section_ids, frame_template_id, label, route_hint, provisional, ai_called, skip_reason, proposal, error, cache_key, fingerprints}`.\n - `test_cache_key_is_compatible_with_cache_parse_key` — round-trips the produced `cache_key` through `cache._parse_key`; asserts `frame_id == \"fid_123\"`, `len(signature_hash) == 64`, no extra `KEY_DELIMITER`. Cross-module structural lock — the u4 producer and the u2 consumer agree on the key shape.\n\n■ validation\n- `python -m pytest tests/phase_z2_ai_fallback/test_step12.py tests/phase_z2_ai_fallback/test_ast_isolation.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_router.py tests/phase_z2_ai_fallback/test_signature.py -q` → **125 passed in 0.32s**.\n- `python -m pytest tests/test_family_contract_baseline.py tests/phase_z2_ai_fallback/ -q` → **192 passed in 1.02s** (Stage 2 baseline regression suite green pre- and post-u4).\n- `git diff --stat` → `step12.py +84/-2`, `test_step12.py +247/-16` (315 insertions, 16 deletions, 2 files).\n- `Grep` audit of `src/phase_z2_ai_fallback/step12.py` for `source_section_ids|template_id|mdx_path|section_id|MDX 0[345]`: every hit is either the docstring describing the LEGACY format, the record-schema docstring, the record initialisation (telemetry — never fed to `build_signature`), or the `template_id` variable that is used **only** as a key for the existing `get_contract_fn` / `frame_visual_loader` / `figma_partial_loader` callbacks. **`template_id` and `source_section_ids` do NOT appear in `build_signature(...)` arguments or in the `cache_key` f-string.** No-hardcoding lock honored.\n- AI isolation contract: no new module imports outside the existing whitelist; `hashlib` and `json` were already allowed by `test_ast_isolation.py:39-42`. No Anthropic/MDX/Phase Q/Phase Z2 runtime imports added.\n\n■ remaining_units\n- [u5, u6]\n\n■ scope_lock observations (NOT acted on)\n- Router-side read fingerprint validation: the router's existing call is `read_proposal(cache_key)` — it does NOT yet pass the new `fingerprints` dict to `read_proposal` (the u3 invalidation comparator is therefore inert through the router today). u4's files list (per Stage 2 plan) is `step12.py + test_step12.py` only; `router.py` is out of scope. Step 12 produces and exposes the fingerprints; wiring them into the router's read call is a follow-up axis. Recording below.\n\n■ follow_up_issue_candidates\n- **router read-side fingerprint validation** — `src/phase_z2_ai_fallback/router.py:66` calls `read_proposal(cache_key)` without `fingerprints=`. To complete the u3 invalidation chain end-to-end (so a stale cache entry with changed `contract_sha` / `partial_sha` / `catalog_sha` misses through the router), the router needs to accept a `fingerprints` kwarg from `gather_step12_ai_repair_proposals` and forward it to `read_proposal`. This is **not** required by the Stage 2 plan's u4 scope, but the cache-invalidation behaviour is incomplete without it. Suggest filing as a small follow-up issue (or folding into u5 if the orchestrator consents — but strictly outside the unit list as written).\n- **catalog_sha source-of-truth** — `catalog_sha_loader` is a `Callable[[], str]`. The actual catalog content (Step 2 plan says the invalidation trigger is `templates/phase_z2/catalog/frame_contracts.yaml`) is not yet wired here — only the loader hook exists. Production wiring of a real catalog sha calculator at the pipeline integration site is a separate task.\n\nDO NOT commit or push (Stage 3 unit policy).\n", "assets": [], "created_at": "2026-05-21T14:02:43+09:00", "updated_at": "2026-05-21T14:02:43+09:00"}, {"id": 22729, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22729", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #4] Stage 3 code-edit review IMP-46 u4\n\nScope: verified only the single executed unit, u4.\n\nVerdict: u4 is correct. Step 12 now builds cache keys as `{frame_id}::{signature_hash}` using the u1 structural signature helper, derives the char-count bucket before hashing, forwards unit cardinality into `v4_result`, computes the three invalidation fingerprints outside `cache.py`, and attaches `cache_key` plus `fingerprints` only for AI-eligible records. The legacy template/section-id cache key is gone from the router call path, fuzzy lookup was not added, and the change does not introduce AI invocation outside the existing Step 12 router call.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath src\\\\phase_z2_ai_fallback\\\\step12.py`\n- `Get-Content -LiteralPath tests\\\\phase_z2_ai_fallback\\\\test_step12.py`\n- `git diff -- src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.py`\n- `rg -n \"gather_step12_ai_repair_proposals\\\\(\" src tests -g \"*.py\"`\n- `python -m pytest tests/phase_z2_ai_fallback/test_step12.py -q`\n- `rg -n \"template_id|source_section_ids|mdx_path|MDX 03|MDX 04|MDX 05|read_proposal\\\\(|fingerprints=\" src/phase_z2_ai_fallback/step12.py tests/phase_z2_ai_fallback/test_step12.py`\n- PowerShell Python focused check that changing `v4_label` mutates the generated cache key while preserving the AI route.\n- `python -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -q`\n- `python -m pytest tests/phase_z2_ai_fallback/test_signature.py tests/phase_z2_ai_fallback/test_cache.py tests/phase_z2_ai_fallback/test_cache_invalidation.py tests/phase_z2_ai_fallback/test_step12.py -q`\nFiles checked:\n- `src/phase_z2_ai_fallback/step12.py`\n- `tests/phase_z2_ai_fallback/test_step12.py`\n- `tests/phase_z2_ai_fallback/test_ast_isolation.py`\nTest results:\n- `tests/phase_z2_ai_fallback/test_step12.py`: 21 passed in 0.04s\n- `tests/phase_z2_ai_fallback/test_ast_isolation.py`: 22 passed in 0.12s\n- u1-u4 focused regression set (`test_signature.py`, `test_cache.py`, `test_cache_invalidation.py`, `test_step12.py`): 94 passed in 0.25s\nVerified facts:\n- `gather_step12_ai_repair_proposals` accepts `catalog_sha_loader` and calls it once per gather invocation when supplied.\n- AI-eligible records receive `cache_key` and `fingerprints`; skipped records keep both fields as `None`.\n- The generated cache key uses the frame id prefix plus a 64-character lowercase SHA256 digest and round-trips through `cache._parse_key`.\n- `frame_template_id` and `source_section_ids` no longer influence the generated cache key; changing those values with the same structural axes preserves the key.\n- Changing structural axes changes the key, including a focused check for the `v4_label` axis with routing held constant.\n- `char_count` is bucketed before signature construction; values within the same bucket preserve the key and crossing a bucket boundary changes it.\n- Fingerprints contain exactly `contract_sha`, `partial_sha`, and `catalog_sha`; contract/partial hashes are deterministic JSON-sorted SHA256 values, and the default catalog sentinel is an empty string.\n- `v4_result[\"cardinality\"]` now mirrors the unit value instead of being hardcoded to `None`.\n- The targeted grep found no MDX sample IDs or `mdx_path` leakage in the u4 Step 12 files; legacy `template_id` / `source_section_ids` references remain only as record fields, loader inputs, and tests proving they are excluded from the cache key.\nRemaining units: [u5, u6]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO", "assets": [], "created_at": "2026-05-21T14:04:35+09:00", "updated_at": "2026-05-21T14:04:35+09:00"}, {"id": 22734, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22734", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "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", "assets": [], "created_at": "2026-05-21T14:10:17+09:00", "updated_at": "2026-05-21T14:10:17+09:00"}, {"id": 22737, "html_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62#issuecomment-22737", "pull_request_url": "", "issue_url": "https://gitea.hmac.kr/Kyeongmin/C.E.L_Slide_test2/issues/62", "user": {"id": 28, "login": "Kyeongmin", "login_name": "", "source_id": 0, "full_name": "이경민", "email": "kyeongmin@noreply.localhost", "avatar_url": "https://gitea.hmac.kr/avatars/d883a35f9197b080b2a0c535c3f9357dcd5c7d40b3c42bd8f40218cb94609a8e", "html_url": "https://gitea.hmac.kr/Kyeongmin", "language": "", "is_admin": false, "last_login": "0001-01-01T00:00:00Z", "created": "2025-12-30T10:51:04+09:00", "restricted": false, "active": false, "prohibit_login": false, "location": "", "website": "", "description": "", "visibility": "public", "followers_count": 0, "following_count": 0, "starred_repos_count": 1, "username": "Kyeongmin"}, "original_author": "", "original_author_id": 0, "body": "[Codex #5] Stage 3 code-edit review IMP-46 u5\n\nScope: verified only the single executed unit, u5.\n\nVerdict: u5 is correct. `Settings.ai_fallback_auto_cache` defaults to `False`, `save_proposal` now accepts a keyword-only `auto_cache: bool = False`, and the gate logic preserves `visual_check_passed` as non-bypassable while allowing `auto_cache=True` to bypass only `user_approved=False`. The CLI adds `--auto-cache` as a `store_true` flag and mutates `settings.ai_fallback_auto_cache=True` after argument parsing. I did not find an added AI invocation or cache-module dependency on Phase Z runtime settings.\n\n=== EVIDENCE ===\nCommands run:\n- `Get-Content -LiteralPath src\\config.py`\n- `Get-Content -LiteralPath src\\phase_z2_ai_fallback\\cache.py`\n- `Get-Content -LiteralPath src\\phase_z2_pipeline.py`\n- `Get-Content -LiteralPath tests\\phase_z2_ai_fallback\\test_cache.py`\n- `Get-Content -LiteralPath tests\\test_phase_z2_ai_fallback_config.py`\n- `rg -n \"auto_cache|auto-cache|ai_fallback_auto_cache|save_proposal\\(\" src\\config.py src\\phase_z2_ai_fallback\\cache.py src\\phase_z2_pipeline.py tests\\phase_z2_ai_fallback\\test_cache.py tests\\test_phase_z2_ai_fallback_config.py`\n- `python -m pytest tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py -q`\n- PowerShell Python introspection for `save_proposal` signature and `Settings().ai_fallback_auto_cache`\n- `python -m pytest tests/phase_z2_ai_fallback/test_ast_isolation.py -q`\n- `git diff -- src/config.py src/phase_z2_ai_fallback/cache.py src/phase_z2_pipeline.py tests/phase_z2_ai_fallback/test_cache.py tests/test_phase_z2_ai_fallback_config.py`\n- PowerShell Python AST check for the `--auto-cache` argparse entry and `settings.ai_fallback_auto_cache` mutation\n- `git status --short`\nFiles checked:\n- `src/config.py`\n- `src/phase_z2_ai_fallback/cache.py`\n- `src/phase_z2_pipeline.py`\n- `tests/phase_z2_ai_fallback/test_cache.py`\n- `tests/test_phase_z2_ai_fallback_config.py`\n- `tests/phase_z2_ai_fallback/test_ast_isolation.py`\nTest results:\n- `tests/phase_z2_ai_fallback/test_cache.py` plus `tests/test_phase_z2_ai_fallback_config.py`: 51 passed in 0.14s\n- `tests/phase_z2_ai_fallback/test_ast_isolation.py`: 22 passed in 0.14s\nVerified facts:\n- `Settings().ai_fallback_auto_cache is False`.\n- `save_proposal` signature is `(key: 'str', proposal: 'AiFallbackProposal', *, visual_check_passed: 'bool', user_approved: 'bool', slide_css: 'str | None' = None, fingerprints: 'dict | None' = None, auto_cache: 'bool' = False) -> 'pathlib.Path'`.\n- `auto_cache` is keyword-only and defaults to `False`.\n- Non-bool `auto_cache` raises `TypeError`.\n- The truth table test covers all eight `(visual_check_passed, user_approved, auto_cache)` combinations; exactly `(True, True, False)`, `(True, True, True)`, and `(True, False, True)` persist.\n- `visual_check_passed=False` raises `AiFallbackCacheGateError` even when `auto_cache=True`, and rejected rows do not create the frame cache directory.\n- `--auto-cache` is registered with `dest=\"auto_cache\"`, `action=\"store_true\"`, and `default=False`.\n- When parsed `args.auto_cache` is true, `src.phase_z2_pipeline` mutates `_settings.ai_fallback_auto_cache = True`.\n- `cache.py` does not import `src.config`, Anthropic, or Phase Z runtime modules for this unit; callers still pass the resolved boolean explicitly.\n- The working tree contains many pre-existing unrelated changes; u5 verification was limited to the declared u5 file set and targeted isolation test.\nRemaining units: [u6]\nrewind_target: continue_same\nFINAL_CONSENSUS: NO\n", "assets": [], "created_at": "2026-05-21T14:12:32+09:00", "updated_at": "2026-05-21T14:12:32+09:00"}]