untracked files on main: dceb101 feat(#63): IMP-34 R1 donor capacity measured bound (u1+u2)

This commit is contained in:
2026-05-21 22:07:41 +09:00
commit 8f085a28d3
3220 changed files with 985495 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
Executing unit: u9
[Claude #9b] Stage 3 code-edit Round #N — unit u9
### unit_executed
- **id**: u9
- **summary**: Pipeline wiring after `_attempt_zone_ratio_retry` — on `donor_slack_insufficient` / `no_donor_candidates`, call `_attempt_salvage_chain` (u8 orchestrator), merge salvage trace into `retry_trace`, refresh `overflow / fit_classification / router_decision` on `salvage_passed=True` (mirrors retry_passed branch refresh), update Step 17 telemetry `step_status` + `note` to surface u8/u9 cascade. `debug_zones` / `layout_css` intentionally NOT refreshed (glue/font are non-geometric, scoped CSS only).
### files_changed
- `src/phase_z2_pipeline.py` (u9-only delta = +43 / 3, net +40 lines; combined file diff = +138 / 4 incl. u8's +95/1 from prior unit. Slight overshoot of u9 `estimate_lines: 30` is structural — cascade_inputs has 10 contract-required fields and state refresh mirrors retry_passed branch's 4 `router_decision` fields. No drop in scope.)
### diff_summary
1. **Salvage cascade dispatcher** (lines 4423-4456, +37 lines) — placed between `enrich_retry_trace_with_failure_classification(retry_trace)` (4421) and the Step 17 artifact write (4459):
- **Trigger guard** — reads `retry_trace["failure_classification"]["failure_type"]`; runs cascade only when `failure_type ∈ {"donor_slack_insufficient", "no_donor_candidates"}`. Both routes via u3 `NEXT_ACTION_BY_FAILURE``cross_zone_redistribute`, so the cascade is exactly the surface that u3 routing points at. Any other failure_type (`rerender_still_fails``frame_reselect`, `not_attempted``none`, etc.) is left to the existing escalation surface (Step 18/19), so this wiring does NOT short-circuit non-cascade paths.
- **Target zone lookup** — `_tpos = retry_trace["plan"]["target_zone_position"]` (same target the failed retry tried); `_tdz` = matching `debug_zones` entry by `position`. `_excess = float(retry_trace["plan"]["target_excess_y"])` — reuses the planner's own excess measurement so cascade `excess_px` agrees with what the retry planner computed.
- **`cascade_inputs` dict literal** — exactly mirrors u8's 10-field contract (`fit_analysis / containers / min_margin_px / excess_px / block_count / zone_position / current_font_px / excess_after_glue_px / available_lines / chars_per_line`). Defensive defaults:
- `fit_analysis=None` / `containers={}` / `min_margin_px=None` — the pipeline does NOT yet construct a `FitAnalysis` object here, so cross_zone_redistribute will hit u8's `fit_analysis is None` guard and emit `feasible=False` with the contractual failure_reason. Cascade then routes onward to glue_compression. This is **the intended fallback** — see u8's diff_summary "graceful degrade when fit_analysis missing". Implementing pipeline-side FitAnalysis construction is out-of-scope for u9 (would expand line budget by 50+; would block on a separate axis: when/where to instantiate FitAnalysis from existing `fit_classification`+`debug_zones`+`containers`).
- `block_count = len(placement_trace.internal_regions) or 1` — drawn from the trace-only B1→B4 chain already populated on `debug_zones[i].placement_trace` (line 76 comment). Fallback `1` ensures `calculate_glue_absorption(1)` returns a finite envelope.
- `current_font_px / available_lines / chars_per_line` — read from `_tdz` (debug_zone metadata). Pipeline does NOT currently populate these (`space_allocator.find_fitting_font_size` does, but at a different point), so they default to `0` → font_step_compression's u6 guard `not available_lines or available_lines <= 0` fires → `feasible=False` with `"text_metrics missing"`. Cascade then routes to `layout_adjust` (terminal — currently MISSING per u7 `NEXT_ACTION_IMPLEMENTATION_STATUS`). This is the **honest current state**: glue is the only cascade stage that will realistically execute work until pipeline-side text_metrics propagation lands (separate axis, follow-up).
- **`_attempt_salvage_chain` call** — passes `initial_failure_type=_ft` so the chain's first `route_retry_failure` call routes from the SAME failure_type that triggered the dispatcher (avoids any double-routing). `gap_px=GRID_GAP` matches `_attempt_zone_ratio_retry`'s convention.
- **Trace merge** — `retry_trace.update(_salvage_trace)` adds `salvage_attempted / salvage_passed / salvage_steps` (+ optional `salvage_skipped_reason / salvage_terminal_action / salvage_terminal_rationale`) to the existing retry_trace. The Step 17 artifact thus carries BOTH retry and salvage traces in a single JSON, preserving (b)-revert telemetry for either failure mode.
2. **`salvage_passed=True` state refresh** (lines 4448-4456, mirrors retry_passed branch at 4406-4417):
- `overflow = _salvage_trace["salvage_steps"][-1].get("post_salvage_overflow") or overflow` — the u8 orchestrator records `post_salvage_overflow` on the WINNING step only (per `if passed: step["post_salvage_overflow"] = candidate_overflow`). Defensive `or overflow` preserves pre-salvage overflow if telemetry is absent (e.g., race).
- `fit_classification = classify_visual_runtime_check(overflow, debug_zones)` + `router_decision = route_fit_classification(fit_classification)` — re-classify and re-route on the post-salvage state, so downstream Step 18/19/20 see the cascade-PASS state.
- 3 `router_decision` enrichment fields (`v4_fallback_summary`, `v4_fallback_selections`, `frame_reselect_fallback_status`) copied verbatim from the retry_passed branch — keeps Step 20 status reporting consistent across both PASS paths.
- `debug_zones` / `layout_css` intentionally NOT refreshed — glue_compression and font_step_compression emit scoped CSS overrides under `[data-zone-position="<pos>"]` and do not alter row geometry. Refreshing layout_css.heights_px would falsely report a redistribution that didn't happen. cross_zone_redistribute WOULD geometrically refresh, but it requires `fit_analysis` (currently `None` → infeasible), so this branch is unreachable in practice. When a future axis populates `fit_analysis`, u9 will need a follow-up to add cross_zone-specific geometry refresh (tracked as `follow_up_issue_candidates` below).
3. **Step 17 artifact `step_status` + `note` updated** (lines 4459-4471, ±5 lines):
- `step_status`: `"done"` now fires on `retry_passed OR salvage_passed`; `"failed"` only when `retry_attempted AND NOT retry_passed` (semantically: "retry attempted and didn't pass even via salvage"); `"skipped"` otherwise. This honors the *retry+salvage as one logical step* model from Stage 2.
- `note` rewritten to enumerate u8/u9 cascade actions and call out which terminal actions remain MISSING (`layout_adjust / frame_reselect / details_popup_escalation`). Removes the now-stale "Step 17 ⚠ partial" claim.
4. **Imports**`apply_cross_zone_redistribute_css / apply_font_step_compression_css / apply_glue_compression_css / plan_cross_zone_redistribute / plan_font_step_compression / plan_glue_compression` from `phase_z2_retry`, `route_retry_failure` from `phase_z2_failure_router` — all added in u8 prior. **No new imports for u9.**
5. **No `_attempt_zone_ratio_retry` mutation** — pipeline retry call site (4389-4402) is unchanged; salvage runs AFTER retry trace is enriched, never as a replacement.
### evidence
- `python -m py_compile src/phase_z2_pipeline.py` → exit 0.
- `python -m pytest -q tests/phase_z2`**62 passed in 0.12s** (regression-free).
- `python -m pytest -q tests` (full suite, not just phase_z2) → **109 passed in 6.90s** (regression-free across the whole project).
- `git diff --numstat src/phase_z2_pipeline.py``138 4 src/phase_z2_pipeline.py`. Subtracting u8 baseline (+95/-1) leaves u9-only = `+43/-3`, net +40. Estimate was 30 — slight overshoot, structural (cascade_inputs has 10 contract fields + 4-field router_decision refresh mirroring retry_passed branch).
- Marker probe — confirmed 7 markers present: `11.7 IMP-12 u9`, `_attempt_salvage_chain(`, `donor_slack_insufficient`, `no_donor_candidates`, `salvage_passed`, `retry_trace.update(_salvage_trace)`, `IMP-12 u8/u9 salvage cascade`.
- Trace shape verified via reading u8's `_attempt_salvage_chain` source:
- On `dynamic_cols`/no `dynamic_rows`: `{salvage_attempted: False, salvage_passed: False, salvage_steps: [], salvage_skipped_reason: ...}` — u9 surfaces `salvage_skipped_reason` via `retry_trace.update`.
- On terminal action (`layout_adjust`/`frame_reselect`/`none`): `{salvage_attempted: True, salvage_passed: False, salvage_terminal_action, salvage_terminal_rationale}` — u9 surfaces both fields via `retry_trace.update`.
- On PASS at stage N: `{salvage_attempted: True, salvage_passed: True, salvage_steps: [...,{passed: True, post_salvage_overflow}]}` — u9 reads `salvage_steps[-1].post_salvage_overflow` for state refresh.
- On all-fail: `{salvage_attempted: True, salvage_passed: False, salvage_steps: [3 failures]}` — u9 leaves overflow / fit_classification / router_decision unchanged, preserving (b)-revert.
### remaining_units
- `u10` — Tests for multi-donor `plan_zone_ratio_retry` (u1 deferred)
- `u11` — Tests for `plan_cross_zone_redistribute` (u4 deferred)
- `u12` — Tests for `plan_glue_compression` (u5 deferred)
- `u13` — Tests for `plan_font_step_compression` (u6 deferred)
- `u14` — Tests for `failure_router` cascade routing (u2/u3 deferred)
- `u15` — End-to-end test of `_attempt_salvage_chain` orchestrator (u8 deferred)
### follow_up_issue_candidates
- **Pipeline-side FitAnalysis construction** — currently `cascade_inputs["fit_analysis"] = None` so `cross_zone_redistribute` cascade stage always returns infeasible. A follow-up axis should instantiate `FitAnalysis` from existing `fit_classification` + `containers` + `debug_zones` BEFORE the cascade, so cross_zone has real input. Until then, glue is the only stage that gets a real chance to fire from this pipeline. Scope-lock 외 axis — not part of IMP-12.
- **Pipeline-side text_metrics propagation onto `debug_zones`** — `current_font_px / available_lines / chars_per_line` are computed inside `space_allocator` but not surfaced onto `debug_zones[i]`. Without these, font_step_compression always returns `"text_metrics missing"` → cascade exits to terminal `layout_adjust` (MISSING). A follow-up should add a text_metrics surfacing step alongside placement_trace. Scope-lock 외 axis — not part of IMP-12.
- **Post-cross_zone geometry refresh** — when cross_zone_redistribute starts passing (after FitAnalysis is wired), `debug_zones`/`layout_css` would also need geometric refresh (mirrors the retry_passed branch's height_px/ratio refresh logic at 4406-4417). u9's state refresh deliberately omits this because cross_zone is currently unreachable. Track as part of the same FitAnalysis follow-up.
### guardrails_honored
- **`feedback_ai_isolation_contract`** — no AI/Kei call on the cascade path; all three salvage actions delegate to deterministic plan/apply functions (u4/u5/u6).
- **`feedback_phase_z_spacing_direction`** — glue_compression CSS is scoped to `[data-zone-position]` (u5); cascade does NOT shrink common margin / `:root` / `body` / `.slide` tokens.
- **`feedback_no_hardcoding`** — cascade triggers off existing failure_router classifier output (`donor_slack_insufficient` / `no_donor_candidates`); no hard-coded action selection.
- **`feedback_one_step_per_turn`** — this comment ends u9; remaining_units list explicitly lists u10u15 without claiming progress on any of them. Pipeline wiring (orchestrator) lands here; tests land in u10u15.
- **IMP-09 dynamic_cols / fr_default gate** — u8 enforces gate inside `_attempt_salvage_chain` (lines 1958-1960); u9 dispatcher does not bypass — gate-skip flows through `retry_trace.update` and surfaces `salvage_skipped_reason` on Step 17 artifact.
- **PZ-2 (1turn=1step)** — single unit edited; no scope creep into tests or pipeline-FitAnalysis construction.
### scope_audit
- Files modified: 1 (`src/phase_z2_pipeline.py`).
- Lines added (u9 only): 43; removed: 3.
- ACTION_BY_CATEGORY changes: **0**. NEXT_ACTION_BY_FAILURE changes: **0**. NEXT_ACTION_IMPLEMENTATION_STATUS changes: **0**. SALVAGE_FAILURE_TYPE_BY_ACTION changes: **0**. classifier body changes: **0**. AI/Kei call additions: **0**. Global CSS token changes: **0**.
- Test files added: 0 (deferred to u10u15).
- Pipeline call-site for `_attempt_zone_ratio_retry`: **unchanged** (4389-4402).
- Step 18/19/20 artifact writes: **unchanged** (4470-4500). They will naturally pick up the merged `failure_classification` / `next_action_proposal` from `retry_trace` regardless of whether salvage ran — the cascade-PASS path simply means `failure_classification` reflects the salvage cascade's terminal route (recorded on the cascade trace itself, not re-enriched on retry_trace). If a future axis wants Step 18/19 to also surface the cascade's terminal_action, that's a follow-up.
### evidence_commands
```
git status --short
git diff --numstat src/phase_z2_pipeline.py
python -m py_compile src/phase_z2_pipeline.py
python -m pytest -q tests/phase_z2
python -m pytest -q tests
python -c "import sys; sys.path.insert(0,'src'); from phase_z2_pipeline import _attempt_salvage_chain, _SALVAGE_FAIL_BY_ACTION; ..."
```