genetics-mcp 2.28.0 → 2.29.1

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 (30) hide show
  1. package/genetics.mjs +133 -449
  2. package/package.json +4 -5
  3. package/skills/genetics-cell-id/SKILL.md +4 -5
  4. package/skills/genetics-report/SKILL.md +58 -38
  5. package/skills/genetics-report/assets/example/cases-briefing-designs.json +11 -11
  6. package/skills/genetics-report/assets/example/cases-briefing.json +3 -3
  7. package/skills/genetics-report/assets/example/cases.json +3 -3
  8. package/skills/genetics-report/assets/example/decision.json +40 -42
  9. package/skills/genetics-report/assets/example/report-briefing-designs.json +61 -61
  10. package/skills/genetics-report/assets/example/report-briefing.json +54 -54
  11. package/skills/genetics-report/assets/example/report.json +34 -45
  12. package/skills/genetics-report/assets/render.py +4 -6
  13. package/skills/genetics-report/assets/subset_font.py +1 -1
  14. package/skills/genetics-report/references/content-pipeline.md +1 -1
  15. package/skills/genetics-report/references/decision-format.md +87 -22
  16. package/skills/genetics-report/references/font-embedding.md +1 -1
  17. package/skills/genetics-report/references/history.md +29 -5
  18. package/skills/genetics-report/references/lim-portal-format.md +47 -24
  19. package/skills/genetics-report/references/process.md +26 -25
  20. package/skills/genetics-report/references/readability.md +5 -5
  21. package/skills/genetics-report/references/visual-first.md +11 -74
  22. package/bin/aarch64-apple-darwin/gcell +0 -0
  23. package/bin/aarch64-unknown-linux-musl/gcell +0 -0
  24. package/bin/manifest.json +0 -7
  25. package/bin/x86_64-apple-darwin/gcell +0 -0
  26. package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
  27. package/bin/x86_64-unknown-linux-musl/gcell +0 -0
  28. package/skills/genetics-report/references/edition-review.workflow.js +0 -196
  29. package/skills/genetics-report/references/linear-read-test.workflow.js +0 -106
  30. package/skills/genetics-report/references/reader-review.workflow.js +0 -404
@@ -20,7 +20,7 @@
20
20
 
21
21
  문제 → 원인 → 해법(지금 상태 포함) → 정할 것. 이것은 **칸 수가 아니라 이야기 순서**이고 **검수·보고판**의 것이다 — 결정 문서판의 순서·칸 수·문단 모양은 SKILL 4~6단계 한 곳이 정본이다. `summary[]` 를 이 순서로 쓰고, 1절에
22
22
  「사건 경과」 표(날짜 · 일어난 일 · 상태 칩) 한 장을 둔다 — 다루는 일의 경과이지 이 문서의 판 이력이 아니다(판 이력은 `history`, SKILL 3b). 신입은 여기서 멈춰도 문서를 안 것이다.
23
- 집행자: 독자 검토 「구조」 관점(readability R12g) · 사람 눈.
23
+ 집행자: 사람 눈(readability R12g).
24
24
 
25
25
  ```json
26
26
  {"label":"Decide","lead":"정식 배포 전에 정할 것은 5항, 나머지는 차기임.",
@@ -54,7 +54,7 @@
54
54
  그 이름으로 적는다. 「복제본 길」「고른 길별 일정」은 장치 이름의 유출이다(SKILL 반드시 지키는 것 12, 렌더러 ⚠ 「선택지를 「길」로 부른 곳」).
55
55
 
56
56
  집행자: `gazette.py text_scan()` ⚠ — 건 행 이름이 다른 절 부제·표 제목·열에 서는 곳, 선택지 기호 하나에 이름이 둘인 곳(표의 열 머리·행 첫 칸만,
57
- 산문은 안 봄) · 한 낱말이 두 값인지(뜻)는 독자 검토 「처음 읽는 사람」 관점과 사람 눈.
57
+ 산문은 안 봄) · 한 낱말이 두 값인지(뜻)는 사람 눈.
58
58
 
59
59
  ## 4. 절마다 시각 장치를 먼저 정한다
60
60
 
@@ -100,19 +100,19 @@
100
100
  아래 목록은 보고 모드 판단 항목(검수 확인 요청 · 옛 결정 브리핑 고치기)의 것이고, 「화면 도안」 절차는 두 모드 모두의 것이다.
101
101
 
102
102
  - **한눈표**: 항 · 지금 · 그대로 두면 · 바꾸면 — 두 길의 결과(이용자에게 보이는 변화)를 **대칭**으로 같은 칸 모양에.
103
- 「FCG 권고」 열은 두지 않는다 — 한 길의 결과만 있는 열도 같다(실사례 2026-09-11, 고객사 결정 브리핑 「답정너 틀」 반려).
103
+ 「권고」 열은 두지 않는다 — 한 길의 결과만 있는 열도 같다(실사례 2026-09-11, 고객사 결정 브리핑 「답정너 틀」 반려).
104
104
  길이 셋 이상(도안 결재형)이면 행이 선택지, 열이 **이용자가 보는 것**(모양 · 누르면 · 넘기면 · 앱의 선례)이다. 편익·해악·불확실성은
105
105
  선택지 비교 행렬 한 곳에만 둔다 — 같은 행을 두 표에 세우지 않는다(readability R12c).
106
106
  **모든 행이 같은 값인 열은 열로 두지 않고 ※ 주석 한 줄로** — 선택지를 가르지 못하는 열이다.
107
- - **대칭은 표 한 장이 아니라 문서 전체의 불변식**이다 — 선택지의 이름·수·순서가 한눈표·선택지 비교·FCG 의견 표·도판 칩·조건표에서 같고,
107
+ - **대칭은 표 한 장이 아니라 문서 전체의 불변식**이다 — 선택지의 이름·수·순서가 한눈표·선택지 비교·보내는 쪽 의견 표·도판 칩·조건표에서 같고,
108
108
  선택지마다 같은 잣대로 채운다.
109
109
  - **관문 흐름도**: 단계 · 정할 것 · 지금 상태 · 정하면 — 「바꾼다」가 하나라도 있으면 어느 길로 가는지.
110
- - 제목은 물음. 본문은 네 문장 안: 첫 문장은 기한과 두 길 이름 → 길마다 달라지는 것 → 근거 절(readability R12b). FCG 의견은 **한 절 한 자리**에만(낼지는 따로 묻는다) — 항마다 「권고안은 …」을 되풀이하지 않는다. 요청 문장은 완전한 높임(SKILL 5a).
110
+ - 제목은 물음. 본문은 네 문장 안: 첫 문장은 기한과 두 길 이름 → 길마다 달라지는 것 → 근거 절(readability R12b). 보내는 쪽 의견은 **한 절 한 자리**에만(낼지는 따로 묻는다) — 항마다 「권고안은 …」을 되풀이하지 않는다. 요청 문장은 완전한 높임(SKILL 5a).
111
111
  - 기한은 날짜나 수가 붙은 상대 기한으로 적는다(「2026년 10월까지」·「받으신 뒤 7일 안」) — 「배포 전까지」·「차기」만 적으면 기한 ⚠ 가 뜬다.
112
112
  - 항이 이용자 화면을 바꾸면(팝업·안내·칸의 유무) 글과 표만으로 묻지 않는다 — 아래 「화면 도안」 절의 도안까지 붙인다.
113
113
  - 집행자: 한눈표·흐름도의 유무는 렌더러 ⚠(lim-portal-format §10-1 「판단 요청 N항이 산문으로만 섭니다」) · 권고 낱말은 `gazette.py` ⚠ W1 ·
114
114
  결정 요청 절 표의 동일값 열은 `gazette.py` `brief_checks()` ⚠ · 선택지 기호 충돌은 `text_scan()` ⚠ · 대칭(같은 잣대·같은 틀·같은 순서)은
115
- 독자 검토 「도판」 관점과 사람의 눈(SKILL 「카피 게이트」).
115
+ 사람의 눈(SKILL 「카피 게이트」).
116
116
 
117
117
  ### 화면 도안 — 결정 요청이 이용자 화면의 변화를 낳으면 도안까지 (2026-09-11)
118
118
 
@@ -128,11 +128,11 @@
128
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 |
129
129
  | 4 | 관보 도판 | 도판 HTML 에 `<img data-dpr data-min-text>` 로 앉힌다. 두 칸이면 `pair.mjs` 가 **R13a 칸 폭**으로 짠다 — 전폭(`assets[].wide: true`) 두 칸이 본문 단 한 장보다 앱의 작은 글자를 크게 세운다(판정은 R1c). 표시 높이는 R9 안 — 긴 화면은 윗부분·아랫부분 크롭을 **좌우 두 칸**으로. 길이 여럿이면 길마다 같은 촬영 규격·같은 틀(R13a) |
130
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 |
131
- | 6 | 원고 | 도안 표(도안 · 모양 · 누르면 · 앱의 선례 — 이용자가 보는 것만, §5) · 뜨는 조건 표 · 기존 화면 흐름(번호 표식) · 「안내를 띄울 자리」 조사 건. 문구 출처 표는 **독자에게 문구 수정을 청할 때만** 둔다. 조건표는 각 행을 **모든 선택지 도안과 앱 코드에 대조**한다 — 선택지마다 값이 다르면 행을 갈라 쓰고, 대조 없이 「공통」이라 적지 않는다. 화면 동작(막는다·다시 뜬다)은 코드에서 확인한 것만 적는다. 권고는 FCG 의견 절 한 곳에만(§5) |
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 를 넘긴다 |
131
+ | 6 | 원고 | 도안 표(도안 · 모양 · 누르면 · 앱의 선례 — 이용자가 보는 것만, §5) · 뜨는 조건 표 · 기존 화면 흐름(번호 표식) · 「안내를 띄울 자리」 조사 건. 문구 출처 표는 **독자에게 문구 수정을 청할 때만** 둔다. 조건표는 각 행을 **모든 선택지 도안과 앱 코드에 대조**한다 — 선택지마다 값이 다르면 행을 갈라 쓰고, 대조 없이 「공통」이라 적지 않는다. 화면 동작(막는다·다시 뜬다)은 코드에서 확인한 것만 적는다. 권고는 보내는 쪽 의견 절 한 곳에만(§5) |
132
+ | 7 | 합성(선택) | Higgsfield `gpt_image_2` 에 도안 PNG 를 참조 그림(role image)으로 주면 한글 UI 글자를 보존한 채 손·기기 합성이 된다(2k·high 2 credits). **크기 감을 보이는 보조 그림 1장** — 선택지 도판들 뒤, 그 절 맨 끝에 두고 선택지 도판보다 앞·크게 두지 않는다. `provider: user-provided`, 캡션 끝은 「— 도안」이다 — AI 로 만든 합성 그림은 실물이 아니라 도안이고(「— 합성」은 실제 캡처에 글자를 옮겨 적은 것에만, readability R14), 합성 도구·참조 그림 같은 제작 기록은 `origin` 에. 출처 줄은 건 § 한 구절(W2 가 원본 위치를 센다). 칩은 도안 이름 그대로. 합성한 한 길이 보내는 쪽 의견이 권하는 길과 같으면 그림이 답을 먼저 말하므로 캡션에 목적(크기 감)을 밝히거나 뺀다. `data-dpr`·`data-min-text` 로 R1c 를 넘긴다 |
133
133
  | 8 | 반출 | Figma 는 MCP 인증이 있을 때만. 없으면 HTML 원본 경로를 문서 origin 과 핸드오프에 적고 반출은 사람 몫으로 남긴다 |
134
134
 
135
- 집행자: 도판 규격은 `figure.mjs`(R0·R0b·R1c·R13) · `pair.mjs` · 도안 사이 대칭·합성 자리·조건표와 도안의 대조는 독자 검토 「도판」 관점 ·
135
+ 집행자: 도판 규격은 `figure.mjs`(R0·R0b·R1c·R13) · `pair.mjs` · 도안 사이 대칭·합성 자리·조건표와 도안의 대조와
136
136
  도안이 있는지는 사람의 눈(process.md 7단계 표 「결정 요청에 화면 변화가 있는데 도안이 없다」). 도안 결재형 본보기는
137
137
  `assets/example/report-briefing-designs.json`(세 길 · 전폭 도판 자리 · 합성 한 장 · 조건표 · 확인 요청 · 확인 프로필 — 값은 전부 자리 표시)이고, 옛 결정 브리핑을 고칠 때만 본다 — 새 결정 문서의 본보기는 `assets/example/decision.json`.
138
138
  실행 사례와 도판 넷의 실물 경로는 genetics(lim) 사례 「결정 요청에 화면 변화가 따르면 고객 디자인 시스템 도안까지」에 있다.
@@ -143,72 +143,9 @@
143
143
  톤 다섯: ok(끝났거나 정상) · wait(사람 판단 대기) · bad(이용자에게 나쁜 상태·오류) · info(참고) · muted(범위 밖·처리 완료).
144
144
  문단 절반이 굵으면 굵기가 위계를 못 나른다 — 절마다 한둘. 문법과 렌더는 `lim-portal-format.md` §11-6.
145
145
 
146
- ## 7. 독자 검토 — 발행 전에 아홉 관점(처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조·선형 읽기·도판·사실 대조·증거 가시성 — 상태 표를 주면 상태 대조까지 열)으로 읽히고, 반박 검증으로 거른다
146
+ ## 7. 독자 검토 — 걷음 (2026-10-19)
147
147
 
148
- `references/reader-review.workflow.js` 를 Workflow 도구로 돌린다. 배열·객체 인자는 JSON 값 그대로 넘긴다(문자열로 싸면 한 덩어리가 된다).
149
-
150
- 먼저 렌더본 텍스트를 **원고 폴더 밖 빈 폴더**에 뽑는다 — `python3 assets/render.py report.json --text <빈 폴더>/본문.txt`.
151
- 렌더가 성공했을 때만 쓰고, 화면에 보이는 글만 화면 순서대로 담는다(그림·영상 자리는 「[그림] <id> · <갈래>」「[영상] <id> · <갈래>」 — 갈래는 렌더러 판정 `gazette.py` `asset_evidence_kind()`, 표 칸은 「 | 」 — `origin`·alt·스크립트·화면 전용 크롬은 없다).
152
- 글 끝에 「[그림 파일]」 목록이 붙는다 — 그림 자리 순서대로 갈래와 경로, 실린 바이트는 텍스트 옆 `<이름>-그림/` 에 풀린다. 검토자는 이 목록의 그림을 열어 본다. 그림 자리에 안 선 도판(로컬 렌더에 바이트가 없는 `file`·`source` 그림 등)은 「[본문에 안 선 도판]」 아래 따로 선다 — 본문 줄이 아니다.
153
- 원고 폴더 안에 쓰면 ⚠ 가 뜬다 — 검토자가 옆 파일(결정 장부·핸드오프)을 열 수 있어서다.
154
- CSS 가 만드는 글자(장식 기호·인쇄 쪽 번호)는 없고, 클래스 없는 굵게가 줄을 바꾸는 자리는 붙어 나온다 — 뜻은 같고 글자 단위로 화면과 같지는 않다.
155
-
156
- ```
157
- { file: "<report.json 절대경로>",
158
- text: "<렌더본 텍스트 절대경로 — 필수>",
159
- reader: "<독자 한 줄 — 할 일이 둘이면 둘 다>", example: "<예시 한 명 한 줄>", bitmapBudget: 3,
160
- scratch: "<측정 스크립트를 둘 스크래치 절대경로>", plates: "<도판 HTML·PNG 작업 폴더(선택)>",
161
- locked: ["<정해진 것 한 줄>", …], latest: "<사용자 최신 지적 원문>",
162
- sources: [{ name, path, kind: "code|doc|pdf" }, …],
163
- decisions: "<0단계 결정 장부 decisions.json 경로(선택)>", state: "<코드 작업 상태 표 경로(선택)>",
164
- handoff: "<HANDOFF.md 경로(선택)>", evidence_plan: "<0단계 증거 계획 evidence.md 경로(선택)>" }
165
- ```
166
-
167
- `sources` 는 문서마다 달라서 스킬에 경로를 적지 않는다. `state` 를 주면 열째 관점이 켜진다. `handoff`·`evidence_plan` 은 증거 가시성 관점만 받는다.
168
- `text` 가 없으면 워크플로가 멈춘다. 「처음 읽는 사람」·「선형 읽기」 두 관점은 이 파일 하나만 받고(정해진 것·결정 장부·최신 지적·상태 표·내부 자료는 안 받는다)
169
- CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 자동 메모리(MEMORY.md)까지 빠지는지는 첫 실행 전사로 확인하기 전이다. 나머지 관점은 위 인자를 그대로 받는다.
170
- 판 갱신 검토(`edition-review.workflow.js`)와 선형 읽기 실측(`linear-read-test.workflow.js`)도 `args.text` 가 필수다 — 선형 읽기 실측은 `file` 도 받는다 — 렌더본이 원고보다 새로운지 재는 데만 쓰고, 읽는 넷에게는 주지 않는다.
171
-
172
- | 관점 | 보는 것 | 경계 |
173
- |---|---|---|
174
- | first-time 처음 읽는 사람 | 정의 없는 용어 · 앞 절 의존 · 한 낱말 두 값 · 긴 문단 · 산문 참조 번호 · 문서가 붙인 이름(정의가 있어도 — class 이름) | 렌더본 텍스트만 · Explore 유형 · 처방은 원래 이름(뜻풀이·새 이름 아님) |
175
- | linear 선형 읽기 | 렌더 순서대로 한 번 읽으며 멈추는 지점 전부(`jumps`) · 문서가 붙인 이름(class 이름) | 렌더본 텍스트만 · Explore 유형 · 「N절을 보라」 처방 금지 |
176
- | decider 결정권자 | 판단 요청이 3초에 읽히나(물음·두 길·기한) · 수치/링크/건수 어긋남 · 답정너 틀 · **할 일마다** 표지 `meta`·현황 줄·읽는 법 첫 물음에 서나(빠지면 severity 4) · 요약면 수치 칸끼리 겹치는 집합인가 | — |
177
- | visual 정보 디자이너 | 그림이면 한 번에 보일 곳 · 표 숫자를 되풀이하는 문단 | — |
178
- | source 원본 대조 | 고객이 누를 수 있는 원본(`source_url`·출처 줄 주소·쪽·본문 링크)과 인용의 일치 | 앱 동작은 판정하지 않는다(facts 몫) — 다른 문서에 없다고 거짓이라 하지 않는다 |
179
- | structure 구조 | PART 물음 ↔ 절 내용 · PART 사이 되풀이 · 요지·읽는 법 물음이 건의 관찰·판단을 바꿔 말한 곳(문장 쌍 전수) · 요지의 이야기 순서와 칸 수, 근거 문단 `body` 비움 | 처방은 빼기로 |
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` 에 |
181
- | facts 사실 대조 | FCG 내부 1차 자료로 수치를 다시 계산 · 앱 동작은 `kind:"code"` 파일:줄로만 · 결정의 주체·자리·시점은 결정 장부·`locked` 와 대조 | 판정 못 하면 미확인 |
182
- | evidence 증거 가시성 | 관찰 주장(화면·흐름·화면 문구·동작을 말한 문장)마다 한 행 — 작성자 근거 · 문서 안 증거 · 독자가 닿나 · 처방(상한 없음). 도안은 증거가 아니다(readability R14 — 자리 세기는 렌더러 `evidence_scan()` ⚠) | `args.handoff`·`args.evidence_plan`(0단계 증거 계획)은 이 관점만 받는다 · 처방은 실물 싣기나 안 본 것을 남은 것으로 — 헤지 낱말 달기 아님 |
183
- | state 상태 대조(`state` 있을 때) | 「반영·적용·고침·보이지 않음·빠짐」 같은 완료형 문장마다 상태 표의 확인 수준(코드·시험판·실기기·운영) | — |
184
-
185
- 단계.
186
- 1. **Read** — 관점마다 발견 8~14개(선형 읽기는 남김없이). 발견마다 `where · problem · severity · class`(사실틀림|구조|읽기|이름|시각|증거|묻기) `· evidence`(연 것·잰 것, 짐작이면 「추정 — 」) `· rewrite · visual`. 재서 통과한 것은 `passed[]` 에 둔다.
187
- 2. **Verify** — severity ≥ 3 가운데 사실틀림 먼저, 그다음 severity 순으로 최대 12건 — **읽기·구조 발견에 4칸을 먼저 뗀다**(사실 틀림 가중 때문에 12칸이 다 차 읽기 발견이 늘 검증 밖이었다). 같은 순위 안에서는 **관점마다 돌아가며** 뽑는다(뒤 관점이 상한 밖으로 밀리지 않게). 발견마다 반박 에이전트가 원본을 직접 열어
188
- `verdict`(진단: 참|부분|거짓|미확인 — 거짓은 반증을 열었을 때만, 확신이 없으면 미확인)·`finding_basis`(확인|추정)·`touches_locked`·`noise_kind`(없음|제작|날짜경로|판이력)·`rx_fit`(처방 적합: 맞음|다시)·`rx_why` 를 판정한다.
189
- 진단과 처방은 따로다 — 처방이 틀려도 주장이 맞으면 진단은 참이다. class 이름은 「독자가 문서 밖(자기 화면·원본 문서·공급사 공식 명칭)에서 본 글자인가」로 판정하고, 문서 안의 정의·캡션·용어표는 반증이 아니다.
190
- 원본 위치 한 구절이 빠진 것은 노이즈가 아니다(SKILL 0.5단계 5a). severity < 3·상한 초과·검증 실패는 반영하지 않고 `raw.unverified` 로 간다.
191
- 3. **JS 거름**(결정적) — **진단 거짓만** → `dropped[]`(사유 포함, 사람이 되살릴 수 있게). 정해진 것을 되돌림 · 노이즈 처방 · 규칙에 걸린 처방(포인터·새 이름 짓기)은 발견을 남기고 `rx` 에 「처방 다시 — 까닭」을 단다. 근거가 추정뿐인지는 `verify.finding_basis` 에 남는다.
192
- 묻기 → `questions[]`. 사실틀림은 **참·부분**일 때만 독립 항목(미확인이면 합치는 쪽으로 가 「(미확인)」이 붙는다). 버림이 0 이면 log 에 「버림 0 — 표본 3건 재반박 권함」.
193
- 4. **Merge** — 나머지를 같은 자리끼리 합쳐 severity 합산 상위 10. 비트맵 예산을 지키고, 더하기 처방과 structure 의 빼기 처방이 부딪히면 빼기가 이긴다.
194
- `rx` 가 달린 발견은 규칙에 맞는 처방을 새로 적는다(이름 발견은 원래 이름으로 — 새 이름·뜻풀이 금지). 선형 읽기 뜀과 이름 발견은 상위 10 과 별개로 전부 돌아간다.
195
-
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` 머리에 「할 일 판정」·「증거 판정」 두 줄이 선다.
199
- `merged.findings` 앞에는 **참·부분으로 판정된** 사실 틀림이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다. 못 연 도판이 있으면 `overall` 머리 줄이 「⚠ 도판 N장 미열람(id…)」 — 그 도판은 사람이 연다. 검토자가 「서버 보관」이라 적은 도판은 「도판 N장 서버 보관」 줄로 따로 선다 — 그 id 가 원고에서 보관 표식(`data` 가 「(생략 — NB)」)인 장인지 확인하고 열람 주소에서 사람이 본다(체크리스트 7단계 독자 검토 행).
200
- 검증이 1건이라도 실패하거나 관점이 결과를 못 내면 `head` 「검토 미완 — 검증 N건 실패」(관점 실패는 이어 적는다)와 `failed: true` 가 붙고 `overall` 첫 줄도 같다 — 실패를 「발견 0」으로 읽지 않고 검토를 다시 돌린다.
201
- 통과 기준(SKILL 체크리스트 7단계 독자 검토 행): `jumps` 가운데 심각도 3 이상 0건 · `names` 0건 · `judgement.evidence_missing` 0 — 못 없앤 것은 까닭을 핸드오프와 공개·교체 승인 물음에(SKILL 체크리스트 10단계 행).
202
-
203
- **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 정해진 것을 되돌리거나 제작 메타(도구·배율·자르기 — 캡처의 증거 범위는 아님, readability R12e)·결정 날짜·판 이력을 더하라는 처방은 받지 않는다 — 그런 처방이 달린 발견은 버리지 않고 `rx` 로 처방만 다시 쓴다.
204
- 반영 순서는 사실 틀림 → 구조 발견(PART·절 합치기·빼기) → 나머지.
205
-
206
- 경위 넷. 고객사 검수 보고(2026-09-08)에서 이 검토가 용어 충돌·수치 불일치·참조 오류·반올림 설명 오류를 잡았다 — 작성자는 못 본 것들이다.
207
- 구조 관점은 2026-09-12 — 네 관점 발견 55개가 「경과 / 원인」 두 PART 의 겹침을 한 건도 짚지 못했고, 더하기 처방을 반영하자 겹침이 커졌다.
208
- 도판·사실 대조 관점과 Verify 는 2026-09-14 — 캡션만 읽은 검토가 틀 대칭 결함을 0건 짚었고, 검토자의 틀린 주장·정해진 것 되돌리기·노이즈 처방이
209
- 거르지 않은 채 작성자에게 올라갔다.
210
- 렌더본 텍스트 입력·이름 물음·진단과 처방 분리·실패 머리는 2026-09-26 — 두 독자 관점이 결정 장부·핸드오프를 받거나 스스로 열어 뜻을 메웠고,
211
- 문서가 정의한 조어는 「정의 있음」으로 통과했고, 진단이 참인 발견이 처방 사유 하나로 버려졌다(경위: `history.md` §17·§18).
148
+ 발행 전 독자 검토(관점 아홉 + 반박 워크플로)는 없다 — 2026-10-19 사용자 결정 「독자 검토를 걷기」(경위: `history.md` §23). 이 문서의 집행자 줄에 남은 「사람 눈」이 그 자리를 맡는다. 절 번호는 다른 문서의 가리킴 때문에 그대로 둔다.
212
149
 
213
150
  ## 8. 실물 확인 — dry_run 경고 0 이 목표, 그 다음은 눈
214
151
 
Binary file
package/bin/manifest.json DELETED
@@ -1,7 +0,0 @@
1
- {
2
- "aarch64-apple-darwin": "ab17d9b3bd9a2a2c5f2ef788a79ff3898aa6a5fcc3ec536a96d4bdee9e964854",
3
- "aarch64-unknown-linux-musl": "20cd63f8119812434e8cbfd38b4399049fe5cba8111a916c96221d922cadaf60",
4
- "x86_64-apple-darwin": "7daaae4a0ff0e124c305d4b60ed726905fc8eb2ee21c5a77e8e3b858caaae354",
5
- "x86_64-pc-windows-gnu": "5805d10cc17e9db2c4c0bbcf43b5ef2f5e99b1195cb53c8fd5aef1c1e91e62f2",
6
- "x86_64-unknown-linux-musl": "010e6be4792373a0a867df05a8c5ff2babd5ad1e25f650b696ea09f1ba58f24f"
7
- }
Binary file
Binary file
Binary file
@@ -1,196 +0,0 @@
1
- // 판 갱신 검토 — 한 판의 **변경 자리**를 규칙·사실 대조로, 새 판 렌더본 전체를 선형 독자로 읽고 심각도 3 이상은 반박한다.
2
- // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다(배열·객체는 JSON 값 그대로).
3
- // args = { file: "<새 판 report.json 절대경로>", // 편집을 적용한 새 판 원고 — 교체에 싣는 인자 파일·교체 뒤 되읽은 판이 아니다
4
- // backup: "<직전 판 백업 절대경로>", // 새 판을 만든 편집의 출발점
5
- // edits: "<edits.json 절대경로>", // assets/patch.py 편집 목록 — 변경 기록의 기본
6
- // patch: "<patch.py 절대경로>", // 편집 목록일 때 — 편집 목록을 굳힌 그 patch.py. 기본 <skill>/assets/patch.py
7
- // // (없거나 그 목록을 못 읽는 판이면 시작 전 검사에서 멈춘다 — 스킬 폴더가 다른 트리의 심링크인 기계는 옛 판일 수 있다)
8
- // skill: "<스킬 폴더 절대경로(선택) — 기본 ~/.claude/skills/genetics-report>", // patch 기본값과 오류 안내의 render.py 가 이 아래
9
- // canon: "<정본을 가리키는 한 줄>", // 수치 편집을 대조할 정본 — 파일 경로와 자리를 글로(「…/점검표.md 의 끝 7줄 · …tsv」). 여러 출처여도 한 줄로.
10
- // // 안 주면 검토자는 doc 글과 handoff 에서 정본을 찾고, 어디에도 없으면 수치 편집을 「정본 없음」으로 적는다
11
- // edit_script: "<edit-N.py 절대경로>", // 옛 기록: put/sub 호출로 변경 자리가 전부 적힌 스크립트(멱등). edits 가 없을 때만 쓴다
12
- // doc: "<문서 이름 한 줄>", // 예: "고객사 결정 브리핑 16판" — 프롬프트 머리 「대상:」에 글자 그대로 실린다
13
- // renderer: "<gazette.py 절대경로>", // 기본 report-portal private/builder/gazette.py
14
- // handoff: "<HANDOFF.md 절대경로>", // 있으면 그 판의 절을 배경으로 준다(선형 독자는 안 받는다)
15
- // plates: ["<도판 id>", …], // 이 판에서 더하거나 바꾼 도판·영상 — 편집 목록이면 시작 전 검사가 두 판을 견주어 스스로 잰다(이 칸은 더할 때만)
16
- // text: "<새 판 렌더본 텍스트 절대경로>" } // 필수 — python3 assets/render.py <새 판 report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt
17
- //
18
- // 언제 쓰나 — 구조(PART·절·표)가 그대로이고 낱말·출처 줄·여백 주석만 고친 판. 구조가 바뀐 판은
19
- // `reader-review.workflow.js`(아홉 관점 — 상태 표가 있으면 열)다. 낱말 손질 판을 검토 없이 발행하자 다음 판 8자리 손질이
20
- // 생겼다(2026-09-15 고객사 결정 브리핑 15판 → 16판): 되풀이된 전제, 주어가 빠진 출처 줄, 인쇄에서 죽는 「누르면」.
21
- //
22
- // 세 관점 — 선형 독자(그 자리에서 뜻이 닫히나·뒤 절 번호를 가리키나·조사·중복) · 규칙 대조(렌더러 text_scan 의
23
- // W2·R12h·RX_PROC·장치 이름·SOURCE_MAX·그림 번호를 코드 줄로 대조) · 사실 대조(옛 문구의 사실이 바뀌지 않았나,
24
- // 백업↔새 판 JSON diff 에 스크립트가 바꾼 자리가 빠짐없이 있고 기록 안 된 변경이 없는지 — 삽입·삭제가 섞이면 줄 수는 호출 수와
25
- // 다르니 자리로 센다, 수치 무변경. 편집 목록이면 직전 판 사본에 patch.py 를 적용한 결과가 새 판과 같은지, 수치를 바꾼 편집은 모두 적는다).
26
- // 심각도 3 이상만 반박한다 — 관점이 끝나는 대로 그 관점의 발견을 심각도 순으로 VERIFY_PER_LENS 건까지(2026-10-02, 전에는 모든 관점을 기다려 6건).
27
- // 넷째 관점 증거 대조(capture — 합의 착각)는 늘 켠다 — 바뀐 자리의 그림 없는 관찰 주장 · 바뀐 사실의 전파를 적고, 도판·영상을 더한 판이면 그림 대조까지
28
- // (2026-10-01 관보 합의 착각 수리: 판 6 에서 손으로 띄운 캡처 대조가 「어긋난 것만」 적어, 그림이 없는 주장은 셈 밖이었다 · 2026-10-02: 글만 고친 판에서는 아예 안 켜졌다).
29
- // 반박에서 증거를 싣는 처방은 말투·지은 이름 규칙 위반으로 기각하지 않는다 — 정직한 고침이 그 두 규칙에 막혀 동사 낮추기만 남았었다(같은 수리).
30
- // 편집 목록(edits)을 정식 입력으로 받는 까닭 — 2026-09-29 관보 한 판이 patch.py 편집 목록으로 만들어졌는데 이 워크플로는
31
- // edit_script 만 받아, 목록을 edit_script 에 넣고 「JSON 편집 목록이다」라는 풀이를 doc 에 적어야 했다(두 번 돌림).
32
- // 선형 독자는 새 판 렌더본 텍스트(args.text) 하나만 받고 CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 편집 스크립트·옛 문구·핸드오프를
33
- // 받은 채 읽으면 처음 읽는 사람이 아니다(2026-09-26 스킬 검토: 변경 판 검토의 선형 독자 15/15 가 원고 밖 과정 파일을 열었다).
34
- // 그래서 변경 자리가 아니라 문서 전체를 처음부터 읽는다. 확인 전: MEMORY.md 까지 빠지는지는 첫 실행 전사의 첨부로 확인한다.
35
- // 반박은 진단(참/부분/거짓/미확인)과 처방 적합을 따로 판정한다. 진단 거짓만 refuted, 처방이 규칙에 걸리면 kept 에 남기고 rx 「처방 다시 — 까닭」.
36
- // 결과 — { head·failed(검증이 실패했거나 관점이 결과를 못 냈거나 선형 독자가 렌더본을 못 열었을 때만 — 「검토 미완 — 검증 N건 실패」),
37
- // kept(진단이 거짓이 아닌 발견 + rx), refuted(진단 거짓), weak(심각도 2 이하 — 사람이 훑어 고를 것), unverified(상한 6 밖·검증 실패 — 사람이 훑는다), passed,
38
- // filled(선형 독자가 이 파일 안 증거 줄을 못 댄 관찰 주장 — 일반 지식으로 메운 것), plates(캡처 대조를 켠 도판 id) }.
39
- // 시작 전 검사(Check): 렌더본 파일(args.text)이 없거나 비었거나 새 판 원고(args.file)보다 오래되면 읽기 전에 멈추고 무엇이 필요한지 말한다.
40
- // 편집 목록이면 patch.py 도 잰다 — 없거나, 새 판에 그 목록을 --dry 로 돌렸을 때 목록을 못 읽거나(모르는 op · 트레이스백 · JSON 아님)
41
- // 「바꿈 N」(N>0 — 새 판에 편집이 아직 적용돼 있지 않다: file 과 backup 을 바꿔 줬을 때)이면 멈춘다. 종료 코드로는 가리지 않는다(금지어가 남은 정상 판도 1 이다).
42
- // 편집 목록이면 같은 명령이 두 판의 도판·영상도 견준다(다섯째 칸 — 더하거나 바꾼 id, 두 판 가운데 하나라도 없으면 null) — 캡처 대조를 켤지 정한다.
43
- // 워크플로 스크립트는 파일을 못 연다 — 에이전트 하나가 명령 한 줄의 출력을 옮기고 판정은 JS 가 한다(reader-review.workflow.js 머리 주석과 같다).
44
- export const meta = {
45
- name: 'report-edition-review',
46
- description: '보고서 한 판을 선형 독자(렌더본 텍스트만)·규칙 대조·사실 대조(변경 자리 — patch.py 편집 목록 또는 edit-N.py)·증거 대조(합의 착각) 네 관점으로 읽고, 관점이 끝나는 대로 심각도 3 이상의 진단·처방을 따로 반박 검증한다',
47
- phases: [
48
- { title: 'Check', detail: '렌더본이 있고 비지 않았고 새 판 원고보다 새로운지 · 편집 목록이면 patch.py 가 그 목록을 읽는지 · 더한 도판 — 명령 한 줄' },
49
- { title: 'Review', detail: '네 관점 병렬 — 선형 독자 · 렌더러 규칙 대조 · 사실 대조 · 증거 대조(합의 착각, 늘 켬 — 도판을 더했으면 그림 대조까지)' },
50
- { title: 'Verify', detail: '관점이 끝나는 대로 그 관점의 심각도 3 이상 발견을 반박(관점마다 최대 2) — 진단 거짓만 refuted' },
51
- ],
52
- }
53
- const A = args || {}
54
- // 스킬 폴더 — patch.py 기본값과 오류 안내의 render.py 가 이 아래다(reader-review.workflow.js 와 같은 상수 — 조사관에게 스킬 파일은 이 경로로 가리킨다).
55
- const SKILL = A.skill || '~/.claude/skills/genetics-report'
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) 가 필요하다')
57
- const MAKE_TEXT = `python3 ${SKILL}/assets/render.py <새 판 report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt`
58
- if (!A.text) throw new Error(`args.text 가 필요하다 — 새 판 렌더본 텍스트 파일 절대경로. 선형 독자는 이 파일 하나만 읽는다. 만드는 법: ${MAKE_TEXT}`)
59
- phase('Check')
60
- // 렌더본이 있고 비지 않았고 새 판 원고보다 새로운지 — 파일 상태는 에이전트가 옮긴 명령 출력으로만 읽고 판정은 여기서 한다(머리 주석 「시작 전 검사」).
61
- const shq = (p) => `'${String(p).replace(/'/g, `'\\''`)}'`
62
- // patch.py — 편집 목록일 때만 쓴다. 글로만 가리키면 검토자가 다른 트리의 옛 사본을 찾는다(이 맥은 스킬 폴더가 심링크라 `find -type d` 로 안 잡힌다 —
63
- // 2026-09-30 적대 검토). 그래서 경로를 주고, 시작 전 검사에서 셋째 파일로 잰다(`~` 는 파이썬이 푼다). 편집 스크립트 쪽 명령은 그대로다.
64
- // 있는지만 재면 모자란다 — 기본 경로가 다른 트리의 옛 patch.py 로 풀리면(심링크 설치본) 새 op·[label=] 경로를 못 읽어 사실 관점이 재다 죽는다.
65
- // 그래서 새 판에 그 목록을 --dry 로 한 번 돌려 넷째 칸(rc·tb·dry·out)으로 옮긴다 — 목록을 **읽는지**와 새 판에 이미 적용돼 있는지(「바꿈 0」)만
66
- // 여기서 가리고, 값이 맞는지는 사실 관점 ①이 잰다. 종료 코드로 가리지 않는다 — 금지어가 남은 정상 판도 「바꿈 0」 뒤에 1 로 끝난다.
67
- // 트레이스백은 따로 한 칸(tb), 「바꿈 N · …」 줄도 따로 한 칸(dry)이다 — out 은 끝 300자라 경로가 길면 「Traceback」 낱말과 앞줄이 잘린다.
68
- const PATCH = A.patch || `${SKILL}/assets/patch.py`
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)'
73
- const STAT_CMD = 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)}`
75
- : `python3 -c 'import json,os,sys; print(json.dumps([${STAT_PY} for p in sys.argv[1:]]))' ${shq(A.text)} ${shq(A.file)}`
76
- const PRE = { type: 'object', properties: { stdout: { type: 'string', description: '명령의 표준 출력 한 줄 — 글자 그대로' }, exit: { type: 'integer' } }, required: ['stdout', 'exit'] }
77
- const pre = await agent(`아래 명령을 Bash 로 **한 번만, 글자 그대로** 실행하고 표준 출력 한 줄을 stdout 에 고치지 말고 옮긴다. 종료 코드는 exit 에. 명령을 바꾸거나 다른 명령·파일을 열지 않는다.\n${STAT_CMD}`,
78
- { label: 'check:files', phase: 'Check', schema: PRE, effort: 'low' })
79
- let st = null
80
- try { st = pre && pre.exit === 0 ? JSON.parse(pre.stdout) : null } catch (e) { st = null }
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 없이 새로 띄운다`)
82
- if (!st[1].f) throw new Error(`args.file 파일이 없다 — ${A.file}. 새 판 report.json 절대경로를 준다`)
83
- if (!st[0].f) throw new Error(`args.text 파일이 없다 — ${A.text}. 만드는 법: ${MAKE_TEXT}`)
84
- if (st[0].b <= 0) throw new Error(`args.text 파일이 비었다 — ${A.text}. 렌더가 실패했을 수 있다 — 다시 뽑고 출력의 ⚠·오류를 본다: ${MAKE_TEXT}`)
85
- if (A.edits && !st[2].f) throw new Error(`patch.py 가 없다 — ${PATCH}. args.patch 로 스킬 설치본의 assets/patch.py 절대경로를 준다(사실 관점이 편집 목록을 이 파일로 잰다)`)
86
- if (A.edits && st[3].rc === -1) throw new Error(`args.edits 파일이 없다 — ${A.edits}. 편집 목록(edits.json) 절대경로를 준다`)
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 로 준다 — 기본 경로가 다른 트리의 옛 판으로 풀렸을 수 있다`)
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 이 그 출발점이다(둘을 바꿔 주지 않았는지 본다)`)
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) : [])])]
93
- const RENDERER = A.renderer || '레포 report-portal 의 private/builder/gazette.py(args.renderer 가 없으면 이 기계에서 경로를 찾아 쓴다 — 못 찾으면 규칙 관점 결과에 「렌더러 못 읽음」으로 적는다)'
94
- // 반박에도 렌더러 경로를 준다 — 2026-09-30 실제 1회에서 반박 하나가 args.renderer 대신 다른 트리의 옛 렌더러를 읽고 판정했다.
95
- const RENDERER_PATH = A.renderer || '레포 report-portal 의 private/builder/gazette.py'
96
- const DOC = A.doc || A.file
97
- // 변경 기록이 무엇인지(CTX)와 사실 관점 ①(기록 대조), 반박이 읽을 기록의 이름(src)만 기록 종류에 따라 갈린다. edit_script 쪽 세 값은 편집 목록을 받기 전과 한 글자도 같았다 — 2026-10-02 CTX 끝에 작성자 자료 한 줄(합의 착각)을 두 기록 모두에 더했다(시험 _WCHG 도 함께 고침).
98
- // 「제자리에서 고친다」 경고는 CTX 에 둔다 — 규칙 관점·반박도 patch.py 를 돌릴 수 있다.
99
- // 정본 — 글로만 「doc·handoff 가 가리키는 정본」이라 하면 검토자는 그 인자 이름을 본 적이 없다(CTX 는 「대상」·「배경」으로만 싣는다).
100
- // args.canon 을 받아 CTX 에 「정본」으로 싣고, 없으면 프롬프트에 **보이는 자리**(「대상」 글 · 배경 파일)를 가리킨다 — 정본을 doc 글에 적어
101
- // 넘기던 호출(2026-09-29 실제 호출: 「새 사실의 정본은 …md 의 끝 7줄」)이 끊기지 않게, 어디에도 없을 때만 「정본 없음」이다(2026-09-30 적대 검토).
102
- const CANON = A.canon ? '②에서 그 수치를 원고 안의 다른 자리와, 위 정본에 대조한다'
103
- : '②에서 그 수치를 원고 안의 다른 자리와 대조하고, 위 「대상」 글이나 배경 파일이 정본을 가리키면 거기에도 대조한다 — 어디에도 정본이 없으면 수치 편집마다 「정본 없음」으로 적는다'
104
- const REC = A.edits ? {
105
- src: '편집 목록',
106
- change: `${A.edits} 의 patch.py 편집 목록에 전부 적혀 있다 — JSON 배열이고 항목마다 op 하나다. patch.py 는 ${PATCH} 이다 — op 의 칸과 뜻은 그 파일 머리 주석의 op 표를 읽는다. patch.py 는 준 파일을 제자리에서 고친다 — 새 판·직전 판에는 --dry 로만 돌리고, 적용은 빈 임시 폴더에 복사한 사본에 한다.`,
107
- count: '① 새 판에 patch.py 를 --dry 로 돌려 「바꿈 0」인지 보고, 직전 판 사본에 편집 목록을 적용한 결과가 새 판과 JSON 값까지 같은지 본다 — 다르면 기록되지 않은 변경이 있다. 두 판을 diff 해 forbid 밖의 편집마다 diff 에 보이는지 path 로 센다(set 한 번이 표의 여러 행을 바꾸므로 diff 줄 수는 편집 수와 다르다). forbid 는 바꾸지 않고 남은 낱말을 센다. 수치(인원·kg·cm·날짜)를 바꾼 편집은 path 와 옛 값 → 새 값으로 모두 센다(목록 밖 변경은 위 사본 대조가 잡는다) — ' + CANON + '.',
108
- } : {
109
- src: '스크립트',
110
- change: `${A.edit_script} 에 put/sub 호출로 전부 적혀 있다(옛 문구 → 새 문구).`,
111
- count: '① 직전 판과 새 판의 JSON 을 diff 해 스크립트의 put/sub·삽입·삭제가 바꾼 자리가 빠짐없이 보이는지, 기록되지 않은 변경이 없는지 자리로 센다(삽입·삭제가 섞이면 diff 줄 수는 호출 수와 다르다). 수치(인원·kg·cm·날짜)가 한 곳도 안 바뀌었는지도 센다.',
112
- }
113
- // 합의 착각(2026-10-02) — 검토자가 작성자의 자료(핸드오프·편집 목록·코드·서버 조회)를 읽고 나면 작성자처럼 「다 아는」 눈으로 판정한다.
114
- // 그 자료는 무엇이 바뀌었나·사실이 참인가를 찾는 지도일 뿐, 독자가 문서 안에서 보는 증거가 아니다 — 모든 관점·반박에 한 줄로 못 박는다.
115
- const CTX = `대상: ${DOC}. 직전 판 → 이 판의 변경은 ${REC.change} 새 판 원고는 ${A.file}, 직전 판은 ${A.backup}.${A.handoff ? ` 배경은 ${A.handoff} 의 이 판 절.` : ''}${A.canon ? ` 정본(수치의 출처)은 ${A.canon}.` : ''} 한글로 답한다. 파일은 읽기만 한다 — 고치지 않는다. 핸드오프·편집 목록·코드·서버 조회는 무엇이 바뀌었나와 사실이 참인가를 찾는 데만 쓴다 — 독자는 그것을 못 본다. 독자에게 보이는 증거로 세는 것은 렌더본 글과 그 그림뿐이다.`
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 — 끝 목록 줄은 대지 않는다). 댈 줄이 없거나 도안뿐이면 「없음」 — 읽으며 떠올린 화면이 이 파일의 어느 줄에서 왔는지 못 대면 일반 지식으로 메운 것으로 센다.'
120
- const TEXT_CTX = `[읽기 전용. 한글로 답한다. 철저함: very thorough — 발췌하지 말고 파일 전체를 처음부터 끝까지 읽는다.]
121
- 읽을 것: ${A.text} — 새 판 렌더본에서 화면에 보이는 글만 화면 순서대로 뽑은 텍스트(표 칸은 「 | 」). ${MARKS}
122
- 이 파일 하나만 연다 — 끝 「[그림 파일]」 목록에 경로가 적힌 그림은 독자가 보는 그림이라 Read 로 열어 봐도 된다. 원고 JSON·편집 스크립트·핸드오프·같은 폴더의 다른 파일은 열지 않는다.
123
- ${FILL_STEP}
124
- 이 파일을 끝까지 읽었으면 opened=true. 열지 못했거나 비어 있으면 opened=false 로 두고 findings·passed·claims 는 비운다 — 다른 파일로 대신 읽지 않는다.
125
- where 는 이 파일에 보이는 절 번호·이름·표 이름으로, evidence 는 이 파일의 줄 번호와 그 줄 글자로 적는다.`
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'] }
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'] }
135
- // 진단과 처방을 따로 판정한다 — 한 칸(refuted)에 「처방이 다른 규칙을 어기면」「확신이 없으면」까지 묶어 진단이 참인 발견이 기각됐다(2026-09-26 스킬 검토).
136
- const VERDICT = { type: 'object', properties: {
137
- diagnosis: { type: 'string', enum: ['참', '부분', '거짓', '미확인'], description: '발견의 주장이 맞나 — 확신이 없으면 미확인' },
138
- rx_fit: { type: 'string', enum: ['맞음', '다시'], description: '처방(fix)이 규칙에 맞나 — 진단과 따로' },
139
- rx_why: { type: 'string', description: '다시면 걸린 규칙과 까닭 한 줄, 맞음이면 빈 문자열' },
140
- reason: { type: 'string' } }, required: ['diagnosis', 'rx_fit', 'rx_why', 'reason'] }
141
- const LENSES = [
142
- { key: 'linear', agentType: 'Explore', text: true, prompt: `${TEXT_CTX}\n[선형 독자] 새 판을 처음 읽는 고객 담당이 되어 파일 처음부터 끝까지 순서대로 읽는다. 자리마다 (a) 그 자리에서 뜻이 닫히는가 (b) 앞에서 정의되지 않은 낱말·번호를 가리키는가 — 뒤 절 번호 가리킴은 금지 규칙이고 표 번호·「다음 절」로 바꿔 놓은 것도 독자를 뒤로 보내면 적는다 (c) 어색한 문장·조사·중복(절 이름과 부제, 같은 답 묶음 안의 되풀이)이 있는가 (d) 인쇄본에서 죽는 화면 어휘(「누르면」)가 있는가 (e) 대상·판·시험·상태·선택지를 부르는 낱말마다, 독자가 문서 밖(자기 화면 글자, 원본 문서, 공급사 공식 명칭)에서 본 글자인가? 이 문서가 붙인 이름이면 정의가 있어도 발견이다. 처방은 뜻풀이가 아니라 원래 이름으로 바꾸는 것이다 — 원래 이름을 이 파일만으로 알 수 없으면 fix 에 이름을 지어 넣지 말고 「원래 이름으로 바꾼다(이 파일에 없음)」라고 적는다. 문제가 없는 자리도 passed 에 적는다. severity 1~5(5=독자가 뜻을 잃음).` },
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자·괄호 부연·표 주석 문장 수 — 를 코드에서 직접 읽고, 변경 자리의 새 문구가 각 규칙을 만족하는지, 새 문구가 다른 규칙을 새로 어기지 않는지 하나씩 대조한다. 판정 근거는 렌더러 파일의 줄 번호로 적는다.` },
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 는 캡처·영상을 싣거나, 우리가 안 본 것이면 그 문장을 남은 것으로 옮기는 것 — 단정 뒤에 헤지 꼬리(「화면 미확인」「(확인 전)」「코드상」「코드로 확인함」)를 붙여 덮지 않는다.` },
149
- ]
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))
171
- // 렌더본을 못 열었다고 답한 관점 — 그 발견은 쓰지 않고 머리에 적는다. 결과를 못 낸 관점(lensFail)과 따로 센다.
172
- const unread = LENSES.filter((l, i) => l.text && reads[i] && reads[i].opened !== true).map((l) => l.key)
173
- const all = reads.filter((r) => r && !unread.includes(r.lens))
174
- const strongAll = all.flatMap((r) => r.findings.filter((f) => f.severity >= 3).map((f) => ({ lens: r.lens, ...f })))
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 : []))
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)}`)
179
- // 진단 거짓만 refuted. 처방이 규칙에 걸린 발견은 kept 에 남기고 rx 를 단다. 상한 밖과 검증 실패는 unverified 로 돌려준다 —
180
- // 전에는 상한 6 밖의 심각도 3 이상 발견이 결과 어디에도 없었고, 검증이 실패한 발견도 kept·refuted 양쪽에서 조용히 빠졌다.
181
- const judged = verdicts.filter((v) => v && v.verdict).map((v) => ({ ...v, rx: v.verdict.rx_fit === '다시' ? `처방 다시 — ${v.verdict.rx_why || '규칙에 걸림'}` : '' }))
182
- const kept = judged.filter((v) => v.verdict.diagnosis !== '거짓')
183
- const vfail = strong.filter((_, i) => !(verdicts[i] && verdicts[i].verdict))
184
- const unverified = [...restAll.map((f) => ({ ...f, why: `관점별 검증 상한 ${VERIFY_PER_LENS} 초과` })), ...vfail.map((f) => ({ ...f, why: '검증 실패' }))]
185
- const weak = all.flatMap((r) => r.findings.filter((f) => f.severity < 3).map((f) => ({ lens: r.lens, ...f })))
186
- const lensFail = LENSES.filter((_, i) => !reads[i]).map((l) => l.key)
187
- if (lensFail.length) log(`⚠ 관점 ${lensFail.length}개가 결과를 못 냈다(API 과부하 등) — resumeFromRunId 로 다시 돌리면 끝난 관점은 캐시를 탄다`)
188
- if (unread.length) log(`⚠ 렌더본을 못 열었다고 답한 관점 ${unread.join(', ')} — 그 발견은 쓰지 않았다. 경로·권한을 고친 뒤 새로 띄운다`)
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 로 돌려준다`)
193
- // 실패는 결과 머리에 적는다 — 실패를 「발견 0」으로 읽지 않게(reader-review.workflow.js 와 같은 머리).
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(' · ')}` : ''
195
- if (failHead) log(failHead)
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 }
@@ -1,106 +0,0 @@
1
- // 선형 읽기 시험 — 원고를 「앞에서 뒤로 딱 한 번」 읽히고, 독자가 다른 자리로 뛰어야 이해되는 지점을 전부 센다.
2
- // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다.
3
- // args = { text: "<렌더본 텍스트 절대경로(필수)>", file: "<그 렌더본을 뽑은 report.json 절대경로(필수)>", reader: "<독자 한 줄>", round: "<회차 표지, 예: 2차 측정 · 4판>",
4
- // skill: "<스킬 폴더 절대경로(선택) — 기본 ~/.claude/skills/genetics-report>" } // 오류 안내의 render.py 가 이 아래 — 읽는 넷은 받지 않는다
5
- // text 는 `python3 assets/render.py <report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt` 로 뽑는다. file 은 렌더본이 원고보다 새로운지 재는 데만
6
- // 쓰고 읽는 넷에게는 주지 않는다 — 독자가 보는 것은 렌더본뿐이고, 읽는 넷은 CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다(2026-09-26 스킬 검토 —
7
- // reader-review.workflow.js 머리 주석). 렌더본이 없거나 비었거나 원고보다 오래되면 읽기 전에 멈추고 무엇이 필요한지 말한다(Check — 파일은
8
- // 에이전트 하나가 명령 한 줄로 재고 판정은 JS 가 한다. 워크플로 스크립트는 파일을 못 연다).
9
- // 읽는 관점이 렌더본을 못 열었다고 답하면(opened=false) 그 관점은 빼고 head 「검토 미완 — 렌더본 못 엶 N(…)」·failed: true 가 붙는다.
10
- // 확인 전: MEMORY.md 까지 빠지는지는 첫 실행 전사의 첨부로 확인한다.
11
- // round 는 재측정 때 반드시 바꾼다 — 같은 프롬프트면 캐시가 옛 답을 돌려준다(genetics-define 스킬 4단계와 같은 함정).
12
- // 돌아오는 것: merged.jumps[](at·needed·severity·fix) 전부 + order_ok/order_note + overall(「뜀 N(3등급 이상 M) · 사소 K」 + 할 일),
13
- // filled[](읽는 관점이 이 파일 안 증거 줄을 못 댄 관찰 주장 — 일반 지식으로 메운 것. 뜀과 따로 센다 — 끝 줄이 overall 에 붙는다).
14
- // 목표: 뜀 0. 3등급(되짚어야 함) 이상이 하나라도 있으면 발행 전에 그 자리에서 푼다.
15
- // 2026-10-01 관보 합의 착각 수리 — 「원고에 없는 지식으로 메웠다면 발견」 자기 신고 한 줄로는 화면 주장을 일반 지식으로 메운 독자가
16
- // 뜀 0 으로 지나갔다. 그래서 화면·흐름 주장마다 이 파일 안 증거 줄을 대게 하고(claims), 못 대면 메운 것으로 센다.
17
- //
18
- // 2026-09-14 실사례(고객사 3차 보고) — 사용자 반려 「읽다가 시험보는 줄 알 것 같음. 보고서가 흐름이 이어지지 않고, 장을 넘나들면서
19
- // 이해를 해야 되는 구조임.」 2판 본문 교차 참조 62곳 → 절 순서를 고치고 교차 참조를 0 으로 만든 3판에도 이 시험이 뜀 45곳을 냈다
20
- // (5등급 1 · 4등급 1 · 3등급 16). 순서는 넷 다 「이야기처럼 이어짐」 — 원인은 「뒤에서 정의되는 낱말」과 「앞에서 쓰는 뒤 절 번호」였다.
21
- // reader-review.workflow.js 의 linear 관점이 같은 읽기를 하나로 하고, 이 파일은 독자 셋 + 결정권자로 **실측**할 때 쓴다.
22
- // 뜀이 아닌 것: 뒤 절이 앞 절을 되짚는 링크(도판 출처 줄의 「건 §n.n」), 활용법(guide)·요약면 수치 칸의 「N절로」(지도 구실),
23
- // 요지 문단 끝의 refs(「근거」 단추). 같은 절 안에서 바로 아래 표로 이어 읽는 것도 뜀이 아니다 — 단 표 칸만으로 뜻이 안 서면 뜀이다.
24
- export const meta = {
25
- name: 'report-linear-read-test',
26
- description: '보고서 렌더본 텍스트(화면에 보이는 글만)를 처음 읽는 사람 셋이 앞에서 뒤로 한 번만 읽으며 「여기서 다른 자리로 뛰어야 이해된다」는 지점을 전부 적고, 결정권자 하나가 요청 절에서 답을 고를 수 있는지 재고, 합쳐서 남은 뜀 지점을 낸다',
27
- phases: [{ title: 'Check', detail: '렌더본이 있고 비지 않았고 원고보다 새로운지 — 명령 한 줄' }, { title: 'Read', detail: '선형 읽기 3 + 결정권자 1' }, { title: 'Merge', detail: '뜀 지점 합치기' }],
28
- }
29
- const A = args || {}
30
- // 스킬 폴더 — 오류 안내의 render.py 가 이 아래다(reader-review.workflow.js 와 같은 상수). 읽는 넷의 프롬프트에는 싣지 않는다 — 렌더본 하나만 읽는다.
31
- const SKILL = A.skill || '~/.claude/skills/genetics-report'
32
- const MAKE_TEXT = `python3 ${SKILL}/assets/render.py <report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt`
33
- if (!A.text) throw new Error(`args.text 가 필요하다 — 렌더본 텍스트 파일 절대경로. 읽는 넷은 이 파일 하나만 읽는다. 만드는 법: ${MAKE_TEXT}`)
34
- if (!A.file) throw new Error('args.file 이 필요하다 — 그 렌더본을 뽑은 report.json 절대경로. 렌더본이 원고보다 새로운지 재는 데만 쓰고 읽는 넷에게는 주지 않는다')
35
- const TEXT = A.text
36
- phase('Check')
37
- // 렌더본이 있고 비지 않았고 원고보다 새로운지 — 파일 상태는 에이전트가 옮긴 명령 출력으로만 읽고 판정은 여기서 한다(머리 주석).
38
- const shq = (p) => `'${String(p).replace(/'/g, `'\\''`)}'`
39
- const STAT_CMD = `python3 -c 'import json,os,sys; print(json.dumps([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)) for p in sys.argv[1:]]))' ${shq(TEXT)} ${shq(A.file)}`
40
- const PRE = { type: 'object', properties: { stdout: { type: 'string', description: '명령의 표준 출력 한 줄 — 글자 그대로' }, exit: { type: 'integer' } }, required: ['stdout', 'exit'] }
41
- const pre = await agent(`아래 명령을 Bash 로 **한 번만, 글자 그대로** 실행하고 표준 출력 한 줄을 stdout 에 고치지 말고 옮긴다. 종료 코드는 exit 에. 명령을 바꾸거나 다른 명령·파일을 열지 않는다.\n${STAT_CMD}`,
42
- { label: 'check:files', phase: 'Check', schema: PRE, effort: 'low' })
43
- let st = null
44
- try { st = pre && pre.exit === 0 ? JSON.parse(pre.stdout) : null } catch (e) { st = null }
45
- if (!Array.isArray(st) || st.length !== 2) throw new Error(`시작 전 검사 실패 — 렌더본·원고 파일 상태를 못 읽었다(${pre ? `exit ${pre.exit} · ${String(pre.stdout).slice(0, 200)}` : '검사 에이전트 결과 없음'}). resumeFromRunId 없이 새로 띄운다`)
46
- if (!st[1].f) throw new Error(`args.file 파일이 없다 — ${A.file}. 렌더본을 뽑은 report.json 절대경로를 준다`)
47
- if (!st[0].f) throw new Error(`args.text 파일이 없다 — ${TEXT}. 만드는 법: ${MAKE_TEXT}`)
48
- if (st[0].b <= 0) throw new Error(`args.text 파일이 비었다 — ${TEXT}. 렌더가 실패했을 수 있다 — 다시 뽑고 출력의 ⚠·오류를 본다: ${MAKE_TEXT}`)
49
- if (st[0].m < st[1].m) throw new Error(`렌더본이 원고보다 오래됐다 — ${TEXT} 가 ${A.file} 보다 ${Math.round(st[1].m - st[0].m)}초 앞선다. 원고를 고친 뒤 다시 뽑는다: ${MAKE_TEXT} (그다음 resumeFromRunId 없이 새로 띄운다)`)
50
- const READER = A.reader || '고객사 담당(비개발자). 이 보고서를 처음 읽는다'
51
- const ROUND = A.round || '1차 측정'
52
- // 그림 자리 표지와 메움 세기는 reader-review.workflow.js 와 같은 글이다.
53
- const MARKS = '그림·영상 자리는 「[그림] <도판 id> · <갈래>」「[영상] <id> · <갈래>」 한 줄이다 — 갈래는 캡처·녹화·도안·합성·문서이고 도안은 우리가 그린 설명 그림이다. 이 표지 줄과 글 끝의 「[그림 파일]」 목록은 검토용이라 독자 화면에 없다(그 id 를 이름 발견으로 세지 않는다).'
54
- const FILL_STEP = '메움 세기 — 화면·흐름·화면 문구·동작을 말하는 문장(「…뜸·보임·켜짐·막힘·넘어감·저장됨」, 「」로 옮긴 화면 글자)마다 claims 에 한 행을 적는다: 그 줄 번호, 그리고 그것을 보여 주는 증거(캡처·녹화·합성 그림 자리 · 원문 인용 · 센 대상이 있는 표)가 선 이 파일의 본문 줄 번호(shown_at — 끝 목록 줄은 대지 않는다). 댈 줄이 없거나 도안뿐이면 「없음」 — 읽으며 떠올린 화면이 이 파일의 어느 줄에서 왔는지 못 대면 일반 지식으로 메운 것으로 센다.'
55
- const CTX = `[${ROUND}] [읽기 전용. 파일을 수정하지 말 것. 한글로 답할 것. 철저함: very thorough — 발췌하지 말고 파일 전체를 처음부터 끝까지 읽는다.]
56
- 읽을 것: ${TEXT} — 보고서 렌더본에서 화면에 보이는 글만 화면 순서대로 뽑은 텍스트(표 칸은 「 | 」). ${MARKS} 순서는 표지 → 발행 이력 → 차례 → 활용법(이 문서 읽는 법) → 요약면 → PART 간지 → 절(요지 문단 끝의 「근거」는 링크 단추 · 건은 「NN-1」… · 판단 항목) → 부록 → 판권지.
57
- 이 파일 하나만 연다 — 끝 「[그림 파일]」 목록에 경로가 적힌 그림은 독자가 보는 그림이라 Read 로 열어 봐도 된다. 원고 JSON·같은 폴더의 다른 파일·저장소·메모·다른 문서는 열지 않는다.
58
- (읽는 관점) ${FILL_STEP}
59
- (읽는 관점) 이 파일을 끝까지 읽었으면 opened=true. 열지 못했거나 비어 있으면 opened=false 로 두고 jumps·claims 는 비운 채 overall 에 까닭을 적는다 — 다른 파일로 대신 읽지 않는다.
60
- 독자: ${READER}
61
- 판정 기준: 앞에서 뒤로 한 번 읽어 내려가면 이해돼야 하고, 어떤 문장도 독자를 다른 절·표·부록·원고 밖 문서로 보내면 안 된다. 뒤 절이 앞 절을 되짚는 링크(도판 출처 줄의 「건 §n.n」)는 뜀이 아니다. 활용법·요약면 수치 칸의 「N절로」는 지도 구실이라 뜀이 아니다.
62
- 대상·판·시험·상태·선택지를 부르는 낱말마다, 독자가 문서 밖(자기 화면 글자, 원본 문서, 공급사 공식 명칭)에서 본 글자인가? 이 문서가 붙인 이름이면 정의가 있어도 발견이다 — jumps 에 적는다. 처방은 뜻풀이가 아니라 원래 이름으로 바꾸는 것이다 — 원래 이름을 이 파일만으로 알 수 없으면 fix 에 이름을 지어 넣지 말고 「원래 이름으로 바꾼다(이 파일에 없음)」라고 적는다.`
63
- const JUMPS = { type: 'object', properties: { jumps: { type: 'array', items: { type: 'object', properties: {
64
- at: { type: 'string', description: '읽던 자리(절·문단·항목·표 이름)' },
65
- needed: { type: 'string', description: '이해하려면 필요했던 것과 그것이 있는 자리(없으면 「원고 어디에도 없음」)' },
66
- severity: { type: 'integer', minimum: 1, maximum: 5, description: '5=이해 불가, 3=되짚어야 함, 1=사소' },
67
- fix: { type: 'string', description: '그 자리에 무엇을 넣거나 빼면 뛰지 않아도 되는가 — 구체 문장으로' } },
68
- required: ['at', 'needed', 'severity', 'fix'] } },
69
- order_ok: { type: 'boolean', description: '절 순서가 앞에서 뒤로 이야기처럼 이어지는가' },
70
- order_note: { type: 'string', description: '순서가 끊기는 곳과 바꿀 순서(있으면)' },
71
- overall: { type: 'string' } }, required: ['jumps', 'order_ok', 'order_note', 'overall'] }
72
- // 읽는 관점은 파일을 열었는지부터 답한다 — 못 연 관점의 「뜀 0」을 「뜀 없음」으로 읽지 않게. 합치기 결과(merged)는 JUMPS 그대로다.
73
- // claims 는 메움 세기(FILL_STEP) — 합치기에는 넘기지 않고 JS 가 센다.
74
- const CLAIM_ROW = { type: 'object', properties: {
75
- line: { type: 'integer', description: '주장이 선 줄 번호' },
76
- claim: { type: 'string', description: '그 문장 — 줄 글자 그대로(길면 앞 60자)' },
77
- shown_at: { type: 'string', description: '그것을 보여 주는 증거가 선 이 파일의 줄 번호와 그 줄 글자 — 없거나 도안뿐이면 「없음」' } },
78
- required: ['line', 'claim', 'shown_at'] }
79
- const JUMPS_READ = { type: 'object', properties: { ...JUMPS.properties, opened: { type: 'boolean', description: '렌더본 파일을 열어 끝까지 읽었으면 true, 못 열었거나 비었으면 false' },
80
- claims: { type: 'array', items: CLAIM_ROW, description: '화면·흐름·화면 문구·동작을 말하는 문장마다 한 행 — 상한 없음' } }, required: [...JUMPS.required, 'opened', 'claims'] }
81
- phase('Read')
82
- const LENSES = [
83
- ['first-time-a', '처음 읽는 사람 A — 문서를 위 렌더 순서대로 딱 한 번 읽는다. 앞에서 뒤로 읽다가 「이 낱말/번호/표가 뭐지, 앞에서 봤나, 뒤에 있나」 하고 멈추는 지점을 전부 적는다. 뒤에 있는 것을 앞에서 전제하거나, 앞에 있던 것을 뒤에서 다시 찾아 올라가야 하거나, 정의가 부록·원고 밖(앞 회차 보고)에만 있으면 전부 뜀이다.'],
84
- ['first-time-b', '처음 읽는 사람 B — 같은 규칙으로 읽되 표·흐름도·도판 캡션에 집중한다. 표 한 칸·흐름도 칸·캡션·여백 주석(marg)의 낱말이 본문 어디서 정의됐는지, 그 정의가 이 자리보다 앞에 있는지 뒤에 있는지를 판정한다. 뒤에 있으면 뜀이다.'],
85
- ['first-time-c', '처음 읽는 사람 C — 절과 절 사이의 이음새만 본다. 각 절의 lede·첫 문단이 바로 앞 절의 무엇을 이어받는지 한 줄로 적고, 이어받지 않고 새로 시작하는 절(맥락 단절)을 뜀으로 적는다. PART 간지의 물음이 그 PART 의 절에서 답해지는지도 본다.'],
86
- ['decider', '결정권자 — 요청 절(회신을 요청하는 판단 항목이 서는 절)의 항목만 읽는다(앞 절은 읽었다고 가정). 항목마다 「그 항목 안의 글만으로 두 길 중 하나를 고를 수 있는가」를 판정하고, 고르려면 다른 자리를 봐야 하는 항목을 뜀으로 적는다. 항목 바로 아래의 표는 같은 절 안에서 이어 읽는 것이라 뜀이 아니다 — 단 표 칸만으로 뜻이 안 서는 항이면 뜀이다. 항목과 표가 서로를 전제하면 뜀이다.'],
87
- ]
88
- const res = await parallel(LENSES.map(([k, inst]) => () => agent(`${CTX}\n[관점: ${k}] ${inst}\n이 파일 전체를 읽고 jumps 를 남김없이(0개면 0개) 적고 order_ok 를 정직하게 판정하라.`, { label: `read:${k}`, phase: 'Read', schema: JUMPS_READ, agentType: 'Explore' })))
89
- const lensFail = LENSES.filter((_, i) => !res[i]).map(([k]) => k)
90
- const unread = LENSES.filter((_, i) => res[i] && res[i].opened !== true).map(([k]) => k)
91
- const reads = res.filter((r) => r && r.opened === true)
92
- log(`선형 읽기 ${reads.length}/${LENSES.length} · 뜀 ${reads.reduce((n, r) => n + r.jumps.length, 0)}${unread.length ? ` · 렌더본 못 엶 ${unread.join(', ')}` : ''}`)
93
- // 메움 — 읽는 관점이 이 파일 안 증거 줄을 못 댄 관찰 주장(FILL_STEP). 뜀과 따로 센다 — 뜀 0 이어도 화면 주장을 일반 지식으로 메웠을 수 있다.
94
- const filled = LENSES.flatMap(([k], i) => (res[i] && res[i].opened === true && Array.isArray(res[i].claims) ? res[i].claims : [])
95
- .filter((c) => /^\s*없음/.test(String(c.shown_at))).map((c) => ({ lens: k, ...c })))
96
- const fillLine = `메움(증거 줄 없음) ${new Set(filled.map((c) => c.line)).size}줄 — ${LENSES.map(([k]) => `${k} ${filled.filter((c) => c.lens === k).length}`).join(' · ')} · 뜀과 따로 센다`
97
- log(fillLine)
98
- // 실패는 결과 머리에 적는다(reader-review.workflow.js 와 같은 머리). 렌더본을 못 연 관점이 하나라도 있으면 합쳐도 검토 미완이다.
99
- const headOf = () => `검토 미완 — ${[lensFail.length && `관점 ${lensFail.length}개 실패(${lensFail.join(', ')})`, unread.length && `렌더본 못 엶 ${unread.length}(${unread.join(', ')})`].filter(Boolean).join(' · ')}`
100
- if (reads.length < 3) { log('읽기 세션이 셋 미만 — 측정이 아니다. 한도가 풀린 뒤 round 를 바꿔 새로 띄운다'); return { head: headOf(), merged: null, filled, raw: reads, failed: true } }
101
- const failHead = unread.length ? headOf() : ''
102
- if (failHead) log(failHead)
103
- phase('Merge')
104
- const merged = await agent(`${CTX}\n[합치기] 아래 관점들의 뜀 지점을 같은 자리끼리 합치고 severity 합산으로 정렬해 전부 남겨라(상한 없음). 관점 하나만 짚었고 severity 1 인 것은 「사소」로 따로 세어 overall 에 수만 적고 jumps 에서는 빼라. 각 항목의 fix 는 가장 구체적인 것을 고르되 「다른 절을 보라」는 처방은 금지 — 그 자리에 넣을 문장 또는 뺄 문장으로만. order_ok 가 하나라도 false 면 order_note 를 합쳐 바꿀 순서를 하나로 제안하라. overall 첫 줄에 「뜀 N(3등급 이상 M) · 사소 K」를 적고, 이어서 「작성자가 지금 바로 할 일」을 순서대로 5줄 안.\n${JSON.stringify(reads.map(({ claims, ...r }) => r))}`, { label: 'merge', phase: 'Merge', schema: JUMPS })
105
- if (merged && typeof merged.overall === 'string') merged.overall = `${merged.overall}\n${fillLine}`
106
- return { ...(failHead ? { head: failHead, failed: true } : {}), merged, filled, raw: reads }