Files
C.E.L_Slide_test2/figma_to_html_agent/PROCESS.md
kyeongmin 9fbe3ac90c add: figma_to_html_agent/blocks/ + 변환 도구 docs 갱신
전체 401 files (397 추가 + 4 수정), 14304 insertions.

추가:
- figma_to_html_agent/blocks/ — Figma 변환 결과 (32 frame, ~79MB).
  각 frame folder = {analysis.md, flat.md, texts.md, index.html, assets/,
  _renders/, _render.py, RELATIONSHIPS.md / STATUS.md / classification.md
  (일부 frame)}.
  Phase Z 의 *figma source layer* — runtime 에서 직접 사용 X, contract /
  partial / builder adapter (미래 axis A) 의 source.
- figma_to_html_agent/DISCUSSION-SUMMARY-20260411.md — 변환 설계 회의 기록.
- figma_to_html_agent/HARNESS.md — 변환 검증 harness.
- figma_to_html_agent/scripts/fetch_figma_screenshots.py — Figma 스크린샷 자동 수집.

수정:
- figma_to_html_agent/PROCESS-CONTROL.md / PROCESS.md / RULES.md —
  변환 프로세스 / 룰 갱신 (R8/R9 lock 강화 등).
- figma_to_html_agent/blocks_index.md — 32 frame 인덱스 갱신.

Phase Z 영향 0 (figma_to_html_agent/blocks/ 가 V4 catalog +
templates/phase_z2/families adapter 의 source — runtime 에서 직접 import X).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:41:05 +09:00

530 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 변환 절차 (10 STEP)
Figma 프레임 1개를 HTML+template으로 변환할 때 매번 동일하게 따르는 운영 핸드북.
> **원칙: 같은 세션에서 여러 프레임 연속 작업 OK.** 컨텍스트가 무거워지면 `/compact` 로 정리하고 계속 진행. 핵심 결정/규칙/산출물은 모두 파일에 박혀있어 compact 후에도 보존됨. 이렇게 해야 누적 학습(R13 등 sub-pattern)이 즉시 적용됨. CLAUDE.md 원칙 7 참조.
> **원칙: 1 프레임 변환 = 1:1 reference + 템플릿 동시 작성.** 1번째 등장이라도 templates_staging/{pattern}.html.j2 + meta.yaml + example.yaml 까지 작성한다. 정적 HTML만 두는 것은 work-creating-work. 사용자가 final 검수 후 design_agent/templates/ 로 직접 프로모션.
---
## STEP 0 — 준비
### 0-A. 에이전트: blocks_index.md 한 번 읽기 (필수)
새 세션은 메모리가 없다. 패턴 발견 트리거(2번째 등장)가 작동하려면 **세션 시작 직후** [blocks_index.md](blocks_index.md)를 한 번 통으로 읽어야 한다.
```
Read figma_to_html_agent/blocks_index.md
```
확인할 것:
- "변환 완료 (현행 방법론)" 섹션의 패턴 목록
- "패턴 카탈로그" 섹션의 등록 패턴 (등장 횟수)
- "templates_staging 대기열" 의 진행 중 패턴
### 0-B. 사용자: 프레임 선택
1. Figma desktop에서 변환할 프레임을 **선택** (클릭) 한다
2. 에이전트에게 "이 프레임 변환해줘"라고 알린다 (프레임 ID/이름 명시 권장)
### 0-C. 에이전트: 패턴 비교
STEP 1~3로 metadata + screenshot 받은 직후, 0-A에서 본 인덱스와 비교:
- 비슷한 구조 발견 → "이거 X 패턴과 비슷합니다. 두 번째 등장이면 templates_staging/ 로 Jinja2 추출 진행할까요?" 사용자에게 확인
- 비슷한 게 없음 → 일반 STEP 4 이하 진행
**확인사항:**
- Figma desktop 앱이 활성 탭에 올바른 파일이 떠 있는가
- `.mcp.json`에 figma-desktop SSE 서버가 등록돼있는가 (`http://127.0.0.1:3845/sse`)
---
## STEP 1~3 — 데이터 수집 (병렬)
세 도구를 **단일 메시지에 multiple tool_use 블록**으로 동시 호출한다 (도구 호출 단위 병렬). 순차 호출하면 같은 노드 ID를 두 번 추출하느라 토큰만 낭비됨.
```
[single message, multiple tool_use blocks]
1. mcp__figma-desktop__get_metadata nodeId="" (현재 선택 노드)
2. mcp__figma-desktop__get_design_context nodeId="" (현재 선택 노드)
3. mcp__figma-desktop__get_screenshot nodeId="" (현재 선택 노드)
```
**주의:** nodeId를 비우면 현재 선택 노드를 사용하므로 metadata 응답을 기다릴 필요 없음. 셋 다 동시에 갈 수 있다.
| 도구 | 얻는 것 | 사용처 |
|------|--------|-------|
| get_metadata | 모든 leaf 노드의 `id, type, name, x, y, width, height` (XML) | bottom-up 플래튼 |
| get_design_context | gradient/filter/font/color (React+Tailwind 코드) | CSS 변환 |
| get_screenshot | Figma가 렌더한 PNG | STEP 8 사람 눈 검증 |
**주의:**
- get_metadata 응답이 100KB+ 면 frame이 너무 커서 자르지 않은 상태. 사용자에게 더 작은 단위 선택 요청
- get_design_context는 응답이 매우 크므로 한 프레임당 1회만 호출
---
## STEP 4 — 자산 정리 (block-tests/assets/shared/ 캐시)
design_context에서 `localhost:3845/assets/{hash}.png|svg` 패턴의 자산 URL 추출.
각 자산에 대해:
1. URL 끝의 hash를 파일명으로 사용
2. `block-tests/assets/shared/{hash}.{ext}`**이미 있으면 다운로드 스킵**
3. 없으면 curl로 다운로드
```bash
cd block-tests/assets/shared
for url in $URLS; do
hash=$(basename "$url")
[ -f "$hash" ] || curl -sSo "$hash" "$url"
done
```
HTML에서 참조 시:
```html
<img src="assets/shared/{hash}.png">
```
(`block-tests/{slug}.html` 기준으로 상대 경로 `assets/shared/`)
**효과:**
- 동일 자산이 여러 프레임에서 등장해도 한 번만 다운로드 (해시 파일명이라 자동 dedup)
- 후속 프레임 변환 시간 단축
- 토큰 절약 (이미 있는지 확인만)
**프레임 매핑 메모:** `block-tests/{slug}_assets.txt`에 사용한 hash 목록 + 의미 라벨 기록 → 추후 재추출 시 빠른 매핑
```
# bim-goals-3circles_assets.txt
84965807....png bg_texture
f05ebf15....png arc_top
2f0f1750....png arc_side
```
**legacy:** 이전에 다운로드한 자산이 `block-tests/assets/frame_{id}/` 에 있다면 그대로 두되, 새 변환부터는 `shared/` 만 사용한다.
---
## STEP 5 — 산출물 3종 작성 (flat.md + texts.md + index.html)
### 산출물 폴더 구조 (프레임별)
```
blocks/{frame_id}/
├── index.html ← ① HTML 블록 (CSS gradient, transform: scale)
├── flat.md ← ② 실측 기록 (모든 노드 좌표/크기/속성)
└── texts.md ← ③ TF-IDF용 텍스트 (모든 텍스트 빠짐없이)
```
**texts.md 작성 규칙:**
- MCP get_design_context에서 모든 텍스트 노드의 텍스트를 추출
- 노드 계층 설명이 아니라 **텍스트만** 깨끗하게 나열
- 빠짐없이. "간략하게" 금지. flat.md에서 TEXT 타입 노드를 전수 추출
- 섹션별로 그룹핑 (열1, 열2, 열3 등)
- TF-IDF 인덱스 구축 시 이 파일을 직접 사용
### flat.md 작성 규칙
`blocks/{frame_id}/flat.md` 파일 생성. 다음 섹션을 반드시 포함:
### 섹션 1. 메타
```markdown
# Frame {ID} — {이름}
> 원본: {W} × {H} px (node {ID})
> Scale: × {S} → {1280} × {H×S} px
> 슬라이드 16:9 안 배치
```
### 섹션 2. 계층 경로 (bottom-up)
모든 leaf 노드를 들여쓰기 트리로 표현. 그룹별 누적 offset 표시.
```
Frame {root} ({W}×{H})
├─ Group "X" (offset → 누적)
│ ├─ TEXT "..." (abs_x, abs_y) {w}×{h}
│ └─ ...
```
### 섹션 3. 이상 탐지 결과
| 검사 | 결과 |
|------|------|
| 회전 단일문자 (bbox 가로 > 세로 × 1.5) | 발견 노드 ID 또는 "없음" |
| 좁은 박스 세로 텍스트 (width < fontSize × 0.8) | ... |
| 중복 노드 (동일 좌표 + 동일 내용) | ... |
| Vector 좌표 metadata vs design_context 불일치 | ... (있으면 어느 쪽 신뢰) |
### 섹션 4. 변형 가능 축 메모 + 슬롯 옵션
이 블록을 템플릿화한다면 무엇이 파라미터가 될지 1~5줄로. **각 슬롯이 required인지 optional인지 표시**:
```markdown
## 변형 가능 축
- columns[N=2~4] (required)
- badge (required)
- bullet_items[1~12] (required)
- bg_image (required)
- bottom_photo (optional) ← 사진 없는 mdx도 이 블록 매칭 가능
- color_palette[N] (required, N과 일치)
```
이 메모가 STEP 10의 `blocks_index.md` 요약 + 향후 templates_staging meta.yaml 의 초안.
### 섹션 5. Sub-pattern 식별 (재사용 가능한 atomic 단위)
이 블록 안에 **다른 블록과 공유 가능한 sub-pattern**이 있는가? RULES.md R13~ 참조.
```markdown
## Sub-patterns
- `bullet-list-with-marker` (R13) — 각 텍스트 앞에 장식 마커
- 위치: 각 컬럼 본문 영역
- 마커: checkbox PNG
- 적용 구조: .bullet-list / .bullet-row / .bullet-icon / .bullet-text
```
Sub-pattern을 **즉시 RULES.md에 등록할 필요는 없다**. 동일 sub-pattern이 2번째 등장하면 그때 R번호 부여해서 정식 등록.
---
## STEP 6 — 그라데이션 수학 변환
각 SVG `<linearGradient>` 데이터를 [scripts/gradient_math.py](scripts/gradient_math.py)로 CSS로 변환.
```bash
python scripts/gradient_math.py \
--w 350 --h 350 \
--x1 110.833 --y1 18.2292 --x2 219.479 --y2 175 \
--stops "0:#FDC69E,1:#E0782C"
```
출력:
```
linear-gradient(145.28deg, #FDC69E 16.04%, #E0782C 55.20%)
```
수학 원리는 [MATH.md §2 참조](MATH.md).
**여러 그라데이션을 한 번에 변환할 땐 Python 인라인 스크립트 사용:**
```python
import sys, os
# scripts/ 디렉토리를 sys.path에 명시 추가 (작업 디렉토리 무관)
sys.path.insert(0, os.path.join('figma_to_html_agent', 'scripts'))
from gradient_math import svg_to_css
svg_to_css(W=350, H=350, x1=110.833, y1=18.2292, x2=219.479, y2=175,
stops=[(0, '#FDC69E'), (1, '#E0782C')])
```
**작업 디렉토리가 `figma_to_html_agent/` 인 경우:**
```python
import sys; sys.path.insert(0, 'scripts')
from gradient_math import svg_to_css
```
**또는 정식 패키지로 사용** (`scripts/__init__.py` 가 있으므로):
```python
# 작업 디렉토리가 figma_to_html_agent/ 일 때
from scripts.gradient_math import svg_to_css
```
⚠️ **금지: 함수 코드를 인라인 Python에 복사 붙여넣기**. 한 번 만든 `gradient_math.py`를 항상 import해서 쓴다. 복사하면 버그 수정 시 여러 곳을 동시에 고쳐야 하고 수식이 미세하게 어긋날 위험.
---
## STEP 7 — HTML 작성
### 7-A. 기본 구조
```html
<div class="slide"> <!-- 1280×720 흰색 -->
<div class="block"> <!-- 1280 × (H×S) -->
<div class="inner"> <!-- 원본 W×H, transform: scale(S) -->
... 모든 요소 (Figma 원본 좌표 사용) ...
</div>
</div>
</div>
```
```css
.inner {
position: absolute;
left: 0; top: 0;
width: {W}px; height: {H}px;
transform: scale({S});
transform-origin: top left;
}
```
**왜 transform: scale을 쓰는가:** 모든 위치/크기/폰트/그림자/스트로크가 한 번의 transform으로 균일하게 축소됨. 매 값을 수동으로 ×S 곱하는 것보다 안전하고 검증 가능. ([MATH.md §1](MATH.md))
### 7-A1. 콘텐츠 주도형 구조 (R17/R19, 권장)
텍스트 분량 변화에 유연한 블록을 만들 때는 아래 구조를 사용한다:
```html
<div class="slide">
<div class="block">
<div class="inner"> <!-- zoom: S (transform:scale 대신, R19) -->
<div class="title">...</div>
<div class="rows"> <!-- flex column: 행 간 자연 flow -->
<section class="row"> <!-- flex column: pill → body → pill -->
<div class="pill-area">...</div>
<div class="body-area"> <!-- flex row: left | divider | right -->
<div class="body-left">텍스트 (자연 flow)</div>
<div class="divider"></div>
<div class="body-right">텍스트 (자연 flow)</div>
</div>
</section>
</div>
</div>
</div>
</div>
```
```css
.inner { width: {W}px; zoom: {S}; }
.rows { display: flex; flex-direction: column; }
.row { display: flex; flex-direction: column; }
.body-area { flex: 1; display: flex; }
.body-left, .body-right { flex: 1; }
```
**핵심 원칙:**
- 본문 텍스트에 absolute 금지 → flex/flow로 자연 배치
- 텍스트 늘면 body-area가 늘고, 아래 행이 밀림
- overflow:hidden으로 텍스트 숨기기 절대 금지
- pill 장식은 section 기준 상대 배치 (콘텐츠를 따름)
- pill의 crop variant와 label position은 분리 (R18)
**사용 시점:** 텍스트 분량이 가변적인 블록 (비교표, 이슈 목록, 불릿 리스트 등)
### 7-A2. MCP 데이터 → HTML 1:1 변환 원칙 (PROCESS-CONTROL 규칙 8)
MCP get_design_context 응답의 React+Tailwind 코드를 HTML/CSS로 변환할 때:
1. **모든 요소를 빠짐없이 반영** — "핵심만", "단순화" 금지
2. **래퍼 구조 유지** — MCP에 flex container + rotate 래퍼가 있으면 HTML에도 동일 구조
3. **시각 속성 절대 생략 금지**:
- `mix-blend-mode: multiply` → CSS에 반드시 포함
- `opacity-80`, `opacity-70` → CSS opacity 반드시 포함
- `transform: rotate()`, `scaleY()` → 래퍼 div 구조로
- `border-radius`, `inset` → Figma 값 그대로
- `background-image` (gradient) → gradient_math.py 변환 또는 MCP 값 직접 사용
- `box-shadow`, `text-shadow`, `letter-spacing` → 값 그대로
4. **검증**: 변환 후 MCP 응답의 CSS 속성 수 vs index.html CSS 속성 수 대조
**변환 순서:**
```
MCP className/style 읽기
→ 각 속성을 CSS로 매핑
→ position: absolute + MCP 좌표(left, top, width, height)
→ 래퍼가 있으면 래퍼 div 생성 + transform 적용
→ 시각 속성 (blend, opacity, radius, shadow) 전부 추가
→ 텍스트 내용 삽입
→ 완성 후 MCP 원본과 속성 수 대조
```
### 7-B. 요소 변환 우선순위
| 요소 종류 | 구현 방법 | 이유 |
|---------|---------|-----|
| 원/사각형 + gradient + blend | **HTML div** + `border-radius` + `linear-gradient` + `mix-blend-mode: multiply` | 동적 재구성 위해 |
| Stroke (경계선) | `border: Npx solid color` + `box-sizing: border-box` | gradient와 함께 사용 가능 |
| Drop shadow blur | `box-shadow: 0 0 {2×stdDev}px {color}` | SVG feGaussianBlur 근사 |
| 곡선 (아크, 비원형) | **SVG `<path>`** 또는 미리 export된 PNG | CSS 불가능 |
| 텍스트 | HTML `<div>` 절대 배치 | 선택 가능, 접근성 |
| 실사 이미지 | `<img>` PNG | 재현 불가 |
| 회전된 도형 | 래퍼 div + `transform: rotate()` ([INSIGHT-GRADIENT.md](INSIGHT-GRADIENT.md)) | gradient 동시 회전 |
### 7-C. 보정 규칙
[RULES.md](RULES.md) R1~R16 모두 적용:
- R1: descender padding-bottom
- R2~R3: 회전/세로 텍스트
- R4: 그라데이션 텍스트
- R5: 다중 fills
- R6: 중복 노드
- R7: 흰 배경
- R8: 스케일 팩터
- R9: 순수 CSS 우선
- R10: blend mode 호환
- R11: stroke 정렬 (inside/outside)
- R12: viewBox padding
---
## STEP 8 — Selenium 렌더링 + 사람 눈 검증
```python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from PIL import Image
import os, time
# _renders/ 폴더 없으면 생성
os.makedirs('block-tests/_renders', exist_ok=True)
opts = Options()
opts.add_argument('--headless=new')
opts.add_argument('--hide-scrollbars')
opts.add_argument('--force-device-scale-factor=1')
opts.add_argument('--window-size=1600,900')
d = webdriver.Chrome(options=opts)
p = os.path.abspath('block-tests/{slug}.html').replace('\\','/')
d.get('file:///' + p)
time.sleep(1.5)
d.save_screenshot('block-tests/_renders/{slug}_full.png')
r = d.execute_script(
'const r=document.querySelector(".slide").getBoundingClientRect();'
'return [r.x,r.y,r.width,r.height];'
)
Image.open('block-tests/_renders/{slug}_full.png').crop(
(int(r[0]), int(r[1]), int(r[0]+r[2]), int(r[1]+r[3]))
).save('block-tests/_renders/{slug}.png')
d.quit()
```
**검증 방식:**
- 자동 픽셀 diff는 하지 않음 (font 렌더 차이로 노이즈만 많음)
- Figma `get_screenshot` 응답과 Selenium 결과를 **사람 눈**으로 비교
- 차이 발견 시 STEP 5~7로 돌아가서 원인 파악 (값 수정 금지)
### STEP 8-B. 전수 대조 검증 (필수, 다음 프레임 진행 전 반드시 통과)
**원칙: "간략하게", "핵심만", "중요한 것만" — 전부 금지. 전수 처리.**
프레임 완료 전 아래 3가지 검증을 모두 통과해야 다음 프레임으로 넘어갈 수 있다:
**① texts.md 전수 검증:**
- MCP get_design_context의 모든 텍스트 노드를 texts.md와 1:1 대조
- texts.md에 없는 텍스트 노드가 하나라도 있으면 실패
- 검증 방법: MCP 응답에서 텍스트 추출 → texts.md와 diff
**② flat.md 전수 검증:**
- MCP get_metadata의 모든 노드가 flat.md에 기록되어 있는지 대조
- 좌표, 크기, 타입, 이름이 빠짐없이 기록되어야 함
- "간략하게만 적었습니다" → 절대 금지. 실측 기록부에 간략은 없다
**③ index.html 전수 검증:**
- texts.md의 모든 텍스트가 index.html에 존재하는지 대조
- flat.md의 모든 시각 요소(bar, line, icon 등)가 index.html에 반영되어 있는지 대조
- 하나라도 누락되면 실패
**검증 실행 방법:**
```
# texts.md vs index.html 교차 대조
texts.md의 각 줄 텍스트 → index.html에서 grep → 없으면 FAIL
# MCP 텍스트 노드 수 vs texts.md 항목 수
MCP 텍스트 노드: N개
texts.md 항목: M개
N == M 이어야 PASS
```
**검증 미통과 시:** 다음 프레임 진행 금지. 누락 항목 수정 후 재검증.
---
## STEP 9 — 결과물 저장
```
block-tests/
├── {slug}.html ← 변환물
├── {slug}_flat.md ← 플래튼/이상/변형 축 메모
└── _renders/
└── {slug}.png ← 검증 스크린샷
```
`{slug}` 명명 규칙: 의미 기반 kebab-case (예: `bim-goals-3circles`, `cards-3col-icon`).
프레임 ID는 metadata로 추적 가능하므로 파일명에 넣지 않음.
---
## STEP 10 — blocks_index.md 1줄 업데이트
`blocks_index.md` 끝에 한 줄 추가:
```markdown
| {slug} | {프레임 ID} | {1줄 변형 축 요약} | {날짜} |
```
이 인덱스가 패턴 발견의 단서가 된다. 다음 변환 시작 전에 이 인덱스를 한 번 훑어서 "이미 비슷한 거 했나?" 확인.
---
## 패턴 → 템플릿화 (1번째부터 즉시)
**규칙: 1번째 등장부터 templates_staging 작성. 정적 HTML만 두는 것 금지.**
| 등장 횟수 | 처리 |
|---------|------|
| **1번째** | `block-tests/{slug}.html` (1:1 reference) + `templates_staging/{pattern_id}.html.j2` (Jinja2 + meta.yaml + example.yaml) **함께 작성** |
| 2번째 | 기존 staging 템플릿이 새 데이터로 잘 렌더되는지 확인. 안 되면 템플릿 수정. example 추가. |
| 3번째 이후 | 동일 |
**왜 1번째부터 템플릿화하나?**
- 변환의 목적은 **블록 라이브러리 구축**, 단순 HTML 복제가 아님
- 1:1 단계에서 발견한 인사이트(R13 등)를 즉시 템플릿에 반영해야 잊지 않음
- 사용자가 검수할 때 "이게 블록으로 어떻게 작동할지" 즉시 확인 가능
- 2번째 등장을 기다리면 사용자 수동 복제 작업이 누적됨 (work-creating-work)
**Stage 2 산출물:**
```
templates_staging/
├── {pattern_id}.html.j2 ← Jinja2 템플릿 본체
└── {pattern_id}.meta.yaml ← when / slots / min_size_px / 변형 축 초안
```
여기까지가 **에이전트 책임의 끝.**
---
## 🚧 프로모션 게이트 (사용자 전용)
> 이 게이트 이후 작업은 **에이전트가 절대 수행하지 않는다.** 모든 design_agent/templates/ 변경은 사용자 본인이 직접 한다.
### 사용자가 수행할 작업
1. **검수**: `templates_staging/{pattern_id}.html.j2` 를 다양한 파라미터로 렌더 테스트
2. **품질 게이트 통과 확인**:
- [ ] 1:1 변환물과 시각적으로 동일한가
- [ ] 슬롯 파라미터를 바꿔도 깨지지 않는가 (원 4개, 라벨 0개 등 극단 케이스)
- [ ] meta.yaml의 when/slots가 design_agent의 다른 블록과 충돌 없는가
3. **이동**: `templates_staging/{pattern_id}.html.j2``design_agent/templates/blocks/{category}/`
4. **등록**: `design_agent/templates/catalog.yaml` 에 when/slots/min_size_px 추가
5. **상태 업데이트**: `blocks_index.md` 의 해당 행 상태 → `promoted`
### 에이전트의 역할
- staging 작성까지만
- 사용자 요청 없이 `design_agent/templates/` 를 절대 읽거나 쓰지 않음
- "templates/ 에 옮겨드릴까요?" 같은 제안 금지 (월권)
- 사용자가 명시적으로 "이 staging 결과 검토해줘"라고 요청하면 → staging 폴더 내에서만 검토
---
## 안티 패턴 (하지 말 것)
| ❌ 하지 말 것 | 이유 |
|------------|-----|
| 사전에 인벤토리/지문/군집 단계 | work-creating-work, 패턴은 변환하면서 발견됨 |
| 1번째 등장은 정적 HTML로만 두기 (templates_staging 미작성) | work-creating-work, 인사이트 잊혀짐. 1번째부터 템플릿 작성 |
| 컨텍스트 차면 강제 새 세션 | compact 사용. 핵심 결정은 모두 파일에 박혀있어 손실 없음 |
| Figma 데이터 안 보고 멀티모달 이미지로 추측 | 미묘한 alpha/blend에서 틀림 |
| "여기 1px 어색하니 다른 곳도 같이 바꾸자" | 사용자 피드백만 정확히 반영 |
| 같은 자산을 매번 새로 다운로드 | `block-tests/assets/shared/` 캐시 활용 |
| 그라데이션 각도/색을 눈대중으로 | gradient_math.py로 수학 도출 |
| gradient_math.py 함수 코드 인라인 복사 | import만 한다. 복사하면 수식 어긋남 |
| 세션 시작에 blocks_index.md 안 읽음 | 패턴 발견 트리거 영영 작동 안 함 |
| `design_agent/templates/` 직접 수정 | 프로모션은 사용자 전용. 에이전트는 staging까지만 |
| "templates/ 옮겨드릴까요?" 제안 | 월권. 사용자가 알아서 함 |
| `prerequisites-3col.html` 을 신규 변환 레퍼런스로 사용 | 구 방법론 (R8/R9 미적용). legacy 표시됨 |