Executing unit: u9 [Claude #N] Stage 3 code-edit Round #2 — unit u9 (rewind fix) ### unit_executed u9 — pipeline wiring for `_attempt_salvage_chain`. Addresses both Codex #10 findings: stale `failure_classification` leaking into Step 18/19 after `salvage_passed=True`, and `fit_analysis=None / containers={}` making `cross_zone_redistribute` permanently infeasible at the real call site. ### files_changed - src/phase_z2_pipeline.py ### diff_summary - u9 block at `src/phase_z2_pipeline.py:4423-4490` now synthesizes a `FitAnalysis` from `debug_zones` + `overflow.zones`: - Each `debug_zones[i].position` becomes a `RoleFit(role=position, allocated_px=height_px, shortfall_px=scrollHeight-clientHeight)`. Sign convention matches `fit_verifier.redistribute()` (positive = deficit, negative = surplus). - All positions share conceptual zone `"slide_body"` via `containers[position] = {"zone": "slide_body", "height_px": int(allocated)}` so `redistribute()` trades shortfalls between them in a single bucket. Phase Q's `calculate_fit` is not used in Phase Z (policy comment at `src/phase_z2_pipeline.py:2839-2843`), so this synthesis is the deterministic substitute. - `FitAnalysis(roles=_fa_roles)` + `_fa_containers` replace the previous `fit_analysis=None, containers={}` placeholders. - Import added inline (`from src.fit_verifier import FitAnalysis, RoleFit`) — matches the existing `from src.fit_verifier import ...` pattern used across `src/space_allocator.py`, `src/renderer.py`, `src/phase_z2_retry.py`. - After `if _salvage_trace.get("salvage_passed"):` block — `enrich_retry_trace_with_failure_classification(retry_trace)` is re-invoked. `classify_retry_failure` short-circuits on `salvage_passed=True` (`src/phase_z2_failure_router.py:167`), so this clears both `retry_trace["failure_classification"]` and `retry_trace["next_action_proposal"]` to `None`, matching the contract enforced at `src/phase_z2_failure_router.py:302-306`. Step 18/19 artifacts now reflect the post-salvage state (no stale `donor_slack_insufficient` / `no_donor_candidates`). - Net delta on the u9 block: +60 / -2 lines (`git diff --numstat src/phase_z2_pipeline.py` reports 164/4 cumulative including the u8 hunk from prior round; u9 block alone is ~22 added lines). - No new module dependencies, no behavior change outside the cascade trigger guard (`_ft in {"donor_slack_insufficient", "no_donor_candidates"}`). ### verification - Smoke test 1 — RoleFit/FitAnalysis construction with synthesized Phase Z roles: ``` redistribute(FitAnalysis(roles={top:RoleFit(shortfall=+30), bottom_l:RoleFit(shortfall=-50), bottom_r:RoleFit(shortfall=-10)}), containers, min_margin_px=10) → can_redistribute=True; redistribution={top:230, bottom_l:150, bottom_r:180}; needs_escalation=False ``` Deficit 30px transferred from bottom_l (40px usable surplus after 10px margin) → top. Confirms the synthesis lets `plan_cross_zone_redistribute` return `feasible=True` when geometry permits, and `feasible=False` (with `can_redistribute=False`) otherwise. - Smoke test 2 — post-salvage enrich refresh: Pre: `failure_classification={'failure_type': 'donor_slack_insufficient', ...}`, `next_action_proposal={'next_proposed_action': 'cross_zone_redistribute'}`, `salvage_passed=True`. After `enrich_retry_trace_with_failure_classification(rt)`: both fields = `None`. Asserted. - `python -m ast` parse OK on `src/phase_z2_pipeline.py`. - `pytest -q tests/phase_z2` → **62 passed** (regression-free baseline maintained; matches prior u1/u2/u3 baseline reported in MID-STAGE COMPACTION). ### scope-lock adherence - Edits confined to `src/phase_z2_pipeline.py` (file count = 1, well within u9's ≤3-file budget). - No `:root` / `body` / `.slide` / global CSS mutation. The cross_zone CSS still emits `[data-role=""]` selectors from `apply_cross_zone_redistribute_css` (u4 surface, untouched); whether Phase Z DOM honors them is a separate axis (see follow-up below). The synthesis correctly lets the *plan* be feasible; if the rerender visual check still fails, the cascade naturally falls through to `glue_compression` via `_SALVAGE_FAIL_BY_ACTION → cross_zone_redistribute_insufficient → NEXT_ACTION_BY_FAILURE → glue_compression`. - No AI call added on normal path (`feedback_ai_isolation_contract` — preserved). - No spacing-direction shrink on common envelope (`feedback_phase_z_spacing_direction` — preserved). - IMP-09 `dynamic_cols` / `fr_default` gate already enforced inside `_attempt_salvage_chain` at `src/phase_z2_pipeline.py:1958-1960` — unchanged. - `(b) revert` semantics preserved: `out_path.write_text(candidate_html)` is only called inside `_attempt_salvage_chain` on `passed=True`. On all-3-fail, the original `final.html` written at line 4306 stays intact. Codex case (c) contract holds. ### remaining_units - u10 — Regression tests for multi-donor `plan_zone_ratio_retry` (`tests/phase_z2/test_phase_z2_retry_multi_donor.py`). Deferred from u1. - u11 — Tests for `plan_cross_zone_redistribute` (`tests/phase_z2/test_phase_z2_cross_zone_redistribute.py`). - u12 — Tests for `plan_glue_compression` (`tests/phase_z2/test_phase_z2_glue_compression.py`). - u13 — Tests for `plan_font_step_compression` (`tests/phase_z2/test_phase_z2_font_step_compression.py`). - u14 — Tests for failure_router cascade (`tests/phase_z2/test_phase_z2_failure_router_cascade.py`). - u15 — End-to-end Step 17 salvage chain tests (`tests/phase_z2/test_phase_z2_step17_salvage_chain.py`). Will lock the post-salvage `failure_classification=None` invariant as a regression guard. ### follow_up_issue_candidates - Selector mismatch in `apply_cross_zone_redistribute_css`: emits `[data-role=""]` rules, but Phase Z's `templates/phase_z2/slide_base.html:242` only declares `data-zone-position`, not `data-role`. The salvage plan is now feasible, but the rerender CSS will not match any Phase Z DOM and the cascade will fall through to `glue_compression`. Two non-mutually-exclusive resolutions, both out-of-scope for u9: 1. Extend `apply_cross_zone_redistribute_css` to also emit `[data-zone-position=""]` when synthesized roles equal zone positions (would require role-vs-position discrimination signal). 2. Add `data-role="{{ zone.position }}"` to `slide_base.html` so Phase Z DOM honors the Phase Q-style selector. - These are template/CSS-emission concerns, not Step 17 wiring. Filing as a follow-up axis preserves Stage 2's u4 contract (already verified) while keeping the u9 scope on pipeline plumbing.