genetics-mcp 2.18.2 → 2.18.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.18.2",
3
+ "version": "2.18.3",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -105,7 +105,7 @@ version: "4.0.0"
105
105
  - `cases[].grade` 는 `grades` 에 정의된 이름이어야 한다.
106
106
  - **`update_report` 는 같은 주소를 덮어쓴다** — 이미 고객에게 나간 링크가 새 내용을 가리키므로, 발행 후 수정은 재발행이 아니라 update 로 한다. 기존 문서는 `list_reports`/`get_report` 로 받아 고쳐 되돌려 보낸다.
107
107
  - **그림 계약**: `update_report` 에서 `assets` 를 **빼고** 보내면 서버가 저장된 그림을 그대로 유지한다. 전부 지우려면 `assets: []` 를 명시하고 `sections[].plates`·`chapters[].asset` 에서도 그 id 를 빼야 한다 — 가리키는 곳이 없는 id 는 422. 한 장만 바꾸려면 그 id 에 새 `source`/`file`, 나머지는 `get_report` 가 준 표식 그대로.
108
- - 초안 상태로 올려 두고 다듬어도 발행은 막히지 않는다. 단 초안 `update_report` 도 **본문 판(`body_rev`)** 을 올리고 사용자가 그 판을 읽으므로 **독자 검토를 통과한 본문 판만 올린다** — 다듬는 중간 판은 로컬 렌더·dry_run 으로 본다.
108
+ - 초안 상태로 올려 두고 다듬어도 발행은 막히지 않는다. 단 초안 `update_report` 도 **본문 판(`body_rev`)** 을 올리고 사용자가 그 판을 읽으므로 **독자 검토를 통과한 본문 판만 올린다** — 다듬는 중간 판은 로컬 렌더·dry_run 으로 본다. 사용자가 「바로 반영」을 지시하면 먼저 올리고 검토는 곧바로 뒤따라 돌려 다음 판에 반영한다(2026-09-15 사용자 결정).
109
109
  ⚠ **운영 반영 확인 전 — 운영 서버는 초안에 `update_report` 를 보내면 그 순간 공개로 바꾼다**(저장된 공개일이 비었으면 오늘로 채운다, 수리는 레포 main 에만 있다). 배포 전까지 초안은 `dry_run` 으로만 다듬고, 굳은 뒤 `publish_report` 한 번 → 사람 확인 → `release_report`. 이미 썼으면 `list_reports` 대괄호가 「데이터로 발행된 문서」인지 보고 `unpublish_report(id)` 로 되돌린다 — 교체 응답 문구(「이미 나간 열람 링크가 그대로…」)는 초안에도 같아 그것만으로는 못 가른다. 결정 브리핑 한 편이 첫 교체로 공개돼 여러 판을 고치는 엿새 동안 열려 있었다(2026-09-09~15).
110
110
  **발행 이력 행(`history`)은 다른 것이다** — 공개 전 수정은 이력 행을 만들지 않는다(3c).
111
111
  - 내용 설계 규칙(아래 0~3단계, 고객 언어 치환, 카피 게이트)은 **전달 경로와 무관하게 동일하게** 적용된다 — 서버가 렌더한다고 해서 원고 품질 단계를 건너뛰지 않는다.
@@ -207,11 +207,11 @@ Lim 보고서, Lim 에 올려줘, 고객 보고서, 수정 내역, 검수 요청
207
207
  | 1 | **한 장 이야기** | 문제 → 원인 → 해법(지금 상태 포함) → 정할 것. 칸 수가 아니라 이야기 순서다 — 요지 칸 수는 4~6단계 「요지」 한 곳이 정본. 다루는 일의 **사건 경과** 표 한 장을 1절에 둔다(이 문서의 판 이력은 `history` — 3c) |
208
208
  | 2 | **예시 한 명** | 실존 이용자 하나의 실제 숫자로 문서 전체를 관통시킨다(같은 사람이 그림·표·건에 반복) |
209
209
  | 3 | **용어 잠금 표** | 같은 낱말이 두 값을 가리키면 그 자리에서 문서가 죽는다. 뜻이 갈리는 낱말은 표 한 장으로 값·정의·보이는 자리를 박는다. 장치 이름(건 행 `labels` 값)은 절 이름·부제·표 열·캡션에 다시 쓰지 않고, 선택지 기호(A·B·C)는 한 목록에만 붙인다. 집행자: `gazette.py` `text_scan()` ⚠(건 행 이름 충돌·선택지 기호 충돌) · 독자 검토 first-time 관점(한 낱말 두 값) · 뜻이 같은지는 사람 눈 |
210
- | 4 | **절마다 시각 장치 먼저** | 개념은 그림, 실제 화면은 번호 표식 캡처, 견주기는 표, 「A 면 B」는 흐름도, 분포는 막대 차트. 글은 그 옆 3줄. 도판 층 R0~R10·R13 은 `figure.mjs`(비교 컷은 `shot.mjs --after` → `pair.mjs`), 글 층 R11 은 렌더러 ⚠ — 값은 `references/readability.md` 「1. 규칙표」 한 곳(경위: `references/history.md` §3) |
210
+ | 4 | **절마다 시각 장치 먼저** | 개념은 그림, 실제 화면은 번호 표식 캡처, 견주기는 표, 「A 면 B」는 흐름도(원인 사슬은 `flow.mode:"cause"`), 분포는 막대 차트, **시간에 따른 값은 시계열**(`charts[].kind:"series"`), **공식 문서·회신 문구는 원문 인용**(`quotes`), **구조·흐름·순서·상태는 Archify 도해**(`diagrams`, `references/diagrams.md`). 글은 그 옆 3줄. 도판 층 R0~R10·R13 은 `figure.mjs`(비교 컷은 `shot.mjs --after` → `pair.mjs`), 글 층 R11 은 렌더러 ⚠ — 값은 `references/readability.md` 「1. 규칙표」 한 곳(경위: `references/history.md` §3) |
211
211
  | 5 | **판단 요청** | 요청은 **고객이 고를 물음**이다 — 제목은 물음, 본문 첫 문장은 기한과 두 길의 이름, 결과는 한눈표 **한 곳에만** 대칭으로. 모양·선택지 대칭 불변식·화면 도안 절차는 `references/visual-first.md` §5 와 `lim-portal-format.md` §12.3b. 집행자: `gazette.py` `brief_checks()` ⚠(기한·동일값 열) · ⚠ W1(권고 낱말) · 선택지 사이 대칭은 독자 검토 figures 관점 · 도안 유무는 **없음 — 사람의 눈**(`references/process.md` 7단계 표) |
212
212
  | 5a | **원본 확인** | 주장·그림마다 원본을 **댈 수 있어야** 한다. 출처 줄은 위치 한 구절(`references/readability.md` R12e — 우리 도안은 비움), 원본 이름·찾을 낱말은 건(`source_url`·`how_to_check`)이나 부록에, 제작 메타는 `origin` 으로(11번). 집행자: 독자 검토 source 관점(누를 수 있는 원본) · facts 관점(앱 동작·내부 수치·결정 주체) · `gazette.py` ⚠ W2 · `text_scan()` 제작 메타 ⚠ — 그 밖은 사람 눈 |
213
213
  | 6 | **표기** | 인라인 표기(`**굵게**`·`==강조==`·`{{ok|칩}}`·`[글자](주소)`·**맨 주소**)와 절·PART 의 `tone`, 차트 행 `tone`, 건의 `source_url`, 요약면 `notice.state`. 그림 가리킴은 손으로 번호를 치지 않고 `{{그림:<assets id>}}` 토큰으로(번호·조사를 렌더러가 셈) — ⚠ **운영 반영 확인 전**이라 배포 뒤 열람 DOM 에서 `a.ref[href^="#fig-"]` 를 세어 확인하기 전에는 쓰지 않는다(`lim-portal-format.md` §11-6). 색은 뜻으로(`lim-portal-format.md` 「11-6. 인라인 표기」) |
214
- | 7 | **독자 검토** | 발행 전 `references/reader-review.workflow.js` — 관점 여덟(상태 표 `args.state` 를 주면 아홉). **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 반영 순서는 사실 틀림 → 구조(합치기·빼기) → 나머지. **빼기가 포인터를 남기면 안 된다** — 「N절을 보라」「건 §n.n」「부록에 있다」로 대신하면 독자가 장을 오가는 시험지가 된다. 낱말·규칙·예시 선언은 **첫 등장 자리에서 한 구절**, 뒤 절의 번호·표 이름·§ 는 앞 절에서 쓰지 않는다(요약 근거는 `refs[]` 로). 선형 읽기 관점의 **뜀 지점(`jumps`) 0 이 목표**(실측은 `references/linear-read-test.workflow.js`, 독자 셋+결정권자). 관점·인자·JS 거름 규칙은 `references/visual-first.md` §7. 집행자: 워크플로 관점 + JS 거름 · 뒤 절 번호는 렌더러 ⚠ 「앞 절이 뒤 절을 가리키는 곳」(`gazette.py text_scan` W5 · readability R12h) · 정의 위치와 `questions`·`dropped` 는 사람 눈(경위: `references/history.md` §9·§12). **낱말만 고친 판도 검토 없이 내지 않는다** — 변경 자리만 보는 `references/edition-review.workflow.js`(세 관점 + 반박)로 돌린다. 렌더러 경고 0 만 보고 낸 15판이 다음 판 8자리 손질을 낳았다(2026-09-15) |
214
+ | 7 | **독자 검토** | 발행 전 `references/reader-review.workflow.js` — 관점 여덟(상태 표 `args.state` 를 주면 아홉). **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 반영 순서는 사실 틀림 → 구조(합치기·빼기) → 나머지. **빼기가 포인터를 남기면 안 된다** — 「N절을 보라」「건 §n.n」「부록에 있다」로 대신하면 독자가 장을 오가는 시험지가 된다. 낱말·규칙·예시 선언은 **첫 등장 자리에서 한 구절**, 뒤 절의 번호·표 이름·§ 는 앞 절에서 쓰지 않는다(요약 근거는 `refs[]` 로). 선형 읽기 관점의 **뜀 지점(`jumps`) 0 이 목표**(실측은 `references/linear-read-test.workflow.js`, 독자 셋+결정권자). 관점·인자·JS 거름 규칙은 `references/visual-first.md` §7. 집행자: 워크플로 관점 + JS 거름 · 뒤 절 번호는 렌더러 ⚠ 「앞 절이 뒤 절을 가리키는 곳」(`gazette.py text_scan` W5 · readability R12h) · 정의 위치와 `questions`·`dropped` 는 사람 눈(경위: `references/history.md` §9·§12). **낱말만 고친 판도 검토 없이 내지 않는다** — 변경 자리만 보는 `references/edition-review.workflow.js`(세 관점 + 반박)로 돌린다. 렌더러 경고 0 만 보고 낸 15판이 다음 판 8자리 손질을 낳았다(2026-09-15). 사용자가 「바로 반영」을 지시하면 먼저 내고 이 검토는 곧바로 뒤따라 돌려 다음 판에 반영한다(같은 날 사용자 결정) |
215
215
  | 8 | **실물 확인** | 발행할 때마다 체크리스트 9단계 두 행 — 블록 목록 대조와 열람 주소 열기. 집행자: 사람 — 발행 뒤 대조 기계는 아직 없다 |
216
216
 
217
217
  상세와 도판 예산(장당 80KB · 합계 400KB), 그림 만드는 법, 반려 사례는 `references/visual-first.md`.
@@ -369,6 +369,7 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
369
369
  | `assets/render.py` | `report.json` → 단일 HTML. **서버 등록부에서 서식을 읽어** 레포 `private/builder/build.py` 를 부른다 — 여기 사본을 두지 마라 |
370
370
  | `assets/subset_font.py` | 폰트 서브셋 생성 (보통 실행 불필요) |
371
371
  | `assets/example/report.json` · `cases.json` | 관보 필드를 전부 채운 본보기. 새 문서는 여기서 베낀다 |
372
+ | `assets/diagram.mjs` | Archify 도해를 관보 `diagrams[].svg` 로 굳힌다 — `render <type> <ir.json> <out.svg> --wide --min-text 16 --tight`(Archify deliver → 계산된 스타일을 속성으로 → 클래스·스크립트·애니메이션 걷음 → 작은 글자 키움·빈 띠 조임·겹침 측정) · `export <html> <out.svg>` · `inspect <svg>`. 렌더러와 같은 잣대(바이트 250KB · viewBox · 표시 글자 15px)로 재고 ✗ 면 exit 3. Archify 자체는 동봉하지 않는다(`references/diagrams.md` §1) |
372
373
  | `assets/figure.mjs` · `shot.mjs` · `mockshot.mjs` · `pair.mjs` | 도판 도구 — 도판 HTML 을 굽고 가독성·틀 문법 관문(R0~R10·R13)을 잰다, `--batch plates.json` 은 일괄 판정과 예산 합계, `--spec` 은 계약 상수(`figure.mjs`) · 웹 화면을 검정 원 번호 표식과 함께 찍고 `--after` 로 블록을 숨긴 전후 두 장을 같은 구간으로 찍는다(`shot.mjs`) · 고객 앱 화면 도안을 2배 밀도로 찍는다(`mockshot.mjs`, `references/visual-first.md` 「화면 도안」) · 전후 두 컷을 좌우 같은 틀·같은 배율로 조립하고 판정까지 찍는다(`pair.mjs`, 본보기 `assets/example/plate-pair.html`) |
373
374
  | `references/linear-read-test.workflow.js` | 선형 읽기 **실측** — 처음 읽는 사람 셋 + 결정권자가 앞에서 뒤로 한 번 읽으며 뛰어야 하는 자리를 전부 센다(`args: {file, reader, round}` — 재측정은 `round` 를 바꿔야 캐시를 안 탄다). 뜀 0 이 목표. 독자 검토(`reader-review.workflow.js`)의 linear 관점이 같은 읽기를 하나로 한다 |
374
375
  | `references/edition-review.workflow.js` | **판 갱신 검토** — 구조는 그대로이고 낱말·출처 줄·여백 주석만 고친 판의 **변경 자리**(edit-N.py 의 put/sub 호출)만 선형 독자·규칙 대조·사실 대조 세 관점으로 읽고 심각도 3 이상은 반박한다(`args: {file, backup, edit_script, doc}`). 사실 관점이 백업↔새 판 JSON diff 에 스크립트가 바꾼 자리가 빠짐없이 있는지(삽입·삭제가 섞이면 줄 수가 아니라 자리로)·수치 무변경을 검산한다. 구조가 바뀐 판은 `reader-review.workflow.js`. 낱말 손질 판을 검토 없이 발행하자 다음 판 8자리 손질이 생겼다(2026-09-15) |
@@ -414,7 +415,7 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
414
415
  `summary`, 절 도입문·여백 주석, `status` 대조표, 부록, 판권지의 사실 서술.
415
416
  **여기서만** `습니다|십시오|합니다|입니다|됩니다|주세요` 가 0건이어야 한다.
416
417
  5a. **상대에게 말 거는 자리는 하십시오체로 맺는다.** 종결까지 존댓말이어야 한다.
417
- 그 자리는 요청 절(`cta`·`open_items`)·PART I 간지·판권지의 회신 안내다.
418
+ 그 자리는 요청 절(`cta`·`open_items`)·PART I 간지·판권지의 회신 안내, 그리고 요청 절의 확인 요청을 가리키는 읽는 법(`guide`) 한 줄이다(2026-09-15 사용자 결정).
418
419
 
419
420
  | 상대가 할 일 | 쓰지 않는다 | 쓴다 |
420
421
  |---|---|---|
@@ -584,6 +585,7 @@ KS X 1001 완성형 2,350자. 관보 서식이 `--w-light: 300` 을 쓰므로 40
584
585
  | `references/history.md` | 규칙이 생긴 반려 경위·원문·실측 — 규칙이 아니다. 규칙 자리의 「(경위: §n)」이 가리킨다 |
585
586
  | `references/lim-portal-format.md` | **포털 발행할 때** — 관보 골격·여백 주석·PART·톤·요약 근거 게이트·운영 제약 |
586
587
  | `references/content-pipeline.md` | 데이터 모델 전수 — 절 `kind` 5종, 등급, 한국어 조판 규칙, 고객 언어 치환 |
588
+ | `references/diagrams.md` | **도해가 필요할 때** — Archify 설치 위치·IR 작성·굳히기(`diagram.mjs`)·가독성 잣대·관보에 앉히는 법 |
587
589
  | `references/font-embedding.md` | 폰트를 바꾸거나 커버리지를 조정할 때 |
588
590
  | `gazette-report` 스킬 (별도) | 조판 규칙의 원전 — 활자 스케일·괘선·인쇄 페이지 문법·검사기 8종 |
589
591
 
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env node
2
+ // diagram.mjs — Archify 도해를 관보 `sections[].diagrams[]` 에 앉힐 **독립 SVG** 로 굳힌다.
3
+ //
4
+ // node diagram.mjs render <type> <ir.json> <out.svg> [--archify <dir>] [--wide] [--json]
5
+ // Archify(`bin/archify.mjs deliver … --quality showcase`)로 HTML 을 만든 뒤 export 한다.
6
+ // node diagram.mjs export <archify.html> <out.svg> [--wide] [--json]
7
+ // 이미 만든 Archify HTML(또는 아무 HTML 의 첫 <svg role="img">)에서 SVG 하나를 뽑는다.
8
+ // node diagram.mjs inspect <in.svg> [--wide] [--json]
9
+ // 렌더러(gazette.py _svg_clean)와 같은 잣대로 잰다 — 바이트 · viewBox · 가장 작은 글자 · 표시 글자.
10
+ //
11
+ // 왜 export 가 필요한가: Archify 의 HTML 은 SVG 가 페이지 CSS(28만 자)의 클래스에 기대고, 뷰어 스크립트가
12
+ // 애니메이션·상세 단계를 만든다. 관보 열람은 스크립트 0·문서 활자 스케일이라, **계산된 스타일을 속성으로 굳히고**
13
+ // 클래스·스크립트·스타일·애니메이션을 걷은 SVG 만 앉힐 수 있다(gazette.py _svg_clean 이 한 번 더 걷는다).
14
+ // 글자 크기는 굳히되 font-family 는 **굳히지 않는다** — 문서의 Pretendard 를 물려받아야 본문과 같은 활자로 선다.
15
+ //
16
+ // 가독성 잣대(스킬 references/readability.md R1): 표시 글자 ≥ 15px. 표시 폭은 본문 단 760 · 전폭(--wide) 1030 이고
17
+ // 표시 글자 = 원본 font-size × 표시 폭 ÷ viewBox 폭. 15 미만이면 이 도구가 ✗ 로 멈추고(exit 3), 렌더러도 ⚠ 로 센다.
18
+ import fs from 'node:fs';
19
+ import os from 'node:os';
20
+ import path from 'node:path';
21
+ import { spawnSync } from 'node:child_process';
22
+ import { pathToFileURL } from 'node:url';
23
+
24
+ const COLUMN = 760, WIDE = 1030, MIN_FONT = 15, MAX_BYTES = 250000; // gazette.py 상수와 같은 값
25
+ const argv = process.argv.slice(2);
26
+ const flags = new Set(argv.filter(a => a.startsWith('--')));
27
+ const opt = (k) => { const i = argv.indexOf(k); return i >= 0 ? argv[i + 1] : undefined; };
28
+ const pos = argv.filter((a, i) => !a.startsWith('--') && (i === 0 || argv[i - 1] !== '--archify'));
29
+ const cmd = pos[0];
30
+ const JSON_OUT = flags.has('--json');
31
+ const wide = flags.has('--wide');
32
+ const usage = 'usage: node diagram.mjs render <type> <ir.json> <out.svg> [--archify <dir>] [--wide] [--json]\n'
33
+ + ' node diagram.mjs export <archify.html> <out.svg> [--wide] [--json]\n'
34
+ + ' node diagram.mjs inspect <in.svg> [--wide] [--json]';
35
+
36
+ function say(obj, text) { if (JSON_OUT) console.log(JSON.stringify(obj)); else console.log(text); }
37
+ function die(msg, code = 2) { console.error(msg); process.exit(code); }
38
+
39
+ // ── Archify 찾기: --archify > ARCHIFY_DIR > 에이전트 스킬 폴더 넷 ──
40
+ function findArchify(explicit) {
41
+ const home = os.homedir();
42
+ const cands = [explicit, process.env.ARCHIFY_DIR,
43
+ path.join(home, '.claude/skills/archify'), path.join(home, '.agents/skills/archify'),
44
+ path.join(home, '.cursor/skills/archify'), path.join(home, '.codex/skills/archify')].filter(Boolean);
45
+ for (const d of cands) if (fs.existsSync(path.join(d, 'bin/archify.mjs'))) return d;
46
+ return null;
47
+ }
48
+
49
+ // ── playwright-core 와 크롬: figure.mjs·shot.mjs 와 같은 자리를 찾는다 ──
50
+ async function browser() {
51
+ const home = os.homedir();
52
+ const cands = [process.env.PLAYWRIGHT_CORE,
53
+ path.join(home, '.claude/skills/gazette-report/scripts/node_modules/playwright-core/index.mjs'),
54
+ path.join(process.cwd(), 'node_modules/playwright-core/index.mjs'),
55
+ path.join(process.cwd(), 'node_modules/playwright/index.mjs')].filter(Boolean);
56
+ let chromium = null;
57
+ for (const c of cands) if (fs.existsSync(c)) { ({ chromium } = await import(pathToFileURL(c).href)); break; }
58
+ if (!chromium) { try { ({ chromium } = await import('playwright')); } catch { /* 아래에서 멈춘다 */ } }
59
+ if (!chromium) die('playwright(-core) 를 못 찾음 — PLAYWRIGHT_CORE=<index.mjs 절대경로> 로 알려 주거나 gazette-report 스킬의 scripts/node_modules 를 두십시오');
60
+ const exe = process.env.CHROME || process.env.PLAYWRIGHT_CHROMIUM || findChromium();
61
+ return chromium.launch(exe ? { executablePath: exe } : {});
62
+ }
63
+ function findChromium() {
64
+ const mac = '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
65
+ if (fs.existsSync(mac)) return mac;
66
+ const home = os.homedir();
67
+ for (const r of [path.join(home, 'Library/Caches/ms-playwright'), path.join(home, '.cache/ms-playwright')]) {
68
+ if (!fs.existsSync(r)) continue;
69
+ const dirs = fs.readdirSync(r).filter(d => /^chromium(_headless_shell)?-\d+$/.test(d)).sort().reverse();
70
+ for (const d of dirs) {
71
+ for (const rel of ['chrome-mac-arm64/Chromium.app/Contents/MacOS/Chromium', 'chrome-mac/Chromium.app/Contents/MacOS/Chromium',
72
+ 'chrome-headless-shell-mac-arm64/chrome-headless-shell', 'chrome-linux/chrome', 'chrome-headless-shell-linux64/chrome-headless-shell']) {
73
+ const p = path.join(r, d, rel); if (fs.existsSync(p)) return p;
74
+ }
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ // ── 페이지 안에서 돌 코드: 계산된 스타일을 속성으로, 런타임 흔적은 걷는다 ──
81
+ const INLINE = `(() => {
82
+ const PROPS = ['fill','fill-opacity','fill-rule','stroke','stroke-width','stroke-dasharray','stroke-dashoffset','stroke-linecap',
83
+ 'stroke-linejoin','stroke-miterlimit','stroke-opacity','opacity','font-size','font-weight','font-style','text-anchor',
84
+ 'dominant-baseline','letter-spacing','paint-order','clip-path','mask','filter','marker-start','marker-mid','marker-end',
85
+ 'text-decoration','transform','transform-origin','mix-blend-mode'];
86
+ const svg = document.querySelector('svg[role="img"]') || document.querySelector('svg');
87
+ if (!svg) return { error: 'svg 가 없다' };
88
+ // 상세 단계·애니메이션은 뷰어의 것 — 가장 자세한 단계로, 움직임 없이.
89
+ const css = [...document.querySelectorAll('style')].map(s => s.textContent).join('\\n');
90
+ const full = /data-detail-level="full"/.test(css) ? 'full' : (/data-detail-level="read"/.test(css) ? 'read' : null);
91
+ if (full) for (const el of document.querySelectorAll('[data-detail-level]')) el.setAttribute('data-detail-level', full);
92
+ const st = document.createElement('style');
93
+ st.textContent = '*{animation:none!important;transition:none!important}';
94
+ document.head.appendChild(st);
95
+ void document.body.offsetHeight;
96
+ const clone = svg.cloneNode(true);
97
+ const live = [svg, ...svg.querySelectorAll('*')];
98
+ const copy = [clone, ...clone.querySelectorAll('*')];
99
+ const DROP = new Set(['script','style','foreignObject','animate','animateTransform','animateMotion','set','iframe','object','embed','video','audio']);
100
+ let minFont = null, texts = 0;
101
+ for (let i = live.length - 1; i >= 0; i--) {
102
+ const a = live[i], b = copy[i];
103
+ if (!b) continue;
104
+ if (DROP.has(a.localName)) { b.remove(); continue; }
105
+ const cs = getComputedStyle(a);
106
+ if (cs.display === 'none' || cs.visibility === 'hidden' || (cs.opacity === '0' && a !== svg)) { b.remove(); continue; }
107
+ const par = (a.parentElement && a !== svg) ? getComputedStyle(a.parentElement) : null;
108
+ let style = '';
109
+ for (const p of PROPS) {
110
+ const v = cs.getPropertyValue(p);
111
+ if (!v) continue;
112
+ if (v === 'none' && !['fill', 'stroke'].includes(p)) continue;
113
+ if (par && par.getPropertyValue(p) === v && !['fill', 'stroke', 'font-size', 'font-weight', 'opacity'].includes(p)) continue;
114
+ style += p + ':' + v + ';';
115
+ }
116
+ if (a.localName === 'text' || a.localName === 'tspan') {
117
+ const fs = parseFloat(cs.fontSize);
118
+ if (Number.isFinite(fs) && (a.textContent || '').trim()) { texts++; minFont = minFont === null ? fs : Math.min(minFont, fs); }
119
+ }
120
+ for (const at of Array.from(b.attributes)) {
121
+ if (at.name === 'class' || at.name.startsWith('data-') || at.name.startsWith('on') || at.name === 'style' || at.name === 'tabindex') b.removeAttribute(at.name);
122
+ if ((at.name === 'href' || at.name === 'xlink:href') && !(at.value.startsWith('#') || at.value.startsWith('data:image/'))) b.removeAttribute(at.name);
123
+ }
124
+ if (style) b.setAttribute('style', style);
125
+ }
126
+ clone.removeAttribute('width'); clone.removeAttribute('height');
127
+ clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
128
+ const vb = (clone.getAttribute('viewBox') || '').trim().split(/[\\s,]+/).map(Number);
129
+ return { svg: new XMLSerializer().serializeToString(clone), minFont, texts, vw: vb[2] || 0, vh: vb[3] || 0, level: full };
130
+ })()`;
131
+
132
+ function measure(svg, wideFlag, extra = {}) {
133
+ const bytes = Buffer.byteLength(svg, 'utf8');
134
+ const vb = /viewBox\s*=\s*"\s*([-\d.]+)[\s,]+([-\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*"/i.exec(svg);
135
+ const vw = vb ? parseFloat(vb[3]) : 0, vh = vb ? parseFloat(vb[4]) : 0;
136
+ const sizes = [...svg.matchAll(/font-size\s*[:=]\s*"?\s*([\d.]+)\s*(px)?/gi)].map(m => parseFloat(m[1])).filter(Number.isFinite);
137
+ const minFont = extra.minFont ?? (sizes.length ? Math.min(...sizes) : null);
138
+ const display = wideFlag ? WIDE : COLUMN;
139
+ const shown = (vw && minFont !== null) ? minFont * display / vw : null;
140
+ const texts = extra.texts ?? (svg.match(/<text\b/gi) || []).length;
141
+ const verdicts = [];
142
+ if (!vw) verdicts.push(['R0', 'fail', 'viewBox 가 없다 — 폭을 알 수 없어 렌더러가 싣지 않는다']);
143
+ if (bytes > MAX_BYTES) verdicts.push(['R0', 'fail', bytes + 'B > 상한 ' + MAX_BYTES + 'B — 노드를 줄이거나 PNG 도판으로']);
144
+ if (shown !== null && shown < MIN_FONT) verdicts.push(['R1', 'fail', '가장 작은 글자 ' + minFont + 'px × (' + display + '/' + vw + ') = 표시 ' + shown.toFixed(1) + 'px < ' + MIN_FONT + 'px — ' + (wideFlag ? '노드·라벨을 줄인다' : '--wide 로 전폭에 세우거나 노드를 줄인다')]);
145
+ if (vw && vh / vw > 1.8) verdicts.push(['R9', 'warn', '세로가 폭의 ' + (vh / vw).toFixed(2) + '배 — 포털이 폭을 깎는다(1.8배 안으로)']);
146
+ if (!texts) verdicts.push(['R4', 'warn', '글자가 0 — alt 만으로 읽히는 도해인지 확인']);
147
+ return { bytes, vw, vh, minFont, display, shownMinFont: shown === null ? null : Math.round(shown * 10) / 10, texts, verdicts };
148
+ }
149
+ function report(out, m, extra) {
150
+ const bad = m.verdicts.some(v => v[1] === 'fail');
151
+ const lines = [out + ' — ' + m.bytes + 'B · viewBox ' + m.vw + '×' + m.vh + ' · 글자 ' + m.texts + '개 · 가장 작은 글자 ' + (m.minFont ?? '?') + 'px → 표시 ' + (m.shownMinFont ?? '?') + 'px(폭 ' + m.display + ')',
152
+ ...m.verdicts.map(v => ' ' + (v[1] === 'fail' ? '✗' : '⚠') + ' ' + v[0] + ' ' + v[2]),
153
+ bad ? ' → 렌더러도 같은 ⚠ 를 낸다. 고친 뒤 다시.' : ' ✓ 관보 diagrams[].svg 에 넣을 수 있다'];
154
+ say({ ok: !bad, out, ...m, ...extra }, lines.join('\n'));
155
+ process.exit(bad ? 3 : 0);
156
+ }
157
+
158
+ async function exportSvg(html, out) {
159
+ const b = await browser();
160
+ try {
161
+ const ctx = await b.newContext({ viewport: { width: 1600, height: 1000 }, reducedMotion: 'reduce', colorScheme: 'light' });
162
+ const page = await ctx.newPage();
163
+ await page.goto(pathToFileURL(path.resolve(html)).href, { waitUntil: 'load' });
164
+ await page.waitForTimeout(700);
165
+ const r = await page.evaluate(INLINE);
166
+ if (!r || r.error) die('export 실패: ' + (r && r.error));
167
+ fs.writeFileSync(out, r.svg, 'utf8');
168
+ return r;
169
+ } finally { await b.close(); }
170
+ }
171
+
172
+ (async () => {
173
+ if (cmd === 'inspect') {
174
+ const [, inp] = pos; if (!inp) die(usage);
175
+ return report(inp, measure(fs.readFileSync(inp, 'utf8'), wide));
176
+ }
177
+ if (cmd === 'export') {
178
+ const [, html, out] = pos; if (!html || !out) die(usage);
179
+ const r = await exportSvg(html, out);
180
+ return report(out, measure(r.svg, wide, { minFont: r.minFont, texts: r.texts }), { source: html, level: r.level });
181
+ }
182
+ if (cmd === 'render') {
183
+ const [, type, ir, out] = pos; if (!type || !ir || !out) die(usage);
184
+ const dir = findArchify(opt('--archify'));
185
+ if (!dir) die('Archify 를 못 찾음 — `npx skills add tt-a1i/archify -g` 로 설치하거나 --archify <dir> / ARCHIFY_DIR 로 알려 주십시오');
186
+ const tmp = path.join(os.tmpdir(), 'diagram-' + process.pid + '.html');
187
+ const d = spawnSync(process.execPath, [path.join(dir, 'bin/archify.mjs'), 'deliver', type, path.resolve(ir), tmp, '--quality', 'showcase', '--json'], { encoding: 'utf8' });
188
+ let receipt = null; try { receipt = JSON.parse(d.stdout); } catch { /* 아래에서 원문을 보인다 */ }
189
+ if (d.status !== 0 || !receipt || !receipt.ok) die('archify deliver 실패(exit ' + d.status + ')\n' + (d.stdout || '') + (d.stderr || ''), 4);
190
+ const r = await exportSvg(tmp, out);
191
+ fs.rmSync(tmp, { force: true });
192
+ return report(out, measure(r.svg, wide, { minFont: r.minFont, texts: r.texts }), { archify: dir, validation: receipt.validation, ir: path.resolve(ir), level: r.level });
193
+ }
194
+ die(usage);
195
+ })();
@@ -0,0 +1,88 @@
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
+ | 예산 | 장당 80KB · 합계 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
+ ## 2. 절차 — IR 을 쓰고, 굳히고, 재고, 앉힌다
33
+
34
+ 1. **종류를 고른다** — Archify `SKILL.md` 의 Type router: `architecture`(구성 요소·경계) · `workflow`(절차·관문·runbook)
35
+ · `sequence`(호출 순서) · `dataflow`(파이프라인·계보) · `lifecycle`(상태 전이). 모르면
36
+ `node bin/archify.mjs guide "<상황>" --json`.
37
+ 2. **IR 을 쓴다** — Archify 의 `schemas/<type>.schema.json` 과 `examples/` 를 읽고 새로 쓴다(사실은 그 문서의 데이터에서).
38
+ 한국어 라벨을 쓴다. 노드 12개 이하, 주 경로 하나. `meta.quality_profile: "showcase"`. `meta.animation` 은 적지 않는다.
39
+ **`meta.legend: {"mode": "hidden"}` 을 적는다** — Archify 의 자동 범례는 영문 7.5px 라 관보 관문(15px)에 걸리고 문서에서는
40
+ 캡션이 그 몫을 한다(실측 2026-09-16). **열은 4개 이하**(viewBox 폭 ≈ 1,000 이하) — 전폭 1030px 에 1:1 로 앉아야 글자가 산다.
41
+ 부제(`sublabel`)는 9자 안, 간선 라벨은 5자 안 — 글자를 키워도(아래 4단계) 노드 폭 168 안에 서는 길이다.
42
+ ⚠ Archify 의 라벨 폭 셈은 라틴 글자 기준이라 **한글 라벨은 예측보다 넓게 선다** — 라벨 겹침 진단이 나면 낱말을 줄이거나
43
+ 노드 `width` 를 준다.
44
+ 3. **검증한다** — `node bin/archify.mjs validate <type> <ir.json> --quality showcase --json` 이 9/9 · 오류 0 · 경고 0.
45
+ 4. **굳힌다** —
46
+ ```bash
47
+ node ~/.claude/skills/genetics-report/assets/diagram.mjs render <type> <ir.json> <out.svg> --wide --min-text 16 --tight --json
48
+ ```
49
+ deliver → 브라우저에서 계산된 스타일을 속성으로 굳힘 → 클래스·스크립트·스타일·애니메이션·원래 표현 속성 걷음 → SVG 하나.
50
+ font-family 는 굳히지 않는다(문서 활자를 물려받게).
51
+ - `--min-text 16`: Archify 는 글자 크기를 컴파일러 상수로 박아(제목 16 · 부제 11 · 간선 8 · 구역 7~10) IR 로는 못 키운다.
52
+ 문턱 아래 글자를 뷰박스 단위 16 으로 키우되 배치는 그대로 두고, **글자 상자 겹침·노드 넘침을 브라우저에서 재서** 영수증에 낸다(R5).
53
+ 노드 부제는 제목과 16px 간격이라 `--min-sub`(기본 15) 까지만 키운다 — 그 위는 제목과 겹친다.
54
+ - `--tight`: Archify 는 화면 비율에 맞춰 아래를 비워 두는데 문서에서는 빈 띠다. 배경 격자를 뺀 내용 상자 + 12 로 viewBox 를 조인다.
55
+ - 영수증의 「작은 글자」 다섯 개가 무엇인지 보고 IR 에서 부제·태그를 줄인다 — 글자를 더 키우는 것은 답이 아니다. 상세 단계는 가장 자세한 단계로 고정한다.
56
+ 5. **잰다** — 위 명령이 함께 잰다: 바이트(≤ 250,000) · viewBox · 가장 작은 글자 × (표시 폭 ÷ viewBox 폭) ≥ 15px(R1) ·
57
+ 세로/폭 ≤ 1.8(R9). ✗ 면 exit 3 — 렌더러도 같은 ⚠ 를 내므로 **여기서 고친다**: 넓은 도해는 `--wide`(표시 1030px),
58
+ 그래도 모자라면 노드·라벨을 줄인다. 글자를 키우는 것은 Archify 의 몫이 아니다 — 도해를 쪼갠다.
59
+ 6. **앉힌다** — `report.json`:
60
+ ```json
61
+ "diagrams": [{
62
+ "id": "upgrade-path", "caption": "업그레이드가 지나는 세 관문",
63
+ "svg": "<svg …>…</svg>", "alt": "스냅샷 복원 → 시험 → 운영 적용",
64
+ "verdict": "직접 확인", "wide": true,
65
+ "origin": "archify workflow v2 · ir/upgrade-path.workflow.json"
66
+ }]
67
+ ```
68
+ `svg` 는 파일 내용을 문자열로 넣는다(JSON 이스케이프). `origin` 은 렌더 안 됨 — IR 경로를 남긴다.
69
+ 그림 번호는 차트 → 흐름도 → 도해 → 도판 순으로 자리가 매긴다. 본문에서 `{{그림:upgrade-path}}` 로 가리킨다.
70
+ 7. **로컬 렌더 → dry_run** — 다른 장치와 같다. 렌더러 ⚠ 문구는 `lim-portal-format.md` §10-1.
71
+
72
+ ## 3. 무엇이 관보에 들어가고 무엇이 빠지나
73
+
74
+ | Archify 가 주는 것 | 관보에서 |
75
+ |---|---|
76
+ | 정지 SVG(노드·간선·라벨·범례) | 그대로 — 활자로 |
77
+ | 팬·줌·검색·초점·경로 추적·프레젠테이션 | 빠진다(스크립트 0). 필요하면 문서 밖 링크로 Archify HTML 을 따로 준다 |
78
+ | 다크/라이트 | 라이트만 굳힌다(`colorScheme: light`) |
79
+ | 애니메이션(`trace`) | 걷는다 — `reducedMotion` + `animation:none` 으로 정지 상태를 굳힌다 |
80
+ | 브랜드 마크(`brand`) | `data:image/` 로 박힌 것만 남고 외부 주소는 걷힌다 |
81
+ | 뷰어 UI 언어(`meta.locale`) | 무관 — 뷰어가 안 실린다 |
82
+
83
+ ## 4. 하지 않는 것
84
+
85
+ - Mermaid 를 관보에 직접 싣지 않는다. Mermaid 원문은 Archify 의 입력이다(Archify `SKILL.md` 「Mermaid input」) — Archify IR 로
86
+ 다시 쓰고 위 절차를 밟는다.
87
+ - SVG 를 손으로 고치지 않는다. 고칠 것은 IR 이고, 굳히기는 다시 돌린다(재현성).
88
+ - 도해 하나에 문장을 넣지 않는다(R4). 설명은 캡션 한 문장과 본문.
@@ -119,3 +119,5 @@
119
119
  뒤늦게 돌린 세 관점 검토(선형 독자·규칙 대조·사실 대조 + 반박)가 낮은 지적 14를 냈다 — 같은 답 묶음 안의 전제 되풀이, 주어가 빠져 「4차가 접수한 것」으로 읽히는 출처 줄, 인쇄에서 죽는 「누르면」, 절 이름과 부제의 되풀이. → 16판 8자리 · `edition-review.workflow.js` · 0.5단계 표 7행 · 체크리스트 7단계 독자 검토 행.
120
120
  - **검사기가 남의 문서의 절 번호를 뒤 절로 오인** — R12h 정규식이 「검수 보고 4차 제5절」·「제10절」·「제11절」을 이 문서(7절까지)의 뒤 절로 세어, 15판이 세 자리의 위치 구절을 억지로 지웠다(3.17kg 의 자리가 사라짐).
121
121
  → `text_scan` W5 「N차 … 제N절」 예외 · 16판이 표 이름(「두 계산식의 차이 실측」)으로 위치를 되살림. 표 번호 선참조(「표 7-1」)를 셀지는 미결.
122
+ - **사용자 결정 둘(2026-09-15)** — 두 물음 「읽는 법에 확인 요청 한 줄을 둬도 되나」·「「바로 반영」 지시가 판 갱신 검토보다 앞서도 되나」에 둘 다 「응」.
123
+ → SKILL 5a 요청 자리 목록·108행·0.5단계 7행 · process.md · lim-portal-format §10 경어체 행·§12.5b · genetics lim 결정 두 장.
@@ -262,6 +262,8 @@ python3 assets/render.py <report.json>
262
262
  | `note` | `kind:"status"` 에서 표 아래 한 줄 | 그 줄이 빠진다 |
263
263
  | `flow` | 의존 흐름도 한 개(11-2절). `kind` 와 무관하다 | 도해가 빠진다 |
264
264
  | `plates` | 절 본문 도판의 `assets[].id` 목록(11-1절) | 도판이 빠진다 |
265
+ | `quotes` | 원문 인용 목록(11-8절). `kind` 와 무관하다 | 인용이 빠진다 |
266
+ | `diagrams` | 도해(인라인 SVG) 목록(11-9절). `kind` 와 무관하다 | 도해가 빠진다 |
265
267
 
266
268
  ### ⚠ `no` 와 `id` 는 **안 읽힌다**
267
269
 
@@ -424,13 +426,13 @@ python3 assets/render.py <report.json>
424
426
  |---|---|---|
425
427
  | 「고객사 로고가 SVG 로 등록돼 있어 표지에 못 싣습니다」·「표지용 파생본이 아직 없습니다」 | 소속처 로고 설정의 상태다 | 소속처 관리 화면(사람) |
426
428
  | 「바깥에서 받아 오는 것이 있습니다」 — 그 호스트가 **본문 링크**(`[글자](주소)`·맨 주소·건 `source_url`)뿐일 때 | 입고 검사가 `<a href>` 호스트까지 센다(§11-6). 누르는 링크는 받아 오는 자원이 아니다. `<img src>` 등으로 뜨면 작성자 몫 | 서버 검사 수리 전까지 사람이 호스트 목록을 대조 |
427
- | 경어체 경고 — PART I 간지(`parts[]`)의 요청 문장에서만 날 때 | PART I 간지는 요청 자리(§14-2)인데 서버 문체 관문이 이 자리는 아직 센다 | 사람이 자리를 확인 |
429
+ | 경어체 경고 — PART I 간지(`parts[]`)의 요청 문장이나 읽는 법(`guide`)의 확인 요청 한 줄에서만 날 때 | 둘 다 요청 자리(§14-2 · §12.5b)인데 서버 문체 관문이 이 자리는 아직 센다 | 사람이 자리를 확인 |
428
430
  | 레포의 렌더러·서버 관문이 운영보다 앞선 동안, 새 규칙대로 고친 자리에 운영 `dry_run` 이 옛 규칙으로 내는 경고 | 운영은 배포된 판으로 센다 — 되돌리면 새 규칙에 걸리고, 배포되면 사라진다 | 새 판에서 0 인지 먼저 잰다 — 렌더러 경고는 레포 작업본을 부르는 로컬 `render.py`, 서버 문체 관문·입고 검사·서식 등록부 칸은 레포 개발 스택의 `dry_run`(절차: genetics lim `cases/dev-스택이-OrbStack-덧판https-base_url으로-떠-있으면-…`). 0 이면 핸드오프에 「배포 뒤 해소」 한 줄 |
429
431
 
430
432
  요청 절의 경어체는 이제 예외가 아니다. 서버 문체 관문(`report_tone.php report_tone_warnings()`)은 요청 자리(`open_items[].body`·`cta`·
431
433
  `colophon.note`·`asks` 절 `marg`)와 「」 인용을 빼고 세고, 로컬 `tone.mjs` 는 판단 항목 본문과 「」 인용을 뺀다.
432
434
  ⚠ 서버 쪽 자리 가림은 운영 반영 확인 전(2026-09-14 갈래)이다 — 배포 전의 운영 `dry_run` 이 요청 자리에서 내는 경어체 경고는 이 표의 셋째 행처럼 자리를 확인하고 넘긴다. 그래도 경어체 경고가
433
- 뜨면 기록 자리(`open_items[].title` 포함)나 `guide` 에 요청 문장이 샌 것이다 — 요청 문장은 요청 절로 옮긴다(SKILL 5a 「보고 칸에 요청문을 두지 마라」).
435
+ 뜨면 기록 자리(`open_items[].title` 포함)나 `guide` 에 요청 문장이 샌 것이다 — 요청 문장은 요청 절로 옮긴다(SKILL 5a 「보고 칸에 요청문을 두지 마라」). 단 `guide` 에서 요청 절의 확인 요청을 가리키는 한 줄은 허용한다(2026-09-15 사용자 결정) — 그 한 줄에서만 나는 경어체 경고는 §10 통과 기준 표의 행으로 넘긴다.
434
436
  건의 `how_to_check` 에서 걸린 명령형은 「보고 칸에 요청문이 N곳 있습니다 — 요청 절(open_items)로 옮깁니다」로 따로 뜬다.
435
437
  톤 게이트는 경고이지 거절이 아니다(실측 2026-09-11: 경어체 경고를 안은 `update_report` 2회 모두 발행 성공).
436
438
  「경고 0」을 요청 자리를 개조식으로 내려서 맞추려다 **말투 반려가 네 번 났다**(genetics `decisions/고객-문서-요청-자리는-하십시오체로-맺는다-…`).
@@ -447,7 +449,12 @@ python3 assets/render.py <report.json>
447
449
 
448
450
  | ⚠ | 문턱 | 처방 |
449
451
  |---|---|---|
450
- | 절 N 「이름」이 글로만 서 있습니다(M자) | 산문 400자 이상인데 표·흐름도·차트·도판 0 | 그 절의 요지를 표·흐름도·차트·도판 중 하나로 세운다(§11-1~11-4) |
452
+ | 절 N 「이름」이 글로만 서 있습니다(M자) | 산문 400자 이상인데 표·흐름도·차트·도판·인용·도해 0 | 그 절의 요지를 표·흐름도·차트·도판·인용·도해 중 하나로 세운다(§11-1~11-9) |
453
+ | 절 %s 인용 %d 에 출처 줄이 없습니다 — 원본 위치 한 구절(주소·쪽·행)을 적습니다 | `quotes[]` 에 `source_text`·`source_url` 둘 다 없다 | 원문의 위치 한 구절(주소·쪽·행)을 적는다(§11-8) |
454
+ | 절 %s 도해 「%s」의 글자가 표시 %dpx 로 섭니다 — %dpx 이상이 되게 wide 로 두거나 노드를 줄입니다 | 도해 SVG 의 가장 작은 font-size × (표시 폭 ÷ viewBox 폭) < 15px(readability R1 과 같은 잣대) | `wide: true` 로 전폭에 세우거나 노드·라벨을 줄인다(§11-9) |
455
+ | 절 %s 도해 「%s」가 %d바이트입니다 — 상한 %d바이트, 노드를 줄이거나 도판(PNG)으로 싣습니다 | `diagrams[].svg` 가 250,000B 초과 | 도해를 나누거나 PNG 도판으로 |
456
+ | 절 %s 도해 「%s」에 viewBox 가 없거나 svg 가 아닙니다 — 폭을 알 수 없어 싣지 않습니다 | 루트 `<svg>` 에 viewBox 가 없다 | `diagram.mjs export` 로 굳힌 SVG 를 넣는다(§11-9) |
457
+ | 절 %s 시계열 「%s」의 표식 「%s」가 점에 없습니다 — marks[].x 는 points[].x 와 같은 값이어야 섭니다 | `charts[].marks[].x` 가 어느 `points[].x` 와도 같지 않다 | 점의 x 값 그대로 적는다(§11-7) |
451
458
  | 절 N 「이름」의 k번째 문단이 M자입니다 | 문단 한 개 220자 초과(세 줄) | 두 문장으로 끊거나 수치를 표로 내린다 |
452
459
  | 절 N 「이름」 산문에 참조 번호가 M개입니다 | 산문(문단·도입문·여백 주석)에 6.6·2.12a 같은 번호 4개 이상 | 번호는 표·건·판단 항목에만, 산문은 뜻으로 잇는다 |
453
460
  | 판단 요청 N항이 산문으로만 섭니다 | `open_items` 6항 이상인데 asks 절에 표·흐름도 0 | 한눈표(항 · 지금 · 그대로 두면 · 바꾸면 — 두 길 대칭, 권고 열·시한 칩 없음) + 관문 흐름도(§11-2). |
@@ -638,6 +645,10 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
638
645
  | `cells[].note` | 칸의 둘째 줄(작은 글자). 조건·한정을 담는다 |
639
646
  | `cells[].path` | 경로·기술 문자열. 별도 활자로 서고 `/` 앞에서 끊어 읽는다 |
640
647
 
648
+ **`mode: "cause"` — 원인 사슬.** 「해야 할 것 → 이미 된 것 → 그러면」이 아니라 「사실 → 우리 상태 → 결과」를
649
+ 잇는 사슬(요금이 왜 붙었나, 장애가 왜 났나)에는 첫 칸의 노란 강조 면이 뜻을 비튼다. `"mode": "cause"` 를 주면
650
+ 그 면을 걷어 세 칸이 같은 무게로 선다. 열 수·칸 수 계약은 그대로다(2026-09-16).
651
+
641
652
  **그림 번호는 도판과 한 셈을 쓴다** — 한 절에 흐름도와 도판이 같이 있으면 본문 순서대로
642
653
  `그림 N-1`·`그림 N-2` 가 된다. 따로 세면 같은 번호가 두 번 나와 상호참조가 무엇을 가리키는지
643
654
  알 수 없게 된다.
@@ -810,6 +821,84 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
810
821
  - 짝이 안 맞거나 모르는 톤은 그대로 글자로 나간다 — 조용히 사라지지 않는다.
811
822
  - 편집기 서식 구간(`_runs`)과 함께 와도 된다. 구간이 태그를 씌운 뒤 표기를 푼다.
812
823
 
824
+ ## 11-7. 시계열 — 한 값이 시간에 따라 어떻게 갔나 (2026-09-16)
825
+
826
+ 분포 막대(11-4)는 「몇 개의 수를 견준다」. 하루·달 단위로 잰 값이 **언제 계단을 만드는지**는 선이어야 보인다 —
827
+ AWS 요금 조사(2026-09)에서 트래픽·로그·DB 용량이 8월 1일 앞뒤로 평평하다는 것을 막대로는 말할 수 없었다.
828
+
829
+ ```json
830
+ "charts": [
831
+ { "kind": "series", "caption": "ALB 처리량",
832
+ "unit": "GB", "points": [{ "x": "2026-05-01", "y": "8.7" }, { "x": "2026-05-02", "y": "10.5" }],
833
+ "marks": [{ "x": "2026-08-01", "label": "8/1 과금 시작" }],
834
+ "note": "하루 단위 합산." }
835
+ ]
836
+ ```
837
+
838
+ | 칸 | 뜻 |
839
+ |---|---|
840
+ | `kind` | `"series"`. 없으면 분포 막대다 |
841
+ | `points[].x` · `.y` | x 는 `YYYY-MM-DD` 가 기본(달 눈금과 달별 글자 행을 자동으로 세운다). 그 밖의 문자열도 된다(여섯 눈금). y 는 표기 그대로 |
842
+ | `marks[].x` · `.label` | 사건 세로선. x 는 어느 `points[].x` 와 **같은 값**이어야 선다(아니면 ⚠) |
843
+ | `summary[]` | `{label, value, note?}` — 글자 행을 손으로 줄 때. 없으면 달마다 평균·최대를 자동으로 적는다 |
844
+ | `unit` · `caption` · `note` | 막대와 같다 |
845
+
846
+ 인라인 SVG 로 그리되 **스크립트는 0** 이다. 글자(눈금·표식 라벨)는 SVG text 인데 CSS 활자 등급(`--t-sm`)을
847
+ 물려받아 본문과 같은 크기로 선다. 값을 전부 글자로 남길 수 없으므로 **달마다 평균·최대**가 글자 행으로 남는다 —
848
+ 막대와 같은 원칙(그림이 안 보여도 문서가 선다). 그림 번호는 막대·흐름도·도판과 한 셈이다.
849
+
850
+ ## 11-8. 원문 인용 — 문구를 원문 그대로 (2026-09-16)
851
+
852
+ 스킬 「반드시 지키는 것」 11 은 「인용은 원문 문구 그대로」라고 하는데, 관보에는 그 자리가 없었다 — 표 칸은
853
+ 20자, 산문은 우리 글이다. 공식 문서·계약·회신의 문구를 **손대지 않고** 싣는 자리다.
854
+
855
+ ```json
856
+ "quotes": [
857
+ { "text": "Starting March 1, 2024, Amazon RDS automatically charges you for RDS Extended Support.",
858
+ "lang": "en",
859
+ "source_text": "AWS 문서 › Amazon RDS Extended Support (4문단)",
860
+ "source_url": "https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/extended-support.html",
861
+ "verdict": "직접 확인",
862
+ "translation": "표준 지원이 끝난 다음 날부터 자동으로 과금함." }
863
+ ]
864
+ ```
865
+
866
+ | 칸 | 뜻 |
867
+ |---|---|
868
+ | `text` | 원문 그대로. 인라인 표기(굵게·칩)를 **풀지 않는다**. 줄바꿈은 그대로 선다 |
869
+ | `lang` | 원문 언어 코드. `en` 이면 낱말 단위 접기가 라틴 규칙으로 바뀐다 |
870
+ | `source_text` · `source_url` | 출처 줄(도판과 같은 장치). **둘 다 없으면 ⚠** — 독자가 원본에 닿아야 인용이다 |
871
+ | `verdict` | 도판과 같은 축(직접 확인 · 조사 보고 · 미확인) — 캡션 끝 칩 |
872
+ | `translation` | 우리말 풀이 한 문장(선택). 우리 글이므로 표기를 푼다 |
873
+
874
+ 인용은 절 본문의 **맨 앞**에 선다(원문이 가장 강한 근거이고 표·도해는 그것을 줄인 것). `kind` 와 무관하다.
875
+ 번호는 없다 — 그림·표가 아니다.
876
+
877
+ ## 11-9. 도해 — Archify 를 흡수한다 (2026-09-16)
878
+
879
+ 구조·흐름·순서·데이터 흐름·상태 전이는 Archify(`tt-a1i/archify`, 에이전트 스킬)로 그린다. 관보에는 그 산출물의
880
+ **정지 SVG 만** 인라인으로 앉힌다 — 절차는 스킬 `references/diagrams.md`, 굳히는 도구는 `assets/diagram.mjs`.
881
+
882
+ ```json
883
+ "diagrams": [
884
+ { "id": "upgrade-path", "caption": "업그레이드가 지나는 세 관문",
885
+ "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1200 520\">…</svg>",
886
+ "alt": "스냅샷 복원 → 시험 → 운영 적용", "verdict": "직접 확인", "wide": true,
887
+ "origin": "archify workflow v2 · ir/upgrade-path.workflow.json" }
888
+ ]
889
+ ```
890
+
891
+ | 칸 | 뜻 |
892
+ |---|---|
893
+ | `svg` | `diagram.mjs export` 가 굳힌 SVG 문자열. 서버가 다시 걷는다 — 스크립트·스타일·foreignObject·이벤트·애니메이션·외부 href(문서 안 `#`·`data:image/` 만 남는다). 루트 width/height 는 버리고 viewBox 로 선다 |
894
+ | `id` | `{{그림:id}}` 로 가리키는 이름 · 메모가 무는 `data-uid` |
895
+ | `caption` · `alt` · `source_text` · `verdict` · `note` | 도판과 같다. 우리가 그린 도해의 출처 줄은 비운다(R12e) |
896
+ | `wide` | 전폭(1030px). 글자가 표시 15px 에 못 미치는 넓은 도해는 켠다 — 그리드 밖, 절 끝에 선다 |
897
+ | `origin` | 렌더 안 됨 — IR 경로·생성 도구를 남긴다 |
898
+
899
+ 관문(⚠, §10-1): 바이트 250,000 초과 · viewBox 없음 · **가장 작은 글자 × (표시 폭 ÷ viewBox 폭) < 15px**(readability R1).
900
+ `diagram.mjs` 가 같은 잣대로 먼저 재므로 렌더러 ⚠ 는 마지막 그물이다. 그림 번호는 차트 → 흐름도 → 도해 → 도판 순.
901
+
813
902
  ## 12. 판정 밖의 두 모드 — 가르치는 문서 · R&D 브리핑
814
903
 
815
904
  독자가 **판정**하는 문서(검수 보고)와 **배우는** 문서, **결정하는** 문서는 뼈대가 다르다.
@@ -991,7 +1080,7 @@ Submissions·WHO/SUPPORT 정책 브리프(1:3:25)가 **같은 뼈대**를 요구
991
1080
  ```
992
1081
 
993
1082
  - 표지 `meta`·`notice` 에는 사실문으로만 올린다(「시험판 확인은 받으신 뒤 7일 안」). 요청 문장은 이 건 하나에만 둔다.
994
- - `guide`(읽는 법)·표지·현황 줄·다른 절에 「확인 부탁드립니다」를 흩어 두지 않는다 — 요청은 요청 절에만(SKILL 5a 「보고 칸에 요청문을 두지 마라」).
1083
+ - 표지·현황 줄·다른 절에 「확인 부탁드립니다」를 흩어 두지 않는다 — 요청은 요청 절에만(SKILL 5a 「보고 칸에 요청문을 두지 마라」). `guide`(읽는 법)에는 이 건을 가리키는 요청 한 줄을 둘 수 있다(「시험판 확인은 확인 프로필 표의 네 경우로 부탁드립니다」 꼴, 2026-09-15 사용자 결정).
995
1084
  - 제목이 「… 확인 요청」으로 끝나면 렌더러가 기한 ⚠ 를 「확인 요청 「…」에 기한이 없습니다」로 가른다 — 결정 물음 문구(「결정을 요청한 것이 아닙니다」)로 오분류되지 않는다.
996
1085
  - 무엇을 확인하면 되는지는 §12.3b 「확인 프로필」 표가 받는다.
997
1086
 
@@ -283,7 +283,7 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
283
283
  그래서 검수·보고 문서의 `open_items[].title` 은 전부 1인칭이다 — `검증에 쓴 표본이 작음`, `같은 전화번호를 쓰는 계정의 주인은 우리가 판단할 수 없음`.
284
284
  결정 문서(브리핑)의 판단 항목은 다르다 — 고객이 고를 **물음**이다(SKILL 0.5단계 5행 · lim-portal-format §12.5b).
285
285
  독자의 할 일이 둘이면(결정 + 시험판 확인) 둘째는 `open_items` 두 번째 건 「… 확인 요청」으로 두고 본문 첫 문장에 상대 기한을 넣는다 —
286
- 표지·현황 줄에는 사실문으로만 올리고, 읽는 법(`guide`)에 요청문을 두지 않는다(보고 자리, SKILL 5a). 집행자: `gazette.py` `brief_checks()` 기한 ⚠(「확인 요청」 문구로 갈림) · 독자 검토 decider 관점.
286
+ 표지·현황 줄에는 사실문으로만 올린다. 읽는 법(`guide`)에는 요청 절의 확인 요청을 가리키는 요청 한 줄을 둘 수 있다(2026-09-15 사용자 결정) — 그 밖의 요청문은 두지 않는다(보고 자리, SKILL 5a). 집행자: `gazette.py` `brief_checks()` 기한 ⚠(「확인 요청」 문구로 갈림) · 독자 검토 decider 관점.
287
287
 
288
288
  | 규칙 | 어기면 |
289
289
  |---|---|
@@ -568,7 +568,7 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
568
568
  도구가 `publish` 를 명시적으로 참으로 주지 않는 한 고객에게 안 보인다. 여는 것은 10단계다.
569
569
 
570
570
  수정은 SKILL.md 8단계 「수정 절차」(`get_report` 머리줄 확인 → 백업 → 고침 → 로컬 렌더·dry_run·독자 검토 → `update_report` → 9단계).
571
- **같은 링크가 새 내용을 가리킨다.** 초안 `update_report` 도 **본문 판(`body_rev`)** 을 올리고 사용자가 그 판을 읽으므로 독자 검토를 통과한 본문 판만 올린다 — 발행 이력 행(`history`)은 공개 뒤에만 는다(SKILL 3c).
571
+ **같은 링크가 새 내용을 가리킨다.** 초안 `update_report` 도 **본문 판(`body_rev`)** 을 올리고 사용자가 그 판을 읽으므로 독자 검토를 통과한 본문 판만 올린다. 사용자가 「바로 반영」을 지시하면 먼저 올리고 검토를 곧바로 뒤따라 돌린다 — 발행 이력 행(`history`)은 공개 뒤에만 는다(SKILL 3c).
572
572
  사람이 고친 초안이면 덮기 전에 묻는다 — `update_report` 에 `body_rev` 잠금 칸이 아직 없어 기계가 막지 않는다.
573
573
 
574
574
  포털 셸이 문서 **둘레**에 놓는 것(검토 회신·핵심 요약·진척률 바)은 저작 대상이 아니다.