e9b3d2e9c0
feat(IMP-15): 실행-1 — Step 14 image_aspect_mismatch detection
...
Issue: #45 (IMP-15 실행-1, image axis only).
Adds Selenium-based <img> aspect ratio measurement to Step 14
run_overflow_check + numeric tolerance gate. Tolerance lives as
module-scope constant so tests can import it.
src/phase_z2_pipeline.py (+73/-2):
- L131-L135 IMAGE_ASPECT_DELTA_TOL = 0.05 (module scope, importable)
- L2216-L2261 JS payload extension: image_events[] per <img>
(src, zone_position via closest('.zone') with 'unknown' fallback,
zone_template_id, natural/rendered w+h+ratio, delta, slide-rel bbox)
- L2262 run_overflow_check return extended with image_events
- L2302-L2320 Python aggregation: abs(delta) > TOL ⇒ fail_reasons
append 'image aspect mismatch in zone--<pos>: natural=<n> rendered=<r>
delta=<+d> (template=<tid>, tol=0.05, src=<src>)'.
Null-delta entries (image not loaded) are skipped — no false positive.
Branch placed AFTER existing non-image branches; ordering & strings
for slide/slide-body/zone/clipped_inner unchanged.
- L4425-L4429 Step 14 note: image half closed, table half deferred
to 실행-2.
tests/phase_z2/test_phase_z2_step14_image_check.py (+196, new):
- 3-tier chromedriver resolver mirroring pipeline (PROJECT_ROOT/
chromedriver{,.exe} → PATH → Selenium Manager probe).
- pytestmark: skip when chromedriver unresolvable AND
PHASE_Z_REQUIRE_SELENIUM != '1'; xfail(strict=True) opt-in when =='1'.
- Fixture A: 200×100 img rendered 200×100 → aspect_delta < 0.05, passed.
- Fixture B: 200×100 intrinsic forced to 200×200 → delta > 0.30,
fail_reason present.
- Fixture C: <img> with no .zone ancestor → zone_position == 'unknown'.
Verification (Stage 4 PASS, Claude + Codex independent):
- pytest -q tests/phase_z2/test_phase_z2_step14_image_check.py → 3 passed
- PHASE_Z_REQUIRE_SELENIUM=1 same suite → 3 passed (strict opt-in)
- pytest -q tests/phase_z2 → 90 passed (no regression)
- pytest -q --ignore=tests/matching → 174 passed
Scope-locked: no slide_base.html / catalog / classifier / debug.json /
spec-doc changes. table_events (실행-2), visual_check_passed flip
(실행-3), debug.json image_events surfacing + PHASE-Z spec doc row
(실행-4) remain queued as separate IMP-15 child execution issues.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-18 18:01:28 +09:00
7a52cebfaa
feat(IMP-14): A-4 — slide_base embedded vs standalone mode contract
...
Step 13 owns iframe-vs-standalone CSS contract in slide_base.html via
3-valued embedded_mode enum (auto / embedded / standalone). Removes
SlideCanvas.tsx runtime CSS injection workaround; frontend now passes
?embedded=1 query so auto-mode script attaches html.embedded class and
scopes the standalone body centering/min-height/padding reset.
- templates/phase_z2/slide_base.html: conditional html.embedded class +
CSP-safe auto-mode <script> + additive html.embedded body/.slide rules
- src/phase_z2_pipeline.py: render_slide gains keyword-only embedded_mode
("auto" default) + ValueError guard; 3 existing call sites unchanged
- Front/client/src/components/SlideCanvas.tsx: derive embeddedSrc with
?embedded=1 (query-preserving), drop reset CSS injection block
- tests/phase_z2/test_slide_base_embedded_mode.py: 6 cases — auto script,
CSS rules, embedded/standalone explicit modes, byte-determinism,
invalid-mode guard
2026-05-18 07:21:31 +09:00
56619a0239
feat(IMP-12): Step 16/17 retry refinement — multi-donor + 3-stage salvage cascade
...
Extend Step 17 deterministic action surface so donor_slack_insufficient no longer
abort-terminates at zone_ratio_retry. AI is NOT invoked on the normal salvage path.
Source changes (4 files, scope-locked):
- src/phase_z2_retry.py — plan_zone_ratio_retry: single-primary-donor → multi-donor
greedy aggregation (donors_used / aggregate_slack_used / aggregate_slack_available);
new plan/apply pairs: cross_zone_redistribute (wraps fit_verifier.redistribute,
data-role scoped CSS), glue_compression (wraps space_allocator.compute_glue_css_overrides,
data-zone-position scoped), font_step_compression (wraps find_fitting_font_size,
zone-scoped, defensive feasible=False on missing text_metrics).
- src/phase_z2_failure_router.py — classifier inspects salvage_steps[-1] via
SALVAGE_FAILURE_TYPE_BY_ACTION; NEXT_ACTION_BY_FAILURE rewired into
donor_slack_insufficient/no_donor_candidates → cross_zone_redistribute → glue
→ font_step → layout_adjust; 3 IMPLEMENTED salvage status rows added.
- src/phase_z2_router.py — ACTION_IMPLEMENTATION_STATUS registers 3 new salvage
actions as IMPLEMENTED; ACTION_BY_CATEGORY untouched (cascade-only labels).
- src/phase_z2_pipeline.py — new _attempt_salvage_chain() iterates router
next_proposed_action with retry_budget=1 per action; honors IMP-09 dynamic_cols
/ fr_default gate; preserves (b)-revert on all-fail; wires Step 17 telemetry
(salvage_steps / salvage_passed).
Tests (6 new pytest modules):
- test_phase_z2_retry_multi_donor.py — single sufficient (regression), 1st
insufficient + 2nd sufficient (multi-donor PASS), aggregate insufficient FAIL.
- test_phase_z2_cross_zone_redistribute.py — multi-role zone feasible,
single-role zone short-circuits infeasible.
- test_phase_z2_glue_compression.py — feasible asserts emitted CSS contains
[data-zone-position=...] selector and NO global :root/body/.slide rule.
- test_phase_z2_font_step_compression.py — 15.2 → 13 closes excess; 8px floor;
missing text_metrics → defensive infeasible reason.
- test_phase_z2_failure_router_cascade.py — donor_slack_insufficient → cross_zone
(impl=IMPLEMENTED); 3 new failure types → expected next actions; rerender_still_fails
preserves frame_reselect terminus.
- test_phase_z2_step17_salvage_chain.py — end-to-end (a) cross_zone PASS promotes
final.html, (b) cross_zone FAIL + glue PASS promotes 2nd candidate, (c) all-3
FAIL preserves original final.html (revert).
Guardrails preserved:
- AI calls: 0 on normal path (feedback_ai_isolation_contract)
- Spacing direction: no shrink-common-margin; resolve via donor/glue/font-step
within frame envelope (feedback_phase_z_spacing_direction)
- All CSS overrides scoped to [data-role=...] or [data-zone-position=...]
- IMP-09 dynamic_cols / fr_default gate honored in cascade
- (b)-revert preserved if all 3 salvage actions fail
Refs: gitea#12 IMP-12
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-18 02:07:22 +09:00
1fb973297f
feat(IMP-09): PR 2 — 2-D dynamic dispatch for 5 preset families
...
Stage 3 lock implementation: extend build_layout_css dispatch beyond
the horizontal-2 / vertical-2 1-D dynamic paths. T / inverted-T /
side-T-left / side-T-right / 2x2 now flow through a 2-D track solver
instead of the fr_default sink, with length-locked heights_px (R) +
widths_px (C) on every return path (default and override).
PR 2 scope (u1~u5):
- u1: _aggregate_zone_signals_per_track — per-row + per-col virtual
zones via max(weight) + max(min_height_px) of single-span zones,
falling back to all-span when a track has none.
- u2: _build_grid_dynamic_2d default builder — feeds virtual zones
into compute_zone_layout + compute_zone_layout_cols; emits
computation="2d_dynamic_aggregated", dynamic_rows=True,
dynamic_cols=True.
- u3: _override_to_grid_tracks override builder — single-span
aggregation (max h per row, max w per col), normalize, multiply
by avail_h/avail_w, last-element diff absorb; emits
computation="user_override_geometry"; falls back to u2 when
total_h or total_w == 0.
- u4: build_layout_css dispatcher wiring — topology in
{T, inverted-T, side-T-left, side-T-right, 2x2} routes to
_build_grid_dynamic_2d (default) or _override_to_grid_tracks
(override); legacy [override-warning] stderr removed for the
5 presets; step08 trace gains a 2-D-aware print line that fires
before the dynamic_rows / dynamic_cols branches.
- u5: PR 1 lock test test_top_1_bottom_2_fr_default_populates_geometry
renamed to test_top_1_bottom_2_dynamic_2d_populates_geometry and
flipped to PR 2 reality (computation="2d_dynamic_aggregated",
dynamic_rows=True, dynamic_cols=True).
Fixtures: 10 build_layout_css (5 presets × {default, override}) +
5 retry_gate *_dynamic_2d.yaml locking the retry gate skip reason
"dynamic_cols (2-D topology) ... IMP-09 lock" for the 5 presets.
Tests: python -m pytest -q tests = 104 passed (Stage 2 baseline
10 RED → GREEN, 0 regressions). Kei archive
(build_containers_type_b / page_structure) untouched —
rg "build_containers_type_b|page_structure" src/phase_z2_pipeline.py
returns 0 hits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-17 18:51:23 +09:00
201099e53b
feat(IMP-09): PR 1 — col-axis solver + per-zone geometry mapper + retry gate
...
Stage 3 round 4 lock implementation: extend build_layout_css beyond
the horizontal-2-only dynamic path. Every layout_css return now
carries length-locked col-axis keys (widths_px, width_ratios,
dynamic_cols) matching the parsed css_areas grid (R rows, C cols),
so 2-D layouts (T / 2x2 in PR 2) and the unified
_compute_per_zone_geometry mapper can plug in without further
contract churn.
PR 1 scope:
- _parse_css_areas + _parse_fr_string + _compute_per_zone_geometry
(unified — 1-D and 2-D from the same code path)
- compute_zone_layout_cols (vertical-2 weight-only solver)
- _build_fr_default / _build_rows_dynamic / _build_cols_dynamic
(populate widths_px/heights_px on every return path)
- build_layout_css override branch keeps the warn-and-fallthrough
legacy for unsupported presets (PR 2 promotes to strict raise)
- retry gate in _attempt_zone_ratio_retry skips when dynamic_cols=True
or dynamic_rows=False, with explicit retry_skipped_reason
- Step 8 artifact gains zone_widths_px_planned /
zone_col_ratios_planned (top-level) + zone_width_px_planned /
zone_col_ratio_planned (per-zone)
- debug_zones width injection via _compute_per_zone_geometry
(replaces the legacy row-only zip)
Tests: tests/phase_z2/ — 47 new cases (parse / fr-string / cols solver /
per-zone geometry / build_layout_css contract / retry gate +
6 build_layout_css YAML fixtures + 3 retry_gate fixtures).
Verification: python -m pytest -q tests = 89 passed (was 42).
horizontal-2 grid CSS strings (areas/cols/rows) byte-identical to
legacy; only additive col-axis keys are introduced.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com >
2026-05-16 12:03:23 +09:00