genetics-mcp 2.23.0 → 2.25.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 (33) 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 +2068 -239
  8. package/package.json +2 -2
  9. package/skills/gazette-report/SKILL.md +11 -10
  10. package/skills/gazette-report/scripts/tone.mjs +17 -5
  11. package/skills/gazette-report/scripts/tone.test.mjs +15 -2
  12. package/skills/gazette-report/template/skeleton.html +10 -10
  13. package/skills/genetics-define/SKILL.md +2 -0
  14. package/skills/genetics-report/SKILL.md +123 -102
  15. package/skills/genetics-report/assets/example/cases-briefing.json +1 -1
  16. package/skills/genetics-report/assets/example/cases.json +3 -3
  17. package/skills/genetics-report/assets/example/report-briefing-designs.json +26 -26
  18. package/skills/genetics-report/assets/example/report-briefing.json +3 -3
  19. package/skills/genetics-report/assets/example/report.json +11 -11
  20. package/skills/genetics-report/assets/mcp-call.mjs +160 -0
  21. package/skills/genetics-report/assets/patch.py +411 -0
  22. package/skills/genetics-report/assets/render.py +223 -0
  23. package/skills/genetics-report/assets/xlsx-plate.py +203 -0
  24. package/skills/genetics-report/references/content-pipeline.md +26 -26
  25. package/skills/genetics-report/references/edition-review.workflow.js +120 -23
  26. package/skills/genetics-report/references/font-embedding.md +1 -1
  27. package/skills/genetics-report/references/history.md +76 -2
  28. package/skills/genetics-report/references/lim-portal-format.md +109 -48
  29. package/skills/genetics-report/references/linear-read-test.workflow.js +50 -14
  30. package/skills/genetics-report/references/process.md +54 -48
  31. package/skills/genetics-report/references/reader-review.workflow.js +118 -33
  32. package/skills/genetics-report/references/visual-first.md +34 -17
  33. package/skills/gazette-report/scripts/package-lock.json +0 -24
@@ -1,22 +1,31 @@
1
1
  // 독자 검토 워크플로 — 발행 전에 원고를 여덟 관점(상태 표가 있으면 아홉)으로 읽히고, 발견을 반박 검증해 거른 뒤 합친다(visual-first.md §7).
2
2
  // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다. 배열·객체는 JSON 값 그대로 넘긴다(문자열로 싸면 한 덩어리가 된다).
3
3
  // args = { file: "<report.json 절대경로>",
4
+ // text: "<렌더본 텍스트 절대경로(필수) — `python3 assets/render.py <report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt`>",
4
5
  // reader: "<독자 한 줄: 누구이며 무엇을 하나 — 할 일이 둘이면 둘 다>",
5
6
  // example: "<예시 한 명 한 줄: 번호·측정일·실제 값>", bitmapBudget: 3,
6
- // scratch: "<측정 스크립트·풀어낸 도판을 둘 스크래치 절대경로>",
7
+ // scratch: "<측정 스크립트를 둘 스크래치 절대경로>",
7
8
  // plates: "<도판 HTML·PNG 작업 폴더(선택) — assets[].file 경로가 옮겨졌을 때 찾는 곳>",
8
9
  // locked: ["<정해진 것 한 줄>", …], latest: "<사용자 최신 지적 원문>",
9
10
  // sources: [{ name: "<자료 이름>", path: "<절대경로>", kind: "code|doc|pdf" }, …],
10
11
  // decisions: "<0단계 결정 장부 decisions.json 경로(선택)>",
11
12
  // state: "<코드 작업 상태 표 경로(선택) — 주면 state 관점이 켜진다>" }
12
- // 단계: Read(관점 병렬) → Verify(severity≥3 최대 12건 반박) → JS 거름(dropped·questions·사실 틀림 가르기) → Merge(합쳐 상위 10).
13
- // 돌아오는 것: { merged: { findings[](ids·where·problem·severity·class·evidence·rewrite·visual·verdicts), overall },
13
+ // 단계: Read(관점 병렬) → Verify(severity≥3 최대 12건 반박 — 읽기·구조 발견에 4칸 이상) → JS 거름(dropped·questions·사실 틀림 가르기) → Merge(합쳐 상위 10).
14
+ // 돌아오는 것: { merged: { findings[](ids·where·problem·severity·class·evidence·rewrite·visual·rx·verdicts), overall },
14
15
  // questions[](작성자가 사람에게 물을 것), passed[](관점별로 재서 통과한 것 — 「안 봄」과 가른다),
15
16
  // dropped[](버린 발견 + reasons — 사람이 되살릴 수 있게), jumps[](linear 관점의 뜀 지점 전부 — 0 이 목표),
17
+ // names[](class 이름 발견 전부 — 문서가 붙인 이름·정의 없는 이름, 0 이 목표),
16
18
  // raw: { reads, verdicts, unverified } }
19
+ // 검증이 1건이라도 실패하거나 관점이 결과를 못 내면 맨 앞에 head 「검토 미완 — 검증 N건 실패」(관점 실패는 뒤에 이어 적는다)와
20
+ // failed: true 가 붙고, merged.overall 첫 줄도 같은 문장이다 — 실패를 「발견 0」으로 읽지 않게.
21
+ // 렌더본 관점(first-time·linear)이 파일을 못 열었다고 답하면(opened=false) 그 관점의 발견은 쓰지 않고 head 에 「렌더본 못 엶」을 더한다.
22
+ // 시작 전 검사(Check): args.file · args.text 가 없거나, 렌더본 파일이 없거나 비었거나 원고보다 오래되면 읽기 전에 멈추고 무엇이 필요한지 말한다.
23
+ // 워크플로 스크립트는 파일을 못 연다(Workflow 런타임 — 파일 시스템·Node API 없음). 그래서 에이전트 하나가 정해진 명령 한 줄을 돌려
24
+ // 표준 출력을 글자 그대로 옮기고, 판정은 JS 가 한다. 멈춘 뒤 렌더본을 다시 뽑았으면 resumeFromRunId 없이 새로 띄운다(검사 결과도 캐시를 탄다).
17
25
  // merged.findings 앞에는 사실 틀림(class 사실틀림) 가운데 **Verify 가 참·부분으로 받은 것**이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다.
18
26
  // 미확인으로 판정된 사실 틀림은 합치는 쪽으로 가 problem 끝에 「(미확인)」이 붙는다. 뒤는 합친 상위 10.
19
- // 못 연 도판이 있으면 merged.overall 머리 줄이 「⚠ 도판 N장 미열람」이다. severity<3·상한 초과·검증 실패는 반영하지 않고 raw.unverified 로.
27
+ // 발견을 버리는 조건은 진단 거짓 하나다. 처방이 규칙에 걸리면(정해진 것 되돌리기·노이즈·포인터·새 이름 짓기) 발견은 남기고 rx 에 「처방 다시 — 까닭」.
28
+ // 못 연 도판이 있으면 merged.overall 머리 줄이 「⚠ 도판 N장 미열람」이다(서버 보관이라 열 길이 없던 장은 「도판 N장 서버 보관」 줄로 따로 선다 — 미열람이 아니다). severity<3·상한 초과·검증 실패는 반영하지 않고 raw.unverified 로.
20
29
  // 2026-09-08 고객사 검수 보고 4차에서 이 검토가 용어 충돌(한 낱말 두 값)·수치 불일치·참조 오류·반올림 설명 오류를 잡았다.
21
30
  // 2026-09-11 실사례(고객사 결정 브리핑) 반려 둘에서 더한 것 — decider 관점에 「권고가 제목·열·색·요약에 배어 동의만 받는 틀인지」,
22
31
  // 새 관점 source(원본 대조: 링크를 실제로 열어 인용·쪽·값을 대조, 「흐리다」는 원본 해상도로 재측정). 원고에 확인 없이 「원본이 흐림」이 실렸었다.
@@ -31,20 +40,48 @@
31
40
  // 2026-09-14 실사례(결정 브리핑 재발행) 반려 둘에서 더한 것 — 새 관점 figures(PNG 를 Read 로 열고 두 칸 틀을 잰다)·facts(내부 1차 자료)·state,
32
41
  // Verify 단계와 JS 거름. 캡션만 읽은 검토가 틀 대칭 결함을 0건 짚었고, 검토자의 틀린 주장·정해진 것을 되돌리는 처방·
33
42
  // 제작 방식·결정 날짜·판 이력을 더하라는 노이즈 처방이 거르지 않은 채 작성자에게 올라갔었다.
43
+ // 2026-09-26 스킬 검토(반려 「독자가 이해 못 함」 계열 19건)에서 바꾼 것 —
44
+ // ① 「처음 읽는 사람」·「선형 읽기」가 처음 읽는 사람이 아니었다: 공통 머리말(정해진 것·결정 장부·사용자 최신 지적·상태 표)을 받았고,
45
+ // 13/23·12/16 이 원고 밖 과정 파일(decisions.json·HANDOFF·build_*.py)을 스스로 열었고, JSON 으로 읽어 독자에게 안 보이는 origin 을
46
+ // 근거로 판정했다. 그래서 두 관점은 렌더본 텍스트 하나만 받고(args.text), CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다.
47
+ // 판정 기준이 「정의가 앞에 있나」뿐이라 문서 안에서 정의한 조어(인스턴스·설정에 지어 붙인 이름)가 통과했다 — 문서가 붙인 이름을 묻는 물음과 class 이름을 더했다.
48
+ // ② 발견이 사람에게 닿지 않았다: 검증 12칸이 사실 틀림(+10 가중)으로 차 읽기 발견이 늘 검증 밖이었고(15회 중 14회),
49
+ // 거를 사유가 하나만 있어도 버려 진단이 참인 발견 11건이 기각됐고, 사용량 한도로 검증 12건이 전부 실패한 실행이 「반영할 발견 0」으로 찍혔다.
50
+ // 그래서 읽기·구조에 4칸을 떼어 두고, 진단(참/부분/거짓/미확인)과 처방 적합을 따로 판정해 거짓만 버리고, 실패는 머리에 적는다.
34
51
  // 집행자: 이 워크플로의 agent 읽기(스크립트) + 거름 규칙(JS, 결정적). 렌더러 ⚠ 는 없음 — 링크·도판·코드를 못 여는 환경이면 그 판정은 「미확인」,
35
52
  // 못 연 도판 수는 overall 머리에 박히고, questions[]·dropped[] 는 사람의 눈으로 넘긴다.
36
53
  // PART·절 사이 되풀이도 렌더러·서버 dry_run 이 세지 않는다 — structure 관점이 유일한 기계 쪽 읽기다(readability R12g).
37
54
  export const meta = {
38
55
  name: 'report-reader-review',
39
- description: '보고서 원고(JSON)를 처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조·선형 읽기·도판·사실 대조(상태 표가 있으면 상태 대조까지) 관점으로 읽고, 발견을 반박 검증해 틀린 주장·정해진 것 되돌리기·노이즈 처방·포인터 처방을 거른 뒤 합친다',
56
+ description: '보고서를 처음 읽는 사람·선형 읽기(두 관점은 렌더본 텍스트만)·결정권자·정보 디자이너·원본 대조·구조·도판·사실 대조(상태 표가 있으면 상태 대조까지) 관점으로 읽고, 발견을 반박 검증해 진단이 거짓인 것만 거르고(처방이 규칙에 걸리면 「처방 다시」) 합친다',
40
57
  phases: [
58
+ { title: 'Check', detail: '렌더본이 있고 비지 않았고 원고보다 새로운지 — 명령 한 줄' },
41
59
  { title: 'Read', detail: '독자 8관점 병렬(상태 표가 있으면 9)' },
42
- { title: 'Verify', detail: 'severity≥3 최대 12건 반박 · JS 거름' },
60
+ { title: 'Verify', detail: 'severity≥3 최대 12건(읽기·구조 4칸 이상) 반박 · 진단·처방 따로 · JS 거름' },
43
61
  { title: 'Merge', detail: '사실 틀림 독립 · 나머지 상위 10 · 구조 빼기 우선' },
44
62
  ],
45
63
  }
46
64
  const A = args || {}
65
+ const MAKE_TEXT = 'python3 assets/render.py <report.json> --text <원고 폴더 밖 빈 폴더>/본문.txt'
66
+ if (!A.text) throw new Error(`args.text 가 필요하다 — 렌더본 텍스트 파일 절대경로. 「처음 읽는 사람」·「선형 읽기」 관점은 이 파일 하나만 읽는다. 만드는 법: ${MAKE_TEXT}`)
67
+ if (!A.file) throw new Error('args.file 이 필요하다 — 원고 report.json 절대경로. 나머지 관점이 읽고, 렌더본이 이 원고보다 새로운지 잰다')
47
68
  const FILE = A.file
69
+ const TEXT = A.text
70
+
71
+ phase('Check')
72
+ // 렌더본이 있고 비지 않았고 원고보다 새로운지 — 파일 상태는 에이전트가 옮긴 명령 출력으로만 읽고 판정은 여기서 한다(머리 주석 「시작 전 검사」).
73
+ const shq = (p) => `'${String(p).replace(/'/g, `'\\''`)}'`
74
+ 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(FILE)}`
75
+ const PRE = { type: 'object', properties: { stdout: { type: 'string', description: '명령의 표준 출력 한 줄 — 글자 그대로' }, exit: { type: 'integer' } }, required: ['stdout', 'exit'] }
76
+ const pre = await agent(`아래 명령을 Bash 로 **한 번만, 글자 그대로** 실행하고 표준 출력 한 줄을 stdout 에 고치지 말고 옮긴다. 종료 코드는 exit 에. 명령을 바꾸거나 다른 명령·파일을 열지 않는다.\n${STAT_CMD}`,
77
+ { label: 'check:files', phase: 'Check', schema: PRE, effort: 'low' })
78
+ let st = null
79
+ try { st = pre && pre.exit === 0 ? JSON.parse(pre.stdout) : null } catch (e) { st = null }
80
+ if (!Array.isArray(st) || st.length !== 2) throw new Error(`시작 전 검사 실패 — 렌더본·원고 파일 상태를 못 읽었다(${pre ? `exit ${pre.exit} · ${String(pre.stdout).slice(0, 200)}` : '검사 에이전트 결과 없음'}). resumeFromRunId 없이 새로 띄운다`)
81
+ if (!st[1].f) throw new Error(`args.file 파일이 없다 — ${FILE}. 원고 report.json 절대경로를 준다`)
82
+ if (!st[0].f) throw new Error(`args.text 파일이 없다 — ${TEXT}. 만드는 법: ${MAKE_TEXT}`)
83
+ if (st[0].b <= 0) throw new Error(`args.text 파일이 비었다 — ${TEXT}. 렌더가 실패했을 수 있다 — 다시 뽑고 출력의 ⚠·오류를 본다: ${MAKE_TEXT}`)
84
+ if (st[0].m < st[1].m) throw new Error(`렌더본이 원고보다 오래됐다 — ${TEXT} 가 ${FILE} 보다 ${Math.round(st[1].m - st[0].m)}초 앞선다. 원고를 고친 뒤 다시 뽑는다: ${MAKE_TEXT} (그다음 resumeFromRunId 없이 새로 띄운다)`)
48
85
  const READER = A.reader || '고객사 기획 담당(비개발자). 판단 요청에 회신해야 한다'
49
86
  const EXAMPLE = A.example || '(예시 한 명 미지정 — 원고에서 찾아 쓰라)'
50
87
  const BUDGET = Number(A.bitmapBudget ?? 3)
@@ -54,9 +91,9 @@ const LOCKED = Array.isArray(A.locked) ? A.locked : (A.locked ? [String(A.locked
54
91
  const SOURCES = Array.isArray(A.sources) ? A.sources : []
55
92
  const STATE = A.state || ''
56
93
  const VERIFY_MAX = 12
57
- const CTX = `[원고·도판·저장소 파일은 수정하지 않는다. 측정 스크립트·풀어낸 그림은 scratch 아래에만 쓴다. 한글로 답할 것.]
94
+ const CTX = `[원고·도판·저장소 파일은 수정하지 않는다. 측정 스크립트는 scratch 아래에만 쓴다. 한글로 답할 것.]
58
95
  원고: ${FILE} (Lim 포털 관보 서식 보고서의 발행 JSON. parts[] PART(title·lede·questions — 그 PART 가 답하는 물음), sections[] 절(sections[].part 로 PART 에 속함), cases[] 건, open_items[] 판단 요청, summary, guide 읽는 법, appendix, assets 도판)
59
- 도판: assets[].file 은 열어 볼 로컬 PNG 다. 서버 보관 도판(file 없음)은 get_report 로 받은 data URI 를 scratch 에 풀어 연다. 도판 작업 폴더: ${PLATES}
96
+ 도판: assets[].file 은 열어 볼 로컬 PNG 다. 서버 보관 도판(data 가 「(생략 — NB)」 표식)은 get_report 로 바이트를 받을 수 없다 — 도판 작업 폴더에 같은 도판의 원본 PNG 가 있으면 그것을 열고(파일 이름이 id 와 달라도 캡션·내용으로 찾는다), 없으면 unopened 에 그 장의 id 하나씩, why 를 「서버 보관」으로 둔다. 로고(role 이 logo)는 도판이 아니다 — unopened 에 넣지 않는다. 도판 작업 폴더: ${PLATES}
60
97
  scratch: ${SCRATCH}
61
98
  독자: ${READER}
62
99
  예시 한 명: ${EXAMPLE}
@@ -68,13 +105,24 @@ ${LOCKED.length ? LOCKED.map((s) => `- ${s}`).join('\n') : '(지정 없음)'}
68
105
  내부 1차 자료: ${SOURCES.length ? SOURCES.map((s) => `${s.name} [${s.kind}] ${s.path}`).join(' · ') : '(지정 없음 — 앱 동작·내부 수치 판정은 「미확인」)'}
69
106
  ${STATE ? `상태 표: ${STATE}\n` : ''}작성자 기준: 「글은 직관적이고 흐름을 이해하기 쉽게, 보충 설명은 글이 아니라 그림 같은 시각 자료로. 핵심은 굵게, 상태는 칩, 색은 뜻으로」.
70
107
  표기: **굵게** · ==강조== · {{ok|칩}}(톤 ok·wait·bad·info·muted) · 차트 rows[].tone.`
108
+ // 「처음 읽는 사람」·「선형 읽기」 관점의 머리말 — 위 CTX(정해진 것·결정 장부·사용자 최신 지적·내부 자료·상태 표)를 받지 않는다.
109
+ // 독자가 보는 것은 렌더본뿐이라 검토자도 렌더본에 보이는 글만 받는다(render.py --text — origin·alt·스크립트는 빠져 있다).
110
+ const TEXT_LENSES = new Set(['first-time', 'linear'])
111
+ const TEXT_CTX = `[읽기 전용. 파일을 수정하지 말 것. 한글로 답할 것. 철저함: very thorough — 발췌하지 말고 파일 전체를 처음부터 끝까지 읽는다.]
112
+ 읽을 것: ${TEXT} — 보고서 렌더본에서 화면에 보이는 글만 화면 순서대로 뽑은 텍스트(그림·영상 자리는 「[그림]」「[영상]」, 표 칸은 「 | 」).
113
+ 이 파일 하나만 연다. 원고에 없는 지식으로 뜻을 메웠다면 그것 자체가 발견이다. 원고 JSON·같은 폴더의 다른 파일·저장소·메모·다른 문서는 열지 않는다.
114
+ 이 파일을 끝까지 읽었으면 opened=true. 열지 못했거나 비어 있으면 opened=false 로 두고 findings·passed 는 비운 채 overall 에 까닭을 적는다 — 다른 파일로 대신 읽지 않는다.
115
+ 독자: ${READER}
116
+ where 는 이 파일에 보이는 절 번호·절 이름·건 번호·표 이름으로, evidence 는 이 파일의 줄 번호와 그 줄 글자로 적는다.`
117
+ // 문서가 붙인 이름을 묻는다 — 판정 기준이 「정의가 앞에 있나」뿐이면 문서 안에서 정의한 조어가 통과한다(2026-09-26 스킬 검토).
118
+ const NAME_Q = '그리고 묻는다: 대상·판·시험·상태·선택지를 부르는 낱말마다, 독자가 문서 밖(자기 화면 글자, 원본 문서, 공급사 공식 명칭)에서 본 글자인가? 이 문서가 붙인 이름이면 정의가 있어도 발견이다. 처방은 뜻풀이가 아니라 원래 이름으로 바꾸는 것이다. 이 발견은 class 이름으로 단다. 원래 이름을 이 파일만으로 알 수 없으면 rewrite 에 이름을 지어 넣지 말고 「원래 이름으로 바꾼다(이 파일에 없음)」라고 적는다.'
71
119
  const FINDING = { type: 'object', properties: {
72
120
  where: { type: 'string', description: '절·건·항목·도판 id 와 자리' },
73
121
  problem: { type: 'string', description: '독자가 왜 막히는가, 또는 원고가 원본과 어떻게 다른가 — 한 문장' },
74
122
  severity: { type: 'integer', minimum: 1, maximum: 5 },
75
- class: { type: 'string', enum: ['사실틀림', '구조', '읽기', '시각', '묻기'], description: '사실틀림=원고의 수치·인용·앱 동작·상태·그림↔본문이 원본과 다르다 · 구조=PART·절 합치기·빼기 · 읽기=용어·흐름·문장 · 시각=그림·표·흐름도로 바꿈 · 묻기=원고와 자료만으로 못 정해 작성자가 사람에게 물어야 한다(rewrite 에 물을 문장)' },
123
+ class: { type: 'string', enum: ['사실틀림', '구조', '읽기', '이름', '시각', '묻기'], description: '사실틀림=원고의 수치·인용·앱 동작·상태·그림↔본문이 원본과 다르다 · 구조=PART·절 합치기·빼기 · 읽기=용어·흐름·문장 · 이름=대상·판·시험·상태·선택지를 이 문서가 붙인 이름(정의가 있어도)이나 정의 없는 이름으로 부른다(처방은 원래 이름) · 시각=그림·표·흐름도로 바꿈 · 묻기=원고와 자료만으로 못 정해 작성자가 사람에게 물어야 한다(rewrite 에 물을 문장)' },
76
124
  evidence: { type: 'string', description: '연 것·잰 것 — 파일:줄 · 쪽(행) · px 값 · 주소 · 다시 한 계산. 원고 글만 읽고 낸 판단이면 「추정 — 」으로 시작' },
77
- rewrite: { type: 'string', description: '글로 고친다면(개조식 2~4문장). 필요 없으면 빈 문자열' },
125
+ rewrite: { type: 'string', description: '글로 고친다면 그 자리의 문체로 2~4문장 — 요청 자리(open_items·cta·PART I 간지·판권지 회신 안내)는 사실 문장까지 하십시오체, 그 밖은 주어가 우리·사물·화면인 개조식(독자를 주어로 쓰지 않음). 필요 없으면 빈 문자열' },
78
126
  visual: { type: 'string', description: '그림/표/flow/chart 로 바꾼다면 — 종류(bitmap|flow|table|chart|none) + 축·칸·숫자·라벨. 예시 한 명의 숫자로' } },
79
127
  required: ['where', 'problem', 'severity', 'class', 'evidence', 'rewrite', 'visual'] }
80
128
  const PASSED = { type: 'array', description: '재서 통과한 것 — 안 본 것은 넣지 않는다', items: { type: 'object', properties: {
@@ -82,35 +130,54 @@ const PASSED = { type: 'array', description: '재서 통과한 것 — 안 본
82
130
  const FIND = { type: 'object', properties: { findings: { type: 'array', items: FINDING }, passed: PASSED, overall: { type: 'string' } }, required: ['findings', 'passed', 'overall'] }
83
131
  const FIND_FIG = { type: 'object', properties: { ...FIND.properties, unopened: { type: 'array', description: '열지 못한 도판', items: { type: 'object', properties: {
84
132
  id: { type: 'string' }, why: { type: 'string' } }, required: ['id', 'why'] } } }, required: [...FIND.required, 'unopened'] }
133
+ // 렌더본 관점은 파일을 열었는지부터 답한다 — 못 연 관점의 「발견 0」을 「문제 없음」으로 읽지 않게(머리 주석).
134
+ const FIND_TEXT = { type: 'object', properties: { ...FIND.properties, opened: { type: 'boolean', description: '렌더본 파일을 열어 끝까지 읽었으면 true, 못 열었거나 비었으면 false' } }, required: [...FIND.required, 'opened'] }
85
135
  const VERDICT = { type: 'object', properties: {
86
136
  verdict: { type: 'string', enum: ['참', '부분', '거짓', '미확인'] },
87
137
  finding_basis: { type: 'string', enum: ['확인', '추정'], description: '발견의 evidence 가 원본·코드·잰 값에 닿으면 확인, 원고 글만 읽은 짐작이면 추정' },
88
138
  checked: { type: 'string', description: '이번에 직접 연 원본과 자리 — 파일:줄 · 쪽 · px · 주소. 못 열었으면 까닭' },
89
139
  touches_locked: { type: 'boolean' },
90
140
  noise_kind: { type: 'string', enum: ['없음', '제작', '날짜경로', '판이력'] },
141
+ rx_fit: { type: 'string', enum: ['맞음', '다시'], description: '처방(rewrite·visual)이 규칙에 맞나 — 진단(verdict)과 따로 판정한다' },
142
+ rx_why: { type: 'string', description: '다시면 걸린 규칙과 까닭 한 줄, 맞음이면 빈 문자열' },
91
143
  note: { type: 'string', description: '판정 까닭 한두 문장. 부분이면 맞는 쪽과 틀린 쪽' } },
92
- required: ['verdict', 'finding_basis', 'checked', 'touches_locked', 'noise_kind', 'note'] }
144
+ required: ['verdict', 'finding_basis', 'checked', 'touches_locked', 'noise_kind', 'rx_fit', 'rx_why', 'note'] }
93
145
  const MERGED = { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: {
94
- ...FINDING.properties, ids: { type: 'array', items: { type: 'string' }, description: '합친 발견 id 전부' } },
95
- required: ['ids', ...FINDING.required] } }, overall: { type: 'string' } }, required: ['findings', 'overall'] }
146
+ ...FINDING.properties, ids: { type: 'array', items: { type: 'string' }, description: '합친 발견 id 전부' },
147
+ rx: { type: 'string', description: '처방을 다시 써야 하면 「처방 다시 — 까닭」, 아니면 빈 문자열' } },
148
+ required: ['ids', ...FINDING.required, 'rx'] } }, overall: { type: 'string' } }, required: ['findings', 'overall'] }
96
149
  const LENSES = [
97
- ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳. 처방은 그 자리에서 이해되게 하는 것 — 낱말·규칙·예시 선언은 첫 등장 자리에서 한 구절(20자 안)로 풀고, 뒤 절의 번호·표 이름·§ 를 앞 절에서 쓰지 않는다(뒤 절이 앞 절을 되짚는 방향만). 「N절을 보라」「부록에 있다」는 처방이 아니다 — 그것이 시험지를 만든다(2026-09-14).'],
150
+ ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳. 처방은 그 자리에서 이해되게 하는 것 — 낱말은 원래 이름으로 부르고, 필요하면 첫 등장 자리에서 괄호로 한 번 풀고(20자 안), 규칙·예시 선언은 첫 등장 자리에서 한 구절로 두고, 뒤 절의 번호·표 이름·§ 를 앞 절에서 쓰지 않는다(뒤 절이 앞 절을 되짚는 방향만). 「N절을 보라」「부록에 있다」는 처방이 아니다 — 그것이 시험지를 만든다(2026-09-14). ' + NAME_Q],
98
151
  ['decider', '결정권자 — 회신해야 한다. 판단 요청 각 항에서 「무엇을 고르는지(물음) / 두 길이 각각 무엇을 바꾸는지(그대로 두면 · 바꾸면) / 기한」이 3초 안에 읽히는지. 안 읽히는 항의 재구성안과, 항들을 한눈에 묶는 표·관문 흐름도. 수치·링크·건수가 절마다 어긋나는 곳(신뢰를 깨는 것). 권고가 제목·표 열·색(칩·톤)·요약면에 배어 독자가 동의만 하게 되는 틀(답정너)인지 — 배인 자리마다 짚고, 물음과 두 길 대칭으로 되돌린 재구성안. 그리고 둘을 더 본다: (1) 독자의 **할 일마다**(위 「독자」 줄의 할 일 목록) 그것이 표지 meta · 요약면 현황 줄(notice) · 읽는 법(guide) 첫 물음에 서는지 대조한다 — 한 자리라도 빠지면 severity 4 로 짚고 어느 자리에 무엇을 넣을지 rewrite 에 적는다(요청문이 아니라 사실문으로). (2) 요약면 수치 칸(stats)끼리 **겹치는 집합**인지 — 한 수가 다른 수에 포함되면(전체 건 수와 그중 일부) 하나를 빼거나 라벨에 관계를 드러내는(「그중 …」) 재구성안을 적는다. (3) 고객 자리에서 읽어 **우리를 어떻게 보이게 하는 문장**(자격증명이 오간 경로, 우리가 늦게 안 것, 우리 절차 미비)이 있으면 severity 4 로 짚고 rewrite 는 「그 문장 삭제, 권고만」 — 그 사건 범위 안의 조언은 남긴다.'],
99
152
  ['visual', '정보 디자이너 — 글로 설명하는데 그림이면 한 번에 보일 곳. 원리·갈림·타임라인·분포·자리별 상태. 각 그림의 축·칸·숫자·라벨을 구체로. 표의 숫자를 문장으로 되풀이하는 문단, 굵게·칩·톤을 둘 자리.'],
100
153
  ['source', '원본 대조 — 고객이 누를 수 있는 원본과 원고 인용의 일치를 본다. 원고가 가리키는 원본을 실제로 연다: 건 cases[].source_url, 도판 출처 줄 assets[].source_text 안의 주소·쪽, 부록 출처 목록과 본문 글자 안의 맨 주소·[글자](주소). assets[].origin 은 렌더되지 않는 제공자 기록이라 대조 대상이 아니다. 인용 문구·쪽(행)·값을 원문과 대조한다. 어긋나는 인용, 원문에 없는 문장, 쪽·절 번호 오기, 고객이 눌러도 안 열리는 링크를 발견으로(원고가 원문과 다르면 class 사실틀림). 「흐리다」「못 연다」는 주장은 원본 해상도로 재측정해(PDF 는 pdfimages -list 전체, 웹은 실제로 열어) 잰 값을 evidence 에 적고, 못 열었으면 「미확인」이라 적는다 — 짐작으로 판정하지 않는다. 앱 동작·내부 수치·결정의 주체는 facts 관점 몫이라 여기서 판정하지 않는다 — 다른 문서에 그 말이 없다고 앱 동작을 거짓이라 하지 않는다. rewrite 에는 원문 문구 그대로 + 원본 이름·쪽(행)·주소로 고친 출처 줄.'],
101
154
  ['structure', '구조 검토자 — parts[] 의 title·lede·questions 와 그 PART 에 속한 절(sections[].part)을 맞대어 읽는다. 짚을 곳: 앞 PART 의 요지 lead·marg.key·표 행이 뒤 PART 의 물음에 먼저 답하는 곳 / 같은 사실(시각·금액·건 제목·핵심 명제)이 두 PART 이상에 서는 곳 — 선 자리를 전부 세어 적는다 / 같은 건의 세 배율(summary·status·cases)이 asks 없이 서로 다른 PART 로 갈린 곳 / 두 절의 marg.key 가 낱말만 다르고 같은 명제인 곳 / 어느 PART 도 선언하지 않은 물음에 답하는 절 / 결정권자 독자인데 status 대조표가 건 제목을 한 번 더 세우는 곳 / 요지 문단(summary)·읽는 법(guide) 물음이 건의 관찰·판단을 바꿔 말한 곳 — 문장 쌍(요지·읽는 법 쪽 ↔ 건 쪽)으로 전수해 evidence 에 적는다. 요약면 수치·요지 lead 가 문서 전체를 줄여 말하는 것은 설계라 결함이 아니다 — 단 요지 문단이 건을 하나씩 옮겨 적으면 결함. 요지(summary)의 모양도 여기서 본다: 이야기 순서(문제 → 원인 → 해법 → 정할 것)대로 서는지, 결정 문서면 칸이 4~6문단인지, 결정에 직접 닿지 않는 **근거 문단의 body 가 비어 있는지**(lead 한 줄 + refs 만 — 본문은 건이 가진다). rewrite 에는 합칠 절·지울 문장을 적는다 — 더하기가 아니라 빼기로. 단 뺀 자리에 뒤 절 번호 포인터를 남기지 않는다: 그 자리에서 뜻이 서게 하는 한 구절(낱말 정의·조건 이름)은 남긴다.'],
102
- ['linear', '선형 읽기 — 렌더 순서(표지 → 이력 → 차례 → 활용법 → 요약면 → PART 간지 → 절 → 부록 → 판권지)대로 앞에서 뒤로 딱 한 번 읽는다. 「이 낱말/번호/표가 뭐지, 앞에서 봤나, 뒤에 있나」 하고 멈추는 지점을 남김없이 적는다 — 뒤에 있는 것을 앞에서 전제하거나(선참조: 요지 본문 안의 §n.n, 흐름도 「건」 열의 건 번호, 도판 출처 줄의 §. refs[] 는 「근거」 단추라 뜀이 아니되 문단 내용과 맞는 건이어야 함), 앞의 정의를 뒤에서 다시 찾아 올라가야 하거나, 정의가 부록·원고 밖(앞 회차 보고)에만 있으면 전부 뜀이다. 표 한 칸·흐름도 칸·캡션·여백 주석의 낱말도 본다. 결정권자 자리에서는 판단 항목 각각이 그 항목 안의 글만으로 두 길 중 하나를 고를 수 있는지, 항목과 한눈표가 서로를 전제하는지 본다. where 에 읽던 자리, problem 에 필요했던 것과 그것이 있는 자리(없으면 「원고 어디에도 없음」), rewrite 에 그 자리에 넣거나 뺄 문장 — 「다른 절을 보라」는 처방 금지. class 는 읽기, severity 5=이해 불가 · 3=되짚어야 함 · 1=사소. 절 순서가 이야기처럼 이어지는지도 overall 에 적는다.'],
155
+ ['linear', '선형 읽기 — 렌더 순서(표지 → 이력 → 차례 → 활용법 → 요약면 → PART 간지 → 절 → 부록 → 판권지)대로 앞에서 뒤로 딱 한 번 읽는다. 「이 낱말/번호/표가 뭐지, 앞에서 봤나, 뒤에 있나」 하고 멈추는 지점을 남김없이 적는다 — 뒤에 있는 것을 앞에서 전제하거나(선참조: 요지 본문 안의 §n.n, 흐름도 「건」 열의 건 번호, 도판 출처 줄의 §. refs[] 는 「근거」 단추라 뜀이 아니되 문단 내용과 맞는 건이어야 함), 앞의 정의를 뒤에서 다시 찾아 올라가야 하거나, 정의가 부록·원고 밖(앞 회차 보고)에만 있으면 전부 뜀이다. 표 한 칸·흐름도 칸·캡션·여백 주석의 낱말도 본다. 결정권자 자리에서는 판단 항목 각각이 그 항목 안의 글만으로 두 길 중 하나를 고를 수 있는지, 항목과 한눈표가 서로를 전제하는지 본다. where 에 읽던 자리, problem 에 필요했던 것과 그것이 있는 자리(없으면 「원고 어디에도 없음」), rewrite 에 그 자리에 넣거나 뺄 문장 — 「다른 절을 보라」는 처방 금지. class 는 읽기(아래 물음의 발견은 이름), severity 5=이해 불가 · 3=되짚어야 함 · 1=사소. 절 순서가 이야기처럼 이어지는지도 overall 에 적는다. ' + NAME_Q],
103
156
  ['figures', `도판 검토자 — 캡션·alt 만으로 판정하지 않는다. 먼저 readability.md §1 R13 행과 visual-first.md §4 를 읽는다. assets[] 마다 file(PNG)을 Read 로 열어 눈으로 보고, 같은 폴더에 같은 이름 .html 이 있으면 머리 주석과 CSS 를 읽는다. 두 칸(비교 컷)이면 칸마다 틀 윗선·아랫선 y 를 재어 px 차를 evidence 에 적는다 — .html 이 있으면 헤드리스 크롬에서 틀 요소의 getBoundingClientRect, 없으면 PNG 픽셀 스캔으로. 측정 스크립트는 ${SCRATCH}/figures/ 아래에만 둔다. 짚을 곳: R13 위반(좌우 배치·틀 높이·배율·틀 안 글자·표식 색·칩 이름표) / 틀에 잘림(윗선 여백 0 포함) / 선택지 도안 가운데 하나만 합성·실사·크게·먼저 선 곳 / 조건표·캡션이 말한 동작이 그림에 없거나 그림이 본문을 반박하는 곳 — class 사실틀림 / 캡션 「왼쪽은…」과 실제 칸의 불일치 / 칩 이름과 캡션·표 이름이 다른 곳. 열어서 잰 값이 기준 안인 도판은 passed 에 id 와 잰 값을. 못 연 도판은 unopened 에 id 와 까닭을 적고, 그 도판에 대한 판정은 「미확인」이다.`],
104
157
  ['facts', '사실 대조 — FCG 내부 1차 자료(내부 1차 자료 목록)와 결정 장부로 원고의 수치·앱 동작·결정의 주체와 시점을 판정한다. 수치는 원고의 입력값에서 다시 계산한다(반올림·날짜 산술 포함). 앱 동작은 kind=code 파일을 열어 파일:줄로만 판정한다 — 다른 문서에 그 말이 없다는 것은 반증이 아니다. 결정은 누가·어디서 정했는지 결정 장부·정해진 것과 대조한다(주체 없는 「정한 사항」, 장부와 다른 차수·자리). 판정 못 하면 「미확인」이라 적는다. source 관점과의 경계: 고객이 누를 수 있는 원본(링크·쪽)과 인용의 일치는 source 몫이고, 여기는 고객에게 보이지 않는 내부 자료 몫이다. 원고가 틀린 발견은 class 사실틀림. 다시 계산해 맞은 수치·코드와 맞은 동작은 passed 에 파일:줄·계산식과 함께 적는다.'],
105
158
  ...(STATE ? [['state', '상태 대조 — 상태 표를 연다. 원고에서 「반영·적용·고침·보이지 않음·빠짐」 같은 완료형 동사가 선 문장마다 상태 표의 실제 확인 수준(코드 · 시험판 · 실기기 · 운영)과 대조한다. 확인 수준보다 앞선 시제·단정(코드만 고쳤는데 앱에서 보이지 않는다고 쓴 문장)은 class 사실틀림으로 두고, rewrite 에 확인 수준에 맞춘 문장을 적는다. 수준과 맞는 문장은 passed 에.']] : []),
106
159
  ]
107
160
  phase('Read')
108
- const res = await parallel(LENSES.map(([k, inst]) => () => agent(`${CTX}\n[관점: ${k}] ${inst}\n원고 전체를 읽고 findings 를 ${k === 'linear' ? '남김없이(0개면 0개)' : '8~14개'}, severity 로 정직하게 매겨라. 발견마다 class 와 evidence 를 단다. 재서 통과한 것은 passed 에. overall 에 이 원고의 흐름이 어디서 끊기는지 3문장.`, { label: `read:${k}`, phase: 'Read', schema: k === 'figures' ? FIND_FIG : FIND })))
161
+ // 렌더본 관점(TEXT_LENSES)은 CLAUDE.md 를 싣지 않는 내장 유형 Explore 로 돈다 — 기본 워크플로 에이전트는 CLAUDE.md 가 첨부로 실려
162
+ // 프롬프트만 고쳐서는 0 상태가 안 된다(2026-09-26 스킬 검토: 첨부에 MEMORY.md 가 실린 에이전트 22/23·15/16).
163
+ // Explore 는 읽기 전용이고 모델은 세션 모델을 물려받는다(Claude Code 2.1.278 내장 정의 omitClaudeMd·model inherit).
164
+ // 확인 전: MEMORY.md(자동 메모리)와 하네스가 전하는 사용자 요청까지 빠지는지는 첫 실행 전사의 첨부로 확인해야 한다.
165
+ const res = await parallel(LENSES.map(([k, inst]) => () => agent(`${TEXT_LENSES.has(k) ? TEXT_CTX : CTX}\n[관점: ${k}] ${inst}\n${TEXT_LENSES.has(k) ? '이 파일 전체' : '원고 전체'}를 읽고 findings 를 ${k === 'linear' ? '남김없이(0개면 0개)' : '8~14개'}, severity 로 정직하게 매겨라. 발견마다 class 와 evidence 를 단다. 재서 통과한 것은 passed 에. overall 에 이 원고의 흐름이 어디서 끊기는지 3문장.`, { label: `read:${k}`, phase: 'Read', schema: k === 'figures' ? FIND_FIG : (TEXT_LENSES.has(k) ? FIND_TEXT : FIND), ...(TEXT_LENSES.has(k) ? { agentType: 'Explore' } : {}) })))
109
166
  const failed = LENSES.filter((_, i) => !res[i]).map(([k]) => k)
110
- const reads = res.map((r, i) => r && { lens: LENSES[i][0], ...r }).filter(Boolean)
111
- log(`독자 관점 ${reads.length}/${LENSES.length} · 발견 ${reads.reduce((n, r) => n + r.findings.length, 0)}${failed.length ? ` · 실패 ${failed.join(', ')}` : ''}`)
167
+ // 렌더본을 못 열었다고 답한 관점 — 그 발견은 쓰지 않고 머리에 적는다(검사를 통과한 뒤에도 권한·경로 문제로 못 열 수 있다).
168
+ const unread = LENSES.filter(([k], i) => TEXT_LENSES.has(k) && res[i] && res[i].opened !== true).map(([k]) => k)
169
+ const reads = res.map((r, i) => r && { lens: LENSES[i][0], ...r }).filter((r) => r && !unread.includes(r.lens))
170
+ log(`독자 관점 ${reads.length}/${LENSES.length} · 발견 ${reads.reduce((n, r) => n + r.findings.length, 0)}${failed.length ? ` · 실패 ${failed.join(', ')}` : ''}${unread.length ? ` · 렌더본 못 엶 ${unread.join(', ')}` : ''}`)
112
171
  const fig = reads.find((r) => r.lens === 'figures')
113
- const figHead = !fig ? '⚠ 도판 관점 실패 — 도판 전부 미열람' : (fig.unopened.length ? `⚠ 도판 ${fig.unopened.length}장 미열람(${fig.unopened.map((u) => u.id).join(', ')})` : '')
172
+ // 서버 보관 도판(원본 PNG 가 이 기계에 없다)은 검토자가 열 길이 없다 — 열 수 있었는데 못 연 「미열람」과 갈라 적는다(그 장은 사람이 열람 주소에서 본다).
173
+ // 가르는 열쇠는 검토자가 적은 글(「서버 보관」)이라 판정이 아니라 **분류 표시**다 — 통과는 사람이 그 id 가 원고의 보관 표식인지 보고 정한다(SKILL 체크리스트 7행).
174
+ // 한 줄로 묶어 세면 보관 그림이 있는 문서를 고친 판은 통과 기준 「⚠ 도판 N장 미열람 없음」(SKILL 체크리스트 7행)을 채울 길이 없다(2026-09-30 검토).
175
+ const held = fig ? fig.unopened.filter((u) => /서버 보관/.test(`${u.why} ${u.id}`)) : []
176
+ const missed = fig ? fig.unopened.filter((u) => !held.includes(u)) : []
177
+ const figHead = !fig ? '⚠ 도판 관점 실패 — 도판 전부 미열람' : [
178
+ missed.length ? `⚠ 도판 ${missed.length}장 미열람(${missed.map((u) => u.id).join(', ')})` : '',
179
+ held.length ? `도판 ${held.length}장 서버 보관 — 검토자가 열지 못했다, 열람 주소에서 사람이 본다(${held.map((u) => u.id).join(', ')})` : '',
180
+ ].filter(Boolean).join('\n')
114
181
  if (figHead) log(figHead)
115
182
 
116
183
  phase('Verify')
@@ -128,35 +195,51 @@ all.filter((f) => f.severity >= 3).sort((a, b) => rank(b) - rank(a)).forEach((f)
128
195
  turn.set(f, seen[k])
129
196
  })
130
197
  const eligible = [...turn.keys()].sort((a, b) => rank(b) - rank(a) || turn.get(a) - turn.get(b) || LENS_AT[a.lens] - LENS_AT[b.lens])
131
- const toVerify = eligible.slice(0, VERIFY_MAX)
132
- const unverified = [...all.filter((f) => f.severity < 3).map((f) => ({ ...f, why: 'severity<3' })), ...eligible.slice(VERIFY_MAX).map((f) => ({ ...f, why: `검증 상한 ${VERIFY_MAX} 초과` }))]
198
+ // 검증 칸을 등급별로 나눈다 — 읽기·구조 발견에 READ_MIN 칸을 먼저 떼어 둔다. 사실 틀림 +10 가중 때문에 나누지 않으면
199
+ // 12칸이 사실 틀림으로 다 차 읽기 발견이 늘 검증 밖이었다(2026-09-26 스킬 검토: 15회 중 14회).
200
+ // 이름 발견은 칸과 별개로 names[] 에 전부 돌아간다.
201
+ const READ_MIN = 4
202
+ const readSlots = eligible.filter((f) => f.class === '읽기' || f.class === '구조').slice(0, READ_MIN)
203
+ const toVerify = [...readSlots, ...eligible.filter((f) => !readSlots.includes(f)).slice(0, VERIFY_MAX - readSlots.length)]
204
+ const unverified = [...all.filter((f) => f.severity < 3).map((f) => ({ ...f, why: 'severity<3' })), ...eligible.filter((f) => !toVerify.includes(f)).map((f) => ({ ...f, why: `검증 상한 ${VERIFY_MAX} 초과` }))]
133
205
  log(`검증 ${toVerify.length}건 · 검증 밖 ${unverified.length}건(raw.unverified — 반영하지 않음)`)
134
206
  const vs = await parallel(toVerify.map((f) => () => agent(`${CTX}\n[반박 검증 — 발견 ${f.id}] 아래 발견은 ${f.lens} 관점 검토자가 냈다. 검토자 주장도 틀린다. 기본 자세는 반박이다 — 발견이 가리킨 원본(코드 파일:줄 · PDF 쪽 · PNG · 주소)과 원고의 그 자리를 직접 열어 판정한다. 열지 못했으면 verdict 는 미확인이다. 원고 글만 읽고 참이라 하지 않는다.
135
- verdict: 발견의 주장(problem)이 참/부분/거짓/미확인. class 가 묻기면 「원고와 자료만으로 정말 못 정하는가」를 판정한다 — 자료에 답이 있으면 거짓.
207
+ 렌더본 텍스트: ${TEXT} — first-time·linear 관점은 이 파일만 읽은 독자다. 그 관점의 읽기·이름 발견은 이 파일(독자에게 보이는 글)로 판정한다 — 원고 JSON 의 origin·결정 장부·사용자 지적은 독자가 못 보는 것이라 반증이 아니다.
208
+ class 가 이름이면 판정 물음은 「이 낱말을 독자가 문서 밖(자기 화면 글자·원본 문서·공급사 공식 명칭)에서 본 적이 있는가」다 — 문서 안의 정의·캡션·용어표는 반증이 아니다(2026-09-26: 「거울 정의 없음」 같은 발견이 「표 캡션에 정의 있음」으로 기각됐었다). 원래 이름이 정말 없는 새 개념이면 verdict 는 참으로 두고 note 를 「원래 이름 없음」으로 시작한다.
209
+ verdict(진단): 발견의 주장(problem)이 참/부분/거짓/미확인. 거짓은 반증을 열어 확인했을 때만 — 확신이 없으면 미확인이다. class 가 묻기면 「원고와 자료만으로 정말 못 정하는가」를 판정한다 — 자료에 답이 있으면 거짓.
210
+ 진단과 처방은 따로 판정한다 — 처방이 틀려도 주장이 맞으면 verdict 는 참이다. 처방 적합은 아래 세 칸이 잰다.
211
+ rx_fit: 처방이 「N절을 보라」「부록에 있다」 같은 포인터이거나, 원래 이름 대신 이 문서가 새 이름·설명형 이름을 붙이거나, 만든 이름은 두고 뜻풀이만 더하면 다시. 그 밖에 규칙에 맞으면 맞음. rx_why 에 걸린 규칙과 까닭.
136
212
  touches_locked: 처방(rewrite·visual)이 정해진 것이나 결정 장부의 확인된 결정을 되돌리면 true.
137
213
  noise_kind: 처방이 본문·캡션·출처 줄에 제작 방식(합성·재현·캡처 도구·그린 방법)을 더하면 제작, 결정 날짜·결정 경로(몇 차·어느 회의에서)를 더하면 날짜경로, 판 이력(몇 판에서 바뀜)을 더하면 판이력, 아니면 없음. 원본 위치 한 구절(원본 이름·쪽·행·주소·건 §)이 빠졌다는 처방은 노이즈가 아니라 SKILL.md 5a 결함이다 — 없음.
138
214
  ${JSON.stringify(f)}`, { label: `verify:${f.id}`, phase: 'Verify', schema: VERDICT })))
139
- // JS 거름(결정적) — 거짓·정해진 것 되돌리기·노이즈 처방·추정뿐인 근거는 dropped(사유 포함), 묻기는 questions, 사실 틀림은 합치지 않는 독립 항목.
215
+ // JS 거름(결정적) — 진단 거짓만 dropped(사유 포함), 묻기는 questions, 사실 틀림은 합치지 않는 독립 항목.
216
+ // 정해진 것 되돌리기·노이즈·규칙에 걸린 처방은 발견을 버리지 않고 rx 「처방 다시 — 까닭」을 단다. 전에는 사유 하나로 발견째 버려
217
+ // 진단이 참인 발견 11건이 기각됐다(2026-09-26 스킬 검토 — 그중 지어 붙인 인스턴스 이름이 정의 없이 나온다는 발견). 근거가 추정뿐인지는 verify.finding_basis 에 남는다.
140
218
  const verdicts = toVerify.map((f, i) => ({ id: f.id, ...(vs[i] || { verdict: '검증 실패' }) }))
141
219
  const verdictOf = Object.fromEntries(verdicts.map((v) => [v.id, v.verdict]))
142
- const dropped = [], questions = [], errors = [], candidates = []
220
+ const dropped = [], questions = [], errors = [], candidates = [], vfail = []
143
221
  toVerify.forEach((f, i) => {
144
222
  const v = vs[i]
145
- if (!v) { unverified.push({ ...f, why: '검증 실패' }); return }
146
- const reasons = [v.verdict === '거짓' && '거짓', v.touches_locked && '정해진 것을 되돌림', v.noise_kind !== '없음' && `노이즈:${v.noise_kind}`, v.finding_basis === '추정' && '근거 추정뿐'].filter(Boolean)
147
- const g = { ...f, verdict: v.verdict, verify: v }
148
- if (reasons.length) dropped.push({ ...g, reasons })
223
+ if (!v) { vfail.push(f.id); unverified.push({ ...f, why: '검증 실패' }); return }
224
+ const rxWhy = [v.touches_locked && '정해진 것을 되돌림', v.noise_kind !== '없음' && `노이즈:${v.noise_kind}`, v.rx_fit === '다시' && (v.rx_why || '규칙에 걸림')].filter(Boolean)
225
+ const g = { ...f, verdict: v.verdict, verify: v, rx: rxWhy.length ? `처방 다시 — ${rxWhy.join(' · ')}` : '' }
226
+ if (v.verdict === '거짓') dropped.push({ ...g, reasons: ['거짓', ...rxWhy] })
149
227
  else if (f.class === '묻기') questions.push(g)
150
228
  // 사실 틀림의 독립 자리(개수 제한 밖·맨 앞)는 **Verify 가 참·부분으로 받은 것**만이다.
151
229
  // 미확인(근거를 못 연 것)까지 그 자리에 두면 확인되지 않은 주장이 개수 제한 없이 맨 앞에 서고 「(미확인)」 표시도 안 붙는다(2026-09-14 검증).
152
230
  else if (f.class === '사실틀림' && (v.verdict === '참' || v.verdict === '부분')) errors.push(g)
153
231
  else candidates.push(g)
154
232
  })
155
- log(`거름: 버림 ${dropped.length} · 묻기 ${questions.length} · 사실 틀림(참·부분) ${errors.length} · 합칠 것 ${candidates.length}`)
233
+ log(`거름: 버림 ${dropped.length} · 묻기 ${questions.length} · 사실 틀림(참·부분) ${errors.length} · 합칠 것 ${candidates.length}${vfail.length ? ` · 검증 실패 ${vfail.length}` : ''}`)
156
234
  if (!dropped.length) log('버림 0 — 표본 3건 재반박 권함')
235
+ // 실패는 결과 머리에 적는다 — 사용량 한도로 검증 12건이 전부 실패한 실행이 「반영할 발견 0」으로 찍혔다(2026-09-26 스킬 검토:
236
+ // wf_01b3c88d-58b·wf_8d4619aa-bfb). 관점이 결과를 못 낸 것도 같다 — linear 가 죽으면 jumps 가 비어 「뜀 0」으로 읽힌다.
237
+ const failHead = (vfail.length || failed.length || unread.length) ? `검토 미완 — ${[vfail.length && `검증 ${vfail.length}건 실패`, failed.length && `관점 ${failed.length}개 실패(${failed.join(', ')})`, unread.length && `렌더본 못 엶 ${unread.length}(${unread.join(', ')})`].filter(Boolean).join(' · ')}` : ''
238
+ if (failHead) log(failHead)
157
239
 
158
240
  phase('Merge')
159
- const m = (candidates.length || errors.length) ? await agent(`${CTX}\n[합치기] 아래 「합칠 것」은 Verify 를 통과한 발견이다(verify 에 판정). 같은 자리끼리 합치고(중복 제거 — ids 에 합친 발견 id 를 모두 적는다), severity 합산으로 정렬해 상위 10개만 남겨라. 각 항목에 가장 구체적인 rewrite 와 visual 을 고르되, 비트맵은 ${BUDGET}장까지만 'bitmap' 으로 두고 나머지는 flow/table/chart 로 바꿔 적어라. 다른 관점의 더하기 처방(표·문장·행 추가)이 structure 관점의 빼기 처방과 부딪히면 빼기가 이긴다 — 같은 사실을 다른 PART·절에 또 세우는 처방은 버린다. 단 빼기가 뒤 절 번호 포인터를 남기면 안 된다 — linear 관점의 뜀 지점과 부딪히는 「N절·건 §·부록을 보라」 처방은 「그 자리에 한 구절」로 바꾼다. linear 관점의 뜀 지점은 상위 10 과 별개로 전부(jumps) 남는다. verdict 가 미확인인 발견을 합치면 problem 끝에 「(미확인)」을 남긴다.
241
+ const m = (candidates.length || errors.length) ? await agent(`${CTX}\n[합치기] 아래 「합칠 것」은 Verify 를 통과한 발견이다(verify 에 판정). 같은 자리끼리 합치고(중복 제거 — ids 에 합친 발견 id 를 모두 적는다), severity 합산으로 정렬해 상위 10개만 남겨라. 각 항목에 가장 구체적인 rewrite 와 visual 을 고르되, 비트맵은 ${BUDGET}장까지만 'bitmap' 으로 두고 나머지는 flow/table/chart 로 바꿔 적어라. 다른 관점의 더하기 처방(표·문장·행 추가)이 structure 관점의 빼기 처방과 부딪히면 빼기가 이긴다 — 같은 사실을 다른 PART·절에 또 세우는 처방은 버린다. 단 빼기가 뒤 절 번호 포인터를 남기면 안 된다 — linear 관점의 뜀 지점과 부딪히는 「N절·건 §·부록을 보라」 처방은 「그 자리에 한 구절」로 바꾼다. linear 관점의 뜀 지점과 class 이름 발견은 상위 10 과 별개로 전부(jumps·names) 남는다. verdict 가 미확인인 발견을 합치면 problem 끝에 「(미확인)」을 남긴다.
242
+ rx 가 빈 문자열이 아닌 발견은 진단은 맞고 처방이 규칙에 걸린 것이다 — 그 rewrite·visual 을 고르지 말고 규칙에 맞는 처방을 새로 적되(이름 발견은 원래 이름으로 — 새 이름·뜻풀이 금지), 못 적으면 rewrite 를 rx 문장 그대로 둔다. 합친 항목의 rx 칸에는 처방을 다시 써야 하면 「처방 다시 — 까닭」, 아니면 빈 문자열.
160
243
  「사실 틀림」은 원고가 원본과 다른 곳(그림↔본문 반박 포함)이다 — 사실 대조에서 틀림·부분으로 판정된 발견은 다른 발견에 합쳐 넣지 않고 독립 항목으로 이미 앞에 서며 개수 제한 밖이다. findings 에 다시 적지 말고 overall 순서에만 반영하라. overall 에는 「작성자가 지금 바로 할 일 순서」를 5줄로 — 사실 틀림 고침이 맨 먼저, 그다음 구조(PART·절 합치기·빼기), 용어 잠금과 수치·참조·원본 대조, 그림은 마지막이다.
161
244
  [사실 틀림 — 참고만]
162
245
  ${JSON.stringify(errors)}
@@ -166,12 +249,14 @@ const pick = ({ where, problem, severity, class: c, evidence, rewrite, visual })
166
249
  const body = m ? m.overall : ((candidates.length || errors.length) ? '합치기 실패 — raw.reads 를 사람이 훑는다' : '반영할 발견 0 — passed·dropped 를 사람이 훑는다')
167
250
  const merged = {
168
251
  findings: [
169
- ...errors.map((e) => ({ ids: [e.id], ...pick(e), verdicts: [`${e.id}:${e.verdict}`] })),
252
+ ...errors.map((e) => ({ ids: [e.id], ...pick(e), rx: e.rx, verdicts: [`${e.id}:${e.verdict}`] })),
170
253
  ...(m ? m.findings : []).map((x) => ({ ...x, verdicts: x.ids.map((id) => `${id}:${verdictOf[id] || '?'}`) })),
171
254
  ],
172
- overall: figHead ? `${figHead}\n${body}` : body,
255
+ overall: [failHead, figHead, body].filter(Boolean).join('\n'),
173
256
  }
174
257
  const passed = reads.flatMap((r) => r.passed.map((p) => ({ lens: r.lens, ...p })))
175
258
  // linear 관점의 뜀 지점은 상위 10·검증 상한과 별개로 전부 돌려준다 — 0 이 목표라 하나도 숨기지 않는다(linear-read-test.workflow.js 와 같은 셈).
176
259
  const jumps = (reads.find((r) => r.lens === 'linear') || {}).findings || []
177
- return { merged, questions, passed, dropped, jumps, raw: { reads, verdicts, unverified } }
260
+ // 이름 발견(문서가 붙인 이름·정의 없는 이름)도 관점·검증 상한과 별개로 전부 돌려준다 — 검증 단계까지 간 것이 164건 중 4건이었다(2026-09-26 스킬 검토).
261
+ const names = all.filter((f) => f.class === '이름')
262
+ return { ...(failHead ? { head: failHead, failed: true } : {}), merged, questions, passed, dropped, jumps, names, raw: { reads, verdicts, unverified } }
@@ -24,7 +24,7 @@
24
24
 
25
25
  ```json
26
26
  {"label":"Decide","lead":"정식 배포 전에 정할 것은 5항, 나머지는 차기임.",
27
- "body":"…==5항 중 「바꾼다」가 하나라도 있으면 2차 시험판을 거침==(제2절 흐름도).","refs":["6.6"]}
27
+ "body":"…==5항 중 「바꾼다」가 하나라도 있으면 <독자 화면의 채널 이름> 앱에서 한 번 더 확인함==(제2절 흐름도).","refs":["6.6"]}
28
28
  ```
29
29
 
30
30
  ## 2. 예시 한 명 — 문서 전체를 한 사람의 숫자로 관통
@@ -33,7 +33,7 @@
33
33
  한다. 독자는 숫자 하나를 기억하고 그 숫자가 어디로 흘러가는지 따라간다. 예시 이용자 한 명의 값 한 벌(성별·키 · 측정일 ·
34
34
  측정값 · 산식이 낸 값 · 화면 칸의 값)이 문서 끝까지 같은 숫자로 흐르는 것이 그 모양이다(값은 그 문서의 몫이고 스킬에 적지 않는다).
35
35
  되풀이는 **자리마다 다른 물음**에 답할 때만이다 — 같은 물음(무엇이 빠졌나·얼마였나)에 그림·요지·표·건이 같은 숫자로 답하면
36
- 되풀이다(readability R12f·R12g). 셈은 한 자리(표 또는 요약면 수치)에 두고 다른 자리는 그곳을 가리킨다.
36
+ 되풀이다(readability R12f·R12g). 셈은 한 자리(표 또는 요약면 수치)에 두고 뒤 자리가 그곳을 되짚는다 — 앞 절이 뒤 절을 가리키지 않는다(SKILL 0.5단계 7행).
37
37
  이름은 가린다(캡처의 닉네임 칸은 `shot.mjs --mask`).
38
38
 
39
39
  ## 3. 용어 잠금 표 — 같은 낱말이 두 값을 가리키면 거기서 죽는다
@@ -41,6 +41,8 @@
41
41
  검토 워크플로가 첫째로 잡은 것이 이것이었다: 같은 값 이름이 한 절에서는 산식이 낸 값, 다른 절에서는
42
42
  결과지 칸의 값을 가리켰다. 뜻이 갈리는 낱말은 **판단 요청 절이나 용어 부록에** 표 한 장으로 박는다 —
43
43
  열: 이름 · 값(예시 한 명) · 어떻게 정해지나 · 어디에 보이나. 산식 안의 값과 화면 칸 이름은 다른 낱말을 쓴다.
44
+ **이름 열은 0단계 이름 대응표(SKILL 0단계 물음 5)의 「독자 화면·문서의 이름」에서만 가져온다** — 표로 정의했다고 작업하며 지은 이름이 독자의 말이 되지는 않는다.
45
+ 2026-09-21 반려가 그 모양이었다: 문서 안에 판 이름 넷의 표가 있었는데도 「정의한 적 없는 단어」로 읽혔다(경위: `history.md` §16). 대응표에 원래 이름이 없는 새 개념만 설명형으로 부른다.
44
46
 
45
47
  잠그는 것은 값 이름만이 아니다.
46
48
  - **장치 이름** — `labels` 로 바꾼 건 행 이름(물음·관찰·판단·재현 방법)은 다른 절의 이름·부제·표 열·캡션에 다시 쓰지 않는다.
@@ -68,7 +70,6 @@
68
70
 
69
71
  도판 HTML 머리 주석은 그림 번호가 아니라 `assets[].id` 로 적는다 — 번호는 자리가 매기고 도판을 옮기면 바뀐다. 본문에서 그림을 가리킬 때도
70
72
  번호를 손으로 적지 않고 `{{그림:<id>}}` 토큰으로 쓴다(문법·번호 순서는 `lim-portal-format.md` §11-1·§11-6).
71
- ⚠ 토큰은 **운영 반영 확인 전**이다 — 배포 뒤 열람 DOM 에서 `a.ref[href^="#fig-"]` 를 세어 확인하기 전에 쓰면 고객 화면에 글자로 찍힌다(§11-6).
72
73
 
73
74
  글은 장치 옆 **3줄(220자) 안**. 표의 숫자를 문장으로 되풀이하지 않는다 — 검토가 잡은 둘째 결함이었다
74
75
  (제8절 문단 8개가 표 3개를 문장으로 반복). 산문에 참조 번호(6.6 · 2.12a)를 넣지 않는다 — 번호는 표·건·항목의 몫이다.
@@ -137,47 +138,63 @@
137
138
 
138
139
  `references/reader-review.workflow.js` 를 Workflow 도구로 돌린다. 배열·객체 인자는 JSON 값 그대로 넘긴다(문자열로 싸면 한 덩어리가 된다).
139
140
 
141
+ 먼저 렌더본 텍스트를 **원고 폴더 밖 빈 폴더**에 뽑는다 — `python3 assets/render.py report.json --text <빈 폴더>/본문.txt`.
142
+ 렌더가 성공했을 때만 쓰고, 화면에 보이는 글만 화면 순서대로 담는다(그림·영상 자리는 「[그림]」「[영상]」, 표 칸은 「 | 」 — `origin`·alt·스크립트·화면 전용 크롬은 없다).
143
+ 원고 폴더 안에 쓰면 ⚠ 가 뜬다 — 검토자가 옆 파일(결정 장부·핸드오프)을 열 수 있어서다.
144
+ CSS 가 만드는 글자(장식 기호·인쇄 쪽 번호)는 없고, 클래스 없는 굵게가 줄을 바꾸는 자리는 붙어 나온다 — 뜻은 같고 글자 단위로 화면과 같지는 않다.
145
+
140
146
  ```
141
147
  { file: "<report.json 절대경로>",
148
+ text: "<렌더본 텍스트 절대경로 — 필수>",
142
149
  reader: "<독자 한 줄 — 할 일이 둘이면 둘 다>", example: "<예시 한 명 한 줄>", bitmapBudget: 3,
143
- scratch: "<측정 스크립트·풀어낸 도판을 둘 스크래치 절대경로>", plates: "<도판 HTML·PNG 작업 폴더(선택)>",
150
+ scratch: "<측정 스크립트를 둘 스크래치 절대경로>", plates: "<도판 HTML·PNG 작업 폴더(선택)>",
144
151
  locked: ["<정해진 것 한 줄>", …], latest: "<사용자 최신 지적 원문>",
145
152
  sources: [{ name, path, kind: "code|doc|pdf" }, …],
146
153
  decisions: "<0단계 결정 장부 decisions.json 경로(선택)>", state: "<코드 작업 상태 표 경로(선택)>" }
147
154
  ```
148
155
 
149
156
  `sources` 는 문서마다 달라서 스킬에 경로를 적지 않는다. `state` 를 주면 여덟째 관점이 켜진다.
157
+ `text` 가 없으면 워크플로가 멈춘다. 「처음 읽는 사람」·「선형 읽기」 두 관점은 이 파일 하나만 받고(정해진 것·결정 장부·최신 지적·상태 표·내부 자료는 안 받는다)
158
+ CLAUDE.md 를 싣지 않는 Explore 유형으로 돈다 — 자동 메모리(MEMORY.md)까지 빠지는지는 첫 실행 전사로 확인하기 전이다. 나머지 관점은 위 인자를 그대로 받는다.
159
+ 판 갱신 검토(`edition-review.workflow.js`)와 선형 읽기 실측(`linear-read-test.workflow.js`)도 `args.text` 가 필수다 — 선형 읽기 실측은 `file` 도 받는다 — 렌더본이 원고보다 새로운지 재는 데만 쓰고, 읽는 넷에게는 주지 않는다.
150
160
 
151
161
  | 관점 | 보는 것 | 경계 |
152
162
  |---|---|---|
153
- | first-time 처음 읽는 사람 | 정의 없는 용어 · 앞 절 의존 · 한 낱말 두 값 · 긴 문단 · 산문 참조 번호 | — |
163
+ | first-time 처음 읽는 사람 | 정의 없는 용어 · 앞 절 의존 · 한 낱말 두 값 · 긴 문단 · 산문 참조 번호 · 문서가 붙인 이름(정의가 있어도 — class 이름) | 렌더본 텍스트만 · Explore 유형 · 처방은 원래 이름(뜻풀이·새 이름 아님) |
164
+ | linear 선형 읽기 | 렌더 순서대로 한 번 읽으며 멈추는 지점 전부(`jumps`) · 문서가 붙인 이름(class 이름) | 렌더본 텍스트만 · Explore 유형 · 「N절을 보라」 처방 금지 |
154
165
  | decider 결정권자 | 판단 요청이 3초에 읽히나(물음·두 길·기한) · 수치/링크/건수 어긋남 · 답정너 틀 · **할 일마다** 표지 `meta`·현황 줄·읽는 법 첫 물음에 서나(빠지면 severity 4) · 요약면 수치 칸끼리 겹치는 집합인가 | — |
155
166
  | visual 정보 디자이너 | 그림이면 한 번에 보일 곳 · 표 숫자를 되풀이하는 문단 | — |
156
167
  | source 원본 대조 | 고객이 누를 수 있는 원본(`source_url`·출처 줄 주소·쪽·본문 링크)과 인용의 일치 | 앱 동작은 판정하지 않는다(facts 몫) — 다른 문서에 없다고 거짓이라 하지 않는다 |
157
168
  | structure 구조 | PART 물음 ↔ 절 내용 · PART 사이 되풀이 · 요지·읽는 법 물음이 건의 관찰·판단을 바꿔 말한 곳(문장 쌍 전수) · 요지의 이야기 순서와 칸 수, 근거 문단 `body` 비움 | 처방은 빼기로 |
158
- | figures 도판 | readability R13·§4 를 읽고 `assets[].file` PNG 를 **Read 로 연다**. 같은 이름 `.html` 이 있으면 머리 주석·CSS 를 읽는다. 두 칸이면 틀 윗선·아랫선 y 를 재어 px 차를 evidence 에. 짚을 곳 — R13 위반 · 틀 잘림(윗선 여백 0 포함) · 선택지 도안 가운데 하나만 합성·실사·크게·먼저 · 그림↔본문 반박 · 캡션 「왼쪽은…」 불일치 · 칩 이름과 캡션·표 이름 불일치 | 캡션·alt 만으로 판정하지 않는다. 서버 보관 도판은 `get_report` data URI 를 scratch 에 풀어 연다. 못 연 도판은 `unopened` 에 |
169
+ | 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` 에 |
159
170
  | facts 사실 대조 | FCG 내부 1차 자료로 수치를 다시 계산 · 앱 동작은 `kind:"code"` 파일:줄로만 · 결정의 주체·자리·시점은 결정 장부·`locked` 와 대조 | 판정 못 하면 미확인 |
160
171
  | state 상태 대조(`state` 있을 때) | 「반영·적용·고침·보이지 않음·빠짐」 같은 완료형 문장마다 상태 표의 확인 수준(코드·시험판·실기기·운영) | — |
161
172
 
162
173
  단계.
163
- 1. **Read** — 관점마다 발견 8~14개. 발견마다 `where · problem · severity · class`(사실틀림|구조|읽기|시각|묻기) `· evidence`(연 것·잰 것, 짐작이면 「추정 — 」) `· rewrite · visual`. 재서 통과한 것은 `passed[]` 에 둔다.
164
- 2. **Verify** — severity ≥ 3 가운데 사실틀림 먼저, 그다음 severity 순으로 최대 12건. 같은 순위 안에서는 **관점마다 돌아가며** 뽑는다(뒤 관점이 상한 밖으로 밀리지 않게). 발견마다 반박 에이전트가 원본을 직접 열어
165
- `verdict`(참|부분|거짓|미확인)·`finding_basis`(확인|추정)·`touches_locked`·`noise_kind`(없음|제작|날짜경로|판이력)를 판정한다.
174
+ 1. **Read** — 관점마다 발견 8~14개(선형 읽기는 남김없이). 발견마다 `where · problem · severity · class`(사실틀림|구조|읽기|이름|시각|묻기) `· evidence`(연 것·잰 것, 짐작이면 「추정 — 」) `· rewrite · visual`. 재서 통과한 것은 `passed[]` 에 둔다.
175
+ 2. **Verify** — severity ≥ 3 가운데 사실틀림 먼저, 그다음 severity 순으로 최대 12건 — **읽기·구조 발견에 4칸을 먼저 뗀다**(사실 틀림 가중 때문에 12칸이 다 차 읽기 발견이 늘 검증 밖이었다). 같은 순위 안에서는 **관점마다 돌아가며** 뽑는다(뒤 관점이 상한 밖으로 밀리지 않게). 발견마다 반박 에이전트가 원본을 직접 열어
176
+ `verdict`(진단: 참|부분|거짓|미확인 — 거짓은 반증을 열었을 때만, 확신이 없으면 미확인)·`finding_basis`(확인|추정)·`touches_locked`·`noise_kind`(없음|제작|날짜경로|판이력)·`rx_fit`(처방 적합: 맞음|다시)·`rx_why` 를 판정한다.
177
+ 진단과 처방은 따로다 — 처방이 틀려도 주장이 맞으면 진단은 참이다. class 이름은 「독자가 문서 밖(자기 화면·원본 문서·공급사 공식 명칭)에서 본 글자인가」로 판정하고, 문서 안의 정의·캡션·용어표는 반증이 아니다.
166
178
  원본 위치 한 구절이 빠진 것은 노이즈가 아니다(SKILL 0.5단계 5a). severity < 3·상한 초과·검증 실패는 반영하지 않고 `raw.unverified` 로 간다.
167
- 3. **JS 거름**(결정적) — 거짓 · 정해진 것을 되돌림 · 노이즈 처방 · 근거가 추정뿐 → `dropped[]`(사유 포함, 사람이 되살릴 수 있게). 묻기 → `questions[]`. 사실틀림은 **참·부분**일 때만 독립 항목(미확인이면 합치는 쪽으로 가 「(미확인)」이 붙는다).
168
- 버림이 0 이면 log 에 「버림 0 — 표본 3건 재반박 권함」.
179
+ 3. **JS 거름**(결정적) — **진단 거짓만** → `dropped[]`(사유 포함, 사람이 되살릴 수 있게). 정해진 것을 되돌림 · 노이즈 처방 · 규칙에 걸린 처방(포인터·새 이름 짓기)은 발견을 남기고 `rx` 에 「처방 다시 — 까닭」을 단다. 근거가 추정뿐인지는 `verify.finding_basis` 에 남는다.
180
+ 묻기 → `questions[]`. 사실틀림은 **참·부분**일 때만 독립 항목(미확인이면 합치는 쪽으로 가 「(미확인)」이 붙는다). 버림이 0 이면 log 에 「버림 0 — 표본 3건 재반박 권함」.
169
181
  4. **Merge** — 나머지를 같은 자리끼리 합쳐 severity 합산 상위 10. 비트맵 예산을 지키고, 더하기 처방과 structure 의 빼기 처방이 부딪히면 빼기가 이긴다.
182
+ `rx` 가 달린 발견은 규칙에 맞는 처방을 새로 적는다(이름 발견은 원래 이름으로 — 새 이름·뜻풀이 금지). 선형 읽기 뜀과 이름 발견은 상위 10 과 별개로 전부 돌아간다.
170
183
 
171
- 돌아오는 것: `{ merged: { findings[], overall }, questions[], passed[], dropped[], raw: { reads, verdicts, unverified } }`.
172
- `merged.findings` 앞에는 **참·부분으로 판정된** 사실 틀림이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다. 못 연 도판이 있으면 `overall` 머리 줄이 「⚠ 도판 N장 미열람(id…)」 — 그 도판은 사람이 연다.
184
+ 돌아오는 것: `{ head?, failed?, merged: { findings[](… · rx · verdicts), overall }, questions[], passed[], dropped[], jumps[], names[], raw: { reads, verdicts, unverified } }`.
185
+ `merged.findings` 앞에는 **참·부분으로 판정된** 사실 틀림이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다. 못 연 도판이 있으면 `overall` 머리 줄이 「⚠ 도판 N장 미열람(id…)」 — 그 도판은 사람이 연다. 검토자가 「서버 보관」이라 적은 도판은 「도판 N장 서버 보관」 줄로 따로 선다 — 그 id 가 원고에서 보관 표식(`data` 가 「(생략 — NB)」)인 장인지 확인하고 열람 주소에서 사람이 본다(체크리스트 7단계 독자 검토 행).
186
+ 검증이 1건이라도 실패하거나 관점이 결과를 못 내면 `head` 「검토 미완 — 검증 N건 실패」(관점 실패는 이어 적는다)와 `failed: true` 가 붙고 `overall` 첫 줄도 같다 — 실패를 「발견 0」으로 읽지 않고 검토를 다시 돌린다.
187
+ 통과 기준(SKILL 체크리스트 7단계 독자 검토 행): `jumps` 가운데 심각도 3 이상 0건 · `names` 0건 — 못 없앤 것은 까닭을 핸드오프에.
173
188
 
174
- **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 정해진 것을 되돌리거나 제작 방식·결정 날짜·판 이력을 더하라는 처방은 받지 않는다.
189
+ **Verify 를 통과한 발견만 반영한다 — 검토자 주장도 틀린다.** 정해진 것을 되돌리거나 제작 방식·결정 날짜·판 이력을 더하라는 처방은 받지 않는다 — 그런 처방이 달린 발견은 버리지 않고 `rx` 로 처방만 다시 쓴다.
175
190
  반영 순서는 사실 틀림 → 구조 발견(PART·절 합치기·빼기) → 나머지.
176
191
 
177
- 경위 셋. 고객사 검수 보고(2026-09-08)에서 이 검토가 용어 충돌·수치 불일치·참조 오류·반올림 설명 오류를 잡았다 — 작성자는 못 본 것들이다.
192
+ 경위 넷. 고객사 검수 보고(2026-09-08)에서 이 검토가 용어 충돌·수치 불일치·참조 오류·반올림 설명 오류를 잡았다 — 작성자는 못 본 것들이다.
178
193
  구조 관점은 2026-09-12 — 네 관점 발견 55개가 「경과 / 원인」 두 PART 의 겹침을 한 건도 짚지 못했고, 더하기 처방을 반영하자 겹침이 커졌다.
179
194
  도판·사실 대조 관점과 Verify 는 2026-09-14 — 캡션만 읽은 검토가 틀 대칭 결함을 0건 짚었고, 검토자의 틀린 주장·정해진 것 되돌리기·노이즈 처방이
180
195
  거르지 않은 채 작성자에게 올라갔다.
196
+ 렌더본 텍스트 입력·이름 물음·진단과 처방 분리·실패 머리는 2026-09-26 — 두 독자 관점이 결정 장부·핸드오프를 받거나 스스로 열어 뜻을 메웠고,
197
+ 문서가 정의한 조어는 「정의 있음」으로 통과했고, 진단이 참인 발견이 처방 사유 하나로 버려졌다(경위: `history.md` §17·§18).
181
198
 
182
199
  ## 8. 실물 확인 — dry_run 경고 0 이 목표, 그 다음은 눈
183
200
 
@@ -185,8 +202,8 @@
185
202
  단 경고가 규칙의 뜻과 어긋나는 자리를 짚으면(남의 문서 절 번호를 이 문서의 뒤 절로 세는 것처럼) 원고를 깎지 않는다 — 오탐으로 판정해 렌더러를 고치고 대조군 시험을 둔다. 그 수리가 운영에 오르기 전이면 번호 대신 이름으로 위치를 살린다. 렌더러·서버 관문이 운영보다 앞선 동안 옛 규칙이 내는 경고는 `lim-portal-format.md` §10 통과 기준 표의 행으로 넘긴다.
186
203
  2. 발행 뒤 **열람 주소를 실제로 연다**(Cursor 인앱 브라우저 `iab` 나 헤드리스 크롬). 그림 수·이미지 로드 수·표·흐름도·차트 수를 세고
187
204
  판단 절과 그림 절을 캡처로 본다. 「구조 확인 outline」은 블록이 섰는지만 말하지 보이는지는 말하지 않는다.
188
- 3. 큰 원고(70KB 이상)는 채팅에 붙여넣지 말고 MCP 서버를 자식으로 띄워 파일 인자로 보낸다
189
- (`node mcp/genetics.mjs` 를 stdio JSON-RPC 로 — 스크래치 `mcp-call.mjs` 30줄).
205
+ 3. 큰 원고(70KB 이상)는 채팅에 붙여넣지 말고 파일 인자로 보낸다 — `node assets/mcp-call.mjs <도구> <인자.json> [결과 파일]`
206
+ (회차 폴더에 같은 도구를 다시 짓지 않는다 — 그렇게 지은 사본들이 64KiB 넘는 응답에서 한글을 깨뜨렸다).
190
207
 
191
208
  ## 9. 하지 않는 것
192
209
 
@@ -1,24 +0,0 @@
1
- {
2
- "name": "scripts",
3
- "lockfileVersion": 3,
4
- "requires": true,
5
- "packages": {
6
- "": {
7
- "dependencies": {
8
- "playwright-core": "^1.62.1"
9
- }
10
- },
11
- "node_modules/playwright-core": {
12
- "version": "1.62.1",
13
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
14
- "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
15
- "license": "Apache-2.0",
16
- "bin": {
17
- "playwright-core": "cli.js"
18
- },
19
- "engines": {
20
- "node": ">=20"
21
- }
22
- }
23
- }
24
- }