feat(#64): IMP-35 details_popup_escalation u1~u10 + Stage 3 R7 anchor re-pin
Land the production + test surface for the Step 17 cascade POPUP terminal (DETERMINISTIC -> POPUP -> AI_REPAIR -> USER_OVERRIDE) per Stage 2 plan R2. u11 (baseline-red invariance gate) was already landed in7c93031ahead of this commit; this commit completes u1~u10 plus the Stage 3 R7 follow-up anchor re-pin for test_imp17_comment_anchor.py. Implementation units (Stage 2 R2 contract): u1 frame_reselect_insufficient failure_type + post-frame remeasure (q4) - src/phase_z2_failure_router.py, src/phase_z2_pipeline.py u2 NEXT_ACTION_BY_FAILURE row + impl_status flip - src/phase_z2_failure_router.py u3 Router details_popup_escalation MISSING->IMPLEMENTED + executor stub - src/phase_z2_router.py u4 step17.py AI split-decision contract (POPUP cascade_stage + route_for_label + skip_reason); API gated - src/phase_z2_ai_fallback/step17.py u5 Step 17 POPUP gate executor; popup_escalation_plan + has_popup marker - src/phase_z2_pipeline.py, src/phase_z2_ai_fallback/step17.py u6 Composition popup binding -- yaml strategy -> zone payload - src/phase_z2_composition.py u7 Pipeline composer -> render_slide wiring (popup_html / preview_text / has_popup) - src/phase_z2_pipeline.py u8 slide_base.html <details>/<summary> popup wrapper - templates/phase_z2/slide_base.html u9 display_strategies.yaml inline_preview + popup metadata - templates/phase_z2/regions/display_strategies.yaml u10 MDX preservation invariant: popup=full source / body=summary or subset (asserted by tests/phase_z2/test_popup_mdx_preservation.py) u11 (already in7c93031) -- baseline-red invariance gate Stage 3 R7 follow-up (anchor re-pin, test-only): - tests/orchestrator_unit/test_imp17_comment_anchor.py Pre-anchor additions in src/phase_z2_pipeline.py (u1 / u5 / u7) shifted the restructure/reject route-hint comments 578/579 -> 586/587. Re-pinned the two guard tests (and docstring re-pin lineage 564 -> 570 -> 578 -> 586). Production code untouched. Verification (Stage 4 R1): pytest -q tests/orchestrator_unit/test_imp17_comment_anchor.py -> 2 passed / 0.02s pytest -q <10 IMP-35 unit files in tests/phase_z2 + tests/phase_z2_ai_fallback> -> 136 passed / 15.94s Baseline-red invariance gate (tests/test_imp47b_step12_ai_wiring.py + tests/test_phase_z2_ai_fallback_config.py) -> 4 failed / 6 passed; FAILED set === IMP35_BASELINE_RED_NODE_IDS (frozen registry from7c93031). Contract holds. Codex Stage 4 R1 = YES (independent verify). Guardrails honored: - MDX content preservation: popup carries full source, body holds summary or subset only (CLAUDE.md 자세히보기 원칙; feedback_phase_z_spacing_direction -- capacity expanded, no margin shrink). - AI isolation contract: Step 17 POPUP gate is deterministic; AI hook surface is split-decision contract only, API call gated. - No hardcoding: escalation thresholds derived from existing overflow detector outputs; preview_chars deterministic from container px. - 1 commit = 1 decision unit: u1~u10 land together as the planned production surface; u11 was deliberately split into7c93031as Stage 3 R7 carve-out, and the R7 anchor re-pin rides with this commit because it is the direct shift consequence of the u1/u5/u7 pre-anchor additions. - Scope-locked: .claude/settings.json explicitly excluded (Stage 4 exit report contract). Out of scope (per Stage 1 + Stage 2): - AI_REPAIR API activation (post IMP-35 axis). - IMP-34 zone resize, IMP-36 responsive fit (chain partners, separate issues). - Print-time auto-expand JavaScript for <details>. - Popup escalation in stages other than Step 17. - Baseline-red body repair (4 frozen failures) -- separate follow-up issue; u11 only guards the count. - frame_reselect algorithm changes (entry point only). - templates/phase_z2/slide_base.html path rename. source_comment_ids: Stage 1: claude_stage1_problem_review_imp35, codex_stage1_verification_imp35_yes Stage 2: Claude #4 R2 plan, Codex #5 R2 YES Stage 3: Claude #86 (R7 anchor re-pin), Codex #87 YES Stage 4: Claude #88 R1, Codex #89 R1 YES Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape
|
||||
from phase_z2_composition import (
|
||||
LAYOUT_PRESETS,
|
||||
CompositionUnit,
|
||||
compose_zone_popup_payload,
|
||||
derive_parent_id,
|
||||
plan_composition,
|
||||
resplit_all_reject_merges,
|
||||
@@ -57,7 +58,7 @@ from phase_z2_mapper import (
|
||||
map_with_contract,
|
||||
)
|
||||
from phase_z2_classifier import classify_visual_runtime_check
|
||||
from phase_z2_router import route_fit_classification
|
||||
from phase_z2_router import plan_details_popup_escalation, route_fit_classification
|
||||
from phase_z2_retry import (
|
||||
DEFAULT_SAFETY_MARGIN_PX,
|
||||
apply_cross_zone_redistribute_css,
|
||||
@@ -85,6 +86,13 @@ from phase_z2_placement_planner import plan_placement
|
||||
# stays in src/config.py + src/phase_z2_ai_fallback/router.py.
|
||||
from src.phase_z2_ai_fallback.step12 import gather_step12_ai_repair_proposals
|
||||
|
||||
# IMP-35 (#64) u5 — Step 17 deterministic POPUP gate executor. Runs after
|
||||
# the salvage cascade exhausts at cascade-terminal action
|
||||
# ``details_popup_escalation`` (router u3 / failure_router u2) and BEFORE
|
||||
# the AI_REPAIR cascade stage. Stamps ``popup_escalation_plan`` and the
|
||||
# idempotent ``has_popup`` marker onto retry_trace per unit. No AI call.
|
||||
from src.phase_z2_ai_fallback.step17 import run_step17_popup_gate
|
||||
|
||||
|
||||
# ─── Constants ──────────────────────────────────────────────────
|
||||
|
||||
@@ -2476,6 +2484,41 @@ def _attempt_salvage_chain(
|
||||
return trace
|
||||
|
||||
|
||||
def _remeasure_after_frame_reselect(
|
||||
*, candidate_path: Path, plan: Optional[dict] = None,
|
||||
) -> dict:
|
||||
"""IMP-35 (#64) u1 — post-frame remeasure helper for the cascade terminal.
|
||||
|
||||
Contract (q4 / Stage 2): frame_reselect_insufficient is detected by an
|
||||
*explicit overflow re-measure* after a V4 top-k alternate frame swap —
|
||||
NOT a failure-flag carryover. This helper runs run_overflow_check on the
|
||||
re-rendered candidate HTML and shapes the salvage_steps entry that
|
||||
classify_retry_failure / SALVAGE_FAILURE_TYPE_BY_ACTION read.
|
||||
|
||||
Future frame_reselect orchestrator (post-IMP-35) writes the candidate
|
||||
HTML and calls this helper to append the entry to retry_trace.salvage_steps.
|
||||
On passed=True the orchestrator promotes the candidate to final.html; on
|
||||
passed=False the classifier emits frame_reselect_insufficient → u2 routes
|
||||
onto details_popup_escalation (Step 17 POPUP gate / u5).
|
||||
"""
|
||||
candidate_overflow = run_overflow_check(candidate_path)
|
||||
passed = bool(candidate_overflow.get("passed", False))
|
||||
return {
|
||||
"action": "frame_reselect",
|
||||
"plan": plan,
|
||||
"passed": passed,
|
||||
"candidate_path": (
|
||||
str(candidate_path.relative_to(PROJECT_ROOT))
|
||||
if candidate_path.is_absolute() else str(candidate_path)
|
||||
),
|
||||
"post_salvage_overflow": candidate_overflow,
|
||||
"failure_reason": (
|
||||
None if passed
|
||||
else (candidate_overflow.get("fail_reasons") or "post-frame remeasure: overflow persists")
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def render_slide(slide_title: str, slide_footer: Optional[str],
|
||||
zones_data: list[dict], layout_preset: str,
|
||||
layout_css: dict, gap_px: int = GRID_GAP,
|
||||
@@ -4232,6 +4275,11 @@ def run_phase_z2_mvp1(
|
||||
# first-render invariant holds; u5 will surface the provisional flag as
|
||||
# a zone class + needs-adaptation badge.
|
||||
if unit.frame_template_id == "__empty__":
|
||||
# IMP-35 u7 — popup payload wiring. Empty-shell units never go
|
||||
# through the Step 17 POPUP gate (no raw content to escalate),
|
||||
# so compose_zone_popup_payload returns the no-popup branch
|
||||
# (has_popup=False, popup_html=None, preview_text=None).
|
||||
_popup_payload = compose_zone_popup_payload(unit, 0)
|
||||
zones_data.append({
|
||||
"position": position,
|
||||
"template_id": "__empty__",
|
||||
@@ -4241,6 +4289,7 @@ def run_phase_z2_mvp1(
|
||||
"assignment_source": "imp30_u4_empty_shell",
|
||||
"section_assignment_override": False,
|
||||
"provisional": bool(getattr(unit, "provisional", False)),
|
||||
**_popup_payload,
|
||||
})
|
||||
debug_zones.append({
|
||||
"position": position,
|
||||
@@ -4411,6 +4460,15 @@ def run_phase_z2_mvp1(
|
||||
# needs-adaptation badge. Default False keeps non-provisional zones
|
||||
# byte-identical to pre-u5; only u1-synthesized rank-1 fills or u4
|
||||
# empty-shell synthesize provisional=True units.
|
||||
#
|
||||
# IMP-35 u7 — popup payload wiring. `compose_zone_popup_payload(unit,
|
||||
# min_height_px)` reads u6 binding (yaml strategy + popup_body_source)
|
||||
# AND derives a px-budgeted preview from min_height_px. Surfaces three
|
||||
# uniform render-context fields per zone (has_popup / popup_html /
|
||||
# preview_text) plus the full u6 binding under `popup_binding` for
|
||||
# u8 / u9 downstream consumers. Non-popup units (has_popup=False)
|
||||
# return the no-popup branch — byte-identical zone shape pre-u7.
|
||||
_popup_payload = compose_zone_popup_payload(unit, min_height_px)
|
||||
zones_data.append({
|
||||
"position": position,
|
||||
"template_id": unit.frame_template_id,
|
||||
@@ -4420,6 +4478,7 @@ def run_phase_z2_mvp1(
|
||||
"assignment_source": plan_assignment_source,
|
||||
"section_assignment_override": plan_section_override,
|
||||
"provisional": bool(getattr(unit, "provisional", False)),
|
||||
**_popup_payload,
|
||||
})
|
||||
debug_zones.append({
|
||||
"position": position,
|
||||
@@ -4475,6 +4534,12 @@ def run_phase_z2_mvp1(
|
||||
pos = record["position"]
|
||||
if pos in renderable_positions:
|
||||
continue
|
||||
# IMP-35 u7 — popup payload wiring for unrenderable empty
|
||||
# plan record. No CompositionUnit exists for this branch
|
||||
# (section-assignment plan produced no unit), so we stamp the
|
||||
# no-popup defaults directly. Keeps the zone shape uniform
|
||||
# across all three append paths so slide_base.html (u8) does
|
||||
# not have to branch on the presence of popup fields.
|
||||
zones_data.append({
|
||||
"position": pos,
|
||||
"template_id": "__empty__",
|
||||
@@ -4486,6 +4551,10 @@ def run_phase_z2_mvp1(
|
||||
record.get("skipped_reason")
|
||||
or "section_assignment_override_empty_or_unrenderable"
|
||||
),
|
||||
"has_popup": False,
|
||||
"popup_html": None,
|
||||
"preview_text": None,
|
||||
"popup_binding": None,
|
||||
})
|
||||
debug_zones.append({
|
||||
"position": pos,
|
||||
@@ -5615,6 +5684,54 @@ def run_phase_z2_mvp1(
|
||||
# fields become None (no failure to classify, no escalation pending).
|
||||
enrich_retry_trace_with_failure_classification(retry_trace)
|
||||
|
||||
# 11.8 IMP-35 (#64) u5 — Step 17 deterministic POPUP gate executor.
|
||||
# Runs after the salvage cascade exits at cascade-terminal action
|
||||
# `details_popup_escalation` (router u3 IMPLEMENTED + failure_router u2
|
||||
# cascade row). Stamps popup_escalation_plan + idempotent has_popup
|
||||
# marker per unit onto retry_trace["popup_gate_records"]. Deterministic
|
||||
# gate — no AI call (feedback_ai_isolation_contract); the u4
|
||||
# api_gated split-decision hook is a separate cascade-stage record
|
||||
# consumed only when a future IMP activates the Anthropic API.
|
||||
# Consumer side (composition popup binding / render wiring) lands in
|
||||
# u6 / u7. q1 (per-unit), q2 (idempotent via has_popup), q3
|
||||
# (deterministic from fit_classification) — see Stage 2 plan.
|
||||
# next_proposed_action is the single canonical signal: it is set by
|
||||
# enrich_retry_trace_with_failure_classification via failure_router u2
|
||||
# (NEXT_ACTION_BY_FAILURE), which routes frame_reselect_insufficient ->
|
||||
# details_popup_escalation. This check is independent of whether the
|
||||
# salvage chain block ran, so the popup gate fires for any retry path
|
||||
# that lands on the cascade-terminal popup action.
|
||||
_next_action = (
|
||||
retry_trace.get("next_action_proposal") or {}
|
||||
).get("next_proposed_action")
|
||||
if _next_action == "details_popup_escalation":
|
||||
_popup_cls_by_zone = {
|
||||
c.get("zone_position"): c
|
||||
for c in (fit_classification.get("classifications") or [])
|
||||
if c.get("category") in {
|
||||
"structural_major_overflow",
|
||||
"tabular_overflow",
|
||||
}
|
||||
}
|
||||
_zone_by_ssids = {
|
||||
tuple(z.get("source_section_ids") or []): z.get("position")
|
||||
for z in debug_zones
|
||||
}
|
||||
|
||||
def _classification_for_unit(u):
|
||||
ssids = tuple(getattr(u, "source_section_ids", []) or [])
|
||||
zone_pos = _zone_by_ssids.get(ssids)
|
||||
return _popup_cls_by_zone.get(zone_pos) if zone_pos else None
|
||||
|
||||
retry_trace["popup_gate_records"] = run_step17_popup_gate(
|
||||
units,
|
||||
classification_for_unit=_classification_for_unit,
|
||||
route_for_label=_imp05_route_hint,
|
||||
plan_for_classification=plan_details_popup_escalation,
|
||||
)
|
||||
retry_trace["popup_gate_executed"] = True
|
||||
retry_trace["popup_gate_terminal_action"] = "details_popup_escalation"
|
||||
|
||||
# ─── Step 17: Implemented Action (retry) ───
|
||||
_write_step_artifact(
|
||||
run_dir, 17, "retry_trace",
|
||||
|
||||
Reference in New Issue
Block a user