Executing unit: u1 [Claude #3] Stage 3 code-edit Round #1 — unit u1 ### unit_executed - **id**: u1 - **summary**: Extend `plan_zone_ratio_retry` from single-primary-donor to multi-donor greedy aggregation; add `donors_used` + `aggregate_slack_used` to plan dict. ### files_changed - `src/phase_z2_retry.py` (33 insertions, 13 deletions; net +20 lines, within u1 estimate ≤35) ### diff_summary Replaced single-donor feasibility branch in `plan_zone_ratio_retry` with multi-donor greedy aggregation along slack-desc donor order (already sorted): 1. **Feasibility check** — compare `target_added_px` against `aggregate_slack_available = sum(d.slack for d in donor_candidates)` instead of `primary_donor.slack`. 2. **Greedy allocation** — when feasible, walk donor_candidates in slack-desc order, taking `min(donor.slack, remaining)` from each until `remaining == 0`. Each donor contributes only what is needed; later donors with sufficient remaining slack are untouched. 3. **New plan fields** — `donors_used: list[dict]` with `{position, reduced_px, slack_before, slack_after}` per donor; `aggregate_slack_used: int`; `aggregate_slack_available: int`. 4. **Backward-compat surface preserved** — `donor_zone_position` (primary donor), `donor_reduced_px` (primary donor's take, not total), `donor_max_slack` (primary donor's slack on failure path). Pipeline log line at `phase_z2_pipeline.py:1886` continues to render without change (u9 will update for multi-donor telemetry). 5. **Failure-reason classifier preservation** — failure_reason text retains `primary donor ... slack ... target_added_px` substrings (`feedback_phase_z2_failure_router.py:126-130` matches), so existing `donor_slack_insufficient` classification still fires on aggregate-insufficient case. u2/u3 will rewire routing. ### evidence - `python -c "from src.phase_z2_retry import plan_zone_ratio_retry; print('OK')"` → `OK` - `python -m pytest -q tests/phase_z2` → **62 passed in 0.13s** (regression-free; u10 will add the multi-donor unit tests) - `git diff --stat src/phase_z2_retry.py` → `1 file changed, 33 insertions(+), 13 deletions(-)` - Scope: 1 file modified (matches u1 `files: [src/phase_z2_retry.py]`); no other source file touched. ### scope_lock_compliance - ✅ deterministic (no AI call added) — `feedback_ai_isolation_contract` - ✅ no global spacing shrink (planner only manipulates donor zone heights within `min_height_px` floor) — `feedback_phase_z_spacing_direction` - ✅ no `ACTION_BY_CATEGORY` change — `Stage 1 scope_lock` - ✅ files ≤3 and estimate_lines ≤50 — actual: 1 file, +33/-13 - ✅ retry_budget=1 preserved (no orchestrator change in this unit) - ✅ failure_reason substring stability preserves u2/u3 classifier surface ### remaining_units - u2 — failure_router classifier extension (3 new salvage failure types) - u3 — `NEXT_ACTION_BY_FAILURE` rewire (donor_slack_insufficient → cross_zone_redistribute) - u4 — `plan_cross_zone_redistribute` + scoped role-height CSS - u5 — `plan_glue_compression` + frame-scoped CSS - u6 — `plan_font_step_compression` + zone-scoped CSS - u7 — `ACTION_IMPLEMENTATION_STATUS` registration for 3 new salvage actions - u8 — `_attempt_salvage_chain` orchestrator (Step 17 pipeline) - u9 — pipeline wiring + Step 17 telemetry (`salvage_steps`, `salvage_passed`) - u10 — multi-donor plan tests - u11 — `plan_cross_zone_redistribute` tests - u12 — `plan_glue_compression` tests - u13 — `plan_font_step_compression` tests - u14 — failure_router cascade tests - u15 — end-to-end `_attempt_salvage_chain` tests ### follow_up_issue_candidates None. All observed deviations are addressed by remaining units (u9 will update the pipeline log line; u2 will replace substring-matching classifier with explicit feasibility-branch signal). ### next u2 — extend `phase_z2_failure_router` classifier to surface 3 new salvage failure types (`cross_zone_redistribute_insufficient`, `glue_absorption_insufficient`, `font_step_insufficient`). rewind_target: continue_same FINAL_CONSENSUS: NO