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)
This commit is contained in:
2026-05-13 12:13:11 +09:00
parent a4fdc7ad89
commit c7b0f5bde1
4 changed files with 298 additions and 0 deletions

View File

@@ -201,6 +201,23 @@ _MOCK_CONSTRUCTION_BIM_USAGE = {
], ],
} }
# Track A frame 4 — bim_dx_comparison_table (frame 18).
# NEW builder = compare_table_2col + NEW parser = compare_row_2col_item.
# slot_payload : title, col_a_label, col_b_label, rows=[{label, col_a, col_b}].
_MOCK_BIM_DX_COMPARISON = {
"title": "BIM 과 DX 의 이해",
"col_a_label": "BIM",
"col_b_label": "DX",
"rows": [
{"label": "범위", "col_a": "Only 3D", "col_b": "BIM &lt;&lt; DX (ENG. + Mgmt 포함)"},
{"label": "S/W", "col_a": "상용 S/W (Revit 등)", "col_b": "상용 + 전용 40~80개"},
{"label": "프로세스", "col_a": "기존 2D 설계방식 유지", "col_b": "근본적 문제의식 통한 개선"},
{"label": "성과물", "col_a": "3D 모델 중심", "col_b": "공학 정보 + 콘텐츠 연계"},
{"label": "활용", "col_a": "분야별 단절", "col_b": "전 생애주기 활용 시스템"},
{"label": "수행개념", "col_a": "수동적 / 집단적", "col_b": "적극·구체적 실현 방안"},
],
}
SELF_CHECK_FIXTURES: dict[str, dict] = { SELF_CHECK_FIXTURES: dict[str, dict] = {
"three_parallel_requirements": _MOCK_THREE_PARALLEL, "three_parallel_requirements": _MOCK_THREE_PARALLEL,
"process_product_two_way": _MOCK_PROCESS_PRODUCT, "process_product_two_way": _MOCK_PROCESS_PRODUCT,
@@ -208,6 +225,7 @@ SELF_CHECK_FIXTURES: dict[str, dict] = {
"three_persona_benefits": _MOCK_THREE_PERSONA_BENEFITS, "three_persona_benefits": _MOCK_THREE_PERSONA_BENEFITS,
"construction_goals_three_circle_intersection": _MOCK_CONSTRUCTION_GOALS, "construction_goals_three_circle_intersection": _MOCK_CONSTRUCTION_GOALS,
"construction_bim_three_usage": _MOCK_CONSTRUCTION_BIM_USAGE, "construction_bim_three_usage": _MOCK_CONSTRUCTION_BIM_USAGE,
"bim_dx_comparison_table": _MOCK_BIM_DX_COMPARISON,
} }

View File

@@ -234,9 +234,39 @@ def parse_quadrant_item(unit: tuple[str, list[str]]) -> dict:
return {"label": label, "body": body} return {"label": label, "body": body}
def parse_compare_row_2col_item(unit: tuple[str, list[str]]) -> dict:
"""F18-style — bold = category label, nested 2 bullets = col_a / col_b values.
Pattern : top bullet = **카테고리**, nested = `- BIM: ...` / `- DX: ...` 또는
단순 ordering (첫 nested = col_a, 두번째 = col_b). prefix "BIM:" / "DX:"
있으면 stripping.
Returns:
{label, col_a, col_b}
"""
top_line, nested_lines = unit
label = _extract_bold_or_plain(top_line)
# nested bullets — strip bullet marker, take first 2
nested = []
for l in nested_lines:
l_strip = l.strip()
if re.match(r"^[\*\-]\s", l_strip):
txt = re.sub(r"^[\*\-]\s+", "", l_strip)
# strip optional "BIM:" / "DX:" prefix (anything before colon ≤ 5 chars)
m = re.match(r"^[A-Za-z가-힣]{1,8}\s*:\s*(.+)$", txt)
if m:
txt = m.group(1).strip()
txt = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", txt)
nested.append(txt)
col_a = nested[0] if len(nested) > 0 else ""
col_b = nested[1] if len(nested) > 1 else ""
return {"label": label, "col_a": col_a, "col_b": col_b}
ITEM_PARSERS: dict[str, Callable] = { ITEM_PARSERS: dict[str, Callable] = {
"pillar_item": parse_pillar_item, "pillar_item": parse_pillar_item,
"quadrant_item": parse_quadrant_item, "quadrant_item": parse_quadrant_item,
"compare_row_2col_item": parse_compare_row_2col_item,
} }
@@ -485,11 +515,55 @@ def _build_cycle_intersect_3(section, units, contract) -> dict:
return payload return payload
def _build_compare_table_2col(section, units, contract) -> dict:
"""F18-style — compare table with 2 columns + N category rows.
payload :
title : section.title
col_a_label : 좌 column header (예: "BIM")
col_b_label : 우 column header (예: "DX")
rows : list[{label, col_a, col_b}] — top_bullets 각각 → row
builder_options :
item_parser : ITEM_PARSERS key (예: `compare_row_2col_item`)
col_a_label_default : col_a header (MDX 첫 행에 명시 안 되면 사용. default "")
col_b_label_default : col_b header (default "")
max_rows : N (default 999 — practical 한계). 초과 시 _truncated_count
"""
options = contract["payload"]["builder_options"]
parser_name = options["item_parser"]
parser = ITEM_PARSERS.get(parser_name)
if parser is None:
raise ValueError(
f"Contract '{contract['template_id']}' references item_parser='{parser_name}' "
f"but ITEM_PARSERS has no such entry."
)
col_a_label = options.get("col_a_label_default", "")
col_b_label = options.get("col_b_label_default", "")
max_rows = options.get("max_rows", 999)
payload: dict = {}
payload.update(_resolve_title(section, contract["payload"], contract))
payload["col_a_label"] = col_a_label
payload["col_b_label"] = col_b_label
visible = list(units[:max_rows])
rows = [parser(u) for u in visible]
payload["rows"] = rows
if len(units) > max_rows:
payload["_truncated_count"] = len(units) - max_rows
return payload
PAYLOAD_BUILDERS: dict[str, Callable] = { PAYLOAD_BUILDERS: dict[str, Callable] = {
"items_with_role": _build_items_with_role, "items_with_role": _build_items_with_role,
"process_product_pair": _build_process_product_pair, "process_product_pair": _build_process_product_pair,
"quadrant_flat_slots": _build_quadrant_flat_slots, "quadrant_flat_slots": _build_quadrant_flat_slots,
"cycle_intersect_3": _build_cycle_intersect_3, "cycle_intersect_3": _build_cycle_intersect_3,
"compare_table_2col": _build_compare_table_2col,
} }

View File

@@ -386,3 +386,63 @@ construction_bim_three_usage:
body_key_pattern: "category_{n}_body" body_key_pattern: "category_{n}_body"
empty_label: "" empty_label: ""
empty_body: [] empty_body: []
bim_dx_comparison_table:
# Reason : V4 UAI=1 (01-2 "용어간 상호관계") — UAI tier strongest after F12/F11.
# Pattern : table / compare-rows — 2-column compare table, N category rows.
# Track A frame 4 (Codex round 41 accepted, V4 priority strict).
# NEW builder `compare_table_2col` + NEW parser `compare_row_2col_item`.
template_id: bim_dx_comparison_table
frame_id: 1171281195
family: table
source_shape: top_bullets
cardinality:
strict: 2 # 2 columns (BIM vs DX 등)
overflow_policy: abort_or_review
role_order:
- col_a
- col_b
# min_height_px derivation :
# Figma frame compare table — N rows × ~40 px row height.
# Phase Z compact (title 30 + header 30 + 6 data rows × 35 + padding 30) = 300
# + safety buffer (longer cell content 보호) 50 = **350**. F14 와 동등 class.
# Confirm via smoke + R3 artifact.
visual_hints:
min_height_px: 350
accepted_content_types:
- text_block
# Frame Slot 선언 : header (col_a/col_b) + rows[].
sub_zones:
- id: col_a_header
role: header
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f18b__header > .f18b__header-cell:nth-child(2)"
- id: col_b_header
role: header
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f18b__header > .f18b__header-cell:nth-child(3)"
- id: rows
role: data
accepts: [text_block]
cardinality: { min: 1, max: 12 } # N category rows (typical 4-8)
partial_target_path: ".f18b__rows"
payload:
title:
source: section.title
# NEW builder `compare_table_2col` (phase_z2_mapper.py) +
# NEW parser `compare_row_2col_item` (ITEM_PARSERS).
builder: compare_table_2col
builder_options:
item_parser: compare_row_2col_item # NEW parser — top_bullet → {label, col_a, col_b}
col_a_label_default: "" # MDX 명시 또는 frame default
col_b_label_default: ""
max_rows: 12 # typical 4-8, overflow 보호

View File

@@ -0,0 +1,146 @@
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
{#
─────────────────────────────────────────────────────────────────────────────
Visual Provenance — figma_to_html_agent/blocks/1171281195/ (frame 18)
─────────────────────────────────────────────────────────────────────────────
Frame 18 = "BIM 과 DX 의 이해" (table / compare-rows). 2개 column header (BIM vs DX) +
중앙 category column + N category rows (범위 / S/W / 프로세스 / 성과물 / 활용 /
확장성 / 수행개념 / 주체 등). compare 관점의 다면 비교 표 디자인.
본 partial = Track A frame 4 (Codex round 41 accepted, V4 UAI tier). NEW builder
`compare_table_2col` + NEW parser `compare_row_2col_item` 도입.
3-layer architecture (matrix §0) :
- V4 = matching authority — V4 ranked this frame use_as_is for 01-2.
- figma_to_html = source/evidence — analysis/texts/index.html/flat/assets full.
- Phase Z = runtime orchestration — 본 commit 추가 runtime 자원.
PROMOTED — CSS :
- title gradient (#000 → #883700, F13/F14/F12/F11 zone-title family)
- header row : col_a/col_b background tint + bold typography
- row 별 alternating background (zebra) — readability ↑
- cell border : subtle divider (#A5BBB4 family — F11 와 통일)
NOT PROMOTED (P1 case-by-case, compact zone fit) :
- Figma 의 column header 의 *raster icon* (있는 경우) — compact 에서 효과 ↓
- 다양한 색 강조 / 한자 deco (Figma source 보존, 미 promote)
ADAPTED :
- Figma 표 absolute positioning + zoom → Phase Z flex/grid (3-column table)
- typography → token-fixed (zone-title / sub-title / caption / body)
- row 별 height auto (content-fit)
─────────────────────────────────────────────────────────────────────────────
min_height_px derivation :
title 30 + header 30 + 6 data rows × 35 (compact) + padding 30 = 300
+ safety buffer (longer cell content) 50 = **350** (F14 와 동등 class).
Confirm via smoke + R3 artifact + V4 use_as_is target sample (MDX 01-2 미 trigger
in MDX 03 regression, 단 future batch verification 가능).
─────────────────────────────────────────────────────────────────────────────
slots :
- title : section.title
- col_a_label : 좌 column header (예: "BIM")
- col_b_label : 우 column header (예: "DX")
- rows : list[{label, col_a, col_b}] — N category rows
─────────────────────────────────────────────────────────────────────────────
4-class failure taxonomy :
- class 1 adapter readiness : NEW builder `compare_table_2col` + NEW parser
`compare_row_2col_item` 등록 필수 (PAYLOAD_BUILDERS / ITEM_PARSERS). 본 commit.
- class 2 content-fit : row 수 max 12 (builder limit). cell content 한 줄 한계
— 긴 문장 시 truncate 또는 wrap (CSS overflow-hidden).
#}
<style>
.f18b {
width: 100%; height: 100%;
display: flex; flex-direction: column;
gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
}
.f18b__title {
font-size: var(--font-zone-title);
font-weight: 700;
line-height: var(--lh-zone-title);
background-image: linear-gradient(180deg, #000 0%, #883700 100%);
-webkit-background-clip: text; background-clip: text;
color: transparent;
flex-shrink: 0;
filter: drop-shadow(0 0 3px rgba(50,44,30,0.4));
}
/* compare table — 3-column (category / col_a / col_b) */
.f18b__table {
display: grid;
grid-template-columns: 1fr 2fr 2fr;
gap: 0;
flex: 1 1 auto;
min-height: 0;
border: 1px solid #A5BBB4;
border-radius: 6px;
overflow: hidden;
background: #fff;
}
/* header row */
.f18b__header {
display: contents;
}
.f18b__header-cell {
background: linear-gradient(180deg, rgba(50,31,9,0.85) 0%, rgba(50,31,9,0.95) 100%);
color: #fff;
font-weight: 700;
font-size: var(--font-caption);
line-height: 1.2;
padding: 6px 8px;
text-align: center;
border-bottom: 2px solid #A5BBB4;
}
.f18b__header-cell--category {
background: linear-gradient(180deg, rgba(120,90,40,0.85) 0%, rgba(120,90,40,0.95) 100%);
font-style: italic;
}
/* data rows */
.f18b__rows {
display: contents;
}
.f18b__cell {
padding: 5px 8px;
font-size: var(--font-body);
line-height: var(--lh-body);
border-bottom: 1px solid rgba(165,187,180,0.4);
color: #1a1a1a;
overflow: hidden;
}
.f18b__cell--category {
background: rgba(255,250,240,0.7);
font-weight: 700;
color: #5a4b2e; /* PROMOTED — brown family */
text-align: center;
}
/* zebra striping (visual readability) — odd rows alternate */
.f18b__row:nth-child(odd) .f18b__cell:not(.f18b__cell--category) {
background: rgba(248,250,252,0.6);
}
</style>
<div class="f18b" data-frame-id="1171281195" data-template-id="bim_dx_comparison_table">
<div class="f18b__title">{{ slot_payload.title }}</div>
<div class="f18b__table">
<div class="f18b__header">
<div class="f18b__header-cell f18b__header-cell--category">구분</div>
<div class="f18b__header-cell">{{ slot_payload.col_a_label | safe }}</div>
<div class="f18b__header-cell">{{ slot_payload.col_b_label | safe }}</div>
</div>
<div class="f18b__rows">
{% for row in slot_payload.rows %}
<div class="f18b__row" style="display:contents;">
<div class="f18b__cell f18b__cell--category">{{ row.label | safe }}</div>
<div class="f18b__cell f18b__cell--col-a">{{ row.col_a | safe }}</div>
<div class="f18b__cell f18b__cell--col-b">{{ row.col_b | safe }}</div>
</div>
{% endfor %}
</div>
</div>
</div>