genetics-mcp 2.26.0 → 2.28.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 (35) hide show
  1. package/bin/aarch64-apple-darwin/gcell +0 -0
  2. package/bin/aarch64-unknown-linux-musl/gcell +0 -0
  3. package/bin/manifest.json +5 -5
  4. package/bin/x86_64-apple-darwin/gcell +0 -0
  5. package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
  6. package/bin/x86_64-unknown-linux-musl/gcell +0 -0
  7. package/genetics.mjs +1543 -192
  8. package/package.json +1 -1
  9. package/skills/gazette-report/SKILL.md +11 -1
  10. package/skills/gazette-report/scripts/boundary.py +8 -1
  11. package/skills/gazette-report/scripts/tone.mjs +31 -4
  12. package/skills/gazette-report/scripts/tone.test.mjs +34 -0
  13. package/skills/genetics-cell-id/SKILL.md +73 -54
  14. package/skills/genetics-define/SKILL.md +34 -1
  15. package/skills/genetics-report/SKILL.md +67 -69
  16. package/skills/genetics-report/assets/example/cases-briefing-designs.json +15 -5
  17. package/skills/genetics-report/assets/example/cases-briefing.json +14 -5
  18. package/skills/genetics-report/assets/example/cases.json +25 -9
  19. package/skills/genetics-report/assets/example/decision.json +315 -0
  20. package/skills/genetics-report/assets/example/report-briefing-designs.json +2 -2
  21. package/skills/genetics-report/assets/example/report-briefing.json +1 -1
  22. package/skills/genetics-report/assets/example/report.json +65 -21
  23. package/skills/genetics-report/assets/render.py +204 -49
  24. package/skills/genetics-report/references/content-pipeline.md +6 -6
  25. package/skills/genetics-report/references/decision-format.md +113 -0
  26. package/skills/genetics-report/references/diagrams.md +52 -94
  27. package/skills/genetics-report/references/edition-review.workflow.js +81 -31
  28. package/skills/genetics-report/references/history.md +44 -0
  29. package/skills/genetics-report/references/lim-portal-format.md +93 -236
  30. package/skills/genetics-report/references/linear-read-test.workflow.js +33 -10
  31. package/skills/genetics-report/references/process.md +34 -26
  32. package/skills/genetics-report/references/readability.md +9 -8
  33. package/skills/genetics-report/references/reader-review.workflow.js +167 -25
  34. package/skills/genetics-report/references/visual-first.md +56 -42
  35. package/skills/genetics-report/assets/diagram.mjs +0 -208
@@ -48,7 +48,7 @@
48
48
  - **장치 이름** — `labels` 로 바꾼 건 행 이름(물음·관찰·판단·재현 방법)은 다른 절의 이름·부제·표 열·캡션에 다시 쓰지 않는다.
49
49
  건 행 「판단」과 의견 표 열 「판단」이 함께 서면 독자는 둘을 같은 것으로 읽는다 — 의견 절의 낱말은 「의견」이다.
50
50
  - **선택지 기호** — A·B·C 는 **한 목록에만** 붙인다. 도안 셋에 A·B·C 를 붙였으면 다른 표의 길 넷에는 기호 대신 이름을 쓴다.
51
- 같은 선택지는 한눈표·선택지 비교·의견 표·도판 칩·캡션에서 같은 이름·같은 순서로 부른다(`lim-portal-format.md` §12.3b 대칭 불변식).
51
+ 같은 선택지는 한눈표·선택지 비교·의견 표·도판 칩·캡션에서 같은 이름·같은 순서로 부른다.
52
52
  - **스킬의 장치 이름** — 이 문서와 `lim-portal-format.md` 가 선택지를 「길」(두 길·바꾸는 길), 열을 「지금·그대로 두면·바꾸면」이라 부르는 것은
53
53
  **집필자에게 하는 말**이지 원고의 낱말이 아니다. 원고에서 선택지는 고객이 부르는 이름(「복제본 전환」「직접 업그레이드」「현행 유지」)으로, 표 열 이름도
54
54
  그 이름으로 적는다. 「복제본 길」「고른 길별 일정」은 장치 이름의 유출이다(SKILL 반드시 지키는 것 12, 렌더러 ⚠ 「선택지를 「길」로 부른 곳」).
@@ -58,45 +58,54 @@
58
58
 
59
59
  ## 4. 절마다 시각 장치를 먼저 정한다
60
60
 
61
- | 말하려는 것 | 장치 | 만드는 법 |
62
- |---|---|---|
63
- | 개념·원리(왜 갈리나) | 개념도(비트맵) | HTML 한 장을 **표시 폭 그대로**(본문 단·전폭 폭은 `references/readability.md` §1 머리) 쓰고 `assets/figure.mjs` 로 굽는다(배율 auto). 글자·행간·뜻 색·그림 안 문장의 값과 그리는 순서는 readability §1·§3 |
64
- | 실제 화면의 확인 자리 | 번호 표식 캡처 | `assets/shot.mjs` — 페이지 안에 ①②③ 배지를 얹은 채 요소를 찍는다. 이름은 `--mask`. 번호 표식 캡처는 비교 컷이 아니다 — 틀 안 글자 0(R13b)의 원 번호 한 글자 예외에 기댄다. 원 번호는 **읽는 순서만**, 대상의 이름은 틀 위 칩으로(원 번호를 이름표로 쓰지 않는다). 배지는 `shot.mjs` 기본 검정 원(R13c) |
65
- | 여러 항목을 축으로 견주기 | 표(`tables`) | 칸은 짧게(20자·5열 이상 12자), 수는 `align:"r"`. 상태 칩 `{{ok|보통}}`(문법 lim-portal-format §11-6) |
66
- | 「A 가 되면 B 가 되고 C 가 된다」 | 흐름도(`flow`) | 4열 고정. 판단 절에는 「정하면 어느 길로」 관문 흐름도 |
67
- | 분포·비중 | 막대 차트(`charts`) | 행마다 `tone` 으로 뜻 색. 두 축 비교는 `toggle` |
68
- | 사건 경과·타임라인 | 표 | 날짜 · 일어난 일 · 상태 또는 건 번호. **일어난 일만** 적고 까닭·기제·금액 판정은 건이 가진다 — 건 절에서 같은 사건을 열만 바꿔 다시 세우지 않는다 |
69
- | **화면이 바뀌는 선택지**(비표시·제거·추가·이동) | 「그대로 두면 · 지금」 / 「바꾸면 · 재현」 두 컷 한 쌍(좌우) | `shot.mjs --hide … --after … --geom` 으로 전후를 찍고(뒤 컷은 그 자리를 실제로 빼 아래 내용이 올라온 채) `pair.mjs` 로 조립한다 — 좌표를 손으로 옮기지 않는다. 앞 컷은 바뀔 자리를 **검정** 실선, 뒤 컷은 빈 자리를 **회색 점선**. 두 컷은 **좌우 같은 틀·같은 배율·같은 윗선** — 짧은 쪽은 흰 여백, 늘리거나 따로 잘라 맞추지 않는다. 빈 자리에 글자를 얹지 않는다(회색 점선이 말한다). 이름표 칩은 같은 모양, 문구는 기본 「그대로 두면」/「바꾸면」, 한눈표가 없으면 캡션의 이름. 값은 readability R13. 예시 값·재현이라는 사실은 도판 HTML 머리 주석과 `origin` 에만 적는다(보이는 자리에는 캡션 한 낱말, R12e). **빨강·초록은 이용자 상태를 나르는 그림에만** — 선택지 두 컷에 칠하면 색이 두 길의 옳고 그름을 먼저 판정한다(실사례 2026-09-11, 고객사 결정 브리핑 「답정너 틀」 반려). 여러 항이 같은 자리를 가리키면 칩을 「세 항 공통」으로 — 항 번호를 패널에 나눠 붙이면 독자는 항마다 자리가 다르다고 읽는다(2026-09-08 반려, 독자 시험 3/3 오독). 집행자: `figure.mjs` R13a~d(배치·틀 안 글자·표식 색·칩) · `pair.mjs`(규격을 코드가 씀) · 칩 문구의 뜻과 도판 사이 대칭은 독자 검토 「도판」 관점과 사람 눈 |
70
-
71
- 도판 HTML 머리 주석은 그림 번호가 아니라 `assets[].id` 로 적는다 — 번호는 자리가 매기고 도판을 옮기면 바뀐다. 본문에서 그림을 가리킬 때도
72
- 번호를 손으로 적지 않고 `{{그림:<id>}}` 토큰으로 쓴다(문법·번호 순서는 `lim-portal-format.md` §11-1·§11-6).
73
-
74
- 글은 장치 옆 **3줄(220자) 안**. 표의 숫자를 문장으로 되풀이하지 않는다 — 검토가 잡은 둘째 결함이었다
75
- (제8절 문단 8개가 표 3개를 문장으로 반복). 산문에 참조 번호(6.6 · 2.12a)를 넣지 않는다 — 번호는 표·건·항목의 몫이다.
76
-
77
- ### 판독 — 도판은 열람 화면(본문 단 760px)에서 재고, 잣대는 글자 크기 하나가 아니다 (2026-09-08 · 09-09 반려)
78
-
79
- 880px 로 그린 합성 그림이 9.5~12.5px 로 떠 반려됐고(9/8), 14px 게이트를 통과한 그림이 「가독성에 하자」로 다시 반려됐다(9/9 —
80
- 라벨 15px·행간 1.3·라벨 간격 2px·띠 대비 1.2:1·뜻 색 5·그림 안 문단 150자·1x 손실 인코딩). 2026-09-14 에는 글자를 통과한
81
- 비교 컷이 위아래로 쌓이고 틀 안에 글자를 얹은 채 「A/B 대칭으로 보여줘야 되는 거 아님?」으로 반려됐다. **값의 정본은 `references/readability.md` §1
82
- 규칙표** 이고, 도판 층(R0~R10·R13 — 캡처 틀 문법 포함)은 `figure.mjs` 가, 글 층(R11·R12)은 렌더러 `text_scan()` 이 그 표를 그대로 센다 — 여기서는 되풀이하지 않는다. 그리는 순서는 그 문서 §3.
83
-
84
- ### 도판 예산
85
-
86
- 상한은 서버 거절 조건이다(장당·합계 — `lim-portal-format.md` §9, 인코딩은 §11-1 「도형과 실촬 화면은 인코딩이 갈린다」).
87
- 실제 화면 캡처는 필요한 요소만(전체 페이지 금지). 넘치면 **중복 증거 도판을 빼고 부록 자료 목록에 사유를 적는다**
88
- — 같은 주장을 다른 자료가 이미 담은 도판(예: 같은 수치를 싣는 두 기종 매뉴얼 가운데 한 쪽), 본문 근거 표에 이미 남긴 논문 그림이 뺄 첫째다.
89
- 합계는 `figure.mjs --batch plates.json --report report.json` 이 센다(장별 판정·provider·크기·누계, 90% 에서 ⚠) — 발행 거절로 알지 않는다.
61
+ 말하려는 것의 **내용 모양**(구조 · 흐름 · 순서 · 상태 · 변화 · 분포 · 시간에 따른 값 · 견주기 · 원문 · 화면)이 장치를 고른다. 아래 표는 장치 목록
62
+ `private/builder/devices.py`(정본 한 벌)에서 굽는다 — 렌더러 관문 · 도구 설명도 같은 목록을 읽고, 표에 없는 장치 · 옵션을 적으면 `tools/contract-drift.py` 가 빨갛게 센다.
63
+
64
+ <!-- 장치 목록에서 굽는 글 «device-table» — 손으로 고치지 않는다: python3 tools/contract-devices.py --write -->
65
+ | 장치 | 내용 모양 | 둘 수 있는 칸 → 원고 칸 | 서버 조건 · 한도 | 쓰는 법 |
66
+ |---|---|---|---|---|
67
+ | **표** | 견주기(글이 섞인 항목 — 글이 섞일 때) · 순서(일어난 일의 차례 — 사건 경과) | 절 `sections[].tables[]` · 건 `cases[].tables[]` | 칸 20자(5열 이상 12자) · ※ 주석 2문장 · 본문 단에 안 드는 표는 렌더러가 전폭 띠로 넓힌다 — 쪼개지 않는다 | 수 열은 align "r", 상태는 칩(`{{ok\|보통}}`). 캡션은 이름(20자 안)이고 설명은 note 로. 사건 경과는 날짜 · 일어난 일 · 상태 세 열에 일어난 일만 — 까닭은 건이 진다. 모든 행이 같은 값인 열은 열로 두지 않고 ※ 주석 한 줄로. 행끼리 크기를 견주는 열은 bar{col: 열 이름}(칸 막대 — 칸 글 앞의 수, % 칸이면 끝이 100). 칩 색의 뜻은 note 에 칩 그대로 적는다. |
68
+ | **흐름도** | 흐름(「A 가 되면 B 가 되고 C 가 된다」 · 원인 사슬) | 절 `sections[].flow` | 머리 4칸 · 행마다 칸 3(칸 성격은 자리가 정한다) · 칸 20자 · note 12자 | 세 칸은 해야 할 것 → 이미 된 것 → 그러면 — 읽는 차례가 다르면 열 이름을 그 차례로 고쳐 적는다. 원인 사슬(사실 → 우리 상태 → 결과)은 mode "cause". 시스템 안쪽의 절차 · 데이터가 지나는 길은 도해(절차도 · 데이터 흐름도)다. |
69
+ | **막대 차트** | 견주기(항목마다 수 하나 — 수일 때) · 분포(비중 · 수의 크기) | 절 `sections[].charts[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].chart` | 한 차트 한 축척 — 값 글은 막대 끝 | form 을 안 적으면 막대(length)다. 값은 표기 그대로("118,716") 적고 이름 있는 행만 선다. 기준선은 ref[]{value, label}(점선) · 일부만 잰 값은 rows[].partial(속 빈 막대) · 뜻 색은 rows[].tone · 두 축은 toggle{a, b}(한 축척). 단위가 % 면 0~100 바탕. |
70
+ | **점 차트** | 견주기(같은 축 위의 위치 — 0 인 값도 점으로 선다) | 절 `sections[].charts[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].chart` | — | form "position" — 같은 축 위의 점이라 값이 0 인 행도 선다(길이로 견주기 어려운 작은 값 · 기준선 둘레의 차이). ref · partial(속 빈 점) · tone 은 막대와 같다. 흔들림 · 오차 범위는 rows[].range [아래, 위](옅은 막대) + range_key · value_key (그림 위 한 줄 — 쉬운 말로) — 값은 오른쪽 한 열에 선다. 차이가 운이 아닌지는 0 기준 ref 와 「적어도 · 많게는」 범위로. |
71
+ | **기울기** | 변화(두 조건 · 두 때 사이에 묶음마다 값이 어떻게 움직였나) | 절 `sections[].charts[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].chart` | 축척은 0 부터 — 작은 차이를 가파른 선으로 부풀리지 않는다 | form "slope" · toggle{a, b}(두 조건 이름) · rows[{label, a, b, tone?}] — 묶음마다 a → b 를 잇는 선. 이름은 양 끝에 서고, 강조할 묶음 하나만 tone(info). 묶음 크기(몇 건)는 이름에 함께 적는다. |
72
+ | **구성 띠** | 분포(한 덩이를 이루는 조각 — 합이 100%) | 절 `sections[].charts[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].chart` | 비율은 정수 %이고 합이 100 이 되게 렌더러가 맞춘다 | form "share" · rows[{label, value, tone?}] — 100% 띠 하나와 아래 조각마다 이름 · 값 · 비율. 한 덩이의 조각이면 같은 톤을 주고(짙기 셋으로 갈린다), 다른 그림에서 다른 뜻으로 쓴 색은 피한다. 합은 캡션이 말한다. |
73
+ | **시계열** | 시간에 따른 값(언제 계단이 생겼나) | 절 `sections[].charts[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].chart` | 0 이 아닌 점이 12개를 넘으면 값 글은 가장 큰 점 · 마지막 점에만 · 표식은 points[].x 와 같은 x 에만 선다 | kind "series" · points[{x, y}] — x 가 YYYY-MM-DD 면 달마다 평균 · 최대 글자 행이 저절로 선다(summary 로 바꿔 줄 수 있다). 기본 모양은 점과 선이고 form "length" 면 날마다 막대. 사건은 marks(세로선) · 기준은 ref(가로 점선). 쌓은 막대는 points[].parts + parts[{label, tone?}](조각 이름 — 그림 위 한 줄), 사건 이름은 marks[].detail(1~3줄 — 「신규 551건 · 그 달 866건」 아래 이슈 이름). x 가 YYYY-MM 이면 막대마다 「3월」 · 해가 바뀌는 자리에 「2026년」. |
74
+ | **원문 인용** | 원문(공식 문서 · 계약 · 회신의 문구) | 절 `sections[].quotes[]` · 건 `cases[].quotes[]` · 사실 근거 `decision_brief.facts[].source[].quote` | 출처 줄(source_text · source_url) 하나 이상 — 없으면 ⚠ | 문구를 손대지 않고 싣는다(표기를 풀지 않고 줄바꿈 그대로). 한 건의 거절문 · 회신은 그 건의 quotes 에. 우리말 풀이는 translation 한 문장. 결정 블록의 사실 근거에서는 quote 가 원문 한 줄(글자)이다. |
75
+ | **구성도** | 구조(구성 요소 · 경계 · 연결) | 절 `sections[].diagrams[].ir` · 결정 그림 칸 `decision_brief.decisions[].figures[].diagram` | 본문 단 760px 4열 격자 · 전폭 1030px(wide) 5열 격자 | 로고는 내장 107개만 — 주소로 가져온 로고(brand 객체)는 굽기 전에 거절. 저장소 근거(meta.repository · components[].sources)는 서버가 걷고 그림만 굽는다(⚠). 가로 연결 라벨은 15px 에서 약 1.9배로 넓어진다 — 열 간격을 넓히거나 세로 연결로. |
76
+ | **절차도** | 흐름(절차 · 관문 · 운영 절차) | 절 `sections[].diagrams[].ir` · 결정 그림 칸 `decision_brief.decisions[].figures[].diagram` | 본문 단 760px 4열 · 전폭 1030px(wide) 6열 | 누가 하는지(줄 lanes) · 멈추는 자리(예외 줄 variant "exception" · 연결 role "error")가 있는 차례는 이것으로 그린다. schema_version 2 만 굽는다 — 1 은 거절되고 사유에 판 올림 명령이 실린다. 노드 폭이 모자라면 사유의 고칠 값(라벨 ~N px)대로 그 노드에 width 를 준다. 주 경로를 굵게 보이려면 그 연결에 variant "emphasis". |
77
+ | **순서도** | 순서(누가 누구에게 무엇을 부르나) | 절 `sections[].diagrams[].ir` · 결정 그림 칸 `decision_brief.decisions[].figures[].diagram` | 본문 단 760px 참여자 4명 · 전폭 1030px(wide) 참여자 6명 | 참여자가 한도를 넘으면 그림을 나눈다. 메시지 메모가 다른 생명선을 지나면 엔진이 거절하고 사유에 남은 폭을 싣는다. |
78
+ | **데이터 흐름도** | 흐름(데이터가 지나는 단계 · 계보) | 절 `sections[].diagrams[].ir` · 결정 그림 칸 `decision_brief.decisions[].figures[].diagram` | 본문 단 760px 단계 3 · 전폭 1030px(wide) 단계 4 | 단계 다섯은 두 폭 어디에도 안 선다 — 나눈다. 연결 라벨은 짧게(라벨이 자기 화살촉을 덮으면 엔진이 먼저 옮기고, 못 옮기면 거절). |
79
+ | **상태 변화도** | 상태(상태와 그 사이의 전이) | 절 `sections[].diagrams[].ir` · 결정 그림 칸 `decision_brief.decisions[].figures[].diagram` | 본문 단 760px 4열 · 전폭 1030px(wide) 5열 | 진행 · 대기 · 성공은 관보 색에서 한 색으로 묶인다 — 뜻은 상태 표식과 범례 이름이 나른다. 띠 구분선을 넘는 yOffset 은 거절. |
80
+ | **두 판 비교** | 변화(구성의 전후 — 더해짐 · 빠짐 · 바뀜 · 옮김) | 절 `sections[].diagrams[].compare` | 본문 단 760px 3열 구성도 두 판 · 전폭 1030px(wide) 3열 구성도 두 판 · 장면(meta.views)은 싣지 않는다 | compare {base, head} — 두 판 모두 구성도 설계 데이터. 겹친 그림 한 장과 바뀐 항목 표(기호 · 구분 · 이름 · 바뀜)로 선다 — 기호가 있어 흑백 인쇄에서도 뜻이 남는다. 한 칸에 ir 와 함께 두면 compare 를 그린다(⚠). |
81
+ | **도판** | 화면(실제 화면의 확인 자리 · 바뀔 화면의 도안) · 변화(화면이 바뀌는 선택지 — 두 컷 비교) · 구조(개념 · 원리 — 개념도) · 원문(스프레드시트 칸 · 문서 쪽) | 절 `sections[].plates[] → assets[]` · 건 `cases[].plates[] → assets[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].plate` · 사실 근거 `decision_brief.facts[].source[].plate` | 장당 300,000B · 합계 2,400,000B · 도판 층 R0~R10 · R13(글자 15px · 대비 · 뜻 색 셋 · 캡처 틀 문법) — references/readability.md §1 · 캡처는 긴 변 300px · 짧은 변 100px 이상 | 캡처는 `shot.mjs`(번호 표식 · 이름 가림 `shot.mjs --mask`) · 화면이 바뀌는 선택지는 `shot.mjs --hide … --after … --geom` 으로 전후를 찍어 `pair.mjs` 로 좌우 한 쌍 · 고객 앱 화면 도안은 `mockshot.mjs` · 개념도는 표시 폭 HTML 을 `figure.mjs` 로(여러 장은 `figure.mjs --batch plates.json --report report.json`) · 스프레드시트 칸은 `xlsx-plate.py`. 직접 찍은 화면은 assets[].file 에 로컬 절대경로. 캡처 캡션 끝에 증거 범위(찍은 환경 · 날짜), 도안은 「— 도안」. |
82
+ | **화면 녹화** | 화면(이용자가 여러 화면을 지나는 동선) | 절 `sections[].plates[] → assets[]` · 건 `cases[].plates[] → assets[]` · 결정 그림 칸 `decision_brief.decisions[].figures[].plate` · 사실 근거 `decision_brief.facts[].source[].plate` | **서버**: 영상 저장 연동(r2)이 켜진 서버에서만 — 꺼진 환경은 발행을 거절한다 · 인쇄에는 재생기가 없고 캡션 글자만 남는다 — 같은 동선의 단계 캡처를 곁에 둔다 | assets[].file 에 로컬 mp4 절대경로(role "video") — 도구가 올리고 ref · 시크바 썸네일(thumbs)을 채운다. 캡션 끝에 찍은 환경 · 날짜. |
83
+
84
+ - 차트(막대 차트·점 차트·기울기·구성 띠·시계열) — 글자 15px · 두 폭 760·312px(그림이 놓이는 단의 폭으로 고르고, 인쇄는 넓은 폭).
85
+ - 도해(구성도·절차도·순서도·데이터 흐름도·상태 변화도·두 판 비교) — **서버**: 도해 굽기가 켜진 서버에서만 새로 굽는다(포털 private/archify.on — 로컬 render.py 는 켜고 부른다) — 꺼진 서버는 굽혀 둔 그림만 싣고 새 그림은 같은 번호의 대기 상자로 둔다(「서버에서 실패한 그림이 N장」 — 원고를 고칠 일이 아니다) · 화면 글자 15px — 고정 판 Archify 가 이 기준으로 굽고, 안 들면 거절하며 사유에 고칠 값(노드 width · 필요한 폭)을 싣는다 · 그림 250,000B · 문서의 그림 속성 합 60,000 · 한 장 10초 · 문서 12초.
86
+ <!-- /장치 목록 «device-table» -->
87
+
88
+ 실물(화면·동선)을 도안으로 대신하면 캡션에 무엇을 대신하는지 보이고(「— 도안」), 그 화면을 말한 주장은 남은 것 칸 수준으로 둔다(readability R14).
89
+ 본문에서 그림은 번호를 손으로 적지 않고 `{{그림:<id>}}` 로 가리킨다(`lim-portal-format.md` §11-6). 글은 장치 옆 **3줄(220자) 안** —
90
+ 표의 숫자를 문장으로 되풀이하지 않고, 산문에 참조 번호(6.6 · 2.12a)를 넣지 않는다(번호는 표·건·항목의 몫이다).
91
+
92
+ **판독 · 예산** — 도판 규칙과 값의 정본은 `references/readability.md` §1(도판 층 R0~R10·R13 은 `figure.mjs`, 글 층 R11·R12 는 렌더러가 센다), 그리는 순서는 그 문서 §3.
93
+ 글자 크기는 여러 잣대 중 하나였다 — 14px 게이트를 통과한 그림이 「가독성에 하자」로, 글자를 통과한 비교 컷이 「A/B 대칭」으로 반려됐다(2026-09-09 · 09-14).
94
+ 실제 화면 캡처는 필요한 요소만 싣는다. 예산(장당 · 합계 — 장치 표)을 넘치면 같은 주장을 다른 자료가 이미 담은 중복 증거 도판을 빼고 부록 자료 목록에 사유를 적는다 —
95
+ 합계는 `figure.mjs --batch plates.json --report report.json` 이 먼저 센다(90% 에서 ⚠).
90
96
 
91
97
  ## 5. 판단 요청 — 6항 이상이면 산문으로 못 고른다
92
98
 
99
+ 결정 모드(결정 블록)에서는 이 절의 한눈표·관문 흐름도·본문 네 문장을 결정 장의 칸(선택지 설명·대칭 표·`no_reply`)이 대신 진다 — `decision-format.md`.
100
+ 아래 목록은 보고 모드 판단 항목(검수 확인 요청 · 옛 결정 브리핑 고치기)의 것이고, 「화면 도안」 절차는 두 모드 모두의 것이다.
101
+
93
102
  - **한눈표**: 항 · 지금 · 그대로 두면 · 바꾸면 — 두 길의 결과(이용자에게 보이는 변화)를 **대칭**으로 같은 칸 모양에.
94
103
  「FCG 권고」 열은 두지 않는다 — 한 길의 결과만 있는 열도 같다(실사례 2026-09-11, 고객사 결정 브리핑 「답정너 틀」 반려).
95
104
  길이 셋 이상(도안 결재형)이면 행이 선택지, 열이 **이용자가 보는 것**(모양 · 누르면 · 넘기면 · 앱의 선례)이다. 편익·해악·불확실성은
96
105
  선택지 비교 행렬 한 곳에만 둔다 — 같은 행을 두 표에 세우지 않는다(readability R12c).
97
106
  **모든 행이 같은 값인 열은 열로 두지 않고 ※ 주석 한 줄로** — 선택지를 가르지 못하는 열이다.
98
107
  - **대칭은 표 한 장이 아니라 문서 전체의 불변식**이다 — 선택지의 이름·수·순서가 한눈표·선택지 비교·FCG 의견 표·도판 칩·조건표에서 같고,
99
- 선택지마다 같은 잣대로 채운다(`lim-portal-format.md` §12.3b 「선택지 대칭 불변식」).
108
+ 선택지마다 같은 잣대로 채운다.
100
109
  - **관문 흐름도**: 단계 · 정할 것 · 지금 상태 · 정하면 — 「바꾼다」가 하나라도 있으면 어느 길로 가는지.
101
110
  - 제목은 물음. 본문은 네 문장 안: 첫 문장은 기한과 두 길 이름 → 길마다 달라지는 것 → 근거 절(readability R12b). FCG 의견은 **한 절 한 자리**에만(낼지는 따로 묻는다) — 항마다 「권고안은 …」을 되풀이하지 않는다. 요청 문장은 완전한 높임(SKILL 5a).
102
111
  - 기한은 날짜나 수가 붙은 상대 기한으로 적는다(「2026년 10월까지」·「받으신 뒤 7일 안」) — 「배포 전까지」·「차기」만 적으면 기한 ⚠ 가 뜬다.
@@ -115,17 +124,17 @@
115
124
  | 순서 | 무엇 | 어떻게 |
116
125
  |---|---|---|
117
126
  | 1 | 규격 실측 | 고객 앱 소스에서 옮긴다 — 색 토큰 SSOT(예 `styles/global.ts` colors) · 표준 모달·안내 카드·CTA·시트의 수치 · 문구 목록의 낱말과 맺음 빈도(「닉네임」인가 「이름」인가, 「~해 주세요」인가). 추정하지 않는다 |
118
- | 2 | 도안 HTML | 화면 폭(휴대폰 375 CSS px)으로 그린다. 값·이름이 예시인지, 배경이 자리 표시인지는 도안 HTML 머리 주석과 `origin` 에만 적는다 — 출처 줄·캡션에 올리지 않는다(readability R12e). 머리 주석은 그림 번호가 아니라 `assets[].id` 로 가리킨다 |
127
+ | 2 | 도안 HTML | 화면 폭(휴대폰 375 CSS px)으로 그린다. 값·이름이 예시인지, 배경이 자리 표시인지는 도안 HTML 머리 주석과 `origin` 에만 적는다 — 출처 줄·캡션에 올리지 않는다(readability R12e). 다만 도안이 지금 있는 화면을 대신하면 캡션에 무엇을 대신하는지 보이고, 그 화면을 말한 주장은 남은 것 칸 수준으로 둔다(§4 표 아래). 머리 주석은 그림 번호가 아니라 `assets[].id` 로 가리킨다 |
119
128
  | 3 | 2배 PNG | `assets/mockshot.mjs <도안.html> <out.png>` — 요소를 2배 밀도로 찍는다(figure.mjs 와 같은 playwright-core·크롬). 전후 두 장은 `shot.mjs --hide … --after … --geom`, 같은 구간은 `--clip`. 원본 PDF·캡처 크롭은 도판 HTML 틀(overflow·img 오프셋)이나 `shot.mjs --clip` 으로 한다 — 외부 크롭 도구는 백지를 낸 적이 있다(R0b). `data-dpr` 은 mockshot·shot 2, 실기기 캡처는 기기 배율, PDF 추출 이미지면 1 |
120
129
  | 4 | 관보 도판 | 도판 HTML 에 `<img data-dpr data-min-text>` 로 앉힌다. 두 칸이면 `pair.mjs` 가 **R13a 칸 폭**으로 짠다 — 전폭(`assets[].wide: true`) 두 칸이 본문 단 한 장보다 앱의 작은 글자를 크게 세운다(판정은 R1c). 표시 높이는 R9 안 — 긴 화면은 윗부분·아랫부분 크롭을 **좌우 두 칸**으로. 길이 여럿이면 길마다 같은 촬영 규격·같은 틀(R13a) |
121
130
  | 5 | 관문 | `figure.mjs`(전폭이면 `--wide`) ✗ 0 — 밀도·최소 글자는 img 의 `data-dpr`·`data-min-text` 가 정본이고 없으면 ✗ R1c. 여러 장이면 `figure.mjs --batch` 매니페스트 한 번으로(인자를 셸 변수에 담지 않는다, readability §3). 도안 img 는 `data-dpr` 2, 실기기 캡처는 기기 배율, `data-min-text` 는 화면의 가장 작은 글자 CSS px |
122
131
  | 6 | 원고 | 도안 표(도안 · 모양 · 누르면 · 앱의 선례 — 이용자가 보는 것만, §5) · 뜨는 조건 표 · 기존 화면 흐름(번호 표식) · 「안내를 띄울 자리」 조사 건. 문구 출처 표는 **독자에게 문구 수정을 청할 때만** 둔다. 조건표는 각 행을 **모든 선택지 도안과 앱 코드에 대조**한다 — 선택지마다 값이 다르면 행을 갈라 쓰고, 대조 없이 「공통」이라 적지 않는다. 화면 동작(막는다·다시 뜬다)은 코드에서 확인한 것만 적는다. 권고는 FCG 의견 절 한 곳에만(§5) |
123
- | 7 | 합성(선택) | Higgsfield `gpt_image_2` 에 도안 PNG 를 참조 그림(role image)으로 주면 한글 UI 글자를 보존한 채 손·기기 합성이 된다(2k·high 2 credits). **크기 감을 보이는 보조 그림 1장** — 선택지 도판들 뒤, 그 절 맨 끝에 두고 선택지 도판보다 앞·크게 두지 않는다. `provider: user-provided`, 「합성」은 캡션 한 낱말(칩 옆 설명)과 `origin` 에, 출처 줄은 건 § 한 구절(W2 가 원본 위치를 센다). 칩은 도안 이름 그대로. 합성한 한 길이 FCG 의견이 권하는 길과 같으면 그림이 답을 먼저 말하므로 캡션에 목적(크기 감)을 밝히거나 뺀다. `data-dpr`·`data-min-text` 로 R1c 를 넘긴다 |
132
+ | 7 | 합성(선택) | Higgsfield `gpt_image_2` 에 도안 PNG 를 참조 그림(role image)으로 주면 한글 UI 글자를 보존한 채 손·기기 합성이 된다(2k·high 2 credits). **크기 감을 보이는 보조 그림 1장** — 선택지 도판들 뒤, 그 절 맨 끝에 두고 선택지 도판보다 앞·크게 두지 않는다. `provider: user-provided`, 캡션 끝은 「— 도안」이다 — AI 로 만든 합성 그림은 실물이 아니라 도안이고(「— 합성」은 실제 캡처에 글자를 옮겨 적은 것에만, readability R14), 합성 도구·참조 그림 같은 제작 기록은 `origin` 에. 출처 줄은 건 § 한 구절(W2 가 원본 위치를 센다). 칩은 도안 이름 그대로. 합성한 한 길이 FCG 의견이 권하는 길과 같으면 그림이 답을 먼저 말하므로 캡션에 목적(크기 감)을 밝히거나 뺀다. `data-dpr`·`data-min-text` 로 R1c 를 넘긴다 |
124
133
  | 8 | 반출 | Figma 는 MCP 인증이 있을 때만. 없으면 HTML 원본 경로를 문서 origin 과 핸드오프에 적고 반출은 사람 몫으로 남긴다 |
125
134
 
126
135
  집행자: 도판 규격은 `figure.mjs`(R0·R0b·R1c·R13) · `pair.mjs` · 도안 사이 대칭·합성 자리·조건표와 도안의 대조는 독자 검토 「도판」 관점 ·
127
136
  도안이 있는지는 사람의 눈(process.md 7단계 표 「결정 요청에 화면 변화가 있는데 도안이 없다」). 도안 결재형 본보기는
128
- `assets/example/report-briefing-designs.json`(세 길 · 전폭 도판 자리 · 합성 한 장 · 조건표 · 확인 요청 · 확인 프로필 — 값은 전부 자리 표시).
137
+ `assets/example/report-briefing-designs.json`(세 길 · 전폭 도판 자리 · 합성 한 장 · 조건표 · 확인 요청 · 확인 프로필 — 값은 전부 자리 표시)이고, 옛 결정 브리핑을 고칠 때만 본다 — 새 결정 문서의 본보기는 `assets/example/decision.json`.
129
138
  실행 사례와 도판 넷의 실물 경로는 genetics(lim) 사례 「결정 요청에 화면 변화가 따르면 고객 디자인 시스템 도안까지」에 있다.
130
139
 
131
140
  ## 6. 표기 — 색은 뜻으로
@@ -134,12 +143,13 @@
134
143
  톤 다섯: ok(끝났거나 정상) · wait(사람 판단 대기) · bad(이용자에게 나쁜 상태·오류) · info(참고) · muted(범위 밖·처리 완료).
135
144
  문단 절반이 굵으면 굵기가 위계를 못 나른다 — 절마다 한둘. 문법과 렌더는 `lim-portal-format.md` §11-6.
136
145
 
137
- ## 7. 독자 검토 — 발행 전에 여덟 관점(처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조·선형 읽기·도판·사실 대조 — 상태 표를 주면 상태 대조까지 아홉)으로 읽히고, 반박 검증으로 거른다
146
+ ## 7. 독자 검토 — 발행 전에 아홉 관점(처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조·선형 읽기·도판·사실 대조·증거 가시성 — 상태 표를 주면 상태 대조까지 열)으로 읽히고, 반박 검증으로 거른다
138
147
 
139
148
  `references/reader-review.workflow.js` 를 Workflow 도구로 돌린다. 배열·객체 인자는 JSON 값 그대로 넘긴다(문자열로 싸면 한 덩어리가 된다).
140
149
 
141
150
  먼저 렌더본 텍스트를 **원고 폴더 밖 빈 폴더**에 뽑는다 — `python3 assets/render.py report.json --text <빈 폴더>/본문.txt`.
142
- 렌더가 성공했을 때만 쓰고, 화면에 보이는 글만 화면 순서대로 담는다(그림·영상 자리는 「[그림]」「[영상]」, 표 칸은 「 | 」 — `origin`·alt·스크립트·화면 전용 크롬은 없다).
151
+ 렌더가 성공했을 때만 쓰고, 화면에 보이는 글만 화면 순서대로 담는다(그림·영상 자리는 「[그림] <id> · <갈래>」「[영상] <id> · <갈래>」 — 갈래는 렌더러 판정 `gazette.py` `asset_evidence_kind()`, 표 칸은 「 | 」 — `origin`·alt·스크립트·화면 전용 크롬은 없다).
152
+ 글 끝에 「[그림 파일]」 목록이 붙는다 — 그림 자리 순서대로 갈래와 경로, 실린 바이트는 텍스트 옆 `<이름>-그림/` 에 풀린다. 검토자는 이 목록의 그림을 열어 본다. 그림 자리에 안 선 도판(로컬 렌더에 바이트가 없는 `file`·`source` 그림 등)은 「[본문에 안 선 도판]」 아래 따로 선다 — 본문 줄이 아니다.
143
153
  원고 폴더 안에 쓰면 ⚠ 가 뜬다 — 검토자가 옆 파일(결정 장부·핸드오프)을 열 수 있어서다.
144
154
  CSS 가 만드는 글자(장식 기호·인쇄 쪽 번호)는 없고, 클래스 없는 굵게가 줄을 바꾸는 자리는 붙어 나온다 — 뜻은 같고 글자 단위로 화면과 같지는 않다.
145
155
 
@@ -150,10 +160,11 @@ CSS 가 만드는 글자(장식 기호·인쇄 쪽 번호)는 없고, 클래스
150
160
  scratch: "<측정 스크립트를 둘 스크래치 절대경로>", plates: "<도판 HTML·PNG 작업 폴더(선택)>",
151
161
  locked: ["<정해진 것 한 줄>", …], latest: "<사용자 최신 지적 원문>",
152
162
  sources: [{ name, path, kind: "code|doc|pdf" }, …],
153
- decisions: "<0단계 결정 장부 decisions.json 경로(선택)>", state: "<코드 작업 상태 표 경로(선택)>" }
163
+ decisions: "<0단계 결정 장부 decisions.json 경로(선택)>", state: "<코드 작업 상태 표 경로(선택)>",
164
+ handoff: "<HANDOFF.md 경로(선택)>", evidence_plan: "<0단계 증거 계획 evidence.md 경로(선택)>" }
154
165
  ```
155
166
 
156
- `sources` 는 문서마다 달라서 스킬에 경로를 적지 않는다. `state` 를 주면 여덟째 관점이 켜진다.
167
+ `sources` 는 문서마다 달라서 스킬에 경로를 적지 않는다. `state` 를 주면 열째 관점이 켜진다. `handoff`·`evidence_plan` 은 증거 가시성 관점만 받는다.
157
168
  `text` 가 없으면 워크플로가 멈춘다. 「처음 읽는 사람」·「선형 읽기」 두 관점은 이 파일 하나만 받고(정해진 것·결정 장부·최신 지적·상태 표·내부 자료는 안 받는다)
158
169
  CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 자동 메모리(MEMORY.md)까지 빠지는지는 첫 실행 전사로 확인하기 전이다. 나머지 관점은 위 인자를 그대로 받는다.
159
170
  판 갱신 검토(`edition-review.workflow.js`)와 선형 읽기 실측(`linear-read-test.workflow.js`)도 `args.text` 가 필수다 — 선형 읽기 실측은 `file` 도 받는다 — 렌더본이 원고보다 새로운지 재는 데만 쓰고, 읽는 넷에게는 주지 않는다.
@@ -168,10 +179,11 @@ CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 자동 메모리(MEM
168
179
  | structure 구조 | PART 물음 ↔ 절 내용 · PART 사이 되풀이 · 요지·읽는 법 물음이 건의 관찰·판단을 바꿔 말한 곳(문장 쌍 전수) · 요지의 이야기 순서와 칸 수, 근거 문단 `body` 비움 | 처방은 빼기로 |
169
180
  | figures 도판 | readability R13·§4 를 읽고 `assets[].file` PNG 를 **Read 로 연다**. 같은 이름 `.html` 이 있으면 머리 주석·CSS 를 읽는다. 두 칸이면 틀 윗선·아랫선 y 를 재어 px 차를 evidence 에. 짚을 곳 — R13 위반 · 틀 잘림(윗선 여백 0 포함) · 선택지 도안 가운데 하나만 합성·실사·크게·먼저 · 그림↔본문 반박 · 캡션 「왼쪽은…」 불일치 · 칩 이름과 캡션·표 이름 불일치 | 캡션·alt 만으로 판정하지 않는다. 서버 보관 도판(「(생략 — NB)」 표식)은 `get_report` 로 바이트를 받을 수 없다 — 원본 PNG 가 도판 폴더에 있으면 그것을 열고(파일 이름이 id 와 달라도 캡션·내용으로 찾는다), 없으면 `unopened` 에 「서버 보관」으로. 로고(`role: logo`)는 도판이 아니다 — `unopened` 에 넣지 않는다. 못 연 도판은 `unopened` 에 |
170
181
  | facts 사실 대조 | FCG 내부 1차 자료로 수치를 다시 계산 · 앱 동작은 `kind:"code"` 파일:줄로만 · 결정의 주체·자리·시점은 결정 장부·`locked` 와 대조 | 판정 못 하면 미확인 |
182
+ | evidence 증거 가시성 | 관찰 주장(화면·흐름·화면 문구·동작을 말한 문장)마다 한 행 — 작성자 근거 · 문서 안 증거 · 독자가 닿나 · 처방(상한 없음). 도안은 증거가 아니다(readability R14 — 자리 세기는 렌더러 `evidence_scan()` ⚠) | `args.handoff`·`args.evidence_plan`(0단계 증거 계획)은 이 관점만 받는다 · 처방은 실물 싣기나 안 본 것을 남은 것으로 — 헤지 낱말 달기 아님 |
171
183
  | state 상태 대조(`state` 있을 때) | 「반영·적용·고침·보이지 않음·빠짐」 같은 완료형 문장마다 상태 표의 확인 수준(코드·시험판·실기기·운영) | — |
172
184
 
173
185
  단계.
174
- 1. **Read** — 관점마다 발견 8~14개(선형 읽기는 남김없이). 발견마다 `where · problem · severity · class`(사실틀림|구조|읽기|이름|시각|묻기) `· evidence`(연 것·잰 것, 짐작이면 「추정 — 」) `· rewrite · visual`. 재서 통과한 것은 `passed[]` 에 둔다.
186
+ 1. **Read** — 관점마다 발견 8~14개(선형 읽기는 남김없이). 발견마다 `where · problem · severity · class`(사실틀림|구조|읽기|이름|시각|증거|묻기) `· evidence`(연 것·잰 것, 짐작이면 「추정 — 」) `· rewrite · visual`. 재서 통과한 것은 `passed[]` 에 둔다.
175
187
  2. **Verify** — severity ≥ 3 가운데 사실틀림 먼저, 그다음 severity 순으로 최대 12건 — **읽기·구조 발견에 4칸을 먼저 뗀다**(사실 틀림 가중 때문에 12칸이 다 차 읽기 발견이 늘 검증 밖이었다). 같은 순위 안에서는 **관점마다 돌아가며** 뽑는다(뒤 관점이 상한 밖으로 밀리지 않게). 발견마다 반박 에이전트가 원본을 직접 열어
176
188
  `verdict`(진단: 참|부분|거짓|미확인 — 거짓은 반증을 열었을 때만, 확신이 없으면 미확인)·`finding_basis`(확인|추정)·`touches_locked`·`noise_kind`(없음|제작|날짜경로|판이력)·`rx_fit`(처방 적합: 맞음|다시)·`rx_why` 를 판정한다.
177
189
  진단과 처방은 따로다 — 처방이 틀려도 주장이 맞으면 진단은 참이다. class 이름은 「독자가 문서 밖(자기 화면·원본 문서·공급사 공식 명칭)에서 본 글자인가」로 판정하고, 문서 안의 정의·캡션·용어표는 반증이 아니다.
@@ -181,12 +193,14 @@ CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 자동 메모리(MEM
181
193
  4. **Merge** — 나머지를 같은 자리끼리 합쳐 severity 합산 상위 10. 비트맵 예산을 지키고, 더하기 처방과 structure 의 빼기 처방이 부딪히면 빼기가 이긴다.
182
194
  `rx` 가 달린 발견은 규칙에 맞는 처방을 새로 적는다(이름 발견은 원래 이름으로 — 새 이름·뜻풀이 금지). 선형 읽기 뜀과 이름 발견은 상위 10 과 별개로 전부 돌아간다.
183
195
 
184
- 돌아오는 것: `{ head?, failed?, merged: { findings[](… · rx · verdicts), overall }, questions[], passed[], dropped[], jumps[], names[], raw: { reads, verdicts, unverified } }`.
196
+ 돌아오는 것: `{ head?, failed?, merged: { findings[](… · rx · verdicts), overall }, judgement: { task, task_by[], evidence_missing, evidence_claims, filled_lines }, evidence: { rows[], missing[] }, filled[], questions[], passed[], dropped[], jumps[], names[], raw: { reads, verdicts, unverified } }`.
197
+ `judgement` 은 판정 둘을 따로 센다 — `task`(렌더본만 읽은 독자가 할 일을 할 수 있나: 할 수 있음·할 수 없음·모름)와 `evidence_missing`(증거 가시성 관점 표 `evidence.rows` 가운데 문서 안 증거 「없음」 행 수, 반박 전). 할 일 판정 하나만 보면 증거 없는 단정이 「예」로 지나간다.
198
+ `filled[]` 는 렌더본만 읽은 독자가 증거 줄을 대지 못한 관찰 주장(일반 지식으로 메운 것)이다. `overall` 머리에 「할 일 판정」·「증거 판정」 두 줄이 선다.
185
199
  `merged.findings` 앞에는 **참·부분으로 판정된** 사실 틀림이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다. 못 연 도판이 있으면 `overall` 머리 줄이 「⚠ 도판 N장 미열람(id…)」 — 그 도판은 사람이 연다. 검토자가 「서버 보관」이라 적은 도판은 「도판 N장 서버 보관」 줄로 따로 선다 — 그 id 가 원고에서 보관 표식(`data` 가 「(생략 — NB)」)인 장인지 확인하고 열람 주소에서 사람이 본다(체크리스트 7단계 독자 검토 행).
186
200
  검증이 1건이라도 실패하거나 관점이 결과를 못 내면 `head` 「검토 미완 — 검증 N건 실패」(관점 실패는 이어 적는다)와 `failed: true` 가 붙고 `overall` 첫 줄도 같다 — 실패를 「발견 0」으로 읽지 않고 검토를 다시 돌린다.
187
- 통과 기준(SKILL 체크리스트 7단계 독자 검토 행): `jumps` 가운데 심각도 3 이상 0건 · `names` 0건 — 못 없앤 것은 까닭을 핸드오프에.
201
+ 통과 기준(SKILL 체크리스트 7단계 독자 검토 행): `jumps` 가운데 심각도 3 이상 0건 · `names` 0건 · `judgement.evidence_missing` 0 — 못 없앤 것은 까닭을 핸드오프와 공개·교체 승인 물음에(SKILL 체크리스트 10단계 행).
188
202
 
189
- **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 정해진 것을 되돌리거나 제작 방식·결정 날짜·판 이력을 더하라는 처방은 받지 않는다 — 그런 처방이 달린 발견은 버리지 않고 `rx` 로 처방만 다시 쓴다.
203
+ **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 정해진 것을 되돌리거나 제작 메타(도구·배율·자르기 — 캡처의 증거 범위는 아님, readability R12e)·결정 날짜·판 이력을 더하라는 처방은 받지 않는다 — 그런 처방이 달린 발견은 버리지 않고 `rx` 로 처방만 다시 쓴다.
190
204
  반영 순서는 사실 틀림 → 구조 발견(PART·절 합치기·빼기) → 나머지.
191
205
 
192
206
  경위 넷. 고객사 검수 보고(2026-09-08)에서 이 검토가 용어 충돌·수치 불일치·참조 오류·반올림 설명 오류를 잡았다 — 작성자는 못 본 것들이다.
@@ -1,208 +0,0 @@
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
- const isText = a.localName === 'text' || a.localName === 'tspan';
109
- const TEXT_ONLY = new Set(['font-size','font-weight','font-style','text-anchor','dominant-baseline','letter-spacing','text-decoration']);
110
- const DEFAULTS = { 'fill-opacity': '1', 'stroke-opacity': '1', 'opacity': '1', 'fill-rule': 'nonzero', 'stroke-miterlimit': '4',
111
- 'stroke-dashoffset': '0px', 'stroke-linecap': 'butt', 'stroke-linejoin': 'miter', 'paint-order': 'normal', 'mix-blend-mode': 'normal',
112
- 'font-style': 'normal', 'letter-spacing': 'normal', 'dominant-baseline': 'auto', 'text-anchor': 'start',
113
- 'transform-origin': '0px 0px', 'clip-path': 'none', 'mask': 'none', 'filter': 'none', 'stroke-dasharray': 'none',
114
- 'marker-start': 'none', 'marker-mid': 'none', 'marker-end': 'none' };
115
- const tf = cs.getPropertyValue('transform');
116
- const identity = tf === 'none' || /^matrix\(1, 0, 0, 1, 0, 0\)$/.test(tf);
117
- let style = '';
118
- for (const p of PROPS) {
119
- const v = cs.getPropertyValue(p);
120
- if (!v) continue;
121
- if (TEXT_ONLY.has(p) && !isText) continue; // 글자 속성은 글자에만 — 도형마다 되풀이하면 크기만 는다
122
- if (p === 'transform' && identity) continue; // 항등 변환은 적지 않는다
123
- if (p === 'transform-origin' && identity) continue;
124
- if (p === 'text-decoration' && /^none\b/.test(v)) continue;
125
- if (DEFAULTS[p] === v && (!par || par.getPropertyValue(p) === v)) continue; // 기본값이고 부모도 같다
126
- if (par && par.getPropertyValue(p) === v && !['fill', 'stroke', 'font-size', 'font-weight', 'opacity'].includes(p)) continue;
127
- style += p + ':' + v + ';';
128
- }
129
- if (a.localName === 'text' || a.localName === 'tspan') {
130
- const fs = parseFloat(cs.fontSize);
131
- if (Number.isFinite(fs) && (a.textContent || '').trim()) { texts++; minFont = minFont === null ? fs : Math.min(minFont, fs); }
132
- }
133
- for (const at of Array.from(b.attributes)) {
134
- if (at.name === 'class' || at.name.startsWith('data-') || at.name.startsWith('on') || at.name === 'style' || at.name === 'tabindex') b.removeAttribute(at.name);
135
- if ((at.name === 'href' || at.name === 'xlink:href') && !(at.value.startsWith('#') || at.value.startsWith('data:image/'))) b.removeAttribute(at.name);
136
- }
137
- if (style) b.setAttribute('style', style);
138
- }
139
- clone.removeAttribute('width'); clone.removeAttribute('height');
140
- clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
141
- const vb = (clone.getAttribute('viewBox') || '').trim().split(/[\\s,]+/).map(Number);
142
- return { svg: new XMLSerializer().serializeToString(clone), minFont, texts, vw: vb[2] || 0, vh: vb[3] || 0, level: full };
143
- })()`;
144
-
145
- function measure(svg, wideFlag, extra = {}) {
146
- const bytes = Buffer.byteLength(svg, 'utf8');
147
- const vb = /viewBox\s*=\s*"\s*([-\d.]+)[\s,]+([-\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*"/i.exec(svg);
148
- const vw = vb ? parseFloat(vb[3]) : 0, vh = vb ? parseFloat(vb[4]) : 0;
149
- const sizes = [...svg.matchAll(/font-size\s*[:=]\s*"?\s*([\d.]+)\s*(px)?/gi)].map(m => parseFloat(m[1])).filter(Number.isFinite);
150
- const minFont = extra.minFont ?? (sizes.length ? Math.min(...sizes) : null);
151
- const display = wideFlag ? WIDE : COLUMN;
152
- const shown = (vw && minFont !== null) ? minFont * display / vw : null;
153
- const texts = extra.texts ?? (svg.match(/<text\b/gi) || []).length;
154
- const verdicts = [];
155
- if (!vw) verdicts.push(['R0', 'fail', 'viewBox 가 없다 — 폭을 알 수 없어 렌더러가 싣지 않는다']);
156
- if (bytes > MAX_BYTES) verdicts.push(['R0', 'fail', bytes + 'B > 상한 ' + MAX_BYTES + 'B — 노드를 줄이거나 PNG 도판으로']);
157
- if (shown !== null && shown < MIN_FONT) verdicts.push(['R1', 'fail', '가장 작은 글자 ' + minFont + 'px × (' + display + '/' + vw + ') = 표시 ' + shown.toFixed(1) + 'px < ' + MIN_FONT + 'px — ' + (wideFlag ? '노드·라벨을 줄인다' : '--wide 로 전폭에 세우거나 노드를 줄인다')]);
158
- if (vw && vh / vw > 1.8) verdicts.push(['R9', 'warn', '세로가 폭의 ' + (vh / vw).toFixed(2) + '배 — 포털이 폭을 깎는다(1.8배 안으로)']);
159
- if (!texts) verdicts.push(['R4', 'warn', '글자가 0 — alt 만으로 읽히는 도해인지 확인']);
160
- return { bytes, vw, vh, minFont, display, shownMinFont: shown === null ? null : Math.round(shown * 10) / 10, texts, verdicts };
161
- }
162
- function report(out, m, extra) {
163
- const bad = m.verdicts.some(v => v[1] === 'fail');
164
- const lines = [out + ' — ' + m.bytes + 'B · viewBox ' + m.vw + '×' + m.vh + ' · 글자 ' + m.texts + '개 · 가장 작은 글자 ' + (m.minFont ?? '?') + 'px → 표시 ' + (m.shownMinFont ?? '?') + 'px(폭 ' + m.display + ')',
165
- ...m.verdicts.map(v => ' ' + (v[1] === 'fail' ? '✗' : '⚠') + ' ' + v[0] + ' ' + v[2]),
166
- bad ? ' → 렌더러도 같은 ⚠ 를 낸다. 고친 뒤 다시.' : ' ✓ 관보 diagrams[].svg 에 넣을 수 있다'];
167
- say({ ok: !bad, out, ...m, ...extra }, lines.join('\n'));
168
- process.exit(bad ? 3 : 0);
169
- }
170
-
171
- async function exportSvg(html, out) {
172
- const b = await browser();
173
- try {
174
- const ctx = await b.newContext({ viewport: { width: 1600, height: 1000 }, reducedMotion: 'reduce', colorScheme: 'light' });
175
- const page = await ctx.newPage();
176
- await page.goto(pathToFileURL(path.resolve(html)).href, { waitUntil: 'load' });
177
- await page.waitForTimeout(700);
178
- const r = await page.evaluate(INLINE);
179
- if (!r || r.error) die('export 실패: ' + (r && r.error));
180
- fs.writeFileSync(out, r.svg, 'utf8');
181
- return r;
182
- } finally { await b.close(); }
183
- }
184
-
185
- (async () => {
186
- if (cmd === 'inspect') {
187
- const [, inp] = pos; if (!inp) die(usage);
188
- return report(inp, measure(fs.readFileSync(inp, 'utf8'), wide));
189
- }
190
- if (cmd === 'export') {
191
- const [, html, out] = pos; if (!html || !out) die(usage);
192
- const r = await exportSvg(html, out);
193
- return report(out, measure(r.svg, wide, { minFont: r.minFont, texts: r.texts }), { source: html, level: r.level });
194
- }
195
- if (cmd === 'render') {
196
- const [, type, ir, out] = pos; if (!type || !ir || !out) die(usage);
197
- const dir = findArchify(opt('--archify'));
198
- if (!dir) die('Archify 를 못 찾음 — `npx skills add tt-a1i/archify -g` 로 설치하거나 --archify <dir> / ARCHIFY_DIR 로 알려 주십시오');
199
- const tmp = path.join(os.tmpdir(), 'diagram-' + process.pid + '.html');
200
- const d = spawnSync(process.execPath, [path.join(dir, 'bin/archify.mjs'), 'deliver', type, path.resolve(ir), tmp, '--quality', 'showcase', '--json'], { encoding: 'utf8' });
201
- let receipt = null; try { receipt = JSON.parse(d.stdout); } catch { /* 아래에서 원문을 보인다 */ }
202
- if (d.status !== 0 || !receipt || !receipt.ok) die('archify deliver 실패(exit ' + d.status + ')\n' + (d.stdout || '') + (d.stderr || ''), 4);
203
- const r = await exportSvg(tmp, out);
204
- fs.rmSync(tmp, { force: true });
205
- 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 });
206
- }
207
- die(usage);
208
- })();