# 골격 화면 규격 — `figures-guide.md`

`/exp:design` 의 **S1 · S4** 가 쓰는 골격과 형식 규칙. 값이 아직 없는 화면이다.
값이 있는 화면은 `drilldown-checklist.md` 로 간다.

> **범위가 줄었다 — S2 · S3 는 이제 이 문서를 쓰지 않는다.**
> 둘은 골격 없이 자유 레이아웃으로 그리고, 규격은 **절 구성과 체크리스트**다.
> 원본은 `.claude/commands/exp/design.md` 의 §S2 · §S3.
> 이유는 E9 — 규격 와이어프레임이 **두 실험 연속으로 승인 대상이 되지 못했다.**
> 그리고 `drilldown-checklist.md` 가 이미 「페이지 구조는 일반화하지 않는다」고 못박아
> 둔 것과 정면으로 부딪히고 있었다.

참조 구현은 `samples/` 이고, **이 문서는 그것을 설명할 뿐 새 규칙을 만들지 않는다.**
문서와 샘플이 어긋나면 샘플이 맞다.

```
samples/s1-scope.sample.html        깔때기 + 지표 표 + 3열 판          ✅ 현역
samples/s4-layout.sample.html       파일 트리 + 영향 표                ✅ 현역

samples/s2-wireframe.sample.html    값 없는 와이어프레임                ⛔ 폐기 — 쓰지 않는다
samples/s3-steps.sample.html        스텝 카드 + 견적 막대              ⛔ 폐기 — 쓰지 않는다
```

**폐기한 둘은 지우지 않고 남긴다.** 네 장의 `<style>` 과 엔진이 바이트 단위로 같아야 한다는
불변식의 참조본이고, 「왜 버렸나」를 묻는 사람이 볼 자리다. **복사해 쓰지는 않는다.**

---

## §0. 불변식 여섯

`.claude/guides/interactive-html-guide.md` §0 에서 그대로 상속한다. 실험 화면에서는 이렇게 읽는다.

```
1  오프라인 자립      CDN·외부 script·웹폰트 금지. CSS/JS 는 인라인.
                     승인 화면은 네트워크 없이 열린다
2  테마 대응          prefers-color-scheme 로 라이트/다크.
                     :root 변수 블록은 샘플에서 그대로
3  반응형             2열(그림|판)이 900px 아래에서 1열로. 본문은 가로 스크롤 안 한다
4  생략 금지          깔때기 단 · 지표 행 · 스텝 카드 여섯 칸을 「…」로 접지 않는다.
                     화면은 늘 FULL. 접는 것은 채팅 델타 기록에서만
5  언어 분리          슬롯 이름 · 필드 · 경로 · 열거값 식별자는 코드형, 사람이 읽는 산문은 한국어
6  골격 고정 데이터 교체  <style> 과 엔진 함수는 건드리지 않는다.
                     샘플을 복사해 §2 의 데이터 배열만 교체한다
```

### `visualization-decision-guide.md` 와 합치지 않는 이유

그 문서는 **설명 페이지**용이다. 목적이 이해라서 「어려운 지점에만 그림을 넣고, 한 페이지에 넷을
넘기지 말라」가 규칙이 된다. 이 문서는 **승인 화면**용이다. 목적이 고칠 데 찾기라서 「섹션마다
반드시 하나, 전부 다 보여라」가 규칙이 된다. **두 규칙은 방향이 반대고, 한 파일에 두면 다음 사람이
어느 쪽을 적용할지 매번 다시 판단한다** — F10 이 정확히 그 실패다(문서가 없어서가 아니라 있는데
안 열려서 매번 다시 논쟁한다). 그래서 파일을 가른다.

**다만 「읽히는 규칙」은 그 문서가 원본이고 여기서 다시 쓰지 않는다.** 축은 단위를 갖는다 ·
기준선은 캡션이 아니라 그림 안에 그린다 · 추정치는 추정치로 표시한다 — 셋 다
`.claude/guides/visualization-decision-guide.md` §3 에 근거까지 있다. 여기서는 **참조로만 건다.**
베껴 두면 두 벌이 되고 두 벌은 갈라진다.
규칙 이름은 §6·§7 에서 다시 부른다. 부르는 것은 이름까지고, **근거의 집은 그 문서 하나다.**

---

## §1. 공통 스켈레톤

네 장의 파일 구조가 같다.

```html
<style>   ← 네 장이 바이트 단위로 같다. 절대 손대지 않는다
</style>
<body>    ← 비어 있다. 화면 전체를 엔진이 그린다
<script>  ← 데이터 블록. 여기만 교체한다 (SCREEN · META · DELTA · 슬롯)
</script>
<script>  ← 엔진 블록. 네 장이 바이트 단위로 같다
</script>
```

엔진의 진입점은 하나다.

```js
document.body.innerHTML = header() + SCREENS[SCREEN]() + approve();
```

`SCREEN` 이 `'s1'|'s2'|'s3'|'s4'` 중 무엇이냐로 본문이 갈린다. 헤더와 하단은 넷이 공유한다.

### 헤더 — `META`

`META` 다섯이 전부 헤더에 나온다 — `experiment` 가 제목, `section` 이 부제, 나머지 셋
(`version` · `builtAt` · `dataAsOf`)이 칩이다. `builtAt` 은 페이지를 만든 시각, `dataAsOf` 는 안의
숫자를 뽑은 시각이고 **둘은 다르다.** `dataAsOf: null` 이면 「숫자 없음 — 값이 안 들어간 스케치」로 뜬다
(와이어프레임이 그 경우다). 헤더 오른쪽에 델타 범례 셋이 늘 붙는다.

### 본문

`.wrap` 이 2열(`minmax(0,1.55fr)` | `minmax(300px,1fr)`)이고 900px 아래에서 1열이 된다.
왼쪽이 넓은 쪽(s1 깔때기 · s4 파일 트리), 오른쪽이 좁은 쪽(s1 지표 표 · s4 영향 표)이다.
폭이 더 필요한 것은 `.wrap` 아래
`.full` 로 내린다(s1 의 3열 판, s4 의 파급 한계). **넓어질 수 있는 것은 전부 `.scroll` 안에 넣는다** — 표와 `pre.tree` 가 그렇다.
본문이 가로로 밀리는 대신 그 상자만 민다.

### 하단 — 승인 요청 블록

`.approve` 하나. 안에 Change Summary 표(`| 유형 | 항목 | Before → After | 왜 | 영향 |`)와
승인 키워드가 있다. **화면이 실재하기 전에 승인을 요청하지 않는다** — §6 을 보라.

---

## §2. 화면별 데이터 슬롯

슬롯 아홉의 계약은 `docs/features/exp-design-command/design.md` §4 가 원본이다.
여기서는 **어느 화면이 무엇을 받는지**만 적는다. 필드 이름은 원본과 한 글자도 다르지 않다.

### s1-scope — `FUNNEL` · `METRICS` · `PARAMS`

```js
FUNNEL  = [{ label, cond, left, pctPrev, axis }]        // axis: '분모' | '분자'
METRICS = [{ name, unit, denom, gate }]                 // gate: null 이면 「이번엔 안 정한다」
PARAMS  = { fixed: [{ name, value }],
            moving:[{ name, values, breaksCompare }],
            open:  [{ name, why }] }
```

- 깔때기는 `.f-row[data-left]` 의 높이를 재서 `drawBand()` 가 SVG 띠를 그린다. 리사이즈마다 다시 그린다.
- `axis` 는 단마다 `.axis` / `.axis.num` 배지로 나온다. **분모↔분자를 안 붙이면 비율이 어디서
  깎였는지 알 수 없다** — F2.
- `gate: null` 은 「이번엔 안 정한다」로 렌더된다. 빈칸이 아니라 결정이다.
- `breaksCompare: true` 면 그 파라미터 아래에 「회차 간 비교 불가」 경고줄(`.warnline`)이 붙는다.

### s2-wireframe — `FIGURE` · s3-steps — `STEPS` · `ESTIMATE` — ⛔ 폐기

**두 슬롯 묶음은 더 이상 쓰이지 않는다.** S2 는 자유 목업, S3 는 자유 구현 명세로 바뀌었고
규격은 커맨드의 §S2 · §S3 절 구성과 체크리스트다.

버린 이유를 한 줄씩 남긴다 — 같은 실수를 다시 하지 않기 위해서다.

| 슬롯 | 무엇이 안 됐나 |
|---|---|
| `FIGURE` | 단일 플롯을 상정해(`kind`·`xAxis`·`yAxis` 하나씩) 다단 결합 그림이 **`kind` 문자열에 문장으로 접혔다** (E1) |
| `FIGURE.drilldown` | `{from,to}` 문자열 배열이라 배치가 **글자로만** 남았다. 승인 시점에 보고 싶었던 것이 그 배치였다 (E2) |
| `STEPS` | 스텝을 **나열**했지 **연결**하지 않았다. 순서·병렬·파일 흐름·담당 주체가 슬롯에 없다 (E4) |
| `ESTIMATE` | LLM 토큰 전용이라 결정론 파이프라인에서 `estTokens:0` → 막대 폭이 `0/0` (E3) |

**`FIGURE.off` 만 뜻이 남는다** — 화면을 끄는 것도 결정이라 카드 한 장을 낸다는 규칙(D-3)은
S2 절에 그대로 살아 있다. 다만 그 카드도 이제 자유 레이아웃으로 그린다.

### s4-layout — `FILES` · `BLAST`

```js
FILES = [{ path, kind, why, section, exists }]          // kind: 'new'|'mod'
BLAST = { checked: [], note }
```

- 트리는 `FILES[].path` 에서 조립한다. 안 바뀌는 상위 폴더도 지운 파일도 남긴다.
- `exists` 는 **파일시스템에 실제로 대본 값**이다. `kind:'mod'` 인데 `false`, `kind:'new'` 인데
  `true` 면 엔진이 `.warnline` 으로 짚는다. 주장으로 채우면 이 검사가 통째로 죽는다.
- `BLAST.checked` 가 빈 배열이면 맨 「없음」과 같다. 어디를 봤는지 이름으로 적는다.

### 둘 다 — `META`

```js
META = { experiment, section, version, builtAt, dataAsOf }
```

---

## §3. 델타 강조

**화면은 늘 FULL 을 그리고 바뀐 것만 강조한다. 채팅에는 델타만 적는다.** 화면에서 접기 시작하면
읽는 쪽이 「안 보이는 게 안 바뀐 건지 지워진 건지」를 되묻는다 — 되물음이 F5 · F7 의 몸통이다.

델타는 슬롯이 아니라 별도 배열로 온다. 아홉 슬롯의 필드는 델타 때문에 바뀌지 않는다.

```js
DELTA = [{ kind: 'new' | 'mod' | 'rm',
           slot,            // 'FUNNEL' | 'METRICS' | 'PARAMS' | 'FILES'  (s1 · s4 만)
           key,             // 그 슬롯에서 행을 찾는 값 — label · name · n · path
           col,             // 선택. 여러 칸으로 갈리는 슬롯에서 어느 칸인지 (fixed/moving/open, filters/drilldown)
           label,           // kind:'rm' 필수 — 지워진 행을 그릴 이름
           before, after,   // kind:'mod' 필수
           why, impact }]   // 셋 다 필수
```

표기는 네 장이 같다. 엔진의 `tag()` · `cls()` · `ba()` · `ghosts()` 넷이 전부 처리한다.

```
＋new       .tag.new  + 행 배경 .r-new      추가
~mod        .tag.mod  + 행 배경 .r-mod      변경. 아래에 before → after 를 붙인다
−removed    .tag.rm   + 행 배경 .r-rm       삭제. 취소선으로 남긴다 — 지우지 않는다
```

`ghosts(slot, col)` 이 `kind:'rm'` 을 본문에 되살린다. **삭제분이 하단 Change Summary 에만 있고
본문에서 사라지면 FULL 이 아니다.** 승인 화면에서 무엇이 빠졌는지는 빠진 자리에 있어야 보인다.

`impact` 칸에 맨 「없음」은 금지다. 어디를 봤는지 쓴다.

---

## §4. 케이던스

```
1  첫 제시     전체를 만든다. 샘플 복사 → 데이터 배열 교체 → 렌더 → 경로를 승인 요청에 넣는다
2  수정        데이터 배열만 고쳐 통째로 다시 낸다. baseline 은 방금 낸 v(n)
3  잡담        설계를 안 바꾸는 질문·확인에는 다시 내지 않는다. 채팅으로 답한다
```

2번이 값싼 이유가 §0 의 불변식 6이다. 골격이 고정이라 회차마다 드는 것은 데이터 배열뿐이다.
**여기서 style 이나 엔진을 손대면 그 순간부터 회차 비용이 첫 제시 비용이 된다.**

---

## §5. 출처 배지 넷

스텝 카드의 칸마다 하나씩 붙는다. **색과 위치가 고정이다.**

| 값 | 표기 | 색 토큰 | 뜻 |
|---|---|---|---|
| `'said'` | 님이 말함 | `--said` | 사용자 발화에 그대로 있던 값 |
| `'draft'` | draft | `--draft` | draft·기존 문서에서 가져온 값 |
| `'mine'` | ⚠ 제가 정함 | `--mine` | 커맨드가 채운 값 |
| `'open'` | 미결 | `--open` | 아직 아무도 안 정했다 |

**자유 레이아웃(S2·S3)에서는 값 옆 인라인 배지로 붙인다** — 「칸」이 없기 때문이다.
붙는 자리는 달라도 쓰임은 하나로 같다.

골격 화면(s1·s4)에서 위치는 `.cell .c-head` 안 `.c-src { margin-left: auto }` —
**칸마다 오른쪽 위, 예외 없이.**
위치가 흔들리면 훑기가 안 된다. 이 배지의 쓰임은 하나다: **`⚠ 제가 정함` 만 눈으로 훑으면 지시가
빈 곳이 나온다.** 가장 비싼 실패가 「일은 했는데 방식이 다름」이었고 — 프롬프트로 평가하라 했는데
정규화 스크립트가 만들어졌다 — 그 값이 어느 칸에서 누구 결정으로 들어왔는지가 안 보였기 때문이다.

`'open'` 이 하나라도 남아 있으면 그 절은 승인 대상이 아니다. 배지는 그것을 눈에 보이게 할 뿐,
대신 막아주지 않는다.

색은 배지 배경으로만 쓰고 글자는 진한 잉크로 고정한다. 흰 글자는 다크 테마의 amber 배경 위에서
안 읽힌다(대비 약 2.1:1) — 정작 제일 자주 훑는 배지가 `⚠ 제가 정함` 이다.

---

## §6. 안티패턴

규칙마다 이게 어디서 나왔는지를 붙인다. 근거 없는 금지는 다음 사람이 푼다.

**「작아서 텍스트로 갈음하겠습니다」 금지.**
깔때기 다섯 단이든 두 단이든 화면은 화면이다. 텍스트로 갈음하면 분모↔분자가 어디서 깎였는지가
문장 안에 숨고, 그것을 못 본 채 승인이 난다. 되돌림 230턴에서 가장 비싼 다섯이 전부 4단계
이후에 터졌고, 그 다섯 중 둘이 모수 재정의(B2)와 표기 오독(B5)이다.

**「곧 렌더링하겠습니다」 · 「나중에 보여드리겠습니다」 금지.**
화면이 실재하기 전의 승인 요청은 **무효고, 재생성 대상이다.** 승인은 화면을 보고 하는 것이지
화면이 생길 것이라는 약속을 보고 하는 게 아니다. 원본
`be-system-design-writer-only-diff` 에서도 같은 문장으로 막혀 있다.

**토큰 절약을 이유로 한 스킵 금지.**
「이 절은 가벼우니 화면 없이 넘어가자」는 결정이 아니라 생략이다. 화면을 끄는 것은 가능하지만
**끈다는 결정과 이유가 문서에 남아야 한다**(설계 D-3). 비용이 걱정이면 §4 의 케이던스로
줄인다 — 잡담에 재렌더하지 않는 것이 절약이고, 첫 제시를 건너뛰는 것은 절약이 아니라 부채다.
비용은 완료 조건 ②에서 따로 잰다.

**기준선을 캡션으로 미는 것 금지.**
`quote-cluster-stats` 가 정확히 여기서 멈췄다. 기준선은 `<line class="base">` 로 그림 안에 있어야
한다. 근거는 `visualization-decision-guide.md` §3.

**추정치를 무표시로 두는 것 금지.**
`ESTIMATE.estTokens` 는 추정치다. 표와 막대는 실측과 추정을 같은 상자에 담기 때문에 표시가 없으면
읽는 쪽이 구별할 방법이 없다. 근거는 같은 문서 §3.

**값을 엔진에 하드코딩하는 것 금지.**
샘플의 엔진 블록에는 실험값이 한 개도 없다. 값이 엔진으로 새면 다음 회차에 데이터 배열만 바꿔서는
화면이 안 바뀌고, §4 의 케이던스가 무너진다.

**`<style>` 과 엔진 함수를 고치는 것 금지.**
네 장의 두 블록은 바이트 단위로 같아야 한다. 폐기한 둘을 포함해서다 — 참조본이 갈리면
불변식을 대볼 데가 없어진다.

**공용 자산을 고치는 것 금지.**
`.claude/scripts/render-section.sh` 는 다른 곳에서 설치·관리하고 다른 커맨드도 쓴다.
검사를 끼워 넣지 말고 `exp-design/render.sh` 로 감싼다. 경계는 `assets.md` 가 원본이다.

**값 없는 스케치에 그럴듯한 값을 넣는 것 금지 — 골격 화면(s1·s4)에 한한다.**
숫자를 넣으면 아직 돌리지도 않은 결과를 승인받게 된다. 그래서 그 화면의 `META.dataAsOf` 는 `null` 이다.

**S2 목업은 이 금지의 예외다.** 「값이 없다」와 「구조가 없다」는 다르고(E2), 배치를 보이려면
숫자가 들어가야 한다. 대신 경계를 긋는다 — **결과에 해당하는 수는 전부 지어낸 값**,
**P1·S1 에서 이미 승인된 설계값(모수 n · 칸 수 · 컷 · 관측창)만 실값**, 그리고 **더미 배너로 둘을
갈라 적는다.** 결과를 조회해 넣으면 설계 단계에서 결과가 나온다(E6).

**맨 「없음」 금지.**
`BLAST.checked` 와 Change Summary 의 영향 칸. 어디를 봤는지 이름으로 쓴다.

---

## §7. 내기 전 확인 목록

**이 목록은 s1 · s4 용이다.** S2 · S3 의 체크리스트(각 17개)는 커맨드 §S2 · §S3 에 있다.

**그리고 체크는 ☑ 로 끝나지 않는다** — 항목마다 「무엇을 보고 판정했나」 한 줄을 붙인다.
이 목록이 이미 있었는데도 E7 이 아래 8번 항목의 실패였다. **근거 칸이 없어서 안 대보고
통과시켜도 표가 안 났다.**

```
□ 두 장의 <style> 과 엔진 블록이 샘플과 같은가 — 데이터 블록만 다른가
□ 외부 script·CDN·웹폰트가 0건인가. file:// 로 열리는가
□ 라이트/다크 둘 다에서 읽히는가
□ 900px 아래에서 1열로 접히고, 본문이 가로로 안 밀리는가
□ 「…」로 접힌 칸이 없는가 — 전부 FULL 인가
□ META 다섯이 다 찼는가. builtAt 과 dataAsOf 를 구별해 적었는가
□ 슬롯의 필수 칸을 빈 문자열로 채운 곳이 없는가 (null 과 빈 배열은 뜻이 다르다)
□ 축에 단위가 있는가 · 기준선이 그림 안에 있는가 · 추정치에 표시가 있는가
      → visualization-decision-guide.md §3
□ s4 — 모든 경로를 파일시스템에 대봤는가. exists 가 실측인가
□ 델타가 FULL 위에 강조로 얹혔는가. 삭제분이 본문에 남아 있는가
□ Change Summary 의 영향 칸과 BLAST.checked 에 맨 「없음」이 없는가
□ 승인 요청에 **실재하는 화면 경로**가 들어갔는가 — 「곧 렌더링」이 아닌가
□ 렌더를 `exp-design/render.sh` 로 했는가 — 공용 스크립트를 직접 부르면 검사가 안 돈다
□ 항목마다 근거 한 줄이 붙었는가 — 근거를 못 쓰면 그 항목은 되돌아갈 자리다
```
