feat(#65): IMP-36 fit/rotation generalization (u1~u8)

Generalize Phase Z frame partial responsive fit / rotation to four canonical
F13/F14/F20/F8 family partials. Surface = 13 canonical partials; 19
builder-only contracts remain explicitly out of scope.

u1  test_imp17_comment_anchor: re-pin L570->L578 (restructure+IMP-17),
    L571->L579 (IMP-29 -> IMP-47B supersession). Stage 1 red baseline gate.
u2  frame_contracts.yaml: add rotation_eligible (P1) + body_fit_pattern2 (P2)
    bool axes on 13 partial-backed contracts. P1 True: F13/F14/F20/F8 (4).
    P2 True: F23 + P1_set (5). F29 columns[1].body_parser column_plain ->
    column_with_transform (P3 parity).
u3  test_imp36_fit_rotation_generalization (NEW, 166 lines): static
    parametrized assertions for P1 metadata + CQ presence, P1 opt-out
    absence, P2 --max-body-lines + clamp + cqh, P2 opt-out absence, 19
    builder-only exclusion.
u4  three_parallel_requirements (F13): introduce f13b-root container-name +
    container-type:size + @container (aspect-ratio<1.5) rotation;
    add inline --max-body-lines + body line-height clamp/cqh/calc.
u5  three_persona_benefits (F14): f14b-root P1 + P2 cqh/jinja body fit.
    Persona colors (#285b4a/#445a2f/#743002) and circle SVG aspect 1/1
    preserved.
u6  dx_sw_necessity_three_perspectives (F20): f20b-root P1 + P2 cqh/jinja
    body fit under IMP-49 partial-fidelity lock.
u7  info_management_what_how_when (F8): f8b-root P1 + P2 cqh/jinja body fit.
u8  test_imp36_overflow_chain_self_fire (NEW, 299 lines): Selenium self-fire
    harness for F13/F14/F20/F8 at aspect 1.78 vs 1.0. Asserts line-height
    changes, font-size invariance across all 4 frames (no per-frame exempt),
    grid columns rotate 3 -> 1, OVERFLOW_CASCADE_ORDER remains 4-tuple.

Stage 4 verification (HEAD 6f1c736 pre-commit baseline):
  u1 2/2 PASS, u3 33/33 PASS, u8 9/9 PASS (live Chrome).
  Regression sweep tests/phase_z2 + tests/orchestrator_unit 335/335 PASS.
  font-size mutations introduced: 0.
  Pre-existing red (test_imp47b_step12_ai_wiring x3, ai_fallback_master_flag
  default_off x1) verified unchanged via stash swap -> not introduced.

Guardrails honored:
  - cqh / clamp / container query only (no shared margin/padding/gap shrink).
  - font-size invariant under aspect change (P2 mutates line-height +
    --max-body-lines only).
  - No cross-frame .fNb__ class borrowing (IMP-49 partial-fidelity lock).
  - F14 circle SVG aspect 1/1 untouched; persona colors preserved.
  - AI isolation: no HTML structure generation; AI calls remain zone-content.
  - 1 turn = 1 step; commit excludes .claude/settings.json and all
    out-of-scope untracked worktree per Stage 4 binding contract.

source_comment_ids: Stage 1 #13/#14; Stage 2 #21/#22; Stage 3 #4 + Codex #4
YES; Stage 4 Claude #1 + Codex #3 PASS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-23 01:18:20 +09:00
parent 6f1c7367e0
commit c1df656312
8 changed files with 644 additions and 20 deletions

View File

@@ -23,6 +23,10 @@ three_parallel_requirements:
frame_id: 1171281190
family: three_parallel
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: true # P1: aspect-ratio container-query rotation
body_fit_pattern2: true # P2: cqh/clamp/--max-body-lines body fit
source_shape: top_bullets
cardinality:
strict: 3
@@ -79,6 +83,10 @@ process_product_two_way:
frame_id: 1171281210
family: two_column_h3
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 2-col compare table — rotation 부적합 opt-out
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: h3_subsections
cardinality:
strict: 2 # F29 frame = 2 visual columns. ≠2 → fallback.
@@ -122,7 +130,7 @@ process_product_two_way:
body_parser: column_with_transform # 첫 top-bullet AS-IS/TO-BE 표 인식
- title_to: banner_right
body_to: product
body_parser: column_plain # 모든 section = 일반 text_lines
body_parser: column_with_transform # IMP-36 (Gitea #65 u2) P3 parity — process column 과 동일 transform 인식 (좌/우 대칭)
bim_issues_quadrant_four:
@@ -130,6 +138,10 @@ bim_issues_quadrant_four:
frame_id: 1171281193
family: bim_issues_quadrant
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 4-quadrant 고정 grid — rotation 부적합
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets
# F16 정책 = pad_to=4 + truncate>4 (legacy 와 동일). cardinality strict 화는 본 transition 범위 외.
# 향후 normal path 안정 후 strict 적용 + 위반 시 fallback path (FitError) 검토.
@@ -193,6 +205,10 @@ three_persona_benefits:
frame_id: 1171281191
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: true # P1: aspect-ratio container-query rotation
body_fit_pattern2: true # P2: cqh/clamp/--max-body-lines body fit
source_shape: top_bullets
cardinality:
strict: 3 # 3 persona = strict.
@@ -258,6 +274,10 @@ construction_goals_three_circle_intersection:
frame_id: 1171281189
family: diagram
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 3-circle SVG diagram — rotation 부적합 (좌표 의존)
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets
cardinality:
strict: 3 # 3 메인 원 — strict.
@@ -328,6 +348,10 @@ construction_bim_three_usage:
frame_id: 1171281182
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 3 stacked rows — rotation 부적합 (수평 row 구조)
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets
cardinality:
strict: 3
@@ -397,6 +421,10 @@ bim_dx_comparison_table:
frame_id: 1171281195
family: table
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 2-col compare table — rotation 부적합 opt-out (issue body 명시)
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
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}`.
@@ -462,6 +490,10 @@ dx_sw_necessity_three_perspectives:
frame_id: 1171281198
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: true # P1: aspect-ratio container-query rotation
body_fit_pattern2: true # P2: cqh/clamp/--max-body-lines body fit
source_shape: top_bullets
cardinality:
strict: 3 # 3 perspective columns
@@ -528,6 +560,10 @@ info_management_what_how_when:
frame_id: 1171281179
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: true # P1: aspect-ratio container-query rotation
body_fit_pattern2: true # P2: cqh/clamp/--max-body-lines body fit
source_shape: top_bullets
cardinality:
strict: 3 # 3 sections (What / How / When)
@@ -587,6 +623,10 @@ sw_reality_three_emphasis:
frame_id: 1171281209
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 미적용 (Stage 2 selection — future eligibility TBD)
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets
cardinality:
strict: 3
@@ -644,6 +684,10 @@ bim_current_problems_paired:
frame_id: 1171281194
family: cards
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 4x2 paired rows — rotation 부적합 (2-axis row×side 구조)
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets # mapper split_source allow-list 정합 (Codex round 60)
layout_variant: paired_rows_4x2_alternating_pills # runtime projection model
cardinality:
@@ -735,6 +779,10 @@ app_sw_package_vs_solution:
frame_id: 1171281203
family: table
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 2-col compare table — rotation 부적합 opt-out
body_fit_pattern2: true # P2: cqh/clamp/--max-body-lines body fit (Stage 1 canonical source)
source_shape: h3_subsections # F29 와 동일 — 2 h3 subsection = 2 column.
cardinality:
strict: 2 # 2 column (Package / Solution) — NOT row count.
@@ -783,6 +831,10 @@ pre_construction_model_info_stacked:
frame_id: 1171281180
family: list
# IMP-36 (Gitea #65 u2) partial-backed contract axis bools.
rotation_eligible: false # P1: 5-color cycle pill list — rotation 부적합
body_fit_pattern2: false # P2: 미적용 (Stage 2 selection)
source_shape: top_bullets
cardinality:
min: 4 # analysis.md min 4 / Figma 5-color cycle design floor.

View File

@@ -65,6 +65,12 @@ slots :
gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
/* IMP-36 (Gitea #65 u6) P1 root — enable container queries on .f20b.
container-type: size unlocks cqh/cqi/cqw + aspect-ratio matching against
this element. container-name: f20b-root namespaces the @container rule
below (partial-fidelity lock per IMP-49 #78 — no cross-frame borrowing). */
container-type: size;
container-name: f20b-root;
}
.f20b__title {
font-size: var(--font-zone-title);
@@ -131,6 +137,25 @@ slots :
color: #1d4d3e; /* PROMOTED — verbatim from upstream :208 (.card-title-1 -webkit-text-stroke) */
font-weight: 700;
}
/* IMP-36 (Gitea #65 u6) P2 body fit — line-height clamp scales with the
per-column bullet count via the inline `--max-body-lines` Jinja style on
each `.f20b__body`. 60cqh ≈ 3-col card body share of `.f20b` root height
(zone-title ~15cqh + card header ~10cqh + remaining ~75cqh, split 60cqh
for text lines + reserve for padding/gap). font-size unchanged
(guardrail #6 — Stage 2 spec). Fallback 3 = file-header default
"body 3-5 bullets per column". */
.f20b__body .text-line {
line-height: clamp(1.15em, calc(60cqh / var(--max-body-lines, 3)), 1.6em);
}
/* IMP-36 (Gitea #65 u6) P1 rotation rule — when the surrounding zone is
narrow (aspect-ratio < 1.5), collapse the 3-column grid to a single
column. The threshold matches the IMP-36 Stage 2 canonical (vertical-2 /
세로형). Card header / body / bullet styles remain unchanged. */
@container f20b-root (aspect-ratio < 1.5) {
.f20b__cols { grid-template-columns: 1fr; }
}
</style>
<!-- IMP-49 #78 u2 — namespace scope note :
@@ -148,7 +173,8 @@ slots :
{# 3 columns — quadrant_flat_slots produces perspective_N_label / perspective_N_body for N=1..3 #}
<div class="f20b__col">
<div class="f20b__header">{{ slot_payload.perspective_1_label | safe }}</div>
<div class="f20b__body">
{# IMP-36 (Gitea #65 u6) P2 — inline `--max-body-lines` per column; code composes (Phase Z guardrail #7), defensive fallback to 3 matches the CSS default. #}
<div class="f20b__body" style="--max-body-lines: {{ (slot_payload.perspective_1_body | length) if slot_payload.perspective_1_body else 3 }};">
{% if slot_payload.perspective_1_body %}
{% for line in slot_payload.perspective_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 %}
@@ -156,7 +182,7 @@ slots :
</div>
<div class="f20b__col">
<div class="f20b__header">{{ slot_payload.perspective_2_label | safe }}</div>
<div class="f20b__body">
<div class="f20b__body" style="--max-body-lines: {{ (slot_payload.perspective_2_body | length) if slot_payload.perspective_2_body else 3 }};">
{% if slot_payload.perspective_2_body %}
{% for line in slot_payload.perspective_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 %}
@@ -164,7 +190,7 @@ slots :
</div>
<div class="f20b__col">
<div class="f20b__header">{{ slot_payload.perspective_3_label | safe }}</div>
<div class="f20b__body">
<div class="f20b__body" style="--max-body-lines: {{ (slot_payload.perspective_3_body | length) if slot_payload.perspective_3_body else 3 }};">
{% if slot_payload.perspective_3_body %}
{% for line in slot_payload.perspective_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 %}

View File

@@ -48,6 +48,12 @@ slots : title, section_1/2/3_label, section_1/2/3_body
gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
/* IMP-36 (Gitea #65 u7) P1 — container-type: size unlocks cqh/cqi/cqw +
aspect-ratio measurement on .f8b. container-name: f8b-root namespaces
the rotation rule below (IMP-49 partial-fidelity lock — no cross-frame
.fNb__ class borrowing). */
container-type: size;
container-name: f8b-root;
}
.f8b__title {
font-size: var(--font-zone-title);
@@ -110,6 +116,16 @@ slots : title, section_1/2/3_label, section_1/2/3_body
position: relative;
padding-left: 14px;
}
/* IMP-36 (Gitea #65 u7) P2 body fit — line-height cqh/clamp against the
bullet count rendered per column. font-size 미변경 (사용자 룰 + Stage 2
guardrail #6). 60cqh = approx body region share of .f8b after title +
section header (title ≈ 15cqh + per-col header ≈ 12cqh → body ≈ 60cqh).
fallback = 4 (file header L42 watch threshold "body 5+ bullets per
column" → typical < 5). Additive cascade override; does not mutate the
pre-existing .f8b__body .text-line block above. */
.f8b__body .text-line {
line-height: clamp(1.15em, calc(60cqh / var(--max-body-lines, 4)), 1.6em);
}
.f8b__body .text-line--bullet::before {
content: "•";
position: absolute;
@@ -119,6 +135,14 @@ slots : title, section_1/2/3_label, section_1/2/3_body
.f8b__col:nth-child(1) .f8b__body .text-line--bullet::before { color: #2563eb; }
.f8b__col:nth-child(2) .f8b__body .text-line--bullet::before { color: #ea580c; }
.f8b__col:nth-child(3) .f8b__body .text-line--bullet::before { color: #16a34a; }
/* IMP-36 (Gitea #65 u7) P1 rotation rule — when zone aspect-ratio narrows
below 1.5 (vertical-2 narrow / 임의 세로형 geometry), flip the 3-column
grid to single column. Card header / body / bullet styles unchanged —
only grid-template-columns flips from 1fr 1fr 1fr to 1fr. */
@container f8b-root (aspect-ratio < 1.5) {
.f8b__cols { grid-template-columns: 1fr; }
}
</style>
<div class="f8b" data-frame-id="1171281179" data-template-id="info_management_what_how_when">
@@ -126,7 +150,7 @@ slots : title, section_1/2/3_label, section_1/2/3_body
<div class="f8b__cols">
<div class="f8b__col">
<div class="f8b__header">{{ slot_payload.section_1_label | safe }}</div>
<div class="f8b__body">
<div class="f8b__body" style="--max-body-lines: {{ (slot_payload.section_1_body | length) if slot_payload.section_1_body else 4 }};">
{% if slot_payload.section_1_body %}
{% for line in slot_payload.section_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 %}
@@ -134,7 +158,7 @@ slots : title, section_1/2/3_label, section_1/2/3_body
</div>
<div class="f8b__col">
<div class="f8b__header">{{ slot_payload.section_2_label | safe }}</div>
<div class="f8b__body">
<div class="f8b__body" style="--max-body-lines: {{ (slot_payload.section_2_body | length) if slot_payload.section_2_body else 4 }};">
{% if slot_payload.section_2_body %}
{% for line in slot_payload.section_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 %}
@@ -142,7 +166,7 @@ slots : title, section_1/2/3_label, section_1/2/3_body
</div>
<div class="f8b__col">
<div class="f8b__header">{{ slot_payload.section_3_label | safe }}</div>
<div class="f8b__body">
<div class="f8b__body" style="--max-body-lines: {{ (slot_payload.section_3_body | length) if slot_payload.section_3_body else 4 }};">
{% if slot_payload.section_3_body %}
{% for line in slot_payload.section_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 %}

View File

@@ -34,6 +34,12 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
gap: 4px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
/* IMP-36 (Gitea #65 u4) P1 — partial-side container query root.
container-type: size 로 aspect-ratio 측정 가능 (cqh / cqi / cqw 도
동일 root 기준). container-name: f13b-root 는 frame_contracts.yaml
rotation_eligible: true 와 짝. */
container-type: size;
container-name: f13b-root;
}
.f13b__title {
font-size: var(--font-zone-title);
@@ -126,6 +132,22 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
}
/* desc 안 .text-line 색 override */
.f13b__desc .text-line { color: #3E3523; }
/* IMP-36 (Gitea #65 u4) P2 — body fit via cqh + clamp + --max-body-lines.
section 의 text_lines 개수가 늘면 line-height 가 비례로 축소. font-size
미변경 (사용자 룰). --max-body-lines fallback = 4 (section 평균 줄 수).
20cqh = 한 section 이 차지하는 .f13b 컨테이너 비율 근사치 (3 section /
col, body 영역 ≈ 80cqh → 25cqh/section 중 line 영역 ≈ 20cqh). */
.f13b__desc {
line-height: clamp(1.2em, calc(20cqh / var(--max-body-lines, 4)), 1.6em);
}
/* IMP-36 (Gitea #65 u4) P1 — aspect-ratio < 1.5 rotation rule. zone 의
가로:세로 비가 1.5 미만으로 좁아지면 (vertical-2 narrow / 또는 임의
세로형 geometry) 3-col grid 가 1-col stack 으로 회전. */
@container f13b-root (aspect-ratio < 1.5) {
.f13b__cols { grid-template-columns: 1fr; }
}
</style>
<div class="f13b" data-frame-id="1171281190" data-template-id="three_parallel_requirements">
@@ -144,7 +166,7 @@ slots: title, pillars[].{label, color_class, sections[].{heading, bullets[]}}
<div class="f13b__section">
<div class="f13b__heading">{{ section.heading | safe }}</div>
{% if section.text_lines %}
<div class="f13b__desc">
<div class="f13b__desc" style="--max-body-lines: {{ section.text_lines | length }};">
{% for line in section.text_lines %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
</div>
{% endif %}

View File

@@ -70,6 +70,13 @@ Asset path runtime resolution :
gap: 6px;
font-family: 'Noto Sans KR', 'Pretendard', sans-serif;
word-break: keep-all;
/* IMP-36 (Gitea #65 u5) P1 — partial-side container query root.
container-type: size 로 aspect-ratio 측정 가능 (cqh / cqi / cqw 도
동일 root 기준). container-name: f14b-root 는 frame_contracts.yaml
rotation_eligible: true 와 짝. Circle badge (.f14b__badge aspect-ratio
1/1) 는 별도 element — 본 root 의 aspect-ratio 측정 대상 아님. */
container-type: size;
container-name: f14b-root;
}
.f14b__title {
font-size: var(--font-zone-title);
@@ -176,6 +183,22 @@ Asset path runtime resolution :
position: relative;
padding-left: 14px;
}
/* IMP-36 (Gitea #65 u5) P2 — body fit via cqh + clamp + --max-body-lines.
persona.body 의 bullet 개수가 늘면 line-height 가 비례로 축소. font-size
미변경 (사용자 룰). --max-body-lines fallback = 7 (Figma 원본 frame 평균
bullet 수, file header L8 참조). 60cqh = .f14b__body 영역 비율 근사치
(title ≈ 15cqh + badge ≈ 18cqh + photo ≈ 7cqh → body ≈ 60cqh). 본 clamp
은 .text-line 의 var(--lh-body) 를 override (cascade 우선순위). */
.f14b__body .text-line {
line-height: clamp(1.15em, calc(60cqh / var(--max-body-lines, 7)), 1.6em);
}
/* IMP-36 (Gitea #65 u5) P1 — aspect-ratio < 1.5 rotation rule. zone 의
가로:세로 비가 1.5 미만으로 좁아지면 3-col grid 가 1-col stack 으로
회전. Circle badge (.f14b__badge aspect-ratio 1/1) 는 col 내부 element
이므로 회전 후에도 원형 유지. */
@container f14b-root (aspect-ratio < 1.5) {
.f14b__cols { grid-template-columns: 1fr; }
}
.f14b__body .text-line--bullet::before {
content: "\2713";
position: absolute;
@@ -226,7 +249,7 @@ Asset path runtime resolution :
</div>
{# body — bullets with CSS check marker #}
<div class="f14b__body">
<div class="f14b__body" style="--max-body-lines: {{ (persona.body | length) if persona.body else 7 }};">
{% if persona.body %}
{% for line in persona.body %}<div class="text-line text-line--bullet{% if line.indent > 0 %} text-line--indent-{{ line.indent }}{% endif %}">{{ line.text | safe }}</div>{% endfor %}
{% endif %}