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>
This commit is contained in:
2026-05-13 15:13:46 +09:00
parent 5c27c492ba
commit 73a98b8ad1
4 changed files with 246 additions and 122 deletions

View File

@@ -281,15 +281,29 @@ SELF_CHECK_FIXTURES: dict[str, dict] = {
"emphasis_3_body": [{"text": "특수성 반영 어려움", "indent": 0}],
},
"bim_current_problems_paired": {
# F17 schema-correction — 8 atomic issues per source texts.md (round 55~73 lock).
# paired_rows_4x2_alternating_pills : 4 rows × 2 cells, row 1/3 pill top, row 2/4 pill bottom.
"title": "현황 및 문제점",
"issue_1_label": "개념 부재",
"issue_1_body": [{"text": "CAD 확장판 오인", "indent": 0}],
"issue_2_label": "잘못된 접근방식",
"issue_2_body": [{"text": "도구로만 인식", "indent": 0}],
"issue_3_label": "방향성 상실",
"issue_3_body": [{"text": "대형 S/W 의존", "indent": 0}],
"issue_4_label": "전제조건 오류",
"issue_4_body": [{"text": "건축·토목 혼용", "indent": 0}],
# row 1 : BIM 의미 인식 오류 (개념 부재 + 잘못된 접근방식)
"row_1_left_label": "개념 부재",
"row_1_left_body": [{"text": "BIM을 CAD 확장판으로 오인, 3D 도구 정도로만 인식", "indent": 0}],
"row_1_right_label": "잘못된 접근방식",
"row_1_right_body": [{"text": "단순 업무효율 도구로만 인식, 교육으로 해결될 것으로 판단", "indent": 0}],
# row 2 : 기술 방향 의존 (방향성 상실 + 전제조건 오류)
"row_2_left_label": "방향성 상실",
"row_2_left_body": [{"text": "대형 S/W 회사 제시 내용 추종, 자체 목표설정 기능 상실", "indent": 0}],
"row_2_right_label": "전제조건 오류",
"row_2_right_body": [{"text": "건축·토목 동일 전제로 건축 방식을 토목에 그대로 적용", "indent": 0}],
# row 3 : 실행 주체 혼란 (수행주체 혼란 + 수행방식 무지)
"row_3_left_label": "수행주체 혼란",
"row_3_left_body": [{"text": "학자·발주처 주도, 실행주체 기업·기술자는 기존 방식 고수", "indent": 0}],
"row_3_right_label": "수행방식 무지",
"row_3_right_body": [{"text": "2D 결과 전제, 3D 수행 경험 부재, 비용·시간 증가·품질 미흡", "indent": 0}],
# row 4 : 외부 의존성 (외산S/W 기술예속 + H/W 미비)
"row_4_left_label": "외산S/W 기술예속",
"row_4_left_body": [{"text": "외산 범용 S/W 만으로 BIM 가능 인식, 기술예속 가속", "indent": 0}],
"row_4_right_label": "H/W 미비",
"row_4_right_body": [{"text": "탁상용 PC·Monitor 수준, 고품질 모델 표출 한계", "indent": 0}],
},
}

View File

@@ -577,12 +577,73 @@ def _build_compare_table_2col(section, units, contract) -> dict:
return payload
def _build_paired_rows_4x2_slots(section, units, contract) -> dict:
"""F17-style — paired_rows_4x2_alternating_pills. top_bullets 8 units → 2-axis keyed slots.
1-axis (quadrant_flat_slots = TL/TR/BL/BR) vs 2-axis (row × side) :
- quadrant : index 1..4 → quadrant_N_{label,body}
- paired_rows_4x2 : index 1..8 → row_R_SIDE_{label,body} where R = ceil(i/2), SIDE = left|right
deterministic index mapping per Codex round 60 §Q3 answer + round 70 §1 :
unit 1 → row_1_left unit 2 → row_1_right
unit 3 → row_2_left unit 4 → row_2_right
unit 5 → row_3_left unit 6 → row_3_right
unit 7 → row_4_left unit 8 → row_4_right
strict 8 : under/over → FitError before render (Codex round 60 §3, round 62 acceptance
criterion "no pad_to/truncate_at fallback hides cardinality mismatch").
parser = quadrant_item (label + body heading-less) — F17 atomic issue = single label + single body.
builder_options :
item_parser : ITEM_PARSERS key (default = "quadrant_item")
label_key_pattern : "row_{r}_{side}_label"
body_key_pattern : "row_{r}_{side}_body"
rows : 4
sides : ["left", "right"]
"""
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."
)
label_key = options.get("label_key_pattern", "row_{r}_{side}_label")
body_key = options.get("body_key_pattern", "row_{r}_{side}_body")
rows = options.get("rows", 4)
sides = options.get("sides", ["left", "right"])
expected = rows * len(sides)
if len(units) != expected:
raise ValueError(
f"Contract '{contract['template_id']}' requires strict {expected} units "
f"(rows={rows} × sides={len(sides)}), got {len(units)}. "
f"silent pad/truncate is disabled for paired_rows_4x2_slots."
)
payload: dict = {}
payload.update(_resolve_title(section, contract["payload"], contract))
parsed = [parser(u) for u in units]
idx = 0
for r in range(1, rows + 1):
for side in sides:
payload[label_key.format(r=r, side=side)] = parsed[idx]["label"]
payload[body_key.format(r=r, side=side)] = parsed[idx]["body"]
idx += 1
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,
"paired_rows_4x2_slots": _build_paired_rows_4x2_slots,
}

View File

@@ -635,68 +635,95 @@ sw_reality_three_emphasis:
bim_current_problems_paired:
# Reason : V4 RS=1 (4 MDX sample 안 restructure 1). 32-frame all-in scope —
# **catalog-completeness activation** (NOT V4 endorsement — V4 신호 유효 단 restructure
# tier 이며 직접 use_as_is/light_edit 추천 없음).
# Pattern : cards/paired-rows-2x2 — 4 problem cards in 2x2 grid (BIM 수행 4 문제).
# Track A frame 8. Builder 재사용 = `quadrant_flat_slots` (F16 pattern, pad_to=4).
# Reason : V4 RS=1 (4 MDX sample 안 restructure 1). source 가 8 atomic issues 임을 confirm.
# **schema-correction** — round 55~73 의 review-loop 의 calibration frame.
# 자체 round 71 §4 F17 done-definition lock per Codex round 70/72 + Claude round 63 §5.
# Pattern : cards/paired-rows-4x2 — 4 paired rows × 2 issue cells (top/bottom pill alternation).
# Track A frame 8. 신규 builder = `paired_rows_4x2_slots` (quadrant_flat_slots → 2-axis).
template_id: bim_current_problems_paired
frame_id: 1171281194
family: cards
source_shape: top_bullets
source_shape: top_bullets # mapper split_source allow-list 정합 (Codex round 60)
layout_variant: paired_rows_4x2_alternating_pills # runtime projection model
cardinality:
strict: 4 # 4 issues (2x2 grid)
strict: 8 # 8 atomic issues (4 rows × 2 cells per source texts.md)
overflow_policy: abort_or_review
# row × side deterministic mapping per Codex round 70 §1.
role_order:
- issue_1
- issue_2
- issue_3
- issue_4
- row_1_left
- row_1_right
- row_2_left
- row_2_right
- row_3_left
- row_3_right
- row_4_left
- row_4_right
# min_height_px : F16 (quadrant 4) 와 동등 class.
# 2 row × 2 col × (label 24 + body 60) + title 30 + gaps + padding = ~280 + buffer 70 = **350**.
# min_height_px : provisional 380 (8 cells + title + gaps), smoke 후 1 회 조정 가능.
# pill_positions : source index.html line 161~225 의 row-별 pill 상/하 alternation (top/bottom/top/bottom).
# row_gap_after : source line 199-200 의 `<div class="row-gap">` (행 2-3 사이 시각 단락) per Codex round 58 §3.
visual_hints:
min_height_px: 350
min_height_px: 380
pill_positions: [top, bottom, top, bottom]
row_gap_after: [2]
accepted_content_types:
- text_block
sub_zones:
- id: issue_1
- id: row_1_left
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__grid > .f17b__cell:nth-child(1)"
- id: issue_2
partial_target_path: ".f17b__row:nth-child(1) > .f17b__cell--left"
- id: row_1_right
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__grid > .f17b__cell:nth-child(2)"
- id: issue_3
partial_target_path: ".f17b__row:nth-child(1) > .f17b__cell--right"
- id: row_2_left
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__grid > .f17b__cell:nth-child(3)"
- id: issue_4
partial_target_path: ".f17b__row:nth-child(2) > .f17b__cell--left"
- id: row_2_right
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__grid > .f17b__cell:nth-child(4)"
partial_target_path: ".f17b__row:nth-child(2) > .f17b__cell--right"
- id: row_3_left
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__row:nth-child(3) > .f17b__cell--left"
- id: row_3_right
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__row:nth-child(3) > .f17b__cell--right"
- id: row_4_left
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__row:nth-child(4) > .f17b__cell--left"
- id: row_4_right
role: main_text
accepts: [text_block]
cardinality: { strict: 1 }
partial_target_path: ".f17b__row:nth-child(4) > .f17b__cell--right"
payload:
title:
source: section.title
# Builder 재사용 = `quadrant_flat_slots` (F16 pattern, pad_to=4).
# F16 = TL/TR/BL/BR quadrant. F17 = paired-rows-2x2 (top-left/top-right/bottom-left/bottom-right).
# 차이 = key prefix only ("quadrant_N" vs "issue_N"). partial layout 동일 2x2 grid.
builder: quadrant_flat_slots
# 신규 builder quadrant_flat_slots 와 차이 : 1-axis (TL/TR/BL/BR) → 2-axis (row × side).
# strict 8 + no pad/truncate (silent loss 차단, Codex round 60 §3).
# parser = quadrant_item 재사용 (label + body heading-less, Codex round 60).
builder: paired_rows_4x2_slots
builder_options:
item_parser: quadrant_item # 재사용
pad_to: 4
truncate_at: 4
label_key_pattern: "issue_{n}_label"
body_key_pattern: "issue_{n}_body"
empty_label: ""
empty_body: []
item_parser: quadrant_item
label_key_pattern: "row_{r}_{side}_label"
body_key_pattern: "row_{r}_{side}_body"
rows: 4
sides: [left, right]

View File

@@ -1,80 +1,124 @@
<!-- Phase Z-2 MVP-1.5b frame-derived adapted block.
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt. -->
<!-- Phase Z-2 MVP F17 schema-correction partial — paired_rows_4x2_alternating_pills.
§17 룰 — Figma 시각 언어 promote, geometry 만 zone-compatible adapt.
본 partial = round 55~73 review-loop 의 calibration frame implementation. -->
{#
─────────────────────────────────────────────────────────────────────────────
Frame 17 = "현황 및 문제점" (cards/paired-rows-2x2). 4 problem cards in 2x2 grid.
Track A frame 8 (V4 RS=1 — restructure tier, V4 catalog-completeness 정합).
Builder 재사용 = `quadrant_flat_slots` (F16 pattern, pad_to=4) + `issue_{n}_*` keys.
Frame 17 = "현황 및 문제점" (cards/paired-rows-4x2-alternating-pills).
**source = 8 atomic issues** (4 paired rows × 2 issue cells per row).
이전 strict-4 → source 의 절반 누락. round 55~73 review-loop 의 schema-correction.
PROMOTED CSS : per-issue warning theme (4 distinct color tints), title gradient.
NOT PROMOTED : Figma source decoration / banner / texture / numbered badges — figma_to_html 보존.
ADAPTED : Figma absolute → 2x2 CSS grid + token-fixed typography.
Visual provenance — figma_to_html_agent/blocks/1171281194/{index.html, texts.md, assets}.
- title gradient : rgb(204,82,0) → rgb(136,55,0) (source line 50)
- row border : 3px solid #60A451 (source line 69)
- row bg : rgba(250,237,203,0.15) (source line 68)
- body text : #0c271e (source line 130)
- 분할선 (cell 사이) : 2px dashed #60A451 (source line 135)
- pill alternation : row 1/3 = pill 상단, row 2/4 = pill 하단 (source line 161~225)
- row 2-3 visual gap : source `<div class="row-gap">` (height 65px) line 199-200
slots : title, issue_1/2/3/4_label, issue_1/2/3/4_body.
PROMOTED CSS : source-faithful unified green/cream/brown theme + pill shape +
top/bottom/top/bottom pill alternation + row 2-3 visual gap.
NOT PROMOTED (scope cap per Codex round 64/72 — CSS fallback 정당) :
- pill asset crop variant (`.crop-left` left -45.3% width 145.3% / `.crop-right` width 151.25%) —
source 의 PNG asset 정확 crop reproduction 은 single-pass scope 외 (Codex round 62 §6
+ round 68 scope cap). CSS approximation = pill shape + alternation + theme 모두 보존.
- bottom pill flip image transform (source line 117) — 단순 column-reverse 로 처리.
ADAPTED : Figma absolute → 4-row flex layout + token-fixed typography.
slots (17) : title +
row_{1..4}_{left,right}_{label,body} (8 × 2 = 16).
no row headers (source 부재, infer 금지 per Codex round 58 §1 + round 70 §3.6).
#}
<style>
.f17b {
width: 100%; height: 100%;
display: flex; flex-direction: column;
gap: 6px;
gap: 8px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
}
/* title — source line 50 gradient + drop-shadow (text-shadow approximation) */
.f17b__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%);
background-image: linear-gradient(rgb(204,82,0), rgb(136,55,0));
-webkit-background-clip: text; background-clip: text;
color: transparent;
flex-shrink: 0;
filter: drop-shadow(0 0 3px rgba(50,44,30,0.4));
}
/* 2x2 grid */
.f17b__grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 8px;
/* rows 컨테이너 (4 paired rows) */
.f17b__rows {
display: flex; flex-direction: column;
gap: 4px;
flex: 1 1 auto;
min-height: 0;
}
.f17b__cell {
display: flex; flex-direction: column;
border: 2px solid;
border-radius: 8px;
overflow: hidden;
background: #fff;
min-height: 0;
}
/* per-issue color theme (problem warning family) */
.f17b__cell:nth-child(1) { border-color: #dc2626; } /* red */
.f17b__cell:nth-child(2) { border-color: #ea580c; } /* orange */
.f17b__cell:nth-child(3) { border-color: #d97706; } /* amber */
.f17b__cell:nth-child(4) { border-color: #b45309; } /* deep amber */
.f17b__label {
/* row 2 뒤 visual gap (visual_hints.row_gap_after = [2]) — source line 199-200 */
.f17b__row:nth-child(2) { margin-bottom: 10px; }
/* row = 좌우 2 cells flex */
.f17b__row {
display: flex; flex-direction: row;
gap: 6px;
flex: 1 1 0;
min-height: 0;
border: 2px solid #60A451;
border-radius: 8px;
background: rgba(250,237,203,0.15);
overflow: hidden;
position: relative;
}
/* cell — pill alternation per visual_hints.pill_positions [top,bottom,top,bottom] */
.f17b__cell {
flex: 1 1 0;
display: flex; flex-direction: column;
padding: 8px 10px;
min-width: 0;
gap: 4px;
}
/* 분할선 — source line 135 dashed 2px #60A451 (cell 사이) */
.f17b__cell--right { border-left: 2px dashed #60A451; }
/* row 1, 3 = pill 상단 (default column) */
.f17b__row:nth-child(1) .f17b__cell,
.f17b__row:nth-child(3) .f17b__cell { flex-direction: column; }
/* row 2, 4 = pill 하단 (column-reverse — pill 이미지 flip 은 단순 reverse 로 approximate per source line 117) */
.f17b__row:nth-child(2) .f17b__cell,
.f17b__row:nth-child(4) .f17b__cell { flex-direction: column-reverse; }
/* pill — CSS approximation (source assets/*.png crop variant fallback per round 64/72 scope cap).
pill shape (rounded) + green/cream/brown theme 보존. */
.f17b__pill {
display: inline-flex; align-items: center; justify-content: center;
background-image: linear-gradient(180deg, #60A451 0%, #3d6d34 100%);
color: #fff;
font-weight: 700;
font-size: var(--font-sub-title);
line-height: 1.15;
padding: 5px 10px;
padding: 4px 14px;
border-radius: 999px;
text-align: center;
flex-shrink: 0;
letter-spacing: -0.02em;
align-self: flex-start;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
}
.f17b__cell:nth-child(1) .f17b__label { background: linear-gradient(180deg, #dc2626 0%, #991b1b 100%); }
.f17b__cell:nth-child(2) .f17b__label { background: linear-gradient(180deg, #ea580c 0%, #9a3412 100%); }
.f17b__cell:nth-child(3) .f17b__label { background: linear-gradient(180deg, #d97706 0%, #92400e 100%); }
.f17b__cell:nth-child(4) .f17b__label { background: linear-gradient(180deg, #b45309 0%, #78350f 100%); }
/* body — source line 130 color #0c271e */
.f17b__body {
flex: 1 1 auto;
overflow: hidden;
padding: 5px 8px 6px;
color: #1a1a1a;
color: #0c271e;
min-height: 0;
display: flex; flex-direction: column;
gap: 2px;
@@ -83,55 +127,33 @@ slots : title, issue_1/2/3/4_label, issue_1/2/3/4_body.
color: inherit;
font-size: var(--font-body);
line-height: var(--lh-body);
position: relative;
padding-left: 12px;
}
.f17b__body .text-line--bullet::before {
content: "!";
position: absolute;
left: 2px; top: 0;
font-weight: 700;
}
.f17b__cell:nth-child(1) .f17b__body .text-line--bullet::before { color: #dc2626; }
.f17b__cell:nth-child(2) .f17b__body .text-line--bullet::before { color: #ea580c; }
.f17b__cell:nth-child(3) .f17b__body .text-line--bullet::before { color: #d97706; }
.f17b__cell:nth-child(4) .f17b__body .text-line--bullet::before { color: #b45309; }
</style>
<div class="f17b" data-frame-id="1171281194" data-template-id="bim_current_problems_paired">
<div class="f17b__title">{{ slot_payload.title }}</div>
<div class="f17b__grid">
<div class="f17b__cell">
<div class="f17b__label">{{ slot_payload.issue_1_label | safe }}</div>
<div class="f17b__body">
{% if slot_payload.issue_1_body %}
{% for line in slot_payload.issue_1_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div>
<div class="f17b__cell">
<div class="f17b__label">{{ slot_payload.issue_2_label | safe }}</div>
<div class="f17b__body">
{% if slot_payload.issue_2_body %}
{% for line in slot_payload.issue_2_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div>
<div class="f17b__cell">
<div class="f17b__label">{{ slot_payload.issue_3_label | safe }}</div>
<div class="f17b__body">
{% if slot_payload.issue_3_body %}
{% for line in slot_payload.issue_3_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div>
<div class="f17b__cell">
<div class="f17b__label">{{ slot_payload.issue_4_label | safe }}</div>
<div class="f17b__body">
{% if slot_payload.issue_4_body %}
{% for line in slot_payload.issue_4_body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
<div class="f17b__rows">
{% for r in [1, 2, 3, 4] %}
<div class="f17b__row">
<div class="f17b__cell f17b__cell--left">
<div class="f17b__pill">{{ slot_payload['row_' ~ r ~ '_left_label'] | safe }}</div>
<div class="f17b__body">
{% set body_left = slot_payload['row_' ~ r ~ '_left_body'] %}
{% if body_left %}
{% for line in body_left %}<div class="text-line{% if line.indent and line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div>
<div class="f17b__cell f17b__cell--right">
<div class="f17b__pill">{{ slot_payload['row_' ~ r ~ '_right_label'] | safe }}</div>
<div class="f17b__body">
{% set body_right = slot_payload['row_' ~ r ~ '_right_body'] %}
{% if body_right %}
{% for line in body_right %}<div class="text-line{% if line.indent and line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
</div>