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)
This commit is contained in:
2026-05-13 12:21:02 +09:00
parent c7b0f5bde1
commit f7a9240fe5
2 changed files with 40 additions and 16 deletions

View File

@@ -237,25 +237,21 @@ def parse_quadrant_item(unit: tuple[str, list[str]]) -> dict:
def parse_compare_row_2col_item(unit: tuple[str, list[str]]) -> dict: 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. """F18-style — bold = category label, nested 2 bullets = col_a / col_b values.
Pattern : top bullet = **카테고리**, nested = `- BIM: ...` / `- DX: ...` 또는 Pattern : top bullet = **카테고리**, nested = first 2 bullets.
단순 ordering (첫 nested = col_a, 두번째 = col_b). prefix "BIM:" / "DX:" *Parser 는 prefix stripping 안 함* (Codex round 43 §F1-b — narrow alias 정정).
있으면 stripping. Prefix stripping 은 *builder 의 strip_col_prefix_aliases option* 으로 위임.
Returns: Returns:
{label, col_a, col_b} {label, col_a, col_b}
""" """
top_line, nested_lines = unit top_line, nested_lines = unit
label = _extract_bold_or_plain(top_line) label = _extract_bold_or_plain(top_line)
# nested bullets — strip bullet marker, take first 2 # nested bullets — strip bullet marker, take first 2 (no prefix stripping)
nested = [] nested = []
for l in nested_lines: for l in nested_lines:
l_strip = l.strip() l_strip = l.strip()
if re.match(r"^[\*\-]\s", l_strip): if re.match(r"^[\*\-]\s", l_strip):
txt = re.sub(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) txt = re.sub(r"\*\*(.+?)\*\*", r"<strong>\1</strong>", txt)
nested.append(txt) nested.append(txt)
col_a = nested[0] if len(nested) > 0 else "" col_a = nested[0] if len(nested) > 0 else ""
@@ -525,10 +521,13 @@ def _build_compare_table_2col(section, units, contract) -> dict:
rows : list[{label, col_a, col_b}] — top_bullets 각각 → row rows : list[{label, col_a, col_b}] — top_bullets 각각 → row
builder_options : builder_options :
item_parser : ITEM_PARSERS key (예: `compare_row_2col_item`) item_parser : ITEM_PARSERS key (예: `compare_row_2col_item`)
col_a_label_default : col_a header (MDX 첫 행에 명시 안 되면 사용. default "") col_a_label_default : col_a header (MDX 미명시 시 fallback. F1-a fix)
col_b_label_default : col_b header (default "") col_b_label_default : col_b header (MDX 미명시 시 fallback)
max_rows : N (default 999 — practical 한계). 초과 시 _truncated_count strip_col_prefix_aliases : list[str] — col_a/col_b 값의 prefix `<alias>:`
를 strip (Codex round 43 §F1-b — narrow alias).
예 : ["BIM", "DX"]. default [] (no stripping).
max_rows : N (default 999 — practical 한계).
""" """
options = contract["payload"]["builder_options"] options = contract["payload"]["builder_options"]
parser_name = options["item_parser"] parser_name = options["item_parser"]
@@ -541,6 +540,7 @@ def _build_compare_table_2col(section, units, contract) -> dict:
col_a_label = options.get("col_a_label_default", "") col_a_label = options.get("col_a_label_default", "")
col_b_label = options.get("col_b_label_default", "") col_b_label = options.get("col_b_label_default", "")
strip_aliases = options.get("strip_col_prefix_aliases", []) or []
max_rows = options.get("max_rows", 999) max_rows = options.get("max_rows", 999)
payload: dict = {} payload: dict = {}
@@ -548,8 +548,27 @@ def _build_compare_table_2col(section, units, contract) -> dict:
payload["col_a_label"] = col_a_label payload["col_a_label"] = col_a_label
payload["col_b_label"] = col_b_label payload["col_b_label"] = col_b_label
# Compile precise prefix patterns per alias (Codex round 43 §F1-b narrow).
strip_patterns = [
re.compile(rf"^{re.escape(a)}\s*[:]\s*(.+)$")
for a in strip_aliases
]
def _strip_alias(value: str) -> str:
for pat in strip_patterns:
m = pat.match(value)
if m:
return m.group(1).strip()
return value
visible = list(units[:max_rows]) visible = list(units[:max_rows])
rows = [parser(u) for u in visible] rows = []
for u in visible:
row = parser(u)
if strip_patterns:
row["col_a"] = _strip_alias(row.get("col_a", ""))
row["col_b"] = _strip_alias(row.get("col_b", ""))
rows.append(row)
payload["rows"] = rows payload["rows"] = rows
if len(units) > max_rows: if len(units) > max_rows:

View File

@@ -398,8 +398,10 @@ bim_dx_comparison_table:
family: table family: table
source_shape: top_bullets source_shape: top_bullets
# NOTE (Codex round 43 §F1-c) : top-level `cardinality.strict: 2` = *column 수*
# (col_a / col_b). data row 수 는 별 — `sub_zones.rows.cardinality` 의 `{min:1, max:12}`.
cardinality: cardinality:
strict: 2 # 2 columns (BIM vs DX 등) strict: 2 # 2 columns (col_a / col_b) — NOT row count
overflow_policy: abort_or_review overflow_policy: abort_or_review
role_order: role_order:
@@ -443,6 +445,9 @@ bim_dx_comparison_table:
builder: compare_table_2col builder: compare_table_2col
builder_options: builder_options:
item_parser: compare_row_2col_item # NEW parser — top_bullet → {label, col_a, col_b} item_parser: compare_row_2col_item # NEW parser — top_bullet → {label, col_a, col_b}
col_a_label_default: "" # MDX 명시 또는 frame default col_a_label_default: "BIM" # F1-a (Codex round 43) — explicit default
col_b_label_default: "" col_b_label_default: "DX" # F1-a — explicit default
strip_col_prefix_aliases: # F1-b (Codex round 43) — narrow alias 만 strip
- "BIM"
- "DX"
max_rows: 12 # typical 4-8, overflow 보호 max_rows: 12 # typical 4-8, overflow 보호