feat(#39): IMP-30 first-render invariant + abort bypass (2 paths)

Restore first-render invariant: final.html + Step 20 slide_status MUST be
written for every input where Step 0~5 succeed. Two abort paths replaced
with provisional/empty-shell synthesis; MDX content preserved, AI-free.

- u1 V4Match.provisional + lookup_v4_match_with_fallback(allow_provisional)
  chain_exhausted -> synthesize rank-1 provisional (opt-in, default-off)
- u2 CompositionUnit.provisional propagation (single / parent_merged /
  parent_merged_inferred constructors)
- u3 select_composition_units(allow_provisional_fill=True) last-resort
  fill + _candidate_state="selected_provisional"
- u4 pipeline.py path-(a) abort guard replaced with provisional retry +
  terminal __empty__ shell (no sys.exit(1))
- u5 zones_data.provisional -> slide_base.html zone--provisional class +
  data-provisional + needs-adaptation badge (template-only)
- u6 compute_slide_status additive provisional_first_render_count/_units
  (overall enum unchanged per IMP-05 Codex #10 D4)
- u7 regression: tests/test_phase_z2_imp30_first_render.py (28 tests) +
  tests/test_phase_z2_v4_fallback.py (+5 cases)

Guardrails verified: MVP1_ALLOWED_STATUSES unchanged, no calculate_fit,
no LLM in fallback path, no MDX 03/04/05 hardcoding.

Anchor sync (Rule 13): tests/orchestrator_unit/test_imp17_comment_anchor.py
re-pinned 564/565 -> 570/571 to track V4Match.provisional shift at
src/phase_z2_pipeline.py:179-184.

Cross-ref: IMP-05 (#5) §5 defer + Codex #2 first-render invariant.
This commit is contained in:
2026-05-21 00:40:58 +09:00
parent b4872ba6ce
commit 1efbf672bd
6 changed files with 2105 additions and 33 deletions

View File

@@ -4,6 +4,10 @@ Stage 1 finding: line 564 previously referenced a non-existent ID ("IMP-31").
The legitimate slot is IMP-17 (Gitea #17, carve-out — AI fallback only, normal path 밖).
Line 565 (IMP-29 frontend zone-level override) must remain untouched.
Anchor re-pin (2026-05-20, IMP-30 u1 follow-up): V4Match.provisional field added at
src/phase_z2_pipeline.py:179-184 shifted the route-hint table down by six lines.
Pinned line numbers updated from 564/565 → 570/571 to track the actual anchor location.
Run: pytest -q tests/orchestrator_unit/test_imp17_comment_anchor.py
"""
from pathlib import Path
@@ -16,14 +20,14 @@ def _lines() -> list[str]:
return PIPELINE.read_text(encoding="utf-8").splitlines()
def test_line_564_references_imp17_not_imp31():
line = _lines()[563] # 1-indexed line 564
assert "restructure" in line, f"line 564 anchor drifted: {line!r}"
assert "IMP-17" in line, f"line 564 must reference IMP-17 (carve-out): {line!r}"
assert "IMP-31" not in line, f"line 564 must not reference non-existent IMP-31: {line!r}"
def test_line_570_references_imp17_not_imp31():
line = _lines()[569] # 1-indexed line 570
assert "restructure" in line, f"line 570 anchor drifted: {line!r}"
assert "IMP-17" in line, f"line 570 must reference IMP-17 (carve-out): {line!r}"
assert "IMP-31" not in line, f"line 570 must not reference non-existent IMP-31: {line!r}"
def test_line_565_still_references_imp29():
line = _lines()[564] # 1-indexed line 565
assert "reject" in line, f"line 565 anchor drifted: {line!r}"
assert "IMP-29" in line, f"line 565 must still reference IMP-29 frontend override: {line!r}"
def test_line_571_still_references_imp29():
line = _lines()[570] # 1-indexed line 571
assert "reject" in line, f"line 571 anchor drifted: {line!r}"
assert "IMP-29" in line, f"line 571 must still reference IMP-29 frontend override: {line!r}"