Commit Graph

164 Commits

Author SHA1 Message Date
8c60f7cc85 docs(IMP-20): frame contract validation reference + cross-link -- documented-axis close 2026-05-20 00:02:18 +09:00
e60aacc3dc docs(IMP-19): zone ratio reference + cross-link -- documented-axis close
Stage 5 commit for IMP-19 (gitea #19) — docs-only, no runtime surface.

- new: docs/architecture/IMP-19-ZONE-RATIO-REFERENCE.md (header + A1 consumer
  + A2 producer + A3 Phase Z solver delta + A4 IMP-09 boundary + A5
  re-activation gate / GR1-GR4).
- update: PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md — IMP-19 row pending ->
  documented + reference doc link; IMP-09 row carries soft back-link to the
  IMP-19 reference doc.
- update: PHASE-Q-INSIGHT-TO-22STEP-MAP.md §3 I4 row — prepend IMP-19 anchor
  + reference doc link (step/classification preserved).

Guardrails (Stage 1/2 binding contract): src/ untouched, no role-based
["배경","본심"] hardcoding into Phase Z, IMP-09 solver ownership preserved,
soft-link integrity holds. IMP-19 remains dormant until the A5 gate fires.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 21:28:17 +09:00
02e2ae0afb docs(#54): F-4 legacy annotation + F-5 fixture convention -- AUDIT-01 housekeeping
INTEGRATION-AUDIT-01 (#50) §10.4 / §10.5 housekeeping carry-over.

F-4: annotate 14 remaining legacy Phase R'/Q sample-text hits across 10
src/ files with inline marker `# [legacy Phase R'/Q example -- INTEGRATION-AUDIT-01 §10.4]`.
Comment-only. No string-literal / regex / sample dict value mutated.
fit_verifier.py L612 marker keeps Phase Z partial-live import graph
(FitAnalysis / RoleFit / redistribute / salvage) byte-precise.

F-5: docs-only addendum -- §10.5.1 in INTEGRATION-AUDIT-01-REPORT.md +
tests/CLAUDE.md fixture convention note. No root tests/fixtures/ dir
created; existing tests/phase_z2/fixtures/ convention preserved. Documents
test-only sample-reference allowance vs src/** runtime prohibition.

Out of scope: Phase Z source 11 hits (phase_z2_content_extractor /
failure_router / mapper / retry), production behavior change, #19 work.

Verified: pytest -q tests/phase_z2/ = 157 PASS. git diff +210/-0
(35 src/docs lines + 175 new tests/CLAUDE.md). No behavioral delta.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 20:23:36 +09:00
8f06a4c99f docs(IMP-52): reconcile Phase Z family count drift -- F-2 option (c)
Audit follow-up F-2 (INTEGRATION-AUDIT-01 §10.2). Phase Z families surface
showed 11 tracked / 11 contracted / 13 on disk. The 2 untracked WIP files
(app_sw_package_vs_solution.html, pre_construction_model_info_stacked.html)
are now declared in _WIP_FILES.md as uncontracted and out-of-scope for the
runtime matcher; promote/remove is gated on #42. The 11/11 tracked +
contracted baseline is unchanged. A new pytest enforces tracked families ↔
frame_contracts.yaml set-equality modulo the WIP allowlist parsed from
_WIP_FILES.md, so future drift fails fast in CI before #42 expands to 32
frames.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 19:15:04 +09:00
191b6a9d85 docs(IMP-53): resolve audit charter F-1 -- C3 producer path
Annotate `INTEGRATION-AUDIT-01-REPORT.md` §5.1 row C3 with an inline
correction pointer noting that the historical charter cites
`src/phase_z2_mapper.py + consumers` but the live `fit_classification`
producer is `src/phase_z2_classifier.py`. Stamp §5.4 F-1 surface-note
and §10.1 F-1 heading as `RESOLVED via IMP-53 (2026-05-19)`.

Documentation-only change. Runtime / templates / catalog / MATRIX /
OVERVIEW untouched. Historical §5.1 C3 quote preserved verbatim per
anchor_sync_rules. pytest -q tests = 303 passed (baseline parity).

Refs IMP-53 (F-1) -- gitea issue #53
2026-05-19 16:42:12 +09:00
2bb0acac19 docs(IMP-51): reconcile Phase Z backlog status with audit-01 (F-3)
Per INTEGRATION-AUDIT-01 (#50) §6.2 / §9.3 cond.1 / §10.3:
- §1 IMP-02..IMP-11: pending -> implemented (10 rows, BACKLOG_STALE flip)
- §2 IMP-12..IMP-16: pending -> implemented (5 rows, BACKLOG_STALE flip)
- §2 IMP-17: pending -> documented (deferred) (carve-out boundary preserved)
- §2 IMP-18: documented unchanged (AGREE row)
- §2 footnote: IMP-15 child issues note (#45 e9b3d2e / #46 2827622 /
  #47 535c484 / #48 614c533 / #49 verification-only) — no standalone rows

Hard gate before #19 Stage 2 planning. Doc-only carve-out:
no src/templates/tests changes. Status strings match audit §6.2 verbatim.
Out-of-audit-scope rows (IMP-01, IMP-19/20, IMP-21..28) preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 16:08:39 +09:00
c37a554fb1 docs(IMP-50): backlog audit completion row for IMP-50
Append IMP-50 audit completion row referencing INTEGRATION-AUDIT-01-REPORT
(commit 8c7d693) with CONDITIONAL GO for #19 decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:59:39 +09:00
8c7d6935b1 docs(IMP-50): Phase Z integration audit-01 — report-only carve-out
22 closed improvement issues × 22-step Phase Z pipeline audit.
4-axis verification: scope myopia, pipeline step mapping, cross-issue
conflict, backlog ↔ code reality. Decision: CONDITIONAL GO for #19.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 14:59:18 +09:00
e32f632464 fix(orchestrator): P4a baseline-diff guard + Stage 5 commit scope
P4 had two production issues blocking #50 integration audit deployment:

1. Stage 3 guard had no baseline awareness — flagged ALL forbidden-path
   changes including pre-existing dirty WIP. Empirical: 328 such files
   already in current working tree (tests/matching/ artifacts etc).
   #50 would have hit reject loops immediately without Claude doing
   anything wrong.

2. Stage 5 had no commit-scope guard — if Claude ran `git add -A` and
   committed user's existing WIP, audit commit would be polluted with
   unrelated production changes.

P4a additions:
- _audit_baseline_path / _ensure_audit_baseline / _load_audit_baseline:
  snapshot working-tree dirty paths at run_issue entry for audit issues.
  Resumed runs preserve existing baseline (no overwrite).
- _check_audit_only_violations(baseline=None): accept baseline set,
  subtract from violations — only flags NEW forbidden changes introduced
  after audit start.
- _check_audit_commit_scope: verify HEAD commit's file list matches
  AUDIT_ALLOWED_COMMIT_GLOBS (INTEGRATION-AUDIT-*.md, BACKLOG.md).
- run_issue: save baseline on audit-mode entry only — no impact on
  normal issues.
- Stage 5 (commit-push) YES gate: new guard rejects on out-of-scope
  files with remediation prompt (git reset --soft + force-with-lease).

19 new tests:
- baseline subtraction (5): pre-existing removed, None=keep-all,
  empty-set=catch-all, full-coverage filter, Windows path normalize.
- baseline persist (5): roundtrip, no-overwrite on resume, missing
  fallback, corrupt JSON fallback, non-list fallback.
- commit scope detection (7): report-only allowed, backlog allowed,
  src/ rejected, unrelated docs rejected, git error fail-open,
  Windows backslash, empty commit pass.
- allowed globs sanity (2): every glob has audit marker, all under
  docs/architecture/.

Total: 94/94 pytest pass (75 prior + 19 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:29:15 +09:00
4289a500b6 feat(orchestrator): P3 wrapper input/encoding fix + P4 audit-only mode
P3 hotfix (2026-05-18 — verified during #46 retry attempt):
- _run_with_tree_kill: encode input only when Popen is in binary mode.
  Previously force-encoded str→bytes even with encoding= set, breaking
  text-mode stdin pipes with: write() argument must be str, not bytes.
- run_claude path was the only affected call site.
- 3 new C7 regression tests (input+encoding / bytes+binary / auto-encode).
- C3/C6 test fixtures hardened with DEVNULL stdio isolation.

P4 audit-only mode (2026-05-19, prep for #50 integration audit):
- _is_audit_issue: title-based detection for [INTEGRATION-AUDIT*],
  [AUDIT-ONLY], or "integration audit" phrase.
- _audit_mode + --audit-only CLI flag: manual override regardless of title.
- AUDIT_ONLY_NOTE injected into context pack across all stages/rounds.
- Stage 3 (code-edit) YES gate: deterministic git status check.
  Changes touching src/**, templates/**, tests/** auto-reject Stage 3 YES
  and post a supplement-request comment. LLM-independent enforcement.
- 26 new audit-mode tests (title detection, CLI override, forbidden
  prefix detection, allowed paths pass, Windows backslash normalization,
  quoted paths with spaces, git error fail-open, constants sanity).

Total: 75/75 pytest pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 10:18:28 +09:00
cbbc163860 docs(IMP-18): Phase Z SVG gap report — doc-only carve-out
u1: docs/architecture/IMP-18-SVG-GAP-REPORT.md (NEW, 64 lines)
  4 axes: Phase R' _preprocess_svg_data source refs (renderer.py:169-207,
  svg_calculator.py:15-156); Phase Z 15-partial SVG absence (grep
  <svg|viewBox = 0); IMP-04 activation gate; Phase R' read-only guardrail.

u2: docs/architecture/PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md L69
  IMP-18 row: status pending -> documented + gap-doc link appended.

Phase R' source (src/renderer.py, src/svg_calculator.py) and 15 Phase Z
partials remain unmodified. IMP-18 is dormant reference axis; activation
gated on IMP-04 registering an SVG-bearing partial.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 09:33:34 +09:00
e10ec36617 feat(IMP-17): AI repair fallback infra carve-out — design-only boundary + 3-cond AND gate
u1 — src/phase_z2_pipeline.py:564 route hint comment corrected from
non-existent IMP-31 to IMP-17 (carve-out, AI fallback only, normal path 밖).
Line 565 IMP-29 frontend override reference untouched.

u2 — docs/architecture/IMP-17-CARVE-OUT.md (new) defines:
- allowed scope (Step 12 restructure proposal, Step 16/17 retry fallback)
- forbidden scope (normal-path AI calls, MDX compression, HTML structure)
- 3-condition AND activation gate (User GO ∧ B4 frame_selection evidence
  ∧ IMP-04 catalog + IMP-05 V4 fallback live)
- pattern shape reference (link-only): content_editor.py:21,318 +
  sse_utils.py:16-50 (Phase Q Archive Candidate, no port)
- AI 격리 contract + Kei persona 단절 (permanent)

u3 — PHASE-Z-IMPLEMENTATION-ISSUE-BACKLOG.md:68 IMP-17 row gains
carve-out doc link + 3-cond AND gate pointer.

u4 — PHASE-Q-INSIGHT-TO-22STEP-MAP.md AI repair fallback infra registry
row prefixed with IMP-17 + carve-out link; normal_path=no preserved.

Anchor test: tests/orchestrator_unit/test_imp17_comment_anchor.py asserts
line 564 IMP-17 wording AND line 565 IMP-29 preservation (2 tests pass).

Runtime behavior change: 0. Only delta in executable file is one comment
line. Normal-path AI invocation count remains 0.

Refs: gitea #17

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:12:43 +09:00
23ba8b68cd feat(IMP-16): U1 H3 verification utility port + U2 wiring design
U1 (runtime, u1-u10): new Phase Z-owned deterministic verification module
src/phase_z2_verification_utils.py (335 LOC, stdlib only) porting H3 utility
surface — VerificationResult, extract_text_from_html, normalize_for_comparison,
extract_keywords, strip_meta_lines, split_into_sentences, verify_text_preservation,
detect_invented_text. 10 unit tests under tests/phase_z2/test_pz2_vu_*.py (56 tests).

u11 (design-only): docs/architecture/IMP-16-U2-WIRING-DESIGN.md fixes the Step
1/2/14/21/22 reverse-path contract, redesigned frame-contract pattern
reservation (IMP-20), and IMP-07 hard-gate criteria. No runtime wiring lands
in this commit — U2 stays blocked until IMP-07 reverse path is implemented +
verified + runtime-hit.

Guardrails: no src.content_verifier import; no FORBIDDEN_KEI_MEMOS /
generate_with_retry / REQUIRED_PATTERNS / verify_structure / verify_area /
verify_all_areas usage; no AI / Kei / httpx / SSE path; AI-isolation contract
upheld (utility is deterministic).

Tests: 56 targeted PASS (0.19s), 15 regression baseline PASS (7.59s).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 04:42:35 +09:00
614c53358e feat(IMP-15): 실행-4 — debug.json event surfacing + spec taxonomy row
Issue: #48 (IMP-15 실행-4, axis 4: debug.json + spec doc trace).
Parent: #15. Depends on 실행-1/2/3 (events + classifier outputs).

Surfaces the image/table event streams that 실행-1/2/3 already produced
and consumed, mirroring the existing `zone_geometries_px` top-level
precedent (no new pattern introduced). Adds the matching taxonomy row
to the Phase Z fit-classifier/router spec.

src/phase_z2_pipeline.py (+3):
- write_debug_json now lifts `image_events` and `table_events` to
  top-level of `debug.json` via `(visual_runtime_check or {}).get(<k>, [])`,
  exactly mirroring the immediately preceding `zone_geometries_px`
  surfacing line. Defaults to `[]` when `visual_runtime_check` is None
  — additive, no consumer-visible breakage.

docs/architecture/PHASE-Z-FIT-CLASSIFIER-ROUTER-SPEC.md (+1):
- §3.1 taxonomy adds `image_aspect_mismatch` row. Row text explicitly
  marks the signal as post-render `fail_reasons` from Step 14
  visual_runtime_check (rendered vs declared aspect ratio mismatch),
  NOT a router-routed fit_classifier output, and notes the separate
  `image_events` stream surface. Prevents future readers from wiring
  this taxonomy into §3.2 priority list or §4 router action map.

tests/phase_z2/test_debug_json_event_surfacing.py (new, 2 tests):
- `test_write_debug_json_surfaces_image_and_table_events` invokes
  write_debug_json with synthetic visual_runtime_check containing
  both event lists; reads back the on-disk debug.json and asserts
  both keys are present at top level with the exact payloads.
- `test_write_debug_json_defaults_when_visual_runtime_check_none`
  asserts both new keys default to `[]` when visual_runtime_check
  is None — guards the defensive `(… or {})` pattern.

tests/phase_z2/test_spec_taxonomy_image_aspect_mismatch.py (new, 2 tests):
- `test_spec_has_image_aspect_mismatch_row` opens the spec file and
  asserts exactly one `^\| image_aspect_mismatch \|` row exists
  inside the §3.1 table block (no markdown-parser dependency).
- `test_spec_row_marks_post_render_fail_reasons_semantic` asserts the
  row text carries both "Post-render" and "fail_reasons" tokens —
  enforces the Stage 1 guardrail wording.

Verification (Stage 4 PASS, Claude + Codex independent):
- pytest -q tests/phase_z2/test_debug_json_event_surfacing.py \
              tests/phase_z2/test_spec_taxonomy_image_aspect_mismatch.py
  → 4 passed in 0.07s.
- git diff scope: 4 files, +148 insertions / 0 deletions.

Scope-locked: no edits to classifier (실행-3), event generation
(실행-1/2), Step 21 viewer, §3.2 priority list, §4 router action
mapping, or `table_self_overflow` taxonomy row. Pre-existing
dirty/untracked working-tree files left untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 22:25:41 +09:00
535c4848fd feat(IMP-15): 실행-3 — classifier consumes image+table events
Issue #47 (IMP-15 실행-3 axis 3): extend `classify_visual_runtime_check`
to consume the `image_events[]` and `table_events[]` arrays produced by
`run_overflow_check` (실행-1/2) and widen `visual_check_passed`.

Changes (src/phase_z2_classifier.py):
- Remove `overflow.passed=True` early-return so image/table event scans
  always run, even when zone-level overflow was clean.
- Deferred import of `IMAGE_ASPECT_DELTA_TOL` and `TABLE_SCROLL_TOL_PX`
  from `phase_z2_pipeline` (circular-safe SSoT; no duplicate literals).
- New `image_events` scan emits `image_aspect_mismatch` when
  `delta is not None AND |delta| > IMAGE_ASPECT_DELTA_TOL`
  (delta=None ⇒ skip, image not loaded).
- New `table_events` scan emits `tabular_overflow` when
  `wrapper_clipped_index is None AND (excess_x or excess_y > TABLE_SCROLL_TOL_PX)`
  (wrapper-clipped tables deduped against the existing zone cascade).
- `visual_check_passed = overflow.passed AND not classifications` —
  any image/table classification now flips the gate.

Guardrails preserved:
- §3.2 8-rule zone cascade (clipped_inner / zone-self) untouched —
  the new emitters are ADDITIONAL.
- `placement_diagnostics`, `categories_seen`, `unclassified_signals`
  return-shape preserved.
- No `pipeline.py` production changes; no router action or
  `debug.json` passthrough changes.

Tests (tests/phase_z2/test_phase_z2_visual_classifier.py — new):
- `test_image_aspect_mismatch_emits_classification` (|delta|>TOL fires)
- `test_image_aspect_delta_below_tol_no_classification` (≤TOL skipped)
- `test_standalone_table_overflow_emits_classification`
  (wrapper_clipped_index=None, excess>TOL fires)
- `test_table_dedup_when_wrapper_clipped`
  (wrapper_clipped_index set ⇒ no `tabular_overflow` emit)

All 4 pure-dict (no Selenium / chromedriver / pipeline execution).
Tolerances imported from `phase_z2_pipeline` (SSoT enforced via test
import — no classifier-local literals).

Verification (Stage 4):
- New classifier tests: 4/4 PASS.
- Regression `tests/phase_z2/` excluding new file: 93/93 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:45:06 +09:00
2827622858 feat(IMP-16): Step 14 table_self_overflow detection
Add table self-overflow detection with element-identity wrapper dedup,
mirroring the image_aspect_mismatch axis pattern (#45).

JS layer: TABLE_SCROLL_TOL_PX=5 module constant; clippedWrapperMap
built as Map<Element,int> keyed by DOM node reference (NOT className)
so two wrappers with identical class strings remain distinguishable;
table_events collected via querySelectorAll('table').forEach with
closest()-ancestor walk resolving wrapper_clipped_index = int|null.

Py layer: aggregate result['table_events'] and append fail_reason
'table_self_overflow' only when (excess_x>TOL OR excess_y>TOL)
AND wrapper_clipped_index is None; wrapper-clipped path continues
to fail via existing clipped_inner reporting.

Tests (Selenium, chromedriver guard mirrored from image_check):
- Fixture D: standalone <table> overflow → table_self_overflow fail
- Fixture E: <table> in clipped wrapper → dedup suppresses table fail
- Fixture F (F1 acceptance): two wrappers with identical className
  f13b-cell, W1 clipped by non-table child, W2 hosts self-overflow
  <table> with W2 itself NOT clipped → element-identity ensures W2's
  table is not suppressed by W1's class; both fails emitted.

Out of scope: image_events behavior (intact from #45), classifier
pass/fail consumer (→실행-3), debug.json surfacing (→실행-4).

Refs: #46

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 21:06:01 +09:00
f3bff898fb feat(orchestrator): initial orchestrator + subprocess cleanup hardening
Pre-existing P0+P1 fixes (verified via #45 pilot 2026-05-18):
- P0-1: detect_agent first-line only (fixes #45 infinite loop)
- P0-2: stage_start_count sanity reset on external comment delete
- P0-3: 32 pytest cases for parse/detect regressions
- P1-4: execution-issue mode prompt (compact scope-tight)
- P1-5: Stage 2 COMPACT_PLAN_RULE (size budget, no code snippets)
- P1-6: tests:[] orchestrator-level enforcement at Stage 2 YES guard
- P1-7: dual-write CRLF/trailing-whitespace normalize

P3 subprocess cleanup (PID 2780 orphan grandchild regression):
- (pid, create_time) signature tracking — Windows PID reuse safe
- _kill_process_tree: parent-alive traversal path
- _kill_tracked: parent-dead orphan path
- _run_with_tree_kill: 1s monitor thread captures descendants live
- atexit + SIGINT safety net via _SPAWNED set
- 4 subprocess.run sites switched to wrapper (compaction/exit_report/
  run_claude/run_codex)
- 12 cleanup pytest cases incl. C6 PID 2780 regression test

Selenium boundary unchanged — driver.quit() in phase_z2_pipeline.py
and slide_measurer.py already protected by try/finally.

Total: 44/44 pytest pass (32 core + 12 cleanup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 18:56:06 +09:00
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
7d5639ad72 feat(IMP-13): A-3 — build-time frame preview generator (capture_slide_screenshot salvage)
scripts/generate_frame_previews.py iterates figma_to_html_agent/blocks/{frame_id}/index.html,
renders preview.png via Selenium headless (capture_slide_screenshot pattern reuse), and writes
_preview_manifest.json (schema v1) with idempotent stale-detect (mtime+sha256). Build-time only
— no runtime pipeline integration, no AI calls, no MDX/Jinja regen. Stage 2 baseline (commit
56619a0): total=33, renderable=20, missing_index_html=13, orphan=1 (1171281192).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 06:25:05 +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
a79bd8bc43 feat(IMP-11): D-2 — frame min_height_px hint (backend → UI)
Step 9 v4_all_judgments[] now exposes per-candidate min_height_px from
catalog frame_contracts.visual_hints.min_height_px (None when contract
unregistered). SlideCanvas pendingLayout zones render a red ring + 'min H
Npx' badge when zone height falls below the active frame's threshold.
Visual hint only; resize clamp (minSize=0.05) unchanged.

5 axes (single commit per Stage 5 plan):
- u1 backend: src/phase_z2_pipeline.py — Step 9 builder adds min_height_px
  via single get_contract(c.template_id) lookup; reuses _contract for
  catalog_registered (no double-lookup).
- u2 type: Front/client/src/types/designAgent.ts — FrameCandidate gains
  optional minHeightPx?: number.
- u3 mapper: Front/client/src/services/designAgentApi.ts — maps snake-case
  min_height_px → camelCase minHeightPx on v4_all_judgments path;
  v4_candidates fallback remains undefined (graceful).
- u4 active-frame lookup: Front/client/src/components/SlideCanvas.tsx —
  activeFrameId = overrideFrameId ?? defaultFrameId; activeCandidate via
  region.frame_candidates.find.
- u5 hint render: Front/client/src/components/SlideCanvas.tsx —
  zoneHeightPx = height * SLIDE_H (logical px, no double-apply); compare
  against activeCandidate.minHeightPx in pendingLayout mode only; red
  border + badge when below.

Tests: 5/5 pass in tests/test_phase_z2_step9_v4_all_judgments_min_height.py
(source-string + catalog-shape guards + None propagation, registered and
unregistered template_ids).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 22:29:17 +09:00
0fb168befc feat(IMP-10): D-1 — filtered_section_reasons UI (read-only)
Surface step20_slide_status.json.data.filtered_section_reasons in the
frontend Home header. Verbatim mirror of backend payload — no enum
redefinition, no translation, no auto-classification.

Units:
- u1: FilteredSectionReason interface mirroring src/phase_z2_pipeline.py
  :2217-2278 (10 fields incl. override-uncovered source/position variant).
- u2: RunMeta extension + loadRun() mapping with ?? [] back-compat defaults.
- u3: Header badge + <details> disclosure adjacent to existing status
  badge; hidden when filtered_section_ids.length === 0; renders all 10
  schema fields + filter_reasons[] verbatim.

Scope:
- Frontend-only, read-only. No backend / sync script / Kei·AI panel
  changes. Files: Front/client/src/services/designAgentApi.ts (+20),
  Front/client/src/pages/Home.tsx (+25).

Refs: gitea issue #10 (IMP-10 D-1 filtered_section_reasons UI)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:13 +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
8f6cffc2a7 fix(IMP-08): Stage 5 R2 — aligner force-drill on sub-id override targets
Codex #1 (Stage 5) reproduced a smoke regression on the actual checkout :
when V4 carries the parent exact key (e.g., `04-2`) AND the drag/drop
override targets a sub-id (`primary=04-2-sub-1`), the aligner kept the
parent at parent granularity and emit `['04-1', '04-2']`, so the override
flag failed with `unknown section_id(s) ['04-2-sub-1']`.

Fix : `align_sections_to_v4_granularity` gains an optional
`override_target_section_ids` keyword. From each canonical
`${parent}-sub-N` target it derives the parent id and adds it to a
`force_drill_parents` set. Sections in that set are drilled into
sub-sections regardless of whether V4 carries the parent exact key.
Top-level override targets (no derived parent) do not trigger
force-drill, so backward-compat is preserved for parent-granularity
overrides.

The call site in `run_phase_z2_mvp1` collects sub-ids from
`override_section_assignments` and forwards them to the aligner.

Generalization (RULE 0) :
- Trigger is the override schema (`X-sub-N`), not a specific MDX / section /
  frame id. Applies to all 32-frame MDX uniformly.
- Decision is deterministic on the override target shape, independent of
  V4 yaml content.
- Default (no override) path is unchanged byte-for-byte.

Side fixes (forward-only RULE 1 cleanup, no history rewrite) :
- `align_sections_to_v4_granularity` docstring rewritten in English
  (overwrites the Korean docstring committed in 5191aca).
- Step 9 diagnostic comment quoted-string rewritten in English
  (overwrites `"V4 entry 없음"` committed in a422d72).

Tests : 3 new cases in `test_phase_z2_subsection_schema.py` —
`test_align_parent_v4_exact_keeps_section_when_no_override_targets_sub`
(backward-compat axis), `test_align_force_drills_when_override_targets_sub_id_with_parent_in_v4`
(blocker regression), `test_align_top_level_override_target_does_not_force_drill_other_sections`
(force-drill scope guard). Pytest scope-qualified result :
`test_phase_z2_subsection_schema.py` + `_section_assignment_override.py` +
`_v4_fallback.py` = 40 / 40 PASS.

Smoke (axis = sub-id override -> aligner -> assignment plan, both V4 yaml
shapes) :
- HEAD V4 yaml (`04-1`, `04-2.1`, `04-2.2` only) :
  `--override-section-assignment primary=04-2-sub-1` ->
  `aligned_section_ids=['04-1', '04-2-sub-1', '04-2-sub-2']`,
  `plan[0].assignment_source='cli_override'`,
  `plan[0].source_section_ids=['04-2-sub-1']`.
- V4 yaml with `04-2` exact key (Codex's stress case) : identical
  aligned output and identical assignment plan.

Downstream `composition_planner` abort
(`phase_z_status_not_allowed:extract_matched_zone`) is IMP-05 territory,
unchanged in both shapes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 02:28:46 +09:00
ab2764c8d0 feat(IMP-08): U3 — frontend wire (zoneSections override)
Wires the frontend drag/drop zone assignment through to the backend
--override-section-assignment CLI flag.

PipelineOverrides gains an optional zoneSections field
(Record<string, string[]>) carrying canonical ordinal section ids
(e.g., "top": ["04-2-sub-1"]).

Vite middleware /api/run accepts overrides.zoneSections and forwards
each non-empty zone as `--override-section-assignment ZONE=sid[,sid]`.
Empty arrays and non-string sids are filtered to avoid bogus
assignments from a partially-built UI state.

Home.tsx builds the override with a diff-vs-default guard per Codex
Stage 3 R3 B3 fix : createInitialUserSelection seeds zone_sections with
the auto plan, so a literal copy would pollute backend assignment-source
provenance even on a fresh re-render. The diff compares each zone's
section list against sourcePlan.zones[].section_ids and only emits zones
that differ. Toast summary now reports zoneSections=N when forwarded.

Smoke verification : python -m src.phase_z2_pipeline samples/mdx_batch/04.mdx
test_imp08_smoke --override-section-assignment primary=04-2-sub-1 produces
section_assignment_plan with assignment_source=cli_override and
v4_selector_trace.candidates populated via the U1 alias resolver
(04-2-sub-1 -> 04-2.1 V4 entry).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:36:16 +09:00
5191acad85 feat(IMP-08): U2 — aligner canonical sub-id + N-R5 decimal alias guard
align_sections_to_v4_granularity now emits canonical sub-section ids
of the form ${section_id}-sub-${ordinal} (e.g., "04-2-sub-1"), matching
the frontend drag/drop schema. Each drilled sub-section populates
heading_number (decimal "2.1" / integer "1" / None for undecorated)
and v4_alias_keys for legacy V4 keys.

N-R5 decimal-only alias guard : v4_alias_keys is populated only when
heading_number matches re.fullmatch(r"\d+\.\d+", ...). Integer-only
H3 headings (e.g., MDX 05's "### 1", "### 2") and bare H3 headings
produce no alias to avoid sibling-parent V4 collisions (RULE 0
generalization — applies to all 32-frame MDX, not MDX 05-specific).

The drill regex is broadened from r"^###\s+(\d+\.\d+)\s+..." to
r"^###\s+(?:(\d+(?:\.\d+)?)\s+)?(.+?)$" so integer-only and bare H3
headings are now recognised as sub-sections; they previously failed
the regex and were silently kept under the parent section.

Tests : 7 new cases (MdxSection default 4-positional callers, V4 exact
passthrough, decimal drill with alias, integer-only no-alias guard,
bare H3 no-alias, no-H3 passthrough, end-to-end aligner -> resolver
round-trip with legacy V4 alias). 15/15 in test_phase_z2_subsection_schema
+ 14 override + 8 fallback baseline = 37/37 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:33:49 +09:00
a422d72c0b feat(IMP-08): U1 — schema helper + V4 alias resolver (4 lookup sites)
Adds sub-section schema fields (heading_number / v4_alias_keys /
sub_sections) to MdxSection with defaults so existing 4-positional
constructions remain valid. Introduces _resolve_v4_section_key helper
that resolves a V4 mdx_sections key in exact > alias > None order with
no parent/sibling promotion (axis 7 hybrid lock).

Rewires four runtime V4 lookup sites (lookup_v4_match,
lookup_v4_match_with_fallback, lookup_v4_all_judgments,
lookup_v4_candidates) to accept an optional alias_keys kwarg and go
through the resolver. U1 callers pass empty alias lists so behaviour
is byte-identical to the previous exact-match path; U2 will populate
aliases from MDX heading_number metadata.

Closure callers in run_phase_z2 build section_alias_by_id from
MdxSection.v4_alias_keys and forward into lookup_fn /
candidates_lookup_fn / lookup_v4_all_judgments (Step 7-A trace) and
into _select_template_for_overrides single-section selector.

Step 9 candidate report (post-decision diagnostic) is marked with an
inline English exemption comment per N-R6 — runtime selection goes
through _resolve_v4_section_key, the report path stays a direct
dict-shape lookup to avoid debug_zones schema plumbing.

derive_parent_id now recognises canonical ordinal ids
("03-1-sub-2" -> "03-1") first and keeps the legacy decimal fallback
("04-2.1" -> "04-2") for V4 alias compatibility.

Tests : 8 synthetic cases in tests/test_phase_z2_subsection_schema.py
covering derive_parent_id ordinal/decimal/none and the resolver
exact/alias/no-promote/miss cases. 30/30 PASS combined with the 14
override + 8 fallback baseline.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:28:59 +09:00
0f0d3fa91f feat(frontend): add Front/ — Vite/React frontend with backend pipeline integration
Mirror of design_agent_front/design-agent/ for shipping alongside backend.

Vite plugin (vitePluginPhaseZApi) endpoints :
  - POST /api/run   — spawn `python -m src.phase_z2_pipeline` with overrides
  - GET  /api/sample-mdx?mdx=03/04/05 — fixed sample MDX
  - GET  /frame-preview/{n} — figma preview thumbnails
  - GET  /data/runs/{run_id}/{path} — pipeline artifacts (final.html, step*.json, ...)

Env toggle forward (보고용) :
  PHASE_Z_ALLOW_RESTRUCTURE / PHASE_Z_ALLOW_REJECT / PHASE_Z_MAX_RANK=32

Components :
  - LeftMdxPanel (03/04/05 fix list + section tree)
  - SlideCanvas (iframe + slideOverrideCss prop for inline CSS inject)
  - FramePanel (label priority + confidence sort)
  - LayoutPanel

README with mermaid diagrams covering the 5-step demo flow.
node_modules / dist / .manus-logs / .env excluded via .gitignore.
2026-05-14 14:48:42 +09:00
52ccb7fc8b fix(IMP-06): Stage 4 blocker-fix — render_records + plan-aware traces
Three Codex #13 blockers in a single coherent commit.

Blocker 1 (units None hazard) — drop None placeholders from `units` list.
Replace with a separate `render_records` layer built AFTER frame_overrides
apply. units = canonical renderable list (list[CompositionUnit] only);
render_records = canonical per-position view including empty / collision-
skipped / cli_override entries. Downstream loops (Step 6 print, frame_
overrides, zones_data/debug_zones, Step 9 application_plan, compute_slide_
status covered loop) no longer need None guards.

Blocker 2 (no integration test) — add end-to-end pipeline integration
test: `--override-section-assignment top=03-2` on sample 03 MDX produces
zones_data[top].source_section_ids = ['03-2'], debug_zones[top].assignment
_source = 'cli_override', debug_zones[bottom].v4_template_id = '__empty__'
(override_collision whole-skip), step20 filtered_section_ids contains
'03-1', and filtered_section_reasons carries a section_assignment_override
_uncovered entry. Proves the render path — not only comp_debug — reflects
the CLI override.

Blocker 3 (Step 9/20 not plan-aware) — surface plan-aware additive fields
in both render-path debug_zones/zones_data and Step 9 application_plan
units: position, assignment_source, section_assignment_override,
replaced_auto_unit, skipped_collided_auto_units, uncovered_section_ids,
skipped_reason. compute_slide_status appends Codex #10 Catch O list-shaped
filtered_section_reasons entries for override-uncovered sections and
folds them into filtered_section_ids so full_coverage is re-evaluated
post-override.

Exact-id-only collision semantics enforced (Codex #14/#15/#16/#17): S3
and S3-1 are distinct ids; no prefix hierarchy, no parent cascade. Three
new section-id invariant tests added (parent-like vs child-like, exact
duplicate collision detected, distinct ids coexist).

Test : 24 pytest pass (9 helper + 9 case + 3 invariant + 1 case 9b +
1 integration + 1 from v4_fallback baseline) ; smoke 11/11 PASS.

Register `integration` pytest marker in pyproject.toml.
2026-05-14 07:41:12 +09:00
1f15495117 feat(IMP-06): Stage 4 Part 2 — render-path integration (units rebuild + empty zone + Catch K fix)
Refs #6

After `position_assignment_plan` is built, rebuild the `units` list to be
plan-aligned so downstream `zones_data` / `debug_zones` / mapper / render
all see the post-override sequence. This resolves the long-standing
trace-only gap and closes Codex Catch K naturally because the helper now
actually drives downstream materialization.

- run_phase_z2_mvp1: after `_build_position_assignment_plan`, rebuild
  `units` ordered by `position_assignment_plan`. `cli_override` entries
  synthesize a CompositionUnit (resolved template_id + concatenated
  section raw_content + contract frame_id + selection_path="cli_override"
  + override audit in rationale). `auto` entries reuse the original
  planner unit. empty/collision-skipped entries become None placeholders
  so the downstream zone loop can emit an explicit empty zone record
  without distorting layout allocation.
- zones_data / debug_zones loop: handle `unit is None` by appending an
  explicit empty record with template_id="__empty__", content_weight=0,
  min_height_px=0, plus the plan's skipped_reason / replaced_auto_unit /
  skipped_collided_auto_units / uncovered_section_ids audit fields.
- partial render loop: `template_id == "__empty__"` short-circuits to
  `partial_html = ""` so the slide_base zones loop preserves grid identity
  without raising TemplateNotFound.
- Update the helper-invocation comment so it now describes the actual
  Part 2 behavior (units rebuild + empty zone handling). Catch K is no
  longer a future-tense placeholder.

Stage 4 Part 3 (follow-up commit) will add: Step 9 application_plan
plan-aware additive fields, Step 20 list-shaped filtered_section_reasons
entries for override-uncovered sections (Codex #10 Catch O schema), and
integration tests proving zones_data["top"] actually contains the
overridden section ids when --override-section-assignment is supplied.

Regression: 20/20 unit tests pass (9 IMP-06 helper + 8 IMP-05 fallback +
2 catalog invariant + 1 dedicated replaced_auto_unit test), smoke
self-check 11/11 (IMP-04 F17 calibration intact).

No AI, no calculate_fit, no full planner rerun, no frontend, no sample
hardcoding. plan_composition() signature preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:50:35 +09:00
b81e564f65 feat(IMP-06): Stage 4 part 1 — replaced_auto_unit field + comment fix
Refs #6

Stage 4 split per Codex #10 acceptance: this commit lands the schema +
trace refinements required before the render-path rewiring. The actual
units/zones_data/Step 9/Step 20 plan-driven materialization remains in
Stage 4 part 2 (follow-up commit) so each commit is reviewable on its
own and regression-safe.

- _build_position_assignment_plan: add replaced_auto_unit field. Populated
  only when the explicitly overridden position already held an auto unit
  AND that auto unit had different source_section_ids than the override.
  Documents a same-position override replacement as a distinct audit fact,
  separate from skipped_collided_auto_units which captures cross-position
  whole-skips per the locked collision policy.
- Backfill replaced_auto_unit = None on the empty/collision/auto branches
  for schema-stable consumers.
- Update the override-application comment near the helper invocation so it
  no longer claims the helper "reorders units"; Stage 4 part 2 will be the
  commit that wires the plan into the actual render path.
- Helper unit tests: assert replaced_auto_unit shape in the collision
  scenario and add a dedicated case that distinguishes same-sections
  (template swap via --override-frame -> None) from different-sections
  same-position replacement (populated, reason="same_position_override_replacement").

No AI, no calculate_fit, no full planner rerun, no frontend, no sample
hardcoding. plan_composition() signature preserved. helper remains pure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:10:43 +09:00
d596fabde0 feat(IMP-06): zone-section assignment override CLI + plan helper (trace-only)
Refs #6

Backend / CLI / composition path only — frontend bridge remains #38.

- Add `--override-section-assignment ZONE_ID=section_id[,section_id]` to the
  Phase Z entry parser. Parse-time hard errors for malformed payloads, empty
  zone id, empty section list, duplicate zone id, and duplicate section across
  zones (a section may belong to at most one zone).
- Add `_build_position_assignment_plan` helper (pure function, resolved
  `positions` injected). Builds a per-position assignment plan with the
  Codex-locked template_id ladder: (1) `--override-frame` exact unit_id wins,
  (2) exact existing auto unit reuse, (3) single-section direct-executable V4
  selector via `lookup_v4_match_with_fallback(..., raw_content=section.raw_content)`,
  (4) ad-hoc multi-section override without exact auto + without explicit
  override-frame yields `skipped_reason='ad_hoc_merged_no_template'`.
- Lock the collision policy: explicit override wins per position, sections
  appear in at most one position, overlapping auto units are skipped whole
  (no split, no cascade, no replan), uncovered sections from the previous
  same-position auto unit are recorded in `uncovered_section_ids`.
- Additive trace fields on each plan entry: `previous_source_section_ids`,
  `skipped_collided_auto_units`, `uncovered_section_ids`, `v4_selector_trace`,
  `section_assignment_override`. Top-level `comp_debug["section_assignment_plan"]`
  + `comp_debug["section_assignment_summary"]` so Step 9 / debug artifacts can
  derive from a single source of truth.
- Wire `run_phase_z2_mvp1(override_section_assignments=...)` after final layout
  preset resolution: validate ZONE_IDs against active layout positions and
  validate section_ids against aligned sections (fail-fast). The plan is
  attached to `comp_debug` for downstream artifacts. Actual `zones_data` /
  unit-list rewiring is deferred to a follow-up commit so this change stays
  regression-safe; trace artifacts already surface override intent and
  collision impact.
- Add 9 helper unit tests with fully synthetic MOCK_ ids (no real catalog
  / no v4_full32_result.yaml): non-conflicting auto retention, collision
  whole-skip + uncovered tracing, template ladder steps 1/2/4, unit_id
  naming convention, previous_source_section_ids position history,
  empty-position case, summary aggregation invariants.

No AI, no `calculate_fit`, no full planner rerun, no frontend, no sample
hardcoding, no `restructure`/`reject` silent promotion. `plan_composition()`
signature is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 01:51:20 +09:00
23d1b25144 test(IMP-05): tighten Step 9 candidate evidence guard
Refs #5

Replace the hand-built Case 7 payload assertion with a temporary
production-source guard. The test now fails if Step 9 stops emitting
candidate_evidence, breaks the fallback_chain compat alias, or removes
the alias intent comment.

This is intentionally temporary because Step 9 application-plan unit
assembly is inline. Follow-up IMP-32 should extract a helper and replace
this source-string guard with a direct helper test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 00:24:42 +09:00
21476ae000 fix(IMP-05): complete V4 fallback evidence and dedup qualifiers
Refs #5

- Add runtime template_id dedup in lookup_v4_match_with_fallback with
  first-occurrence reservation; duplicate ranks become audit evidence,
  not new fallback candidates.
- Add Step 9 candidate_evidence as the primary per-unit evidence field
  while keeping fallback_chain as a compat alias for legacy readers.
- Add Step 20 fallback_selection_count and selection_paths derived from
  comp_debug.v4_fallback_summary with defensive defaults; top-level
  overall enum unchanged.
- Tighten synthetic fallback tests for duplicate handling (rank-1 reject A
  + rank-2 use_as_is A + rank-3 distinct B → rank-3 wins) and add tests
  for candidate_evidence + alias equality and Step 20 qualifier presence
  with defensive defaults.
- Verify with pytest (10 passed) and smoke_frame_render --self-check
  (11/11 partials, IMP-04 F17 calibration intact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:59:49 +09:00
15c5b9ae00 IMP-05 deterministic V4 candidate bridge — pre-render rank-2/3 fallback + trace schema + dedup invariant test
round 55~73 review-loop lock per Codex #11 final + Claude #13 6-axis L1~L9.

Scope (deterministic only) :
- pre-render rank-2/3 fallback via lookup_v4_match_with_fallback (selector only,
  no calculate_fit migration, no AI, no full planner rerun, no layout topology change,
  no abort behavior change)
- Step 9 informative candidate_evidence schema (additive) — v4_label / phase_z_status
  / catalog_registered / filtered_for_direct_execution / route_hint / decision / reason
- Step 20 qualifier fields (additive) — fallback_used / fallback_selection_count
  / selection_paths[] — top-level enum unchanged
- restructure / reject candidates preserved as non-direct evidence with route hints
  (design_reference_only / ai_adaptation_required) — deferred actual handlers IMP-29/IMP-31
- catalog 1:1 invariant test (separate file tests/test_catalog_invariant.py) —
  fails fast if template_id/frame_id 1:1 mapping ever breaks
- 6 behavior tests fully synthetic with MOCK_ prefix (no real catalog IDs,
  no v4_full32_result.yaml dependency) — monkeypatch get_contract +
  compute_capacity_fit (selector has no DI, function signature unchanged)

Deferred to follow-up issues :
- IMP-30 first-render invariant + abort bypass (zero-unit + section status filter)
- IMP-29 frontend zone-level override (deterministic only)
- IMP-31 AI-assisted frame-aware adaptation

Guardrails locked : no calculate_fit / no AI / no frontend / no full rerun /
no layout topology / no abort behavior change / no 1-2 sample hardcoding.

Tests : 8/8 pass (6 selector behavior + 2 catalog invariant).
Smoke regression : 11/11 partials pass (IMP-04 F17 calibration intact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 23:06:39 +09:00
73a98b8ad1 IMP-04 F17 schema correction — paired_rows_4x2 + pill alternation + source-faithful theme
source = 8 atomic issues (4 paired rows × 2 cells per texts.md), 이전 strict-4
가 source 의 절반 누락. round 55~73 review-loop 의 calibration frame.

- contract : source_shape=top_bullets / layout_variant=paired_rows_4x2_alternating_pills
  / strict 8 (no pad/truncate) / role_order row_{1..4}_{left,right} / visual_hints
  pill_positions + row_gap_after / builder paired_rows_4x2_slots
- builder : new _build_paired_rows_4x2_slots — 2-axis (row × side) deterministic
  index mapping, strict 8 raises before render, quadrant_item parser 재사용
- partial : 4-row × 2-cell flex, pill alternation (row 1/3 top, row 2/4 bottom
  via column-reverse), row 2-3 visual gap, source-faithful color (rgb(204,82,0)
  →rgb(136,55,0) title + #60A451 row border + rgba(250,237,203,0.15) bg + #0c271e
  body + 2px dashed #60A451 cell 분할선), pill = CSS approximation (asset crop
  variant single-pass 비용 高 → fallback per Codex round 62/68 scope cap, pill
  shape + alternation + green/cream/brown theme 보존), no row headers (source
  부재, inference 금지)
- fixture : flat 8 top-bullet (texts.md 8 issues 그대로)
- smoke + R3 : PASS (11/11 self-check, 5535 chars partial, 8 units rendered,
  pill alternation 정합, row 2-3 gap, no invented row headers)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:13:46 +09:00
5c27c492ba feat(catalog): activate bim_current_problems_paired (IMP-04 Track A 8/16)
V4 signal = restructure 1 (4 MDX sample). Catalog-completeness activation —
Codex round 51 guardrail (per-frame source-evidence check, not blind F16
reuse). Source confirmed as 2x2 paired-rows BIM problem layout, distinct
from F16's quadrant-4 framing.

3-layer architecture (matrix §0) :
- V4 = matching authority — restructure tier signal; runtime activation
  prepares Phase Z to assemble this frame when V4 ranks it.
- figma_to_html (1171281194) = source/evidence — 4 BIM problem cards in
  2x2 grid (개념 부재 / 잘못된 접근방식 / 방향성 상실 / 전제조건 오류).
- Phase Z = runtime — adds catalog + partial + smoke fixture.

Builder reuse :
- `quadrant_flat_slots` reused with pad_to=4 + `issue_{n}_label/body` keys.
- `quadrant_item` parser reused.
- F16 quadrant pattern reused, but the partial is a 2-row × 2-column
  problem-theme grid (red/orange/amber/deep-amber), not the F16 TL/TR/BL/BR
  quadrant visual. Source-evidence-driven decision per Codex round 51 §10.

3 file changes :

1. templates/phase_z2/families/bim_current_problems_paired.html
   - 2x2 CSS grid with per-issue problem theme.
   - PROMOTED CSS : per-cell warning gradient (red/orange/amber/deep-amber),
     title gradient (zone-title family), "!" bullet markers in per-cell color.
   - NOT PROMOTED : Figma source banner / numbered badges / texture —
     figma_to_html source evidence preserved for future fidelity review.
   - ADAPTED : Figma absolute positioning → CSS grid 2x2, token-fixed
     typography.

2. templates/phase_z2/catalog/frame_contracts.yaml — F17 contract appended
   - frame_id=1171281194, family=cards, source_shape=top_bullets, strict 4,
     role_order=[issue_1..issue_4].
   - visual_hints.min_height_px = 350 (F16/F14 class — 2-row × 2-col density).
   - accepted_content_types = [text_block].
   - payload.builder = quadrant_flat_slots reuse with issue_{n}_* keys.

3. scripts/smoke_frame_render.py — bundled fixture for F17.

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 11/11 (F17 at
  3856 chars CSS-only)
- python scripts/smoke_frame_render.py bim_current_problems_paired
  --render-to data/runs/imp04_f17_visual : PASS, 0 raster refs
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f17_regression :
  PASS (MDX 03 V4 rank-1 unchanged; F17 not in MDX 03 V4 selection so
  this is non-impacting regression check)

scope-lock honored : V4 logic / mapper / production render / Phase R' /
AI/Kei / 10 existing partials all unchanged.

4-class status :
- class 1 readiness : 
- class 2 content-fit : watch — paragraph-heavy source (each issue body
  is multi-line Korean text). Compact 2x2 cell may need wrap. max-content
  fit checked via R3 artifact.
- class 3 : N/A
- class 4 : N/A

Refs Gitea #4 (IMP-04 Track A frame 8 — V4 RS tier, source-evidence-confirmed)
2026-05-13 13:39:30 +09:00
735e58420e feat(catalog): activate sw_reality_three_emphasis (IMP-04 Track A 7/16)
Catalog-completeness activation — NOT V4 endorsement (Codex round 49 §1+§2
guardrail). V4 signal = 0 across the current 4 MDX sample. Phase Z adds
runtime availability so V4 can route to this frame for future MDX content.

V4 remains the matching authority.

3-layer architecture (matrix §0) :
- V4 = matching authority — current sample evidence does NOT rank this frame
  for the 4 MDX. activation must not be read as V4-driven promotion.
- figma_to_html (1171281209) = source/evidence — 3-emphasis problem cards
  (토목 전문성 부족 / 비효율성 / 실무 적용 불가). full A+T+I+F+S.
- Phase Z = runtime — catalog + partial + smoke fixture added.

Builder reuse (per Codex round 49 §9 — single-frame, no batch yet) :
- `quadrant_flat_slots` reused with pad_to=3 + `emphasis_{n}_label/body`.
- `quadrant_item` parser reused.
- mapper.py unchanged.

3 file changes :

1. templates/phase_z2/families/sw_reality_three_emphasis.html
   - 3-column grid with problem-emphasis theme (red/orange/amber per
     column — frame intent is 약점/한계 진단).
   - PROMOTED CSS : per-emphasis warning color, title gradient, bullet
     markers in per-emphasis color, "!" bullet for warning semantic.
   - NOT PROMOTED : Figma source decoration / banner / texture.
     figma_to_html source evidence preserved.
   - ADAPTED : Figma absolute → flex 3-column grid, token-fixed typography.

2. templates/phase_z2/catalog/frame_contracts.yaml — F28 contract appended
   - frame_id=1171281209, family=cards, source_shape=top_bullets, strict 3,
     role_order=[emphasis_1/2/3].
   - visual_hints.min_height_px = 320.
   - accepted_content_types = [text_block].
   - payload.builder = quadrant_flat_slots (reuse) with emphasis_{n}_* keys.

3. scripts/smoke_frame_render.py — bundled fixture for F28.

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 10/10 (F28 at
  3363 chars CSS-only)
- python scripts/smoke_frame_render.py sw_reality_three_emphasis
  --render-to data/runs/imp04_f28_visual : PASS, 0 raster refs
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f28_regression :
  PASS

scope-lock honored : V4 logic / mapper / production render / Phase R' /
AI/Kei / existing 9 partials all unchanged.

4-class status :
- class 1 readiness : 
- class 2 content-fit : watch (problem description text may be longer than
  3-card body budget)
- class 3 : N/A (V4-zero, catalog-completeness — not a mapping concern)
- class 4 : N/A

Refs Gitea #4 (IMP-04 Track A frame 7 — V4-zero catalog completeness)
2026-05-13 13:27:23 +09:00
46ff611a54 feat(catalog): activate info_management_what_how_when (IMP-04 Track A 6/16)
Catalog-completeness activation — NOT V4 endorsement (Codex round 47 §7
guardrail). V4 signal = 0 across the current 4 MDX evidence sample, but
the 32-frame all-in scope means runtime backend must accept this frame
when V4 ranks it for any future MDX outside the current sample.

V4 remains the matching authority. Phase Z adds runtime availability only.

3-layer architecture (matrix §0) :
- V4 = matching authority — current evidence does not rank this frame for
  the sampled 4 MDX, so this activation must not be read as a V4-driven
  promotion. It is purely catalog completeness so V4 can route to this
  frame when warranted in future MDX content.
- figma_to_html (1171281179) = source/evidence — analysis/texts/index/flat/
  assets all present, source intent is the What/How/When 3-section
  framework.
- Phase Z = runtime — this commit adds catalog contract + partial + smoke
  fixture. Builder reuse, no new builder/parser.

Builder reuse (Codex round 47 secondary criterion under exhausted V4 tier) :
- `quadrant_flat_slots` reused (F16/F11/F20 pattern) with pad_to=3 +
  `section_{n}_label/body` key patterns.
- `quadrant_item` parser reused.
- mapper.py unchanged.

3 file changes :

1. templates/phase_z2/families/info_management_what_how_when.html
   - 3-column grid (What / How / When), each with header + body bullets.
   - PROMOTED CSS : per-section accent color (blue #2563eb What / orange
     #ea580c How / green #16a34a When), title gradient (#000#883700
     zone-title family), bullet markers in per-section accent color.
   - NOT PROMOTED (P1 case-by-case + preservation guardrail) : Figma
     상단 banner / icon / 배경 텍스처 / 장식. figma_to_html source
     evidence preserved for future fidelity review.
   - ADAPTED : Figma 50px+ → token-fixed, absolute positioning → flex grid.

2. templates/phase_z2/catalog/frame_contracts.yaml — F8 contract appended
   - frame_id=1171281179, family=cards, source_shape=top_bullets, strict 3,
     role_order=[section_1 (What), section_2 (How), section_3 (When)].
   - visual_hints.min_height_px = 320 (F11/F20 class — 3-column header+body).
   - accepted_content_types = [text_block].
   - 3 sub_zones (section_1/2/3 main_text).
   - payload.builder = quadrant_flat_slots (reuse) with section_{n}_* keys.

3. scripts/smoke_frame_render.py — bundled fixture for F8 self-check.

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 9/9 (F8 added at
  3577 chars CSS-only)
- python scripts/smoke_frame_render.py info_management_what_how_when
  --render-to data/runs/imp04_f8_visual : PASS, R3 artifact, 0 raster refs
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f8_regression :
  PASS (MDX 03 V4 rank-1 unchanged; F8 V4 signal = 0 across 4 MDX sample,
  so not selected for MDX 03 either — purely catalog completeness)

scope-lock honored (3-layer + 4-class) :
- V4 logic / V4 evidence yaml : unchanged
- Existing PAYLOAD_BUILDERS / ITEM_PARSERS : unchanged (reuse only)
- Existing 8 partials : unchanged
- Composition planner / production render / Phase R' / AI/Kei : unchanged

4-class status :
- class 1 readiness :  contract + builder reuse + partial + smoke + R3
- class 2 content-fit : watch — header single-line, body 3-5 bullets per
  column. 6+ bullets per column may overflow.
- class 3 / 4 : N/A

Refs Gitea #4 (IMP-04 Track A frame 6 — V4-zero catalog completeness)
2026-05-13 13:19:03 +09:00
bc58102b66 feat(catalog): activate dx_sw_necessity_three_perspectives (IMP-04 Track A 5/16)
Reason : V4 LE=2 (03-1 + 01-1) + RS=1 — V4 LE tier strongest remaining
after F12/F11/F18 UAI tier. Track A frame 5 per Codex round 45 V4-priority
acceptance.

3-layer architecture (matrix §0) :
- V4 = matching authority — V4 ranked this frame light_edit for 03-1 (DX 시행
  필수 요건) and 01-1 (용어 정의).
- figma_to_html (1171281198) = source/evidence — 386-line index.html + assets/.
- Phase Z = runtime — this commit adds catalog + partial + smoke fixture.

Builder reuse (no new builder/parser introduced) :
- Reuses existing `quadrant_flat_slots` builder (F16/F11 pattern) with
  pad_to=3 + `perspective_{n}_label` / `perspective_{n}_body` keys.
- Reuses existing `quadrant_item` parser.
- Same flat-keyed label+body grammar as F11; only N=3 + key names differ.
- mapper.py unchanged — secondary builder reuse per Codex round 45.

3 file changes :

1. templates/phase_z2/families/dx_sw_necessity_three_perspectives.html
   - Adapted from figma_to_html_agent/blocks/1171281198/index.html.
   - 3-column grid (BIM 전면설계 / 디지털 전환 S/W / 고부가가치 산업전환).
   - PROMOTED CSS : header bar dark green (#296B55 → #123328 Figma green
     family), header text white bold, title gradient (#000#883700
     F13/F14/F12/F11/F18 zone-title family), card border + bullet markers
     (green family).
   - NOT PROMOTED (P1 case-by-case + preservation guardrail per Codex
     round 37) : 상단 dark green banner ("디지털 전환(DX)은 S/W가 필수다"
     visual block), 좌측 DX circular area (multi-image + center text +
     decor — main rhetorical anchor but cannot fit compact zone),
     hanmaek/한자/배경 텍스처. figma_to_html source evidence preserved.
   - ADAPTED : Figma 90/65/40 px → token-fixed, 1280×426 absolute +
     zoom → Phase Z 3-column grid, 3 perspective cards → flex column.

2. templates/phase_z2/catalog/frame_contracts.yaml
   - F20 contract appended.
   - frame_id=1171281198, family=cards, source_shape=top_bullets, strict 3,
     role_order=[perspective_1, perspective_2, perspective_3].
   - visual_hints.min_height_px = 320 (3 col × header 30 + body bullets ~75
     + title 30 + padding 30 = ~195 + 125 buffer for label/5+ bullet
     overflow; F12/F11/F18 class).
   - accepted_content_types = [text_block].
   - 3 sub_zones (perspective_1/2/3 main_text).
   - payload.builder = quadrant_flat_slots (reuse) with perspective_{n}_*
     key patterns.

3. scripts/smoke_frame_render.py — bundled fixture for F20 self-check.

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 8/8 (F20 added
  at 3160 chars CSS-only)
- python scripts/smoke_frame_render.py dx_sw_necessity_three_perspectives
  --render-to data/runs/imp04_f20_visual : PASS, R3 artifact, 0 raster
  refs (CSS-only)
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f20_regression :
  PASS (MDX 03 V4 rank-1 still F13/F29; F20 light_edit candidate for 03-1
  but F13 was use_as_is at higher rank, so F20 not selected here)

scope-lock honored (3-layer + 4-class) :
- V4 logic / V4 evidence yaml : unchanged
- Existing PAYLOAD_BUILDERS (5 builders) : unchanged. No new builder added.
- Existing ITEM_PARSERS (3 parsers) : unchanged. No new parser added.
- Existing 7 partials : unchanged.
- Composition planner / production render / Phase R' / AI/Kei : unchanged.

4-class status :
- class 1 readiness :  contract + builder reuse + partial + smoke fixture
  + R3 artifact aligned.
- class 2 content-fit : watch — header single-line, body 3-5 bullets per
  column. 6+ bullets per column may overflow.
- class 3/4 : N/A.

Codex review remains useful (per scope-lock §5 "shared catalog/builder
logic" category — quadrant_flat_slots is now reused by F16/F11/F20). New
builder/parser path is NOT this commit.

Refs Gitea #4 (IMP-04 Track A frame 5 — V4 LE tier, builder reuse)
2026-05-13 12:28:49 +09:00
f7a9240fe5 fix(IMP-04): F18 F1 follow-ups — defaults + narrow alias + cardinality clarify
Same-frame F1 follow-up per Codex round 43 (#15527). matrix §4.1 Fix 7
4-class F1 path (no Track A pause, small fixes + Codex re-review).

Three fixes :

1. F1-a — explicit col_a/col_b label defaults
   - Previous (c7b0f5b) used empty defaults `col_a_label_default: ""` /
     `col_b_label_default: ""`, so an upstream MDX path without explicit
     column headers would render blank header cells.
   - Fix : set `col_a_label_default: "BIM"` and `col_b_label_default: "DX"`
     in F18 contract. Frame intent is the BIM-vs-DX comparison, so the
     headers are semantic and must not silently become blank.

2. F1-b — narrow prefix-stripping aliases (parser → builder option)
   - Previous parser used a broad regex
     `^[A-Za-z가-힣]{1,8}\s*:\s*(.+)$` to strip any short prefix before
     `:`. That could accidentally remove meaningful Korean/English
     prefixes from real cell content.
   - Fix : remove auto-stripping from `parse_compare_row_2col_item`.
     Stripping is now configurable via builder option
     `strip_col_prefix_aliases: [<list of exact aliases>]` and applied
     by `_build_compare_table_2col`. F18 contract uses `["BIM", "DX"]`,
     so only `BIM:` / `DX:` (with optional fullwidth `:`) prefixes are
     stripped; other Korean/English colons in real content stay intact.
   - Parser signature unchanged. Builder is the single place that owns
     the stripping policy.

3. F1-c — cardinality semantic clarification
   - Previous top-level `cardinality.strict: 2` was ambiguous: it could
     be read as `row strict: 2`. Rows are actually `1..12` via
     `sub_zones.rows.cardinality`.
   - Fix : add YAML comment that the top-level strict 2 = column count
     (col_a / col_b), not row count. Per-sub_zone cardinality remains
     authoritative for rows.

Verification :
- python -m py_compile src/phase_z2_mapper.py : PASS
- python scripts/smoke_frame_render.py --self-check : PASS 7/7 (F18
  fixture rendered unchanged at 4211 chars; smoke harness only loaded
  the partial, builder/parser logic not directly exercised in smoke)
- Manual builder/parser invocation test with synthetic units :
  - col_a_label / col_b_label resolve to "BIM" / "DX" defaults.
  - `BIM: Only 3D` → `Only 3D` (alias-stripped).
  - `DX: BIM << DX (ENG. 포함)` → `BIM << DX (ENG. 포함)` (alias-stripped).
  - `분야별 단절` → `분야별 단절` (no BIM/DX prefix, untouched).
  - This matches the F1-b narrow-alias intent.
- python scripts/smoke_frame_render.py bim_dx_comparison_table
  --render-to data/runs/imp04_f18_visual_r2 : PASS, R3 artifact written
  with same character count, generic viewer title.

scope-lock honored : no V4 logic, no new builder/parser added (only
behavior refinement of existing F18 builder/parser/contract), no other
partial, no Phase Z production render, no Phase R'/AI/Kei changes.

4-class status (F18 post-F1) :
- class 1 readiness : adapter cleanup complete — defaults explicit,
  aliases narrow, cardinality semantically clear.
- class 2 content-fit : still a watch item (long Korean wrapping,
  6+ rows). max_rows=12 protection unchanged.
- class 3 / 4 : N/A.

Refs Gitea #4 (IMP-04 Track A frame 4 — F1 follow-up per Codex round 43)
2026-05-13 12:21:02 +09:00
c7b0f5bde1 feat(catalog): activate bim_dx_comparison_table (IMP-04 Track A 4/16)
Reason : V4 UAI=1 (01-2 "용어간 상호관계") — UAI tier strongest after F12/F11.
Track A frame 4 per Codex round 41 V4-priority acceptance.

3-layer architecture (matrix §0) :
- V4 = matching authority — V4 ranked this frame use_as_is for 01-2.
- figma_to_html (1171281195) = source/evidence — analysis/texts/index.html/
  flat/assets all present.
- Phase Z = runtime orchestration — adds catalog + new builder + new parser +
  new partial + smoke fixture.

NEW builder + NEW parser (Codex round 41 mandatory review path) :

1. src/phase_z2_mapper.py — NEW `compare_row_2col_item` parser in ITEM_PARSERS
   - input : (top_line, nested_lines)
   - output : {label, col_a, col_b}
   - label = bold from top_line
   - col_a / col_b = first 2 nested bullets, optional prefix stripping ("BIM:"/
     "DX:" or similar ≤8-char tag with colon)
   - inline emphasis preserved as <strong>

2. src/phase_z2_mapper.py — NEW `compare_table_2col` PAYLOAD_BUILDERS entry
   - payload : title + col_a_label + col_b_label + rows[]
   - builder_options : item_parser, col_a/b_label_default, max_rows (default 999)
   - max_rows truncation tracked via _truncated_count

3. templates/phase_z2/families/bim_dx_comparison_table.html — NEW partial
   - 3-column grid (category / col_a / col_b) with header row + N data rows
   - PROMOTED CSS : title gradient (#000#883700, zone-title family), header
     brown bg (rgba(50,31,9,0.85-0.95)), zebra striping, brown family bullet
     accent, subtle border (#A5BBB4 F11 family).
   - NOT PROMOTED (P1 case-by-case + preservation guardrail) : Figma column
     header raster icons, color emphasis variants, hanja deco. figma_to_html
     source evidence remains preserved.
   - ADAPTED : Figma absolute positioning + zoom → Phase Z flex/grid 3-col
     table, typography → token-fixed, row heights auto content-fit.

4. templates/phase_z2/catalog/frame_contracts.yaml — F18 contract appended
   - frame_id=1171281195, family=table, source_shape=top_bullets, strict 2
     (2 columns), role_order=[col_a, col_b].
   - visual_hints.min_height_px = 350 (title 30 + header 30 + 6 rows×35 +
     padding 30 = 300 + 50 buffer; F14-class).
   - accepted_content_types = [text_block].
   - sub_zones : col_a_header / col_b_header (strict 1 each) + rows (min 1,
     max 12 category rows).

5. scripts/smoke_frame_render.py — bundled fixture for F18 self-check (6
   category rows : 범위 / S/W / 프로세스 / 성과물 / 활용 / 수행개념).

Verification :
- python -m py_compile src/phase_z2_mapper.py scripts/smoke_frame_render.py
  : PASS
- python scripts/smoke_frame_render.py --self-check : PASS 7/7 (F18 added
  at 4211 chars CSS-only)
- python scripts/smoke_frame_render.py bim_dx_comparison_table --render-to
  data/runs/imp04_f18_visual : PASS, R3 artifact, 0 raster refs (CSS-only)
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f18_regression :
  PASS (MDX 03 V4 rank-1 still F13/F29; F18 only routes 01-2 per V4)

scope-lock honored (3-layer + 4-class) :
- V4 logic / V4 evidence yaml : unchanged
- Existing PAYLOAD_BUILDERS (4 builders) : unchanged. compare_table_2col added
  as NEW entry.
- Existing ITEM_PARSERS (2 parsers) : unchanged. compare_row_2col_item added
  as NEW entry.
- Existing 6 partials : unchanged.
- Composition planner / production render / Phase R' / AI/Kei : unchanged.

4-class status :
- class 1 readiness :  contract + new builder + new parser + partial +
  smoke fixture + R3 artifact aligned.
- class 2 content-fit : watch — cell content single-line; long Korean
  sentences may wrap. Row height auto handles wrap; max_rows=12 limit
  protects vertical overflow.
- class 3/4 : N/A.

Codex review mandatory per scope-lock §5 (new builder pattern first
introduction : compare_table_2col).

Refs Gitea #4 (IMP-04 Track A frame 4 — V4 UAI tier, NEW builder)
2026-05-13 12:13:11 +09:00
a4fdc7ad89 feat(catalog): activate construction_bim_three_usage (IMP-04 Track A 3/16)
Reason : V4 UAI=1 (01-1) + RS=1 — UAI tier strongest after F12. Track A
frame 3 per Codex round 39 V4-priority acceptance.

3-layer architecture context (matrix §0) :
- V4 = matching authority — V4 ranked this frame as use_as_is for 01-1
  ("용어 정의") and restructure=1 for other sections.
- figma_to_html (1171281182) = rich source/evidence — 401-line index.html
  + 4 PNG assets, full A+T+I+F+S resource available.
- Phase Z = runtime orchestration — this commit adds catalog + partial +
  smoke fixture for the runtime projection.

Builder reuse (Codex round 39 secondary criterion) :
- Reuses existing `quadrant_flat_slots` (F16) with pad_to=3 +
  label_key_pattern="category_{n}_label" + body_key_pattern="category_{n}_body".
- Same parser `quadrant_item` reused — no new builder or parser added.
- F16 / F11 share flat-keyed label+body grammar; only N differs (4 vs 3).

3 file changes :

1. templates/phase_z2/families/construction_bim_three_usage.html
   - Adapted from figma_to_html_agent/blocks/1171281182/index.html.
   - 3 horizontal stacked rows (brown label box + white card with green
     border, per Figma L82-103). Compact Phase Z zone fit.
   - PROMOTED CSS : brown label box (rgba(50,31,9,0.8) + 둥글기 + drop-
     shadow), white card with green border (#A5BBB4), title gradient
     (#000#883700 F13/F14/F12 family), bullet markers in brown family.
   - NOT PROMOTED (P1 case-by-case + preservation guardrail per Codex
     round 37 / matrix §4.1 Fix 7) : title-icon (50×50 small), card_decor
     × 3 (decorative side images), bottom BIM 모델/공사 Process 다이어그램.
     Figma source evidence preserved in figma_to_html_agent/; not promoted
     to Phase Z compact projection.
   - ADAPTED : Figma 50/45/38/30 px → token-fixed, 1248×1066 absolute +
     zoom 0.67542 → Phase Z flex column 3-row stack, 335×124 label box
     → 110×~50 compact (zone fit).

2. templates/phase_z2/catalog/frame_contracts.yaml
   - F11 contract appended after F12.
   - frame_id=1171281182, family=cards, source_shape=top_bullets, strict
     cardinality 3, role_order=[category_1, category_2, category_3].
   - visual_hints.min_height_px = 320 (title 30 + 3×70 row + gaps 12 +
     padding 30 = 282 + 38 safety buffer).
   - accepted_content_types = [text_block] only.
   - 3 sub_zones (category_1/2/3 main_text).
   - payload.builder = quadrant_flat_slots (reuse) with category_{n}_*
     key patterns.

3. scripts/smoke_frame_render.py
   - Bundled fixture for F11 self-check.

Verification :
- python -m py_compile scripts/smoke_frame_render.py : PASS
- python scripts/smoke_frame_render.py --self-check : PASS 6/6 (F11
  added at 3014 chars, compact CSS-only)
- python scripts/smoke_frame_render.py construction_bim_three_usage
  --render-to data/runs/imp04_f11_visual : PASS, R3 artifact written,
  0 raster refs (CSS-only), copy_assets ran (4 PNGs in assets/ dir
  for future fidelity-review if needed)
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f11_regression
  : PASS (MDX 03 V4 rank-1 still F13/F29, F11 not triggered for MDX 03 —
  F11 only routes 01-1 per V4 evidence)

scope-lock honored (3-layer + 4-class) :
- V4 logic / V4 evidence yaml : unchanged
- Existing PAYLOAD_BUILDERS 4 builders (incl. F12's cycle_intersect_3) :
  unchanged. F11 reuses quadrant_flat_slots (secondary criterion).
- Existing ITEM_PARSERS : unchanged. F11 reuses quadrant_item.
- Existing partials (F13/F29/F16/F14/F12) : unchanged.
- Composition planner / production render path / Phase R' / AI/Kei :
  unchanged.

4-class status :
- class 1 adapter/runtime readiness :  contract + builder + partial +
  smoke fixture + R3 artifact aligned.
- class 2 content-fit : watch — 3 rows × ~70 px height. If MDX body has
  4+ bullets per category, may overflow. realistic use case (01-1) shows
  1-2 bullets per category per analysis.md, so within budget.
- class 3 / 4 : N/A.

Refs Gitea #4 (IMP-04 Track A frame 3 — V4 UAI tier, builder reuse)
2026-05-13 12:05:05 +09:00
766fa4639d fix(IMP-04): F12 F1 follow-ups — intersection optional + smoke title generic
Same-frame F1 follow-up commit per Codex round 37 (#15503). Matrix §4.1
Fix 7 4-class: F1 = small same-frame fix + Codex re-review (not F2/F3,
no Track A pause needed).

Two fixes (both Codex-caught) :

1. F1-a — `intersection` sub_zone contract semantics
   - Previous (c67609c) declared `cardinality: { strict: 1 }` on the
     intersection sub_zone, while the builder used `intersection_default: ""`
     and the partial hides empty intersection. That mismatch meant
     production payloads could silently omit the center concept while
     the contract still demanded strict 1.
   - Fix : `cardinality: { min: 0, max: 1 }` — intersection is now
     explicitly optional, matching analysis.md "slots 5개, required 4개"
     (intersection was originally the optional 5th slot).
   - Builder behavior unchanged (intersection_default = "" still
     produces empty when MDX has no center text; partial's :empty hide
     remains semantically correct).

2. F1-b — smoke harness viewer title leakage
   - Previous harness output had `<title>F14 render artifact — {template_id}</title>`
     hardcoded from the original F14-specific implementation.
   - Fix : generic `<title>Phase Z render artifact — {template_id}</title>`.
   - Affects all `--render-to` dev artifacts going forward.

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 5/5
- python scripts/smoke_frame_render.py construction_goals_three_circle_intersection
  --render-to data/runs/imp04_f12_visual_r2 : PASS, generated title now reads
  "Phase Z render artifact — construction_goals_three_circle_intersection"
- Catalog yaml parse : intersection cardinality = {min: 0, max: 1}

scope-lock honored : no V4 logic, no builder/parser logic, no other
partial, no Phase Z production render, no Phase R'/AI/Kei changes. F1
fix scope only.

Refs Gitea #4 (IMP-04 Track A frame 2 — F1 follow-up per Codex round 37)
2026-05-13 11:57:45 +09:00
c67609c083 feat(catalog): activate construction_goals_three_circle_intersection (IMP-04 Track A 2/16)
Reason : V4 strongest UAI tier candidate (use_as_is=1 for 02-1, light_edit=1
for 01-1, restructure=1). Track A frame 2 per Codex rounds 30/33/35 V4-
priority rule. F14 clean pass completed at 834ed39; this is the next
Track A activation.

3-layer architecture context (matrix §0) :
- V4 = matching authority — V4 ranked this frame as use_as_is for the
  "DX의 궁극적 목표" section (02-1) and light_edit for "용어 정의" (01-1).
- figma_to_html (1171281189) = rich source/evidence — 510-line index.html
  base, full analysis/flat/texts/assets present (A+T+I+F+S).
- Phase Z = runtime orchestration — this commit adds the runtime contract,
  builder, partial, and fixture so the V4 candidate can be assembled.

New runtime additions :

1. src/phase_z2_mapper.py — new `cycle_intersect_3` PAYLOAD_BUILDERS entry
   - Reuses existing `quadrant_item` ITEM_PARSERS (label only, body
     ignored) — F16 parser reused, no new parser.
   - Produces flat keys : circle_1_label / circle_2_label / circle_3_label
     + intersection text (optional) — distinct from F16's quadrant_N_body
     structure since this frame's 3 main circles use labels only.
   - pad_to=3, truncate_at=3, configurable via builder_options.

2. templates/phase_z2/families/construction_goals_three_circle_intersection.html
   - Adapted from figma_to_html_agent/blocks/1171281189/index.html.
   - Slot mapping : title + 3 circle labels + optional intersection text.
   - PROMOTED CSS : 3 circle gradients (safety #BC652B/#A24200, productivity
     #897445/#3E3523, trust #296B55/#123328) + outer multiply blend +
     title gradient (#000#883700, F13/F14 zone-title family) + main
     label typography (white text + shadow).
   - NOT PROMOTED (P1 case-by-case, compact zone fit) : 6 accent hanja
     circles (安/質/速/利/通/信), 6 side labels (안전성 제고 etc.), 3
     decoration rects, 3 arc images, bg-texture multiply image. These
     are Figma-side decorative content not in MDX and would clutter a
     Phase Z zone of ~320 px.
   - ADAPTED : Figma 70/50/40 px → token-fixed font sizes, 350×350
     absolute-positioned overlapping circles → 110×110 flex row (cycle
     intent expressed via intersection text instead of geometric overlap).

3. templates/phase_z2/catalog/frame_contracts.yaml — append F12 contract
   - template_id, frame_id 1171281189, family=diagram, source_shape=
     top_bullets, strict cardinality 3, role_order [safety, productivity,
     trust].
   - visual_hints.min_height_px = 320, derived from 3 circle row 80 +
     title 30 + label area 60 + intersection 30 + padding 40 = 240
     + 80 safety buffer (lighter than F14's 350 since CSS-only).
   - accepted_content_types = [text_block] only.
   - 4 sub_zones declared (circle_1/2/3 main_text + intersection emphasis).

4. scripts/smoke_frame_render.py — add bundled fixture for F12 self-check.

Verification :
- python -m py_compile src/phase_z2_mapper.py scripts/smoke_frame_render.py
  : PASS
- python scripts/smoke_frame_render.py --self-check : PASS 5/5 (F12 added
  at 3691 chars CSS-only)
- python scripts/smoke_frame_render.py construction_goals_three_circle_intersection
  --render-to data/runs/imp04_f12_visual : PASS, R3 artifact written. 0
  raster refs (CSS-only partial); copy_assets ran successfully and
  produced data/runs/imp04_f12_visual/assets/construction_goals_three_circle_intersection/
  with the frame's 4 PNG files (unused since partial is CSS-only — assets
  remain available for future raster promotion if visual inspection
  flags fidelity loss).
- python run_mdx03_pipeline.py --phase-z2 --run-id imp04_f12_regression
  : PASS (MDX 03 V4 rank-1 still F13/F29, F12 not selected — F12 only
  triggered by 02-1 / 01-1 sections per V4 evidence)

scope-lock honored : V4 logic / V4 evidence / mapper existing builders /
composition planner / Phase R' / pipeline production render path / AI/Kei
all unchanged. New builder added without modifying existing 3 (mixed
strategy per scope-lock §4).

Calibration status (matrix §4.1 Fix 7 4-class) :
- class 1 adapter readiness : new builder registered, partial loadable,
  contract valid, smoke passing.
- class 2 content-fit : compact 110×110 circles + label, watch for label
  overflow if MDX bullets exceed ~12 chars.
- class 3/4 mapping/routing : not applicable for this commit.
- Codex review mandatory per scope-lock §5 (new builder pattern
  cycle_intersect_3 first introduction).

Refs Gitea #4 (IMP-04 Track A frame 2 — V4 strongest UAI tier)
2026-05-13 11:50:44 +09:00
46e9db30b2 docs(IMP-04): adopt 3-layer architecture + 4-class failure taxonomy
Wording-only matrix clarification per Codex #15477 architecture
correction + #15485 acceptance + Claude rounds 32/34 agreement.

Two foundational changes :

1. New §0 — Architecture context (3-layer)
   - V4 = matching basis / design-selection authority
   - figma_to_html = rich source/evidence layer (visual hierarchy,
     assets, layer structure, geometry, source intent, provenance)
   - Phase Z = runtime orchestration / assembly / recovery layer
     (consumes V4 candidates + source evidence; does NOT replace V4
     as the matching authority)
   - Explicit correction: earlier "Phase Z = execution SoT" framing
     was a 2-layer dichotomy that conflated matching authority (V4)
     with runtime execution (Phase Z). 3-layer is the correct
     architecture.

2. Fix 7 in §4.1 — 4-class failure taxonomy as primary
   - Replaces F1/F2/F3 as the primary technical failure
     classification. F1/F2/F3 remain only as secondary review /
     process severity labels.
   - 4 classes (Codex #15477) :
     - class 1: adapter/runtime readiness (partial/asset/slot/
       contract missing — V4 ranking stays valid)
     - class 2: content-fit issue (overflow / light edit needed)
     - class 3: V4 ↔ figma_to_html mapping issue (metadata wrong)
     - class 4: popup/secondary content routing case
   - Prior-history reclassification documented :
     - 556b448 minimum viable persona — class 1
     - 2fcd8bb strict CSS-only refinement — class 2
     - a1c06b7 MDX02 chain dependency — class 1 (construction_goals
       catalog readiness, NOT V4 logic issue)
     - Round 26 "Phase Z = 실행 SoT" framing — wording error

No source code, runtime catalog, builder, partial, V4 evidence,
mapper, or composition planner changes. Documentation clarification
only.

History preserved : previous commits stand. This is a forward-only
clarification, not a rewrite of past commit messages.

Refs Gitea #4 (IMP-04 — architecture wording fix, 3-layer + 4-class)
2026-05-13 11:42:19 +09:00
834ed3946d test(IMP-04): add F14 render artifact check and fix min-height note
3rd commit in F14 series (calibration point clean pass). Closes the two
Codex round 26 (#15435) blockers:
1. min_height_px self-contradiction
2. F14 actual rendered visual artifact absent

Per Codex round 28 (#15447) agreement (M1 + --render-to extension) and
Claude round 27 (#15438) fix path :

Changes :

1. templates/phase_z2/catalog/frame_contracts.yaml — min_height_px 320 → 350.
   Comment now self-consistent : 70 (badge raster) + 210 (bullet body) +
   36 (photo strip) + 30 (padding) = 346 sum + 4 safety buffer = 350.
   F14 is now F29-class (345) per raster-promoted content density.

2. scripts/smoke_frame_render.py — add `--render-to DIR` dev mode (R3
   acceptance gate). Behavior :
   - StrictUndefined smoke render (unchanged)
   - reuse production `copy_assets(template_id, run_dir)` so the runtime
     asset delivery path is exercised (no logic duplication)
   - wrap partial with minimal viewer HTML (Phase Z token vars + slide-
     sized wrap, browser-openable)
   - fail-fast if rendered HTML references a missing local asset (per
     Codex round 28 §4 recommendation)
   - save artifact to {DIR}/index.html with {DIR}/assets/{template_id}/*
   - production render path (phase_z2_pipeline.render_slide) unchanged
   - small regex fix : asset extraction now captures both `src="..."`
     and `url("...")` references

F14 verification (3rd commit) :
- python -m py_compile scripts/smoke_frame_render.py : PASS
- python scripts/smoke_frame_render.py --self-check : PASS 4/4 (7446 chars
  for persona unchanged from a1c06b7)
- python scripts/smoke_frame_render.py three_persona_benefits --render-to
  data/runs/imp04_f14_visual : PASS, 10 asset refs all resolved, 14
  raster files copied via production copy_assets() to
  data/runs/imp04_f14_visual/assets/three_persona_benefits/
- R3 artifact ready for browser visual inspection at
  data/runs/imp04_f14_visual/index.html (Phase Z slide-sized wrapper +
  promoted persona partial + 10 referenced assets all on disk)

F14 clean pass status :
- min_height_px self-consistency : fixed (M1 = 350)
- Actual rendered artifact : produced and assets resolved
- Visual fidelity inspection : ready for browser/eye review
- Earlier MDX02 chain attempt (commit a1c06b7 body) : superseded;
  MDX02 is not the F14 validation baseline (Claude round 26 / Codex
  round 26 agreement). MDX03 is the matched baseline; F14 visual
  inspection now uses the harness artifact path instead.

scope-lock guardrails honored : 32-frame target, no V4 logic change,
no Phase R' regression, no mapper or composition planner change, no
production render path change. The new harness mode is dev verification
only, isolated from runtime selection.

Refs Gitea #4 (IMP-04 Track A — F14 3rd commit, clean pass gate)
2026-05-13 11:07:37 +09:00
a1c06b779a refactor(persona): F14 2nd refinement — promote raster assets (IMP-04 Track A F2 re-do)
Second calibration refinement after Codex round 22 F2 finding (commit 2fcd8bb
was over-broad CSS-only over-generalization). Approach re-locked via Codex
rounds 23/24 + Claude rounds 23/25 with per-asset case-by-case promotion
policy (round 13 §2.3 restored).

Asset decisions for F14 (Codex round 24 agreement) :

PROMOTED RASTER (Phase Z `copy_assets()` infra — pipeline.py:746 — handles
delivery from figma_to_html_agent/blocks/1171281191/assets/ to runtime
{run_dir}/assets/three_persona_benefits/) :
- col_bg_texture (1 PNG, 3 col 공유)
- bottom photos × 3 (실사 사진, CSS equivalent 불가)
- badge outer × 3 (round ring image)
- badge inner × 3 (round disk image)
→ 10 raster assets total

PROMOTED CSS (디자인 의도, CSS 충분) :
- 발주자/시공자/설계자 accent color (#285b4a / #445a2f / #743002)
- col-overlay solid tint (#d6e7c4 / #e1efe1 / #d0c0ad, opacity 0.8)
- title gradient (#000#883700)
- bullet check marker (✓ unicode)

NOT PROMOTED : 한자/장식 텍스트 (Figma deco, MDX 무관)

min_height_px : 290 → 320 (badge 70 + body 210 + photo 36 + padding 30,
F29 345 class). Codex round 13 §2.2 derive + confirm method.

Partial structure :
- 3-column grid (Phase Z flex/grid, not Figma absolute positioning)
- per column: badge (raster outer/inner + CSS text overlay) → bullet
  body (CSS check marker) → bottom photo (raster, opacity 0.7)
- col-bg texture as ::before background image, overlay tint as ::after
- isolation: isolate + z-index layering for proper raster + overlay stacking

Verification :
- python scripts/smoke_frame_render.py --self-check : PASS 4/4 (persona
  refined 7446 chars, was 5314 in commit 2fcd8bb)

V2 validation gap (Codex round 24 §3 anticipation) :
- python run_mdx03_pipeline.py --phase-z2 --mdx samples/mdx/02. DX의 시행
  목표 및 기대효과.mdx --run-id imp04_persona_v2_mdx02 : FAIL
- TypeError 'NoneType' object is not subscriptable at contract["payload"]
- Root cause : MDX 02 section 02-1 V4 rank-1 = construction_goals_three_
  circle_intersection (frame 12), not yet in catalog. Pipeline aborts at
  02-1 before reaching 02-2.2 (persona target).
- Chain dependency : MDX 02 acceptance requires construction_goals
  activation first.
- F1/F2/F3 classification of this blocker pending Codex round 26 review.

Visual rendering of F14 deferred to either:
(a) construction_goals activation first (Track A priority reorder)
(b) V3 synthetic MDX with persona-only content + V4 evidence extension
(c) V4 dev override / IMP-06 section-override mechanism

This commit progresses the asset-promoted F14 partial. F14 acceptance
gate (actual render inspection) remains open pending the V2 chain
dependency resolution.

scope-lock guardrails honored : 32-frame target, no V4 logic change, no
Phase R' regression, no MDX03/MDX02 hardcoding, no other partial/builder
modified.

Refs Gitea #4 (IMP-04 Track A F2 re-do, persona 2nd refinement)
2026-05-13 10:49:20 +09:00