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

@@ -234,9 +234,39 @@ def parse_quadrant_item(unit: tuple[str, list[str]]) -> dict:
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] = {
"pillar_item": parse_pillar_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
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] = {
"items_with_role": _build_items_with_role,
"process_product_pair": _build_process_product_pair,
"quadrant_flat_slots": _build_quadrant_flat_slots,
"cycle_intersect_3": _build_cycle_intersect_3,
"compare_table_2col": _build_compare_table_2col,
}