genetics-mcp 2.25.0 → 2.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/NATIVE-CODEX-SESSION.md +182 -0
  2. package/bin/aarch64-apple-darwin/gcell +0 -0
  3. package/bin/aarch64-unknown-linux-musl/gcell +0 -0
  4. package/bin/manifest.json +5 -5
  5. package/bin/x86_64-apple-darwin/gcell +0 -0
  6. package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
  7. package/bin/x86_64-unknown-linux-musl/gcell +0 -0
  8. package/codex-session-cli.mjs +82 -0
  9. package/codex-session.mjs +353 -0
  10. package/genetics.mjs +1865 -228
  11. package/package.json +5 -2
  12. package/skills/gazette-report/SKILL.md +11 -1
  13. package/skills/gazette-report/scripts/tone.mjs +45 -9
  14. package/skills/gazette-report/scripts/tone.test.mjs +70 -0
  15. package/skills/genetics-cell-id/SKILL.md +73 -54
  16. package/skills/genetics-define/SKILL.md +34 -1
  17. package/skills/genetics-report/SKILL.md +68 -70
  18. package/skills/genetics-report/assets/example/cases-briefing-designs.json +15 -5
  19. package/skills/genetics-report/assets/example/cases-briefing.json +14 -5
  20. package/skills/genetics-report/assets/example/cases.json +25 -9
  21. package/skills/genetics-report/assets/example/decision.json +313 -0
  22. package/skills/genetics-report/assets/example/report-briefing-designs.json +2 -2
  23. package/skills/genetics-report/assets/example/report-briefing.json +1 -1
  24. package/skills/genetics-report/assets/example/report.json +65 -21
  25. package/skills/genetics-report/assets/render.py +204 -52
  26. package/skills/genetics-report/references/content-pipeline.md +9 -10
  27. package/skills/genetics-report/references/decision-format.md +112 -0
  28. package/skills/genetics-report/references/diagrams.md +52 -94
  29. package/skills/genetics-report/references/edition-review.workflow.js +81 -31
  30. package/skills/genetics-report/references/history.md +35 -0
  31. package/skills/genetics-report/references/lim-portal-format.md +120 -253
  32. package/skills/genetics-report/references/linear-read-test.workflow.js +33 -10
  33. package/skills/genetics-report/references/process.md +34 -27
  34. package/skills/genetics-report/references/readability.md +9 -8
  35. package/skills/genetics-report/references/reader-review.workflow.js +167 -25
  36. package/skills/genetics-report/references/visual-first.md +54 -42
  37. package/skills/genetics-report/assets/diagram.mjs +0 -208
@@ -1,105 +1,63 @@
1
- # 도해 — Archify 를 관보에 흡수한다 (2026-09-16)
2
-
3
- > 「관보에 불가능한 로직이 있다면 가능하게 고도화하고, Archify 까지 호환되도록」(사용자 지시).
4
- > 이 문서는 **Archify 로 도해를 그려 관보 `sections[].diagrams[]` 에 앉히는 절차**의 정본이다.
5
- > 렌더러 쪽 계약(칸·경고)은 `lim-portal-format.md` §11-9, 가독성 잣대는 `readability.md` R1.
6
-
7
- ## 0. 왜 도판(PNG)이 아니라 도해(SVG)인가
8
-
9
- | | 도판 `plates`(PNG) | 도해 `diagrams`(인라인 SVG) |
10
- |---|---|---|
11
- | 글자 | 비트맵 — 확대하면 뭉개지고 검사기가 못 읽는다 | 활자 — 인쇄·확대에서 선명하고 문서의 Pretendard 를 물려받는다 |
12
- | 예산 | 장당 300KB · 합계 2,400KB | 장당 250KB(문서 크기에 직접 더해진다) |
13
- | 만드는 법 | `figure.mjs` 로 굽는다 | `diagram.mjs render/export` 로 굳힌다 |
14
- | 언제 | 실제 화면 캡처 · 비트맵 원본 | 구조·흐름·순서·데이터 흐름·상태 전이 |
15
-
16
- 열람 CSP 는 인라인 SVG 를 허용한다(2026-08-24 실측). 스크립트는 여전히 0 이다 — 그래서 Archify 의
17
- **뷰어(팬·줌·애니메이션·검색)는 관보에 실리지 않고 정지 화면만 실린다.** 그것이 문서의 뜻이다.
18
-
19
- ## 1. Archify 는 어디에 있나
20
-
21
- Archify 는 에이전트 스킬이다(MIT, `tt-a1i/archify`). **이 스킬 패키지에 동봉하지 않는다** — 8MB 이고 자체
22
- 갱신 절차를 가진다. 사용자 기계의 스킬 폴더에 설치돼 있어야 한다.
23
-
24
- ```bash
25
- npx skills add tt-a1i/archify -g # Claude Code · Cursor · Codex 공용 설치기
26
- node ~/.claude/skills/archify/bin/archify.mjs doctor
27
- ```
28
-
29
- `diagram.mjs` 는 `--archify <dir>` → `ARCHIFY_DIR` → `~/.claude/skills/archify` → `~/.agents/skills/archify`
30
- → `~/.cursor/skills/archify` → `~/.codex/skills/archify` 순으로 찾는다. 없으면 설치 안내와 함께 멈춘다(exit 2).
31
-
32
- ## 1.5 실측이 정한 두 길 — `ir`(기본) 과 `svg`(예외)
33
-
34
- Archify 의 SVG 는 노드 라벨 11px · 부제·간선 라벨 8px · 범례 7.5px 를 800px 넘는 viewBox 에 놓는다(화면+줌 전제, 자체 계약은
35
- 「투영 6px 이상」). 관보는 인쇄 우선이라 어떤 글자도 15px 아래로 못 내려가는데(R1), 전폭 1030px 에서도 그 SVG 는 표시 10px 에
36
- 그친다 — 3열 단일 레인(viewBox 808)도 10.2px 였다(2026-09-16 실측, 프리셋 넷 모두 같음). 글자를 키우면 Archify 의 노드 상자·라벨
37
- 마스크가 깨진다. 그래서:
38
-
39
- | 길 | 언제 | 무엇을 넣나 | 가독성 |
40
- |---|---|---|---|
41
- | **`ir`** (기본) | `workflow`(절차·관문·runbook) | Archify validate 를 통과한 IR **그대로** | 관보가 15px 활자·장 색으로 다시 그린다 — R1 자동 충족 |
42
- | `svg` (예외) | architecture · sequence · dataflow · lifecycle | `diagram.mjs export` 의 SVG | R1 ⚠ 를 감수, 크게 보기(1400px)로 읽힌다. 인쇄에서는 작다 |
43
-
44
- `ir` 길에서 Archify 는 **저작·검증 도구**다(타입 스키마·주 경로·간선 의미·검증 영수증). 그린 HTML 은 화면용 부속물로
45
- 문서 밖 링크에 둘 수 있다.
46
-
47
- ## 2. 절차 — IR 을 쓰고, 검증하고, 앉힌다(`ir` 길) / 굳히고 재서 앉힌다(`svg` 길)
48
-
49
- 1. **종류를 고른다** — Archify `SKILL.md` 의 Type router: `architecture`(구성 요소·경계) · `workflow`(절차·관문·runbook)
50
- · `sequence`(호출 순서) · `dataflow`(파이프라인·계보) · `lifecycle`(상태 전이). 모르면
51
- `node bin/archify.mjs guide "<상황>" --json`.
52
- 2. **IR 을 쓴다** — Archify 의 `schemas/<type>.schema.json` 과 `examples/` 를 읽고 새로 쓴다(사실은 그 문서의 데이터에서).
53
- 한국어 라벨을 쓴다. 노드 12개 이하, 주 경로 하나. `meta.quality_profile: "showcase"`. `meta.animation` 은 적지 않는다.
54
- **`meta.legend: {"mode": "hidden"}` 을 적는다** — Archify 의 자동 범례는 영문 7.5px 라 관보 관문(15px)에 걸리고 문서에서는
55
- 캡션이 그 몫을 한다(실측 2026-09-16). **열은 4개 이하**(viewBox 폭 ≈ 1,000 이하) — 전폭 1030px 에 1:1 로 앉아야 글자가 산다.
56
- 부제(`sublabel`)는 9자 안, 간선 라벨은 5자 안 — 글자를 키워도(아래 4단계) 노드 폭 168 안에 서는 길이다.
57
- ⚠ Archify 의 라벨 폭 셈은 라틴 글자 기준이라 **한글 라벨은 예측보다 넓게 선다** — 라벨 겹침 진단이 나면 낱말을 줄이거나
58
- 노드 `width` 를 준다.
59
- 3. **검증한다** — `node bin/archify.mjs validate <type> <ir.json> --quality showcase --json` 이 9/9 · 오류 0 · 경고 0.
60
- 4. **굳힌다** —
61
- ```bash
62
- node ~/.claude/skills/genetics-report/assets/diagram.mjs render <type> <ir.json> <out.svg> --wide --min-text 16 --tight --json
63
- ```
64
- deliver → 브라우저에서 계산된 스타일을 속성으로 굳힘 → 클래스·스크립트·스타일·애니메이션·원래 표현 속성 걷음 → SVG 하나.
65
- font-family 는 굳히지 않는다(문서 활자를 물려받게).
66
- - `--min-text 16`: Archify 는 글자 크기를 컴파일러 상수로 박아(제목 16 · 부제 11 · 간선 8 · 구역 7~10) IR 로는 못 키운다.
67
- 문턱 아래 글자를 뷰박스 단위 16 으로 키우되 배치는 그대로 두고, **글자 상자 겹침·노드 넘침을 브라우저에서 재서** 영수증에 낸다(R5).
68
- 노드 부제는 제목과 16px 간격이라 `--min-sub`(기본 15) 까지만 키운다 — 그 위는 제목과 겹친다.
69
- - `--tight`: Archify 는 화면 비율에 맞춰 아래를 비워 두는데 문서에서는 빈 띠다. 배경 격자를 뺀 내용 상자 + 12 로 viewBox 를 조인다.
70
- - 영수증의 「작은 글자」 다섯 개가 무엇인지 보고 IR 에서 부제·태그를 줄인다 — 글자를 더 키우는 것은 답이 아니다. 상세 단계는 가장 자세한 단계로 고정한다.
71
- 5. **잰다** — 위 명령이 함께 잰다: 바이트(≤ 250,000) · viewBox · 가장 작은 글자 × (표시 폭 ÷ viewBox 폭) ≥ 15px(R1) ·
72
- 세로/폭 ≤ 1.8(R9). ✗ 면 exit 3 — 렌더러도 같은 ⚠ 를 내므로 **여기서 고친다**: 넓은 도해는 `--wide`(표시 1030px),
73
- 그래도 모자라면 노드·라벨을 줄인다. 글자를 키우는 것은 Archify 의 몫이 아니다 — 도해를 쪼갠다.
74
- 6. **앉힌다** — `report.json`. `ir` 길은 4·5 를 건너뛰고 IR 을 그대로 넣는다:
1
+ # 도해 — 설계 데이터를 쓰면 렌더러가 굽는다
2
+
3
+ > 구조 · 흐름 · 순서 · 상태 · 구성의 변화는 Archify 설계 데이터(IR)로 원고 `sections[].diagrams[]`(결정 모드는 `decisions[].figures[].diagram`)에 넣는다.
4
+ > 굽는 곳은 렌더러 한 곳이다 — 서버 발행과 로컬 `render.py` 가 저장소의 고정 판 Archify(`private/builder/archify`)로 관보 글자 기준(화면 15px)에
5
+ > 맞춰 정지 SVG 를 굽는다. 작성자 기계에 Archify · 브라우저를 설치할 일이 없다. 미리 구운 SVG(`svg` 칸)는 받지 않는다(2026-10-07).
6
+ > 설계 근거는 저장소 `DESIGN-gazette-diagrams.md`, 장치 목록의 정본은 `private/builder/devices.py`(아래 1절 표가 거기서 굽힌다).
7
+
8
+ ## 1. 종류와 한도
9
+
10
+ <!-- 장치 목록에서 굽는 글 «diagram-kinds» — 손으로 고치지 않는다: python3 tools/contract-devices.py --write -->
11
+ | 종류 | 원고 | 내용 모양 | 본문 단 760px | 전폭 1030px(`wide`) |
12
+ |---|---|---|---|---|
13
+ | **구성도** | `ir.diagram_type: "architecture"` | 구조(구성 요소 · 경계 · 연결) | 4열 격자 | 5열 격자 |
14
+ | **절차도** | `ir.diagram_type: "workflow"` | 흐름(절차 · 관문 · 운영 절차) | 4열 | 6열 |
15
+ | **순서도** | `ir.diagram_type: "sequence"` | 순서(누가 누구에게 무엇을 부르나) | 참여자 4명 | 참여자 6명 |
16
+ | **데이터 흐름도** | `ir.diagram_type: "dataflow"` | 흐름(데이터가 지나는 단계 · 계보) | 단계 3 | 단계 4 |
17
+ | **상태 변화도** | `ir.diagram_type: "lifecycle"` | 상태(상태와 그 사이의 전이) | 4열 | 5열 |
18
+ | **두 판 비교** | `compare: {base, head}` | 변화(구성의 전후 — 더해짐 · 빠짐 · 바뀜 · 옮김) | 3열 구성도 두 판 | 3열 구성도 두 판 |
19
+
20
+ 서버: 도해 굽기가 켜진 서버에서만 새로 굽는다(포털 private/archify.on — 로컬 render.py 는 켜고 부른다) — 꺼진 서버는 굽혀 둔 그림만 싣고 새 그림은 같은 번호의 대기 상자로 둔다(「서버에서 실패한 그림이 N장」 — 원고를 고칠 일이 아니다).
21
+ 한도: 화면 글자 15px — 고정 판 Archify 가 이 기준으로 굽고, 안 들면 거절하며 사유에 고칠 값(노드 width · 필요한 폭)을 싣는다 · 그림 250,000B · 문서의 그림 속성 합 60,000 · 한 장 10초 · 문서 12초 · 장면(meta.views)은 싣지 않는다.
22
+ <!-- /장치 목록 «diagram-kinds» -->
23
+
24
+ 한도는 「그 크기로 섰다」는 실측이다(라벨 길이에 따라 다르다). 넘으면 엔진이 거절하고 ⚠ 사유에 필요한 폭이 실린다 — 그림을 나누거나 `wide: true`.
25
+ 이용자가 여러 화면을 지나는 동선은 도해가 아니라 화면 녹화와 단계 캡처다(`visual-first.md` §4).
26
+
27
+ ## 2. 쓰는 법
28
+
29
+ 1. **종류를 고른다** — 위 표의 내용 모양으로. 상황이 애매하면 고정 판의 처방 목록이 종류를 권한다:
30
+ `node private/builder/archify/bin/archify.mjs guide "<상황>" --json`(저장소 기준 경로).
31
+ 2. **설계 데이터를 쓴다** — 고정 판 `private/builder/archify/schemas/<종류>.schema.json` 과 `examples/` 를 읽고 그 문서의 사실로 새로 쓴다.
32
+ `meta.locale: "ko"`(범례 · 그림 설명이 한국어로) · 한국어 라벨 · 노드 12개 이하 · 주 경로 하나. 절차도는 `schema_version: 2`.
33
+ Mermaid 원문이 있으면 설계 데이터로 다시 쓴다(모양은 옮기지 않는다).
34
+ 3. **원고에 앉힌다** — `caption` · `alt` 와 함께. 넓은 그림은 `wide: true`(전폭 1030px). 결정 장 그림 칸은 전폭 폭으로 굽힌다.
75
35
  ```json
76
36
  "diagrams": [{
77
- "id": "upgrade-path", "caption": "업그레이드가 지나는 세 관문",
78
- "ir": { "schema_version": 2, "diagram_type": "workflow", "meta": {"title": "…"}, "lanes": [...], "nodes": [...], "edges": [...] },
79
- "alt": "스냅샷 복원 → 시험 → 운영 적용", "verdict": "직접 확인", "wide": true,
80
- "origin": "archify workflow v2 · ir/upgrade-path.workflow.json"
37
+ "id": "save-order", "caption": "저장 뒤 목록을 다시 읽는 순서", "alt": "저장 → 목록 다시 읽기 → 첫 줄 표시",
38
+ "ir": { "schema_version": 2, "diagram_type": "workflow", "meta": { "title": "저장 순서", "locale": "ko" },
39
+ "lanes": [ … ], "nodes": [ … ], "edges": [ … ] }
81
40
  }]
82
41
  ```
83
- `svg` 길은 `"svg": "<svg …>…</svg>"` 로(파일 내용을 문자열로, JSON 이스케이프). `origin` 은 렌더 안 됨 — IR 경로를 남긴다.
84
- `ir` 길의 관보 규칙: 노드 라벨은 열 폭에서 15px 두 줄까지(넘치면 ⚠) · 부제 한 줄 · 되돌아가는 간선은 위 채널 ·
85
- `security` 는 위험색 테두리 · `external`·`dashed` 는 파선 · `mainPath` 는 장 색 굵은 선 · `groups` 는 그리지 않는다.
86
- 그림 번호는 차트 → 흐름도 → 도해 → 도판 순으로 자리가 매긴다. 본문에서 `{{그림:upgrade-path}}` 로 가리킨다.
87
- 7. **로컬 렌더 → dry_run** — 다른 장치와 같다. 렌더러 ⚠ 문구는 `lim-portal-format.md` §10-1.
42
+ 두 판 비교는 `"compare": {"base": <구성도>, "head": <구성도>}` 를 `ir` 대신.
43
+ 4. **굽고 본다** — 로컬 `python3 assets/render.py report.json` 이 굽는다(굽기를 켜고 부른다). 엔진이 거절하면 그 자리가 대기 상자로 서고 ⚠ 에
44
+ 고칠 값이 실린다 — 라벨이 노드보다 길면 그 노드 `width` 를 사유의 수 이상으로, 폭이 모자라면 `wide: true` 나 그림 나누기. 서버는 dry_run 으로 —
45
+ 굽기가 꺼진 서버는 새 그림을 대기 상자로 두고 「서버에서 실패한 그림이 N장」 한 줄을 낸다. 원고를 고칠 일이 아니다(1절 「서버」).
46
+ 5. **본문에서 가리킨다** — `{{그림:<id>}}`. 우리가 그린 도해의 출처 줄과 `verdict` 는 비운다(「직접 확인」은 바깥 원본에만 — `lim-portal-format.md` §11-5).
88
47
 
89
- ## 3. 무엇이 관보에 들어가고 무엇이 빠지나
48
+ ## 3. 문서에 서는 것과 문서 밖
90
49
 
91
50
  | Archify 가 주는 것 | 관보에서 |
92
51
  |---|---|
93
- | 정지 SVG(노드·간선·라벨·범례) | 그대로 — 활자로 |
94
- | 팬·줌·검색·초점·경로 추적·프레젠테이션 | 빠진다(스크립트 0). 필요하면 문서 밖 링크로 Archify HTML 을 따로 준다 |
95
- | 다크/라이트 | 라이트만 굳힌다(`colorScheme: light`) |
96
- | 애니메이션(`trace`) | 걷는다 — `reducedMotion` + `animation:none` 으로 정지 상태를 굳힌다 |
97
- | 브랜드 마크(`brand`) | `data:image/` 로 박힌 것만 남고 외부 주소는 걷힌다 |
98
- | 뷰어 UI 언어(`meta.locale`) | 무관 — 뷰어가 안 실린다 |
52
+ | 정지 그림(노드 · 연결 · 범례 · 내장 로고 · 관계 번호) | 그대로 — 색은 관보 토큰, 글자는 문서 활자 |
53
+ | 요약 카드(`ir.cards`) | 그림 아래 관보 부품 |
54
+ | 장면(`meta.views`, 5까지) | 장면마다 1:1 그림 한 장 + 이름 · 메모(초점 밖은 흐리게) |
55
+ | 두 판 비교 | 겹친 그림 한 장 + 바뀐 항목 표 |
56
+ | 확대 · 검색 · 경로 · 움직임 | 문서 밖 뷰어 — 읽기 화면의 「뷰어로 열기」(굽기가 켜진 서버에서) |
57
+ | 저장소 근거(`meta.repository` · `components[].sources`) | 걷고 굽는다(⚠) — 독자에게 보이려면 부록 표로 |
58
+ | 주소로 가져온 로고(`brand` 객체) · 절차도 판 1 | 거절 — 사유가 내장 로고 이름 · 판 올림 명령을 말한다 |
99
59
 
100
60
  ## 4. 하지 않는 것
101
61
 
102
- - Mermaid 를 관보에 직접 싣지 않는다. Mermaid 원문은 Archify 의 입력이다(Archify `SKILL.md` 「Mermaid input」) — Archify IR 로
103
- 다시 쓰고 위 절차를 밟는다.
104
- - SVG 를 손으로 고치지 않는다. 고칠 것은 IR 이고, 굳히기는 다시 돌린다(재현성).
105
- - 도해 하나에 문장을 넣지 않는다(R4). 설명은 캡션 한 문장과 본문.
62
+ - SVG 를 미리 굽거나 손으로 고치지 않는다 — 고칠 것은 설계 데이터다(다시 구우면 같은 그림이 선다).
63
+ - 도해 하나에 문장을 넣지 않는다(readability R4). 설명은 캡션 한 문장과 본문.
@@ -3,25 +3,30 @@
3
3
  // args = { file: "<새 판 report.json 절대경로>", // 편집을 적용한 새 판 원고 — 교체에 싣는 인자 파일·교체 뒤 되읽은 판이 아니다
4
4
  // backup: "<직전 판 백업 절대경로>", // 새 판을 만든 편집의 출발점
5
5
  // edits: "<edits.json 절대경로>", // assets/patch.py 편집 목록 — 변경 기록의 기본
6
- // patch: "<patch.py 절대경로>", // 편집 목록일 때 — 편집 목록을 굳힌 그 patch.py. 기본 ~/.claude/skills/genetics-report/assets/patch.py
6
+ // patch: "<patch.py 절대경로>", // 편집 목록일 때 — 편집 목록을 굳힌 그 patch.py. 기본 <skill>/assets/patch.py
7
7
  // // (없거나 그 목록을 못 읽는 판이면 시작 전 검사에서 멈춘다 — 스킬 폴더가 다른 트리의 심링크인 기계는 옛 판일 수 있다)
8
+ // skill: "<스킬 폴더 절대경로(선택) — 기본 ~/.claude/skills/genetics-report>", // patch 기본값과 오류 안내의 render.py 가 이 아래
8
9
  // canon: "<정본을 가리키는 한 줄>", // 수치 편집을 대조할 정본 — 파일 경로와 자리를 글로(「…/점검표.md 의 끝 7줄 · …tsv」). 여러 출처여도 한 줄로.
9
10
  // // 안 주면 검토자는 doc 글과 handoff 에서 정본을 찾고, 어디에도 없으면 수치 편집을 「정본 없음」으로 적는다
10
11
  // edit_script: "<edit-N.py 절대경로>", // 옛 기록: put/sub 호출로 변경 자리가 전부 적힌 스크립트(멱등). edits 가 없을 때만 쓴다
11
12
  // doc: "<문서 이름 한 줄>", // 예: "고객사 결정 브리핑 16판" — 프롬프트 머리 「대상:」에 글자 그대로 실린다
12
13
  // renderer: "<gazette.py 절대경로>", // 기본 report-portal private/builder/gazette.py
13
14
  // handoff: "<HANDOFF.md 절대경로>", // 있으면 그 판의 절을 배경으로 준다(선형 독자는 안 받는다)
15
+ // plates: ["<도판 id>", …], // 이 판에서 더하거나 바꾼 도판·영상 — 편집 목록이면 시작 전 검사가 두 판을 견주어 스스로 잰다(이 칸은 더할 때만)
14
16
  // text: "<새 판 렌더본 텍스트 절대경로>" } // 필수 — python3 assets/render.py <새 판 report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt
15
17
  //
16
18
  // 언제 쓰나 — 구조(PART·절·표)가 그대로이고 낱말·출처 줄·여백 주석만 고친 판. 구조가 바뀐 판은
17
- // `reader-review.workflow.js`(여덟 관점)다. 낱말 손질 판을 검토 없이 발행하자 다음 판 8자리 손질이
19
+ // `reader-review.workflow.js`(아홉 관점 — 상태 표가 있으면 열)다. 낱말 손질 판을 검토 없이 발행하자 다음 판 8자리 손질이
18
20
  // 생겼다(2026-09-15 고객사 결정 브리핑 15판 → 16판): 되풀이된 전제, 주어가 빠진 출처 줄, 인쇄에서 죽는 「누르면」.
19
21
  //
20
22
  // 세 관점 — 선형 독자(그 자리에서 뜻이 닫히나·뒤 절 번호를 가리키나·조사·중복) · 규칙 대조(렌더러 text_scan 의
21
23
  // W2·R12h·RX_PROC·장치 이름·SOURCE_MAX·그림 번호를 코드 줄로 대조) · 사실 대조(옛 문구의 사실이 바뀌지 않았나,
22
24
  // 백업↔새 판 JSON diff 에 스크립트가 바꾼 자리가 빠짐없이 있고 기록 안 된 변경이 없는지 — 삽입·삭제가 섞이면 줄 수는 호출 수와
23
25
  // 다르니 자리로 센다, 수치 무변경. 편집 목록이면 직전 판 사본에 patch.py 를 적용한 결과가 새 판과 같은지, 수치를 바꾼 편집은 모두 적는다).
24
- // 심각도 3 이상만 반박 단계(최대 6).
26
+ // 심각도 3 이상만 반박한다 — 관점이 끝나는 대로 그 관점의 발견을 심각도 순으로 VERIFY_PER_LENS 건까지(2026-10-02, 전에는 모든 관점을 기다려 6건).
27
+ // 넷째 관점 증거 대조(capture — 합의 착각)는 늘 켠다 — 바뀐 자리의 그림 없는 관찰 주장 · 바뀐 사실의 전파를 적고, 도판·영상을 더한 판이면 그림 대조까지
28
+ // (2026-10-01 관보 합의 착각 수리: 판 6 에서 손으로 띄운 캡처 대조가 「어긋난 것만」 적어, 그림이 없는 주장은 셈 밖이었다 · 2026-10-02: 글만 고친 판에서는 아예 안 켜졌다).
29
+ // 반박에서 증거를 싣는 처방은 말투·지은 이름 규칙 위반으로 기각하지 않는다 — 정직한 고침이 그 두 규칙에 막혀 동사 낮추기만 남았었다(같은 수리).
25
30
  // 편집 목록(edits)을 정식 입력으로 받는 까닭 — 2026-09-29 관보 한 판이 patch.py 편집 목록으로 만들어졌는데 이 워크플로는
26
31
  // edit_script 만 받아, 목록을 edit_script 에 넣고 「JSON 편집 목록이다」라는 풀이를 doc 에 적어야 했다(두 번 돌림).
27
32
  // 선형 독자는 새 판 렌더본 텍스트(args.text) 하나만 받고 CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 편집 스크립트·옛 문구·핸드오프를
@@ -29,23 +34,27 @@
29
34
  // 그래서 변경 자리가 아니라 문서 전체를 처음부터 읽는다. 확인 전: MEMORY.md 까지 빠지는지는 첫 실행 전사의 첨부로 확인한다.
30
35
  // 반박은 진단(참/부분/거짓/미확인)과 처방 적합을 따로 판정한다. 진단 거짓만 refuted, 처방이 규칙에 걸리면 kept 에 남기고 rx 「처방 다시 — 까닭」.
31
36
  // 결과 — { head·failed(검증이 실패했거나 관점이 결과를 못 냈거나 선형 독자가 렌더본을 못 열었을 때만 — 「검토 미완 — 검증 N건 실패」),
32
- // kept(진단이 거짓이 아닌 발견 + rx), refuted(진단 거짓), weak(심각도 2 이하 — 사람이 훑어 고를 것), unverified(상한 6 밖·검증 실패 — 사람이 훑는다), passed }.
37
+ // kept(진단이 거짓이 아닌 발견 + rx), refuted(진단 거짓), weak(심각도 2 이하 — 사람이 훑어 고를 것), unverified(상한 6 밖·검증 실패 — 사람이 훑는다), passed,
38
+ // filled(선형 독자가 이 파일 안 증거 줄을 못 댄 관찰 주장 — 일반 지식으로 메운 것), plates(캡처 대조를 켠 도판 id) }.
33
39
  // 시작 전 검사(Check): 렌더본 파일(args.text)이 없거나 비었거나 새 판 원고(args.file)보다 오래되면 읽기 전에 멈추고 무엇이 필요한지 말한다.
34
40
  // 편집 목록이면 patch.py 도 잰다 — 없거나, 새 판에 그 목록을 --dry 로 돌렸을 때 목록을 못 읽거나(모르는 op · 트레이스백 · JSON 아님)
35
41
  // 「바꿈 N」(N>0 — 새 판에 편집이 아직 적용돼 있지 않다: file 과 backup 을 바꿔 줬을 때)이면 멈춘다. 종료 코드로는 가리지 않는다(금지어가 남은 정상 판도 1 이다).
42
+ // 편집 목록이면 같은 명령이 두 판의 도판·영상도 견준다(다섯째 칸 — 더하거나 바꾼 id, 두 판 가운데 하나라도 없으면 null) — 캡처 대조를 켤지 정한다.
36
43
  // 워크플로 스크립트는 파일을 못 연다 — 에이전트 하나가 명령 한 줄의 출력을 옮기고 판정은 JS 가 한다(reader-review.workflow.js 머리 주석과 같다).
37
44
  export const meta = {
38
45
  name: 'report-edition-review',
39
- description: '보고서 한 판을 선형 독자(렌더본 텍스트만)·규칙 대조·사실 대조(변경 자리 — patch.py 편집 목록 또는 edit-N.py) 세 관점으로 읽고, 심각도 3 이상은 진단·처방을 따로 반박 검증한다',
46
+ description: '보고서 한 판을 선형 독자(렌더본 텍스트만)·규칙 대조·사실 대조(변경 자리 — patch.py 편집 목록 또는 edit-N.py)·증거 대조(합의 착각) 네 관점으로 읽고, 관점이 끝나는 대로 심각도 3 이상의 진단·처방을 따로 반박 검증한다',
40
47
  phases: [
41
- { title: 'Check', detail: '렌더본이 있고 비지 않았고 새 판 원고보다 새로운지 · 편집 목록이면 patch.py 가 그 목록을 읽는지 — 명령 한 줄' },
42
- { title: 'Review', detail: '세 관점 병렬 — 선형 독자 · 렌더러 규칙 대조 · 사실 대조' },
43
- { title: 'Verify', detail: '심각도 3 이상 발견을 반박(최대 6) — 진단 거짓만 refuted' },
48
+ { title: 'Check', detail: '렌더본이 있고 비지 않았고 새 판 원고보다 새로운지 · 편집 목록이면 patch.py 가 그 목록을 읽는지 · 더한 도판 — 명령 한 줄' },
49
+ { title: 'Review', detail: '네 관점 병렬 — 선형 독자 · 렌더러 규칙 대조 · 사실 대조 · 증거 대조(합의 착각, 늘 켬 — 도판을 더했으면 그림 대조까지)' },
50
+ { title: 'Verify', detail: '관점이 끝나는 대로 그 관점의 심각도 3 이상 발견을 반박(관점마다 최대 2) — 진단 거짓만 refuted' },
44
51
  ],
45
52
  }
46
53
  const A = args || {}
54
+ // 스킬 폴더 — patch.py 기본값과 오류 안내의 render.py 가 이 아래다(reader-review.workflow.js 와 같은 상수 — 조사관에게 스킬 파일은 이 경로로 가리킨다).
55
+ const SKILL = A.skill || '~/.claude/skills/genetics-report'
47
56
  if (!A.file || !A.backup || !(A.edits || A.edit_script)) throw new Error('args.file · args.backup 과 args.edits(patch.py 편집 목록) 또는 args.edit_script(edit-N.py) 가 필요하다')
48
- const MAKE_TEXT = 'python3 assets/render.py <새 판 report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt'
57
+ const MAKE_TEXT = `python3 ${SKILL}/assets/render.py <새 판 report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt`
49
58
  if (!A.text) throw new Error(`args.text 가 필요하다 — 새 판 렌더본 텍스트 파일 절대경로. 선형 독자는 이 파일 하나만 읽는다. 만드는 법: ${MAKE_TEXT}`)
50
59
  phase('Check')
51
60
  // 렌더본이 있고 비지 않았고 새 판 원고보다 새로운지 — 파일 상태는 에이전트가 옮긴 명령 출력으로만 읽고 판정은 여기서 한다(머리 주석 「시작 전 검사」).
@@ -56,17 +65,20 @@ const shq = (p) => `'${String(p).replace(/'/g, `'\\''`)}'`
56
65
  // 그래서 새 판에 그 목록을 --dry 로 한 번 돌려 넷째 칸(rc·tb·dry·out)으로 옮긴다 — 목록을 **읽는지**와 새 판에 이미 적용돼 있는지(「바꿈 0」)만
57
66
  // 여기서 가리고, 값이 맞는지는 사실 관점 ①이 잰다. 종료 코드로 가리지 않는다 — 금지어가 남은 정상 판도 「바꿈 0」 뒤에 1 로 끝난다.
58
67
  // 트레이스백은 따로 한 칸(tb), 「바꿈 N · …」 줄도 따로 한 칸(dry)이다 — out 은 끝 300자라 경로가 길면 「Traceback」 낱말과 앞줄이 잘린다.
59
- const PATCH = A.patch || '~/.claude/skills/genetics-report/assets/patch.py'
68
+ const PATCH = A.patch || `${SKILL}/assets/patch.py`
60
69
  const STAT_PY = 'dict(f=os.path.isfile(p), b=(len(open(p,"rb").read().strip()) if os.path.isfile(p) else -1), m=(os.path.getmtime(p) if os.path.isfile(p) else -1))'
70
+ // 다섯째 칸 — 새 판(a[1])과 직전 판(a[4])의 도판·영상을 견주어, 직전 판에 없던 id 와 file·source·data·ref 가 달라진 id 를 낸다(캡처 대조를 켤지).
71
+ // 두 판 가운데 하나라도 파일이 없으면 null(모름). 캡션만 고친 장은 더한 그림이 아니다.
72
+ const PLATES_PY = 'pl=lambda p: ({str(x.get("id")): repr([x.get(k) for k in ("file", "source", "data", "ref")]) for x in (json.load(open(p)).get("assets") or []) if isinstance(x, dict) and x.get("role") in ("figure", "video")} if os.path.isfile(p) else None); n, o = pl(a[1]), pl(a[4]); pa = (sorted(k for k in n if o.get(k) != n[k]) if n is not None and o is not None else None)'
61
73
  const STAT_CMD = A.edits
62
- ? `python3 -c 'import json,os,subprocess,sys; a=list(map(os.path.expanduser, sys.argv[1:])); st=[${STAT_PY} for p in a[:3]]; r=subprocess.run([sys.executable, a[2], a[1], a[3], "--dry"], capture_output=True, text=True) if all(os.path.isfile(p) for p in a[1:]) else None; print(json.dumps(st + [dict(rc=(r.returncode if r else -1), tb=bool(r and "Traceback" in r.stderr), dry=next((l for l in (r.stdout if r else "").splitlines() if l.startswith("바꿈 ")), ""), out=((r.stdout + r.stderr)[-300:] if r else ""))]))' ${shq(A.text)} ${shq(A.file)} ${shq(PATCH)} ${shq(A.edits)}`
74
+ ? `python3 -c 'import json,os,subprocess,sys; a=list(map(os.path.expanduser, sys.argv[1:])); st=[${STAT_PY} for p in a[:3]]; r=subprocess.run([sys.executable, a[2], a[1], a[3], "--dry"], capture_output=True, text=True) if all(os.path.isfile(p) for p in a[1:4]) else None; ${PLATES_PY}; print(json.dumps(st + [dict(rc=(r.returncode if r else -1), tb=bool(r and "Traceback" in r.stderr), dry=next((l for l in (r.stdout if r else "").splitlines() if l.startswith("바꿈 ")), ""), out=((r.stdout + r.stderr)[-300:] if r else "")), pa]))' ${shq(A.text)} ${shq(A.file)} ${shq(PATCH)} ${shq(A.edits)} ${shq(A.backup)}`
63
75
  : `python3 -c 'import json,os,sys; print(json.dumps([${STAT_PY} for p in sys.argv[1:]]))' ${shq(A.text)} ${shq(A.file)}`
64
76
  const PRE = { type: 'object', properties: { stdout: { type: 'string', description: '명령의 표준 출력 한 줄 — 글자 그대로' }, exit: { type: 'integer' } }, required: ['stdout', 'exit'] }
65
77
  const pre = await agent(`아래 명령을 Bash 로 **한 번만, 글자 그대로** 실행하고 표준 출력 한 줄을 stdout 에 고치지 말고 옮긴다. 종료 코드는 exit 에. 명령을 바꾸거나 다른 명령·파일을 열지 않는다.\n${STAT_CMD}`,
66
78
  { label: 'check:files', phase: 'Check', schema: PRE, effort: 'low' })
67
79
  let st = null
68
80
  try { st = pre && pre.exit === 0 ? JSON.parse(pre.stdout) : null } catch (e) { st = null }
69
- if (!Array.isArray(st) || st.length !== (A.edits ? 4 : 2)) throw new Error(`시작 전 검사 실패 — 렌더본·원고${A.edits ? '·patch.py' : ''} 파일 상태를 못 읽었다(${pre ? `exit ${pre.exit} · ${String(pre.stdout).slice(0, 200)}` : '검사 에이전트 결과 없음'}). resumeFromRunId 없이 새로 띄운다`)
81
+ if (!Array.isArray(st) || st.length !== (A.edits ? 5 : 2)) throw new Error(`시작 전 검사 실패 — 렌더본·원고${A.edits ? '·patch.py·두 판의 도판(새 판·직전 판이 JSON 이어야 한다)' : ''} 파일 상태를 못 읽었다(${pre ? `exit ${pre.exit} · ${String(pre.stdout).slice(0, 200)}` : '검사 에이전트 결과 없음'}). resumeFromRunId 없이 새로 띄운다`)
70
82
  if (!st[1].f) throw new Error(`args.file 파일이 없다 — ${A.file}. 새 판 report.json 절대경로를 준다`)
71
83
  if (!st[0].f) throw new Error(`args.text 파일이 없다 — ${A.text}. 만드는 법: ${MAKE_TEXT}`)
72
84
  if (st[0].b <= 0) throw new Error(`args.text 파일이 비었다 — ${A.text}. 렌더가 실패했을 수 있다 — 다시 뽑고 출력의 ⚠·오류를 본다: ${MAKE_TEXT}`)
@@ -75,11 +87,14 @@ if (A.edits && st[3].rc === -1) throw new Error(`args.edits 파일이 없다 —
75
87
  if (A.edits && (st[3].tb || /모르는 op|JSON 으로 못 읽었습니다/.test(String(st[3].out)))) throw new Error(`patch.py 가 이 편집 목록을 못 읽는다 — ${PATCH} (${String(st[3].out).trim().split('\n').pop().slice(0, 200)}). 편집 목록을 굳힌 그 patch.py 의 절대경로를 args.patch 로 준다 — 기본 경로가 다른 트리의 옛 판으로 풀렸을 수 있다`)
76
88
  if (A.edits && Number((/^바꿈 (\d+) ·/.exec(String(st[3].dry)) || [])[1]) > 0) throw new Error(`새 판에 편집 목록이 아직 적용돼 있지 않다 — ${A.file} 에 --dry 로 돌리니 「${st[3].dry}」. args.file 은 편집을 적용한 새 판, args.backup 이 그 출발점이다(둘을 바꿔 주지 않았는지 본다)`)
77
89
  if (st[0].m < st[1].m) throw new Error(`렌더본이 새 판 원고보다 오래됐다 — ${A.text} 가 ${A.file} 보다 ${Math.round(st[1].m - st[0].m)}초 앞선다. 새 판을 고친 뒤 다시 뽑는다: ${MAKE_TEXT} (그다음 resumeFromRunId 없이 새로 띄운다)`)
90
+ // 이 판에서 더하거나 바꾼 도판·영상 — 편집 목록이면 검사 명령이 잰 것, 그리고 args.plates(편집 스크립트일 때·더할 때).
91
+ if (A.edits && st[4] === null) log('⚠ 이 판에서 더한 도판·영상을 못 쟀다(새 판·직전 판 파일 가운데 하나가 없다) — 캡처 대조는 args.plates 로만 켠다')
92
+ const PLATES = [...new Set([...(A.edits && Array.isArray(st[4]) ? st[4] : []), ...(Array.isArray(A.plates) ? A.plates.map(String) : [])])]
78
93
  const RENDERER = A.renderer || '레포 report-portal 의 private/builder/gazette.py(args.renderer 가 없으면 이 기계에서 경로를 찾아 쓴다 — 못 찾으면 규칙 관점 결과에 「렌더러 못 읽음」으로 적는다)'
79
94
  // 반박에도 렌더러 경로를 준다 — 2026-09-30 실제 1회에서 반박 하나가 args.renderer 대신 다른 트리의 옛 렌더러를 읽고 판정했다.
80
95
  const RENDERER_PATH = A.renderer || '레포 report-portal 의 private/builder/gazette.py'
81
96
  const DOC = A.doc || A.file
82
- // 변경 기록이 무엇인지(CTX)와 사실 관점 ①(기록 대조), 반박이 읽을 기록의 이름(src)만 기록 종류에 따라 갈린다. edit_script 쪽 세 값은 전과 한 글자도 같다.
97
+ // 변경 기록이 무엇인지(CTX)와 사실 관점 ①(기록 대조), 반박이 읽을 기록의 이름(src)만 기록 종류에 따라 갈린다. edit_script 쪽 세 값은 편집 목록을 받기 전과 한 글자도 같았다 — 2026-10-02 CTX 끝에 작성자 자료 한 줄(합의 착각)을 두 기록 모두에 더했다(시험 _WCHG 도 함께 고침).
83
98
  // 「제자리에서 고친다」 경고는 CTX 에 둔다 — 규칙 관점·반박도 patch.py 를 돌릴 수 있다.
84
99
  // 정본 — 글로만 「doc·handoff 가 가리키는 정본」이라 하면 검토자는 그 인자 이름을 본 적이 없다(CTX 는 「대상」·「배경」으로만 싣는다).
85
100
  // args.canon 을 받아 CTX 에 「정본」으로 싣고, 없으면 프롬프트에 **보이는 자리**(「대상」 글 · 배경 파일)를 가리킨다 — 정본을 doc 글에 적어
@@ -95,16 +110,28 @@ const REC = A.edits ? {
95
110
  change: `${A.edit_script} 에 put/sub 호출로 전부 적혀 있다(옛 문구 → 새 문구).`,
96
111
  count: '① 직전 판과 새 판의 JSON 을 diff 해 스크립트의 put/sub·삽입·삭제가 바꾼 자리가 빠짐없이 보이는지, 기록되지 않은 변경이 없는지 자리로 센다(삽입·삭제가 섞이면 diff 줄 수는 호출 수와 다르다). 수치(인원·kg·cm·날짜)가 한 곳도 안 바뀌었는지도 센다.',
97
112
  }
98
- const CTX = `대상: ${DOC}. 직전 판 → 이 판의 변경은 ${REC.change} 새 판 원고는 ${A.file}, 직전 판은 ${A.backup}.${A.handoff ? ` 배경은 ${A.handoff} 의 이 판 절.` : ''}${A.canon ? ` 정본(수치의 출처)은 ${A.canon}.` : ''} 한글로 답한다. 파일은 읽기만 한다 — 고치지 않는다.`
113
+ // 합의 착각(2026-10-02) — 검토자가 작성자의 자료(핸드오프·편집 목록·코드·서버 조회)를 읽고 나면 작성자처럼 「다 아는」 눈으로 판정한다.
114
+ // 그 자료는 무엇이 바뀌었나·사실이 참인가를 찾는 지도일 뿐, 독자가 문서 안에서 보는 증거가 아니다 — 모든 관점·반박에 한 줄로 못 박는다.
115
+ const CTX = `대상: ${DOC}. 직전 판 → 이 판의 변경은 ${REC.change} 새 판 원고는 ${A.file}, 직전 판은 ${A.backup}.${A.handoff ? ` 배경은 ${A.handoff} 의 이 판 절.` : ''}${A.canon ? ` 정본(수치의 출처)은 ${A.canon}.` : ''} 한글로 답한다. 파일은 읽기만 한다 — 고치지 않는다. 핸드오프·편집 목록·코드·서버 조회는 무엇이 바뀌었나와 사실이 참인가를 찾는 데만 쓴다 — 독자는 그것을 못 본다. 독자에게 보이는 증거로 세는 것은 렌더본 글과 그 그림뿐이다.`
99
116
  // 선형 독자의 머리말 — 위 CTX(편집 스크립트·직전 판·핸드오프)를 받지 않는다. 독자가 보는 것은 렌더본뿐이다(render.py --text).
117
+ // 그림 자리 표지와 메움 세기는 reader-review.workflow.js 와 같은 글이다 — 「메웠다면 발견」 자기 신고 한 줄을 주장마다 증거 줄을 대는 절차로 바꿨다(2026-10-01).
118
+ const MARKS = '그림·영상 자리는 「[그림] <도판 id> · <갈래>」「[영상] <id> · <갈래>」 한 줄이다 — 갈래는 캡처·녹화·도안·합성·문서이고 도안은 우리가 그린 설명 그림이다. 이 표지 줄과 글 끝의 「[그림 파일]」 목록은 검토용이라 독자 화면에 없다(그 id 를 이름 발견으로 세지 않는다).'
119
+ const FILL_STEP = '메움 세기 — 화면·흐름·화면 문구·동작을 말하는 문장(「…뜸·보임·켜짐·막힘·넘어감·저장됨」, 「」로 옮긴 화면 글자)마다 claims 에 한 행을 적는다: 그 줄 번호, 그리고 그것을 보여 주는 증거(캡처·녹화·합성 그림 자리 · 원문 인용 · 센 대상이 있는 표)가 선 이 파일의 본문 줄 번호(shown_at — 끝 목록 줄은 대지 않는다). 댈 줄이 없거나 도안뿐이면 「없음」 — 읽으며 떠올린 화면이 이 파일의 어느 줄에서 왔는지 못 대면 일반 지식으로 메운 것으로 센다.'
100
120
  const TEXT_CTX = `[읽기 전용. 한글로 답한다. 철저함: very thorough — 발췌하지 말고 파일 전체를 처음부터 끝까지 읽는다.]
101
- 읽을 것: ${A.text} — 새 판 렌더본에서 화면에 보이는 글만 화면 순서대로 뽑은 텍스트(그림·영상 자리는 「[그림]」「[영상]」, 표 칸은 「 | 」).
102
- 이 파일 하나만 연다. 원고에 없는 지식으로 뜻을 메웠다면 그것 자체가 발견이다. 원고 JSON·편집 스크립트·핸드오프·같은 폴더의 다른 파일은 열지 않는다.
103
- 이 파일을 끝까지 읽었으면 opened=true. 열지 못했거나 비어 있으면 opened=false 로 두고 findings·passed 는 비운다 — 다른 파일로 대신 읽지 않는다.
121
+ 읽을 것: ${A.text} — 새 판 렌더본에서 화면에 보이는 글만 화면 순서대로 뽑은 텍스트(표 칸은 「 | 」). ${MARKS}
122
+ 이 파일 하나만 연다 — 끝 「[그림 파일]」 목록에 경로가 적힌 그림은 독자가 보는 그림이라 Read 로 열어 봐도 된다. 원고 JSON·편집 스크립트·핸드오프·같은 폴더의 다른 파일은 열지 않는다.
123
+ ${FILL_STEP}
124
+ 이 파일을 끝까지 읽었으면 opened=true. 열지 못했거나 비어 있으면 opened=false 로 두고 findings·passed·claims 는 비운다 — 다른 파일로 대신 읽지 않는다.
104
125
  where 는 이 파일에 보이는 절 번호·이름·표 이름으로, evidence 는 이 파일의 줄 번호와 그 줄 글자로 적는다.`
105
126
  const FINDINGS = { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: { where: { type: 'string' }, severity: { type: 'integer' }, claim: { type: 'string' }, evidence: { type: 'string' }, fix: { type: 'string' } }, required: ['where', 'severity', 'claim', 'evidence'] } }, passed: { type: 'array', items: { type: 'string' } } }, required: ['findings', 'passed'] }
106
- // 렌더본 관점(선형 독자)은 파일을 열었는지부터 답한다 — 못 연 관점의 「발견 0」을 「문제 없음」으로 읽지 않게.
107
- const FINDINGS_TEXT = { type: 'object', properties: { ...FINDINGS.properties, opened: { type: 'boolean', description: '렌더본 파일을 열어 끝까지 읽었으면 true, 못 열었거나 비었으면 false' } }, required: [...FINDINGS.required, 'opened'] }
127
+ // 렌더본 관점(선형 독자)은 파일을 열었는지부터 답한다 — 못 연 관점의 「발견 0」을 「문제 없음」으로 읽지 않게. claims 는 메움 세기(FILL_STEP).
128
+ const CLAIM_ROW = { type: 'object', properties: {
129
+ line: { type: 'integer', description: '주장이 선 줄 번호' },
130
+ claim: { type: 'string', description: '그 문장 — 줄 글자 그대로(길면 앞 60자)' },
131
+ shown_at: { type: 'string', description: '그것을 보여 주는 증거가 선 이 파일의 줄 번호와 그 줄 글자 — 없거나 도안뿐이면 「없음」' } },
132
+ required: ['line', 'claim', 'shown_at'] }
133
+ const FINDINGS_TEXT = { type: 'object', properties: { ...FINDINGS.properties, opened: { type: 'boolean', description: '렌더본 파일을 열어 끝까지 읽었으면 true, 못 열었거나 비었으면 false' },
134
+ claims: { type: 'array', items: CLAIM_ROW, description: '화면·흐름·화면 문구·동작을 말하는 문장마다 한 행 — 상한 없음' } }, required: [...FINDINGS.required, 'opened', 'claims'] }
108
135
  // 진단과 처방을 따로 판정한다 — 한 칸(refuted)에 「처방이 다른 규칙을 어기면」「확신이 없으면」까지 묶어 진단이 참인 발견이 기각됐다(2026-09-26 스킬 검토).
109
136
  const VERDICT = { type: 'object', properties: {
110
137
  diagnosis: { type: 'string', enum: ['참', '부분', '거짓', '미확인'], description: '발견의 주장이 맞나 — 확신이 없으면 미확인' },
@@ -113,34 +140,57 @@ const VERDICT = { type: 'object', properties: {
113
140
  reason: { type: 'string' } }, required: ['diagnosis', 'rx_fit', 'rx_why', 'reason'] }
114
141
  const LENSES = [
115
142
  { key: 'linear', agentType: 'Explore', text: true, prompt: `${TEXT_CTX}\n[선형 독자] 새 판을 처음 읽는 고객 담당이 되어 파일 처음부터 끝까지 순서대로 읽는다. 자리마다 (a) 그 자리에서 뜻이 닫히는가 (b) 앞에서 정의되지 않은 낱말·번호를 가리키는가 — 뒤 절 번호 가리킴은 금지 규칙이고 표 번호·「다음 절」로 바꿔 놓은 것도 독자를 뒤로 보내면 적는다 (c) 어색한 문장·조사·중복(절 이름과 부제, 같은 답 묶음 안의 되풀이)이 있는가 (d) 인쇄본에서 죽는 화면 어휘(「누르면」)가 있는가 (e) 대상·판·시험·상태·선택지를 부르는 낱말마다, 독자가 문서 밖(자기 화면 글자, 원본 문서, 공급사 공식 명칭)에서 본 글자인가? 이 문서가 붙인 이름이면 정의가 있어도 발견이다. 처방은 뜻풀이가 아니라 원래 이름으로 바꾸는 것이다 — 원래 이름을 이 파일만으로 알 수 없으면 fix 에 이름을 지어 넣지 말고 「원래 이름으로 바꾼다(이 파일에 없음)」라고 적는다. 문제가 없는 자리도 passed 에 적는다. severity 1~5(5=독자가 뜻을 잃음).` },
116
- { key: 'rules', prompt: `${CTX}\n[규칙 대조] 렌더러 ${RENDERER} 의 text_scan 규칙 — W2 출처 줄(rx_loc: 쪽·행·https·건 §·§ n·부록 N, user-provided 만; rx_meta 제작 설명·그림 번호), R12h 앞 절→뒤 절 가리킴(rx_fwd_case/rx_fwd_sec/rx_other_doc, fwd_spot 이 훑는 칸), RX_PROC 과정 어휘, 장치 이름 충돌(건 행 이름 labels 낱말이 다른 절 표 열·부제에), SOURCE_MAX, 그림·표 번호 가리킴, 문장 60자·괄호 부연·표 주석 문장 수 — 를 코드에서 직접 읽고, 변경 자리의 새 문구가 각 규칙을 만족하는지, 새 문구가 다른 규칙을 새로 어기지 않는지 하나씩 대조한다. 판정 근거는 렌더러 파일의 줄 번호로 적는다.` },
143
+ { key: 'rules', prompt: `${CTX}\n[규칙 대조] 렌더러 ${RENDERER} 의 text_scan·evidence_scan 규칙 — W2 출처 줄(rx_loc: 쪽·행·https·건 §·§ n·부록 N · 캡처는 화면·창·페이지 이름 + 날짜, user-provided 만; rx_meta 제작 설명·그림 번호), 증거 가시성(화면·흐름을 말하는 문장인데 그 건·절에 독자가 볼 증거 도판·인용이 없는 곳 — 도판 갈래는 asset_evidence_kind), R12h 앞 절→뒤 절 가리킴(rx_fwd_case/rx_fwd_sec/rx_other_doc, fwd_spot 이 훑는 칸), RX_PROC 과정 어휘, 장치 이름 충돌(건 행 이름 labels 낱말이 다른 절 표 열·부제에), SOURCE_MAX, 그림·표 번호 가리킴, 문장 60자·괄호 부연·표 주석 문장 수 — 를 코드에서 직접 읽고, 변경 자리의 새 문구가 각 규칙을 만족하는지, 새 문구가 다른 규칙을 새로 어기지 않는지 하나씩 대조한다. 판정 근거는 렌더러 파일의 줄 번호로 적는다.` },
117
144
  { key: 'facts', prompt: `${CTX}\n[사실 대조] ${REC.count} ② 변경 자리마다 새 문구가 옛 문구의 사실을 바꾸지 않았는지 원고 안에서 대조한다 — 가리키는 부록·표·용어가 실제로 있는가, 이름으로 바꾼 위치가 같은 대상인가, 사실문으로 고친 문장이 요지·다른 절과 같은 사실인가. 다른 문서를 인용한 자리는 그 문서를 열 수 있으면 열어 대조하고 못 열면 「확인 불가」로 적는다. 바뀐 사실이 있으면 severity 4 이상.` },
145
+ // 증거 대조(합의 착각) — 전에는 도판·영상을 더한 판에서만 켰다. 글만 고친 판에서는 「그림이 없는 관찰 주장」을 아무도 안 셌고,
146
+ // 작성자가 코드로 아는 동작을 표 칸·요지에 단정한 판이 그대로 나갔다(2026-10-02 관보 한 판 — 표 칸 둘을 화면 없이 단정, 요지는 그 변화를 빠뜨림).
147
+ // 그래서 늘 켠다. 도판을 더했으면 (1) 그림 대조를 함께 한다. 어긋난 것만 적는 대조는 그림이 없는 주장을 셈 밖에 둔다 — 두 방향을 다 적는다.
148
+ { key: 'capture', prompt: `${CTX}\n[증거 대조 — 합의 착각] 작성자는 코드·시험·대화로 아는 것을 독자도 안다고 여기고 쓴다. 독자는 렌더본 ${A.text} 의 글과 그림만 본다.${PLATES.length ? ` (1) 그림이 뒷받침 못 하는 주장: 이 판에서 더하거나 바꾼 도판·영상 ${PLATES.join(', ')} 을 렌더본 끝 「[그림 파일]」 목록에서(거기 없으면 새 판 assets[].file) Read 로 열어 눈으로 본다 — 영상을 못 열면 「영상 못 봄」이라 적고 캡션·출처 줄만 대조한다. 그 그림을 가리키는 문장({{그림:id}})·그 건·절의 관찰 주장·캡션·출처 줄이 말하는 화면 글자·상태·환경(기기·판·날짜·서버)이 그림에 실제로 보이는지 대조한다 — 안 보이거나 다르면 발견(severity 4, 그림이 본문을 반박하면 5).` : ''} (2) 그림이 없는 관찰 주장: 이 판에서 바뀐 자리(${REC.src}의 항목마다 — 표 칸·요지·여백 주석·읽는 법·부제·캡션·부록 행도 센다)에서 화면·흐름·화면 문구·동작을 말하는 문장인데 그 건·절·표 곁에 실제 캡처·녹화·합성·원문 인용·센 대상이 있는 표가 없는 곳 — 도안(캡션 「실제 화면 아님」·「— 도안」·「— 재현」)뿐이면 없음이다(severity 3, 독자가 그 화면에 닿지 못하면 4). (3) 바뀐 사실의 전파: 표 칸·건에서 바꾼 사실이 그것을 요약하는 요지·여백 주석·읽는 법·부제와 같은 묶음인지 — 한쪽만 바뀌었으면 발견(severity 4). 맞는 자리는 passed 에. fix 는 캡처·영상을 싣거나, 우리가 안 본 것이면 그 문장을 남은 것으로 옮기는 것 — 단정 뒤에 헤지 꼬리(「화면 미확인」「(확인 전)」「코드상」「코드로 확인함」)를 붙여 덮지 않는다.` },
118
149
  ]
119
- const reads = await pipeline(LENSES,
120
- (l) => agent(l.prompt, { label: `review:${l.key}`, phase: 'Review', schema: l.text ? FINDINGS_TEXT : FINDINGS, ...(l.agentType ? { agentType: l.agentType } : {}) }).then((r) => r && { lens: l.key, ...r }))
150
+ // 반박 — 관점이 끝나는 대로 그 관점의 심각도 3 이상 발견을 바로 반박한다(2026-10-02). 전에는 네 관점이 다 끝나야 반박 6건을 골라 시작해,
151
+ // 같은 관보의 판 갱신 검토 두 번이 58 · 62분 걸렸다 — 검토 단계 최장 35.9 · 37.1분 + 반박 단계 최장 21.0 · 24.2분(워크플로 전사 실측, 2026-10-02).
152
+ // 관점마다 VERIFY_PER_LENS 건(심각도 순)까지 — 관점 넷이면 최대 8건, 그 밖은 unverified. 관점을 가로지르는 순위는 버린다(그것이 기다림의 까닭이었다).
153
+ // 생각 깊이도 정한다 — 전에는 파일 확인 하나만 정해 나머지 10명이 부른 세션의 깊이(최대)를 물려받았고, 선형 독자 한 턴이 17.6 · 18.2분이었다.
154
+ const VERIFY_PER_LENS = 2
155
+ const verifyOne = (f) => agent(`${CTX}\n[반박] 다음 발견을 원고·${REC.src}·렌더러(${RENDERER_PATH})를 직접 읽어 반박해 본다. 발견: (${f.lens}) ${f.where} — ${f.claim}. 근거: ${f.evidence}. 처방: ${f.fix || '(없음)'}.${f.lens === 'linear' ? ` 이 발견은 렌더본 텍스트 ${A.text} 만 읽은 독자가 냈다 — 독자에게 보이는 글로 판정한다. 문서가 붙인 이름을 짚은 발견이면 문서 안의 정의·캡션은 반증이 아니다 — 독자가 문서 밖에서 본 글자인지를 판정한다.` : ''}${f.lens === 'capture' ? ' 이 발견은 증거 대조(합의 착각)다 — 그림을 가리키면 그 그림 파일을 직접 연다. 판정 물음은 「독자가 문서 안에서 그 증거를 볼 수 있는가」다 — 코드·서버 조회·핸드오프로 주장이 참임을 보여도 반증이 아니다. 도안은 증거가 아니다.' : ''}
156
+ diagnosis(진단): 발견이 참/부분/거짓/미확인. 사실이 아니거나 규칙상 문제가 아니면 거짓. 확신이 없으면 미확인.
157
+ 진단과 처방은 따로 판정한다 — 처방이 다른 규칙을 어겨도(포인터·원래 이름 대신 새 이름 짓기·만든 이름에 뜻풀이만 더하기 포함) diagnosis 는 그대로 두고 rx_fit=다시, rx_why 에 걸린 규칙과 까닭. 규칙에 맞으면 rx_fit=맞음, rx_why 는 빈 문자열.
158
+ 증거를 싣는 처방(캡처·영상·원문 인용·센 대상이 있는 표·캡션 끝 증거 범위 한 구절 — 찍은 환경·날짜·옮겨 적은 부분)은 말투(주어)·지은 이름 규칙 위반을 까닭으로 rx_fit=다시 로 두지 않는다 — 걸린 낱말은 reason 에 적는다. 증거 없는 화면 주장을 헤지 낱말(「화면 미확인」「(확인 전)」「코드상」)·동사 낮추기로만 덮는 처방은 다시.
159
+ reason 은 파일·줄·문구 인용으로.`, { label: `verify:${f.lens}`, phase: 'Verify', schema: VERDICT, effort: 'medium' }).then((v) => ({ ...f, verdict: v }))
160
+ const stages = await pipeline(LENSES,
161
+ (l) => agent(l.prompt, { label: `review:${l.key}`, phase: 'Review', schema: l.text ? FINDINGS_TEXT : FINDINGS, effort: 'high', ...(l.agentType ? { agentType: l.agentType } : {}) }).then((r) => r && { lens: l.key, ...r }),
162
+ async (r, l) => {
163
+ // 결과를 못 냈거나 렌더본을 못 열었다고 답한 관점은 반박하지 않는다 — 아래에서 머리에 적는다.
164
+ if (!r || (l.text && r.opened !== true)) return { read: r, picked: [], rest: [], verdicts: [] }
165
+ const strongL = r.findings.filter((f) => f.severity >= 3).map((f) => ({ lens: r.lens, ...f })).sort((a, b) => b.severity - a.severity)
166
+ const picked = strongL.slice(0, VERIFY_PER_LENS)
167
+ const verdicts = await parallel(picked.map((f) => () => verifyOne(f)))
168
+ return { read: r, picked, rest: strongL.slice(VERIFY_PER_LENS), verdicts }
169
+ })
170
+ const reads = stages.map((x) => (x ? x.read : null))
121
171
  // 렌더본을 못 열었다고 답한 관점 — 그 발견은 쓰지 않고 머리에 적는다. 결과를 못 낸 관점(lensFail)과 따로 센다.
122
172
  const unread = LENSES.filter((l, i) => l.text && reads[i] && reads[i].opened !== true).map((l) => l.key)
123
173
  const all = reads.filter((r) => r && !unread.includes(r.lens))
124
174
  const strongAll = all.flatMap((r) => r.findings.filter((f) => f.severity >= 3).map((f) => ({ lens: r.lens, ...f })))
125
- const strong = strongAll.slice(0, 6)
175
+ const strong = stages.flatMap((x) => (x ? x.picked : []))
176
+ const verdicts = stages.flatMap((x) => (x ? x.verdicts : []))
177
+ const restAll = stages.flatMap((x) => (x ? x.rest : []))
126
178
  log(`발견 ${all.reduce((n, r) => n + r.findings.length, 0)} · 심각도≥3 ${strongAll.length}(반박 ${strong.length}) · passed ${all.reduce((n, r) => n + r.passed.length, 0)}`)
127
- const verdicts = await parallel(strong.map((f) => () =>
128
- agent(`${CTX}\n[반박] 다음 발견을 원고·${REC.src}·렌더러(${RENDERER_PATH})를 직접 읽어 반박해 본다. 발견: (${f.lens}) ${f.where} — ${f.claim}. 근거: ${f.evidence}. 처방: ${f.fix || '(없음)'}.${f.lens === 'linear' ? ` 이 발견은 렌더본 텍스트 ${A.text} 만 읽은 독자가 냈다 — 독자에게 보이는 글로 판정한다. 문서가 붙인 이름을 짚은 발견이면 문서 안의 정의·캡션은 반증이 아니다 — 독자가 문서 밖에서 본 글자인지를 판정한다.` : ''}
129
- diagnosis(진단): 발견이 참/부분/거짓/미확인. 사실이 아니거나 규칙상 문제가 아니면 거짓. 확신이 없으면 미확인.
130
- 진단과 처방은 따로 판정한다 — 처방이 다른 규칙을 어겨도(포인터·원래 이름 대신 새 이름 짓기·만든 이름에 뜻풀이만 더하기 포함) diagnosis 는 그대로 두고 rx_fit=다시, rx_why 에 걸린 규칙과 까닭. 규칙에 맞으면 rx_fit=맞음, rx_why 는 빈 문자열.
131
- reason 은 파일·줄·문구 인용으로.`, { label: `verify:${f.lens}`, phase: 'Verify', schema: VERDICT }).then((v) => ({ ...f, verdict: v }))))
132
179
  // 진단 거짓만 refuted. 처방이 규칙에 걸린 발견은 kept 에 남기고 rx 를 단다. 상한 밖과 검증 실패는 unverified 로 돌려준다 —
133
180
  // 전에는 상한 6 밖의 심각도 3 이상 발견이 결과 어디에도 없었고, 검증이 실패한 발견도 kept·refuted 양쪽에서 조용히 빠졌다.
134
181
  const judged = verdicts.filter((v) => v && v.verdict).map((v) => ({ ...v, rx: v.verdict.rx_fit === '다시' ? `처방 다시 — ${v.verdict.rx_why || '규칙에 걸림'}` : '' }))
135
182
  const kept = judged.filter((v) => v.verdict.diagnosis !== '거짓')
136
183
  const vfail = strong.filter((_, i) => !(verdicts[i] && verdicts[i].verdict))
137
- const unverified = [...strongAll.slice(6).map((f) => ({ ...f, why: '검증 상한 6 초과' })), ...vfail.map((f) => ({ ...f, why: '검증 실패' }))]
184
+ const unverified = [...restAll.map((f) => ({ ...f, why: `관점별 검증 상한 ${VERIFY_PER_LENS} 초과` })), ...vfail.map((f) => ({ ...f, why: '검증 실패' }))]
138
185
  const weak = all.flatMap((r) => r.findings.filter((f) => f.severity < 3).map((f) => ({ lens: r.lens, ...f })))
139
186
  const lensFail = LENSES.filter((_, i) => !reads[i]).map((l) => l.key)
140
187
  if (lensFail.length) log(`⚠ 관점 ${lensFail.length}개가 결과를 못 냈다(API 과부하 등) — resumeFromRunId 로 다시 돌리면 끝난 관점은 캐시를 탄다`)
141
188
  if (unread.length) log(`⚠ 렌더본을 못 열었다고 답한 관점 ${unread.join(', ')} — 그 발견은 쓰지 않았다. 경로·권한을 고친 뒤 새로 띄운다`)
142
- if (strongAll.length > strong.length) log(`반박 상한 6 밖 ${strongAll.length - strong.length}건 — unverified 로 돌려준다`)
189
+ if (restAll.length) log(`관점별 반박 상한 ${VERIFY_PER_LENS} 밖 ${restAll.length}건 — unverified 로 돌려준다`)
190
+ // 메움 — 선형 독자가 이 파일 안 증거 줄을 못 댄 관찰 주장(FILL_STEP). 발견과 따로 센다.
191
+ const filled = all.filter((r) => LENSES.some((l) => l.key === r.lens && l.text)).flatMap((r) => (Array.isArray(r.claims) ? r.claims : []).filter((c) => /^\s*없음/.test(String(c.shown_at))).map((c) => ({ lens: r.lens, ...c })))
192
+ if (filled.length) log(`선형 독자가 증거 줄을 못 댄 관찰 주장 ${filled.length}곳 — filled 로 돌려준다`)
143
193
  // 실패는 결과 머리에 적는다 — 실패를 「발견 0」으로 읽지 않게(reader-review.workflow.js 와 같은 머리).
144
194
  const failHead = (vfail.length || lensFail.length || unread.length) ? `검토 미완 — ${[vfail.length && `검증 ${vfail.length}건 실패`, lensFail.length && `관점 ${lensFail.length}개 실패(${lensFail.join(', ')})`, unread.length && `렌더본 못 엶 ${unread.length}(${unread.join(', ')})`].filter(Boolean).join(' · ')}` : ''
145
195
  if (failHead) log(failHead)
146
- return { ...(failHead ? { head: failHead, failed: true } : {}), kept, refuted: judged.filter((v) => v.verdict.diagnosis === '거짓'), weak, unverified, passed: all.map((r) => ({ lens: r.lens, passed: r.passed })) }
196
+ return { ...(failHead ? { head: failHead, failed: true } : {}), kept, refuted: judged.filter((v) => v.verdict.diagnosis === '거짓'), weak, unverified, passed: all.map((r) => ({ lens: r.lens, passed: r.passed })), filled, plates: PLATES }
@@ -237,3 +237,38 @@ D1(판정표 줄 번호)·v4.2(작업하며 붙인 정책 판 번호)는 작업
237
237
  로컬 렌더와 발행 직전 훅은 `list_rejected_names` 로 받는다. 흔한 낱말인 이름은 정규식 대신 함께 설 낱말(`near`)로 가른다 —
238
238
  렌더 자리는 소속처들이 함께 쓰고 파이썬 정규식은 되짚기 상한이 없다.
239
239
  운영 반영 확인 전 — 서버 배포와 첫 항목 기록 전까지는 포털·로컬 렌더·훅 어느 쪽도 이 목록으로 세지 않는다(`lim-portal-format.md` §10-1 머리 ⚠).
240
+
241
+ ## 19. 합의 착각 · 레거시 작성법 · 목적 전치 — 결정 브리핑을 서식 `lim-decision` 으로 (2026-10-01 ~ 10-06) — SKILL 「세 가지 독자」 · 체크리스트 7 독자 검토(`lim-decision`) · `decision-format.md`
242
+
243
+ 같은 고객사 결정 브리핑(앱 점검)을 고치며 나온 반려다. 반려 원문 —
244
+ 「SSOT 를 분석했으니 글로 이해가 되는 것이고 메타인지적 리뷰를 못 한다」(10-01) · 「관보스킬이 최신인데, 너는 레거시 관보를 올려서 그래」 · 「너무 표차트로 요약적으로만 나열」 ·
245
+ 「genetics작업하라고 시켰더니, diagnosing 플랫폼을 만드는 것 같은데?」(10-02) · 「2~3번은 읽어봐야 "A는 B를 함께봐야 겠구나."」 · 「너도 답정너네」(10-03) · 목적 전치 · 캠벨의 법칙(10-03~10-05).
246
+
247
+ - **합의 착각** — 쓰는 쪽이 아는 것을 읽는 쪽도 안다고 여겼다. 결정 하나의 물음·선택지·사실·의견·회신이 판단 항목 한 줄과 여러 절·건에 흩어져, 독자가 「A 는 B 와 함께 봐야」를 스스로 조립했다.
248
+ 완료 확인을 작성자에게 맡긴 것도 같은 뿌리다 — 작성자가 다른 장을 가린다고 머릿속 맥락까지 가려지지 않는다.
249
+ - **레거시 작성법** — 고쳐 둔 스킬을 읽지 않고 옛 정의로 낸 판이 「레거시 관보」로 반려됐다. 스킬 글이 렌더러보다 앞서거나 뒤처지면 같은 갈림이 난다.
250
+ - **표·토막말** — 칸이 없는 자리를 길이·개수 규칙(문장 60·표 칸 20/12·문단 220·판단 항목 네 문장·「글로만 서 있습니다」)으로 지키려 해 설명이 표 칸 토막말로 밀렸다.
251
+ 근원 판정은 결함 대부분이 원고가 아니라 전제 층(처리 보고에서 온 가구 순서 · 결정이 따로 서지 못하는 절 종류 다섯 · 절마다 여백 주석 세 칸 · 한 문장 한 줄)에서 왔다고 짚었다.
252
+ - **목적 전치 · 캠벨의 법칙** — 고치기가 경고 수를 목표로 돌았고(다른 결정 브리핑에서 세 바퀴 만에 문장이 짧게만 깎였다), 시험 장치를 검사하는 장치가 자랐다. 지표가 목표가 되자 지표가 뜻을 잃었다.
253
+
254
+ 처방(2026-10-06, 사용자 승인 열한 자리) — 규칙을 덧대지 않고 데이터 단위와 뼈대를 바꿨다.
255
+ ① 결정 브리핑만 둘째 서식 `lim-decision` 으로 떼고, 결정을 원고의 한 단위(`decision_brief`)로 — 칸 정의는 도구 스키마 한 곳, 스킬은 「어떻게」만(`decision-format.md`). 판정·배움 문서와 이미 나간 문서는 `lim-report` 그대로.
256
+ ② 서버는 비대칭·헛고리·거짓이 되는 것만 거절하고, 렌더러는 자리 글로만 알린다 — 합계·「경고 0」이 없다. 회신 받는 사람이 빈 결정 문서는 공개되지 않는다.
257
+ ③ 칸이 뜻을 지는 규칙을 그 서식에서 걷었다 — 결정한다판 요약 · 브리핑 서사 공식·검수 15항 · 장치 수 세기 · 번호 가리킴 · 판단 항목 220자 네 문장 · 1쪽 단독 표지 · 한 문장 한 줄 조판(문장 표식은 남김).
258
+ ④ 완료는 작성자가 아니라 원고를 모르는 독자가 확인한다(체크리스트 7 독자 검토(`lim-decision`)).
259
+
260
+ **독자 시험을 돌리는 법** — 스킬·렌더러 구조를 바꿀 때만이다(문서마다 밟는 단계는 `decision-format.md` 6절).
261
+ - 전·후를 같은 독자 물음으로 견주되 점수·발견 수로 모으지 않는다 — 같은 원고에서 나온 세 판의 막힘 총수가 비슷하게 나와 근거가 되지 못했다.
262
+ - 막힌 자리마다 넷 가운데 하나로 가른다: 원고에 그 사실이 없다 / 있는데 다른 자리에 있다 / 있는데 낱말이 막았다 / 로직에 그 칸이 없다. 이 파일로는 마지막 갈래만 보낸다.
263
+ - 시험 물음은 원고를 쓴 사람에게 주지 않고 판마다 바꾸며, 정답지 밖의 발견을 따로 받는다.
264
+ - 로직을 확인하는 첫 시험은 그 칸을 낳은 원고가 아닌 본보기(`assets/example/decision.json`)로 돈다 — 같은 원고로 다시 재면 그 원고의 막힘에 맞춰 지은 칸이 그 막힘을 없애는 것을 볼 뿐이다.
265
+ - 다음 반려에 「행 하나 더」「낱말 하나 더」로 덧대고 싶어지면 칸 정의·뼈대가 모자란 증거로 읽는다.
266
+ - 독자도 AI 에이전트다 — 실제 비전공 결정권자와 같은 자리에서 막힌다는 보장은 없고, 그 차이는 실제 고객의 반려로만 드러난다.
267
+
268
+ ## 20. 관보 v3 — 서식 하나 · 장치 목록 한 벌 (2026-10-07) — SKILL 「세 가지 독자」 · 0단계 물음 3 · 0.5단계 4행 · `visual-first.md` §4 · `diagrams.md` · `decision-format.md` 0절
269
+
270
+ §19 의 둘째 서식은 화면 · 렌더러 · CSS 를 따로 자라게 했고(같은 일을 하는 자리 19곳 가운데 옛 관보 우세 12), 서식 선택은 작성자가 적는 이름과 「브리핑」 낱말 하나였다.
271
+ 도해 · 차트는 옛 길만 쓰였다(공개 19건에 새 엔진 도해 0) — 「무엇을 어느 칸에 어떻게」가 다섯 곳에 손으로 적혀 어긋났다: 도구에 없는 옵션(`--min-text` · `--tight`)
272
+ 안내 → 글자 경고 → 「경고 0」을 맞추려고 도해를 뺌, 관문 처방 문구에서 도해 누락. 대표 결정(2026-10-07)으로 서식은 하나(결정 블록이 있으면 결정 모드)이고,
273
+ 장치는 목록 한 벌(`private/builder/devices.py`)에서 렌더러가 읽고 스킬 · 도구 설명이 굽히며, 목록에 없는 장치 · 옵션을 말하면 `tools/contract-drift.py` 가 빨갛게 센다.
274
+ 결정 블록을 권하는 알림도 낱말이 아니라 내용(요청 절 판단 항목의 기한 + 선택지)으로 선다. 로컬 `render.py` 는 도해 굽기를 켜고 부른다 — 꺼진 채 「⚠0」을 맞추던 길이 도해를 지웠다.