genetics-mcp 2.17.1 → 2.18.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 (26) hide show
  1. package/bin/aarch64-apple-darwin/gcell +0 -0
  2. package/bin/manifest.json +2 -2
  3. package/bin/x86_64-apple-darwin/gcell +0 -0
  4. package/genetics.mjs +71 -16
  5. package/package.json +1 -1
  6. package/skills/genetics-report/SKILL.md +146 -131
  7. package/skills/genetics-report/assets/example/cases-briefing-designs.json +32 -0
  8. package/skills/genetics-report/assets/example/plate-pair-after.png +0 -0
  9. package/skills/genetics-report/assets/example/plate-pair-before.png +0 -0
  10. package/skills/genetics-report/assets/example/plate-pair.html +25 -0
  11. package/skills/genetics-report/assets/example/report-briefing-designs.json +736 -0
  12. package/skills/genetics-report/assets/example/report-briefing.json +27 -22
  13. package/skills/genetics-report/assets/example/report.json +5 -5
  14. package/skills/genetics-report/assets/figure.mjs +390 -63
  15. package/skills/genetics-report/assets/mockshot.mjs +3 -2
  16. package/skills/genetics-report/assets/pair.mjs +138 -0
  17. package/skills/genetics-report/assets/shot.mjs +87 -19
  18. package/skills/genetics-report/references/content-pipeline.md +34 -33
  19. package/skills/genetics-report/references/history.md +117 -0
  20. package/skills/genetics-report/references/illustrations.md +16 -12
  21. package/skills/genetics-report/references/lim-portal-format.md +175 -91
  22. package/skills/genetics-report/references/linear-read-test.workflow.js +47 -0
  23. package/skills/genetics-report/references/process.md +152 -90
  24. package/skills/genetics-report/references/readability.md +48 -24
  25. package/skills/genetics-report/references/reader-review.workflow.js +160 -27
  26. package/skills/genetics-report/references/visual-first.md +100 -35
@@ -1,10 +1,12 @@
1
1
  # 가독성 기본 틀 — 도판·표·문단이 같이 지키는 측정 가능한 규칙 (2026-09-10)
2
2
 
3
- > 근거 사례 둘. ① 2026-09-08 앳플리 4차 「글씨 작아서 문제」 — 880px 합성 그림이 본문 단에서 9.5~12.5px 로 떠 반려.
4
- > 처방은 figure.mjs 의 14px 게이트였다. ② 2026-09-09 앳플리 #173 그림 1-2 「가독성에 하자 있는 상태로 만드는 짓을
3
+ > 근거 사례 셋. ① 2026-09-08 고객사 검수 보고 「글씨 작아서 문제」 — 880px 합성 그림이 본문 단에서 9.5~12.5px 로 떠 반려.
4
+ > 처방은 figure.mjs 의 14px 게이트였다. ② 2026-09-09 고객사 결정 브리핑의 개념도 「가독성에 하자 있는 상태로 만드는 짓을
5
5
  > 왜 계속 하느냐, 가독성이 무엇인지 모르나」 — 그 게이트를 **통과한** 그림이 반려됐다. 라벨 15px · 행간 1.3 · 라벨 간격 2px ·
6
6
  > 표식 간 7px · 띠 대비 1.2:1 · 뜻 색 5 · 그림 안 문단 150자 · 1x 손실 인코딩. 재던 것은 글자 크기 하나였고 나머지는
7
- > 재지 않았다. **가독성은 글자 크기가 아니라 열 가지 잣대의 합이다.** 이 문서가 그 잣대의 값을 정한다.
7
+ > 재지 않았다. ③ 2026-09-14 같은 브리핑 「A/B 대칭으로 보여줘야 되는 거 아님? 시각적 자료에 신경 안 씀?」 — 두 캡처가
8
+ > 위아래로 쌓이고 틀 안에 우리 글자가 얹힌 비교 컷이 게이트 ✗0 으로 나갔다. 이번에는 **캡처 틀의 문법**을 재지 않았다(R13).
9
+ > **가독성은 글자 크기가 아니라 여러 잣대의 합이다.** 이 문서가 그 잣대의 값을 정한다.
8
10
 
9
11
  ## 0. 원칙 한 줄
10
12
 
@@ -16,37 +18,50 @@
16
18
  ## 1. 규칙표 — 값의 정본
17
19
 
18
20
  「표시 px」는 열람 화면의 CSS px 다. 기준 창 1440×900, 본문 단 **760px**, 전폭 **1030px**, 본문 활자 **17px / 1.75**.
19
- `figure.mjs` 기본값 · `gazette.py` 상수 · `visual-first.md` §4 · `lim-portal-format.md` §11-1 은 **이 표를 가리키고 값을 되풀이하지 않는다**
20
- (같은 값이 세 곳에 있으면 갈린다 — 고칠 때는 여기와 코드 상수를 함께 고친다).
21
+ `figure.mjs` 계약 상수(`node figure.mjs --spec` 이 JSON 으로 낸다) · `gazette.py` 상수 · `visual-first.md` §4 · `lim-portal-format.md` §11-1 은
22
+ **이 표를 가리키고 값을 되풀이하지 않는다**(같은 값이 세 곳에 있으면 갈린다 — 고칠 때는 여기와 코드 상수를 함께 고친다).
23
+ 두 칸 비교 컷의 칸 폭·허용 px 처럼 코드가 파생하는 값은 숫자 대신 상수 이름(`PAIR`·`R13`)으로 가리킨다.
21
24
 
22
- | # | 규칙 | 값 | 재는 곳 | 어기면 |
23
- |---|---|---|---|---|
24
- | R1 글자 | 도판 **주 글자**(글자 수 가중 최빈 크기)의 표시 크기 ≥ 본문 **17px**. 어떤 글자도 ≥ **15px** | figure.mjs | ✗ |
25
+ 「재는 곳」이 「사람 눈」인 값은 기계가 안 잰다 — 초록 결과를 그 값의 통과로 읽지 않는다.
26
+
27
+ | # | 규칙 · 값 | 재는 곳 | 어기면 |
28
+ |---|---|---|---|
29
+ | R0 입력 | 소스 폭(선택자 요소의 CSS 폭)이 표시 폭과 2px 넘게 다르면 판정 근거가 틀린 것이다 — 전폭 도판은 `--wide`, 본문 단은 `.wrap` 폭을 본문 단 폭으로. 출력 머리에 「표시 폭 N px — 근거 …」가 찍힌다. `--wide` 로 판정한 PNG 는 `assets[].wide: true` 로 실어야 그 판정이 성립한다 | figure.mjs | ✗(`--allow R0` 만 면제) |
30
+ | R0b 빈 캡처 | 캡처 img 를 64×64 로 줄여 셌을 때 최빈 색이 98% 이상이면 백지다 — 외부 자르기 도구가 낸 적이 있다. 자르기는 도판 HTML 틀(overflow·img 오프셋)이나 `shot.mjs --clip` 으로(§3) | figure.mjs | ✗ |
31
+ | R1 글자 | 도판 **주 글자**(글자 수 가중 최빈 크기)의 표시 크기 ≥ 본문 **17px**. 어떤 글자도 ≥ **15px**. 캡처 안 글자는 R1c | figure.mjs | ✗ |
32
+ | R1c 캡처 안 글자 | 그림마다 `<img data-dpr data-min-text>`(없으면 `--img-dpr`·`--img-text`). 표시 크기 = 최소 글자(CSS px) × 칸 이미지 폭 ÷ (원본 px ÷ dpr) × 표시 배율 ≥ **15px**. 둘 중 하나라도 적지 않으면 ✗ — 기본값이 없다. `data-min-text` 는 화면의 가장 작은 글자 CSS px, `data-dpr` 은 mockshot·shot 2, 실기기 캡처는 기기 배율, PDF 추출 이미지는 1. 원본 폭이 휴대폰 3배 캡처 폭(목록은 `PHONE_3X_WIDTHS`)인데 dpr≠3 이면 ⚠ | figure.mjs | ✗ · 3배 폭 ⚠ |
25
33
  | R2 위계 | 그림 안 **제목(h1~h3) 금지** — 캡션이 제목이다. 강조 값은 주 글자 ×1.3 이상. 크기 단계 ≤ 3 | figure.mjs | 제목 ✗ · 나머지 ⚠ |
26
34
  | R3 행간 | 2줄 이상으로 접히는 요소의 line-height ≥ **1.4** (본문 문단은 1.75 로 서식이 고정) | figure.mjs | ✗ |
27
35
  | R4 정보량 | 그림 안 **문장 0** — 요소 하나의 글자 > 60자 또는 2줄 이상의 서술문 금지. 설명은 캡션 한 문장과 본문으로 | figure.mjs | ✗ |
28
36
  | R5 근접성 | 텍스트 상자 겹침 **0** · 텍스트 상자 사이 ≥ **1em** · 라벨 중심↔자기 표식 가로 오프셋 ≤ 1em · 표식 사이 ≥ 0.5em | figure.mjs | 겹침·표식 ✗ · 간격·오프셋 ⚠ |
29
37
  | R6 대비 | 글자/배경 ≥ **4.5:1**(24px 이상 굵은 글자 3:1). 표시 19px 미만 회색 글자는 7:1 미만이면 ⚠. **뜻을 나르는 도형**(띠·막대·핀)/배경 ≥ **3:1**. 배경은 포털 도판 바탕 `#f5f6f7` 까지 합성 | figure.mjs | ✗ |
30
38
  | R7 색 수 | 채도 있는 색(뜻 색) ≤ **3** + 검정·회색·흰. HSL 색상 30° 버킷으로 센다. 같은 뜻엔 같은 색 | figure.mjs | ✗ |
31
- | R8 선명도 | 출력 픽셀 폭 ≥ 표시 폭 × **1.8**(서버 긴 변 상한 1400 에서 최대 1.84). 평면 그림(HTML 개념도·도표)은 `provider: deterministic`(무손실). 화면 캡처만 `user-provided`(손실 사다리) | figure.mjs · genetics.mjs 힌트 | ⚠ (늘려 그려지면 ✗) |
32
- | R9 세로 | 표시 높이 ≤ **702px**(0.78 × 900) — 포털은 `max-height: 78vh` 로 세로 긴 그림의 **폭을 깎는다**. 넘으면 위아래 두 장으로 | figure.mjs · gazette.py(로컬 render.py — 운영 dry_run 은 렌더러 배포 뒤) | ⚠ |
39
+ | R8 선명도 | 출력 픽셀 폭 ≥ 표시 폭 × min(**1.8**, 1400 ÷ 표시 폭) − 0.01 — 서버 긴 변 상한이 1400 이라 본문 단 1.79 · 전폭 1.35 가 하한이다. 평면 그림(HTML 개념도·도표)은 `provider: deterministic`(무손실). 화면 캡처만 `user-provided`(손실 사다리) | figure.mjs · genetics.mjs 힌트 | ⚠ (늘려 그려지면 ✗) |
40
+ | R9 세로 | 표시 높이 ≤ **702px**(0.78 × 900) — 포털은 `max-height: 78vh` 로 세로 긴 그림의 **폭을 깎는다**. 넘으면 위아래 두 장으로. 단 비교 컷은 위아래로 가르지 않고 두 칸을 같은 구간으로 자른다(R13a). figure.mjs 는 R9 를 판정 맨 앞(R0 다음)에 찍고 이 폭에서 허용 소스 높이를 알린다 — R1·R1c ✗ 가 세로 초과로 폭이 깎인 결과면 그 꼬리말이 붙는다(높이부터 줄인다) | figure.mjs · gazette.py dry_run | ⚠ |
33
41
  | R10 중복·용어 | 제목·캡션·본문이 같은 말을 세 번 하지 않음. 통계 기호(SD·σ·p)는 부록에만 — `--forbid` 정규식 | figure.mjs 옵션 · 사람 눈 | ✗(forbid) |
34
42
  | R11 본문·표 | 문단 사이 ≥ 1.5em · 한 줄 한글 14~45자 · 헤더 ≥ 본문 ×1.5 굵게 · 문단 ≤ 220자 · 산문 참조 번호 ≤ 3 | gazette.css 토큰 · gazette-report `read.mjs` · §10-1 | ⚠ |
35
- | R11a 문장 | **한 문장 ≤ 60자**(공백 제외). 관보는 한 문장을 한 줄로 세우므로 문장 길이가 곧 줄 길이 — 본문 단은 한 줄에 45자, 60자면 1.4줄. 판단 항목·건처럼 가장 많이 읽는 자리는 **45자 안**으로 써 한 줄에 세운다. 접히는 문장은 마침표에서 둘로 끊는다(실측: #173 4판에서 45~57자 문장 20개가 한 번씩 접혔음) | gazette.py `text_scan` | ⚠ |
36
- | R11b 표 칸 | 칸 ≤ **20자**(4열 이하) · ≤ **12자**(5열 이상) — 열 폭 130~180px 에서 두 줄. 설명은 ※ 주석·본문으로. 흐름도 칸 ≤ 20자·note ≤ 12자(칸 폭 200px), 조건은 note. 칸에 넣던 숫자 나열은 **행으로 푼다**(「0~6세 995 · 7~12세 5,368 …」 한 칸 → 네 행) | gazette.py `text_scan` | ⚠ |
37
- | R11c 도판 글 | 캡션 ≤ **60자** 한 문장(「무엇을 보라」만) · 출처 줄 ≤ 80자 | gazette.py `text_scan` | ⚠ |
38
- | R11d 요약면 | 현황 줄 ≤ **80자** 한두 문장 · 수치 칸 라벨 ≤ **16자**(이름표) · 요약 문단 본문 ≤ 160자 세 문장 | gazette.py `text_scan` | ⚠ |
39
- | R11e 절 머리 | 도입문 ≤ 90자 두 문장 · 여백 주석 why 30 · key 45 · advice 40자(19자/줄 단) | gazette.py `text_scan` | ⚠ |
40
- | R11f 판단 항목 | 본문 ≤ 220자 네 문장 — 첫 문장은 기한, 상세는 건으로 | gazette.py `text_scan` | ⚠ |
41
- | R11g 부연 | 한 문장에 괄호 부연 ≤ 1, 괄호 안 ≤ 20자. 숫자 나열은 문장이 아니라 표 | gazette.py `text_scan` | ⚠ |
42
- | R11h 과정 어휘 | 본문에 **진행 상태 어휘 0** — 「정해짐 · 회신 대기 · 남은 물음 · 정해진 것 · N판」, 개수로 상태를 세는 표 제목(「정해진 것 셋과 남은 것 하나」). 독자는 앞 회차·회신·판을 모른다. 정해진 것은 **사실**로 배경(요지)에, 결정 요청에는 **지금 열린 것만**, 회차는 `history` 에만. 원고는 직전 판의 차분이 아니라 독자의 0 상태에서 다시 쓴다(2026-09-11 「뒷 내용을 나는 알고 너는 알아도 독자는 모름」) | gazette.py `text_scan` | ⚠ |
43
- | R12 덩어리 층 | 글 층(R11)이 문장·칸 하나의 길이를 재면 이 층은 **덩어리**(※ 주석 · 판단 항목 · 표·흐름도 · 요청 장치 · 도판 출처 · 같은 항의 되풀이)의 **문장 수와 자릿수**를 잰다. 값은 R12a~f. 전부 ⚠ 이고 거절이 아니다(실사례 2026-09-11, 고객사 결정 브리핑 — 문장은 짧아졌는데 같은 항이 여러 자리에 되풀이돼 반려) | a·b·e gazette.py `text_scan` · c·d·f 없음 — 사람 눈 | ⚠ · — |
43
+ | R11a 문장 | **한 문장 ≤ 60자**(공백 제외). 관보는 한 문장을 한 줄로 세우므로 문장 길이가 곧 줄 길이 — 본문 단은 한 줄에 45자, 60자면 1.4줄. 판단 항목·건처럼 가장 많이 읽는 자리는 **45자 안**으로 써 한 줄에 세운다. 접히는 문장은 마침표에서 둘로 끊는다(실측: 고객사 결정 브리핑의 앞선 판에서 45~57자 문장 20개가 한 번씩 접혔음) | gazette.py `text_scan`(60자) · 45자는 사람 눈 | ⚠ |
44
+ | R11b 표 칸 | 칸 ≤ **20자**(4열 이하) · ≤ **12자**(5열 이상) — 열 폭 130~180px 에서 두 줄. 설명은 ※ 주석·본문으로. 흐름도 칸 ≤ 20자·note ≤ 12자(칸 폭 200px), 조건은 note. 칸에 넣던 숫자 나열은 **행으로 푼다**(「A 구간 1,234 · B 구간 5,678 …」 한 칸 → 구간마다 한 행) | gazette.py `text_scan`(칸·흐름도 칸·note) · 숫자 나열은 사람 눈 | ⚠ |
45
+ | R11c 도판 글 | 캡션 ≤ **60자** 한 문장(「무엇을 보라」만 — 문장 수도 센다) · 출처 줄 ≤ **40자**, 원본 위치 한 구절만(R12e) | gazette.py `text_scan` | ⚠ |
46
+ | R11d 요약면 | 현황 줄 ≤ **80자** 한두 문장 · 수치 칸 라벨 ≤ **16자**(이름표) · 요약 문단 본문 ≤ 160자 세 문장 | gazette.py `text_scan`(자수) · 문장 수는 사람 눈 | ⚠ |
47
+ | R11e 절 머리 | 도입문 ≤ 90자 두 문장 · 여백 주석 why 30 · key 45 · advice 40자(19자/줄 단) | gazette.py `text_scan`(자수) · 도입문 문장 수는 사람 눈 | ⚠ |
48
+ | R11f 판단 항목 | 본문 ≤ 220자 네 문장 — 첫 문장은 기한, 상세는 건으로 | gazette.py `text_scan`(자수 · 문장 수는 R12b) | ⚠ |
49
+ | R11g 부연 | 한 문장에 괄호 부연 ≤ 1, 괄호 안 ≤ 20자(공백 제외). 숫자 나열은 문장이 아니라 표 | gazette.py `text_scan`(괄호 수 · 괄호 안 자수) · 숫자 나열은 사람 눈 | ⚠ |
50
+ | R11h 과정 어휘 | 본문에 **진행 상태 어휘 0** — 「정해짐 · 회신 대기 · 남은 물음 · 정해진 것 · N판」, 개수로 상태를 세는 표 제목(「정해진 것 셋과 남은 것 하나」), 주체 없는 결정 명사(「정한 사항」·「결정된 사항」). 독자는 앞 회차·회신·판을 모른다. 정해진 것은 **사실**로 배경(요지)에 — 다만 결정권이 고객에게 있는 항은 고객이 정한 기록을 원고 밖에서 확인한 뒤에만(lim-portal-format §12.3b 「배경」 행). 결정 요청에는 **지금 열린 것만**, 회차는 `history` 에만. 원고는 직전 판의 차분이 아니라 독자의 0 상태에서 다시 쓴다(2026-09-11 「뒷 내용을 나는 알고 너는 알아도 독자는 모름」) | gazette.py `text_scan` · 결정 기록 확인은 사람 눈 | ⚠ |
51
+ | R12 덩어리 층 | 글 층(R11)이 문장·칸 하나의 길이를 재면 이 층은 **덩어리**(※ 주석 · 판단 항목 · 표·흐름도 · 요청 장치 · 도판 출처 · 같은 항의 되풀이)의 **문장 수와 자릿수**를 잰다. 값은 R12a~g. 전부 ⚠ 이고 거절이 아니다(실사례 2026-09-11, 고객사 결정 브리핑 — 문장은 짧아졌는데 같은 항이 여러 자리에 되풀이돼 반려 · 2026-09-12 회신 없는 정황 보고 — 같은 건이 두 PART 에 문장만 바꿔 섬) | a·b·e gazette.py `text_scan` · c·d·f·g 없음 — 사람 눈(g 는 독자 검토 「구조」 관점도) | ⚠ · — |
44
52
  | R12a 표 주석 | `tables[].note`(※ 주석) ≤ **2문장** — 문장은 렌더러가 문장 블록을 세우는 판별(`boundary.py`)로 센다 — 날짜 마침표(「2026. 9. 30.」·「9. 8.」)는 경계가 아니다. R11a 의 문장 길이도 같은 분할이다. 셋째 문장부터 ⚠ | gazette.py `text_scan` | ⚠ |
45
53
  | R12b 판단 항목 문장 | `open_items[].body` ≤ **4문장**(R11f 의 「네 문장」과 같은 값 — 자수 220 은 R11f, 문장 수는 여기서 센다). 다섯째 문장부터 ⚠ | gazette.py `text_scan` | ⚠ |
46
54
  | R12c 표 하나 | 한 절에 **같은 행 키**(같은 항·같은 선택지를 행으로 세운) 표·흐름도 ≤ **1** | 없음 — 사람 눈 | — |
47
55
  | R12d 요청 장치 | `cta` 기본 **[]** — 결론 한 줄은 `notice.text` 의 몫(lim-portal-format §12.3b) | 없음 — 사람 눈 | — |
48
- | R12e 도판 출처 | 출처 줄(`assets[].source_text`)에 **원본 이름 + 쪽/행 + (주소 또는 건 §)** — 보이는 출처 줄인 `source_text` 에 쪽·행·주소·건 § 가운데 하나는 있어야 한다(R11c 의 80자 안에서). `source_url` 은 도판에 그려지지 않으므로 대신하지 못한다. 기계는 고객·제3자 자료에서 낸 도판(`provider:"user-provided"`)만 센다 — 그린 도판(`deterministic`)·서버가 그린 삽화(`higgsfield`)는 세지 않는다. 파일로 싣는 합성은 `user-provided` 라 출처 줄에 바탕 도안의 자리를 적는다 | gazette.py `text_scan`(W2) | ⚠ |
56
+ | R12e 도판 출처 | 출처 줄(`assets[].source_text`)은 **원본 위치 한 구절** — 쪽·행·주소·건 §·부록 N 가운데 하나(R11c 의 40자 안). 결정 절처럼 건보다 앞에 서는 도판은 뒤 절 § 대신 규격이 실린 부록 N 을 가리킨다(뒤 절 § 는 R12h 에 걸린다). 제3자 자료에서 낸 도판은 그 한 구절로 독자가 원본에 닿아야 한다(주소, 또는 `source_url` 이 있는 건 §). `source_url` 은 도판에 그려지지 않으므로 대신하지 못한다. **제작 방식**(합성·재현·예시·자리 표시·잘라냄·배율·촬영 환경)과 결정 경로는 `origin`(렌더 안 됨)과 도판 HTML 머리 주석에만 적는다. 바깥 원본이 없는 우리 도안·흐름은 출처 줄을 비운다. 그림 번호는 출처 줄에 쓰지 않는다(번호는 옮겨 다닌다). 정직성은 캡션의 한 낱말(「— 도안」·「— 재현」·「— 합성」)이면 된다. 기계는 두 가지를 센다 — 위치 없음은 `provider:"user-provided"`(파일로 싣는 합성 포함)만(W2), 제작 설명·그림 번호는 `role:"figure"` 전부의 출처 줄. 캡션은 안 본다 | gazette.py `text_scan`(W2 · 제작 메타) · 캡션·부록의 노이즈는 사람 눈 | ⚠ |
49
57
  | R12f 되풀이 | **같은 항의 이름·기한**이 문서 전체에서 서는 자리 ≤ **3** — 네 자리부터는 되풀이 | 없음 — 사람 눈 | — |
58
+ | R12g PART 사이 | 같은 사실(시각·금액·건 제목·핵심 명제)이 **두 PART** 에 서면 한쪽은 건 번호로 가리키기만 한다. 요지·대조표·건(같은 건의 세 배율)은 한 PART — 요청 없는 문서에서 「경과 / 원인」으로 가르지 않는다(process.md 「PART 를 먼저 가른다」). 두 절 `marg.key` 가 낱말만 다르고 같은 명제면 되풀이다 — 글자 비교로는 안 잡힌다. 요약면 수치·요지 lead 가 문서 전체를 줄여 말하는 것은 예외(요지 문단이 건을 하나씩 옮겨 적으면 예외 아님). 실사례 2026-09-12: PART I 사실 27개 중 21개가 PART II 에 다시 섬, dry_run 경고 0 | 없음 — 독자 검토 「구조」 관점 · 사람 눈(렌더러 ⚠ 는 제안 단계) | — |
59
+ | R12h 선참조 | 앞 절이 **뒤 절**을 가리키지 않는다 — 요지 문단·판단 항목 본문의 `§n.n`(근거는 `refs[]` 로만 — 「해당 절」·「근거」 단추가 되므로 뜀이 아니다), 흐름도 「건」 열의 뒤 절 건 번호, 도판 출처 줄의 뒤 절 `§`, 「NN절」 낱말. 낱말·규칙·예시 선언은 **첫 등장 자리에서 한 구절**(부록·앞 회차 보고에만 두지 않는다). 뒤 절이 앞 절을 되짚는 방향만 허용. 되풀이 금지(R12g)와 다른 축이다 — 되풀이를 빼되 그 자리에서 뜻이 서게 하는 한 구절은 남긴다. 실사례 2026-09-14 고객사 3차 보고 「읽다가 시험보는 줄 알 것 같음」: 2판 교차 참조 62 → 순서를 고친 3판에도 선형 읽기 뜀 지점 45(정의가 부록·원고 밖 / 앞 절의 뒤 절 번호) → 4판 0 | gazette.py `text_scan`(W5 — 절 i 의 산문(이름·부제·도입문·문단·표·흐름도 좌표·칸·여백 주석·같은 그룹의 건·판단 항목·강조문·도판 캡션·출처 줄)에서 앞 숫자가 i 보다 큰 `§n.n`·「NN절」을 센다. `summary[]`·`refs[]` 는 안 센다. 본보기 두 벌의 뒤 절 참조 10곳과 서식 문서 §12.5b 를 같은 날 같이 고쳤다) · 정의 위치(①)는 독자 검토 「선형 읽기」 관점(`reader-review.workflow.js` linear, jumps 0 목표) · 사람 눈 | ⚠ · — |
60
+ | R13 캡처 틀 문법 | 캡처를 품은 **틀**을 선언 없이 찾는다 — img 에서 위로 올라가며 테두리가 보이는 첫 조상(자기 포함), 없으면 가장 바깥 overflow 자르개, 그것도 없으면 img 자신(`figure.mjs` `frameOf`). 틀이 둘 이상이면 「비교 컷」이다. 값은 R13a~d. 틀이 하나뿐인 도판(번호 표식 캡처·합성 한 장)에는 R13a 의 가로세로비·R13b·R13c 만 선다. 알려진 한계: 칩 이름표까지 품은 카드에 테두리를 두르면 그 카드가 틀로 잡혀 칩 글자가 R13b 로 걸린다 — 테두리는 캡처 상자에 둔다 | figure.mjs | a·b·c ✗ · d ⚠ |
61
+ | R13a 좌우 같은 틀 | 비교 틀은 **좌우로 나란히** 선다 · 나란한 틀의 높이 차·폭 차 ≤ `R13.heightTol`·`widthTol`(2px) · 이미지 배율 차 ≤ 1% · 가로세로비 왜곡 ≤ 1%(왜곡은 틀 하나여도). 칸 폭 = (표시 폭 − 좌우 여백×2 − 사이) ÷ 2 — 숫자는 `PAIR`(전폭 `wide` · 본문 단 `column`)가 정본이고 `pair.mjs` 가 그대로 쓴다. 틀 높이는 두 칸 중 큰 쪽, 짧은 쪽은 흰 여백 — 늘리거나 따로 잘라 맞추지 않는다. 예외: 모든 틀이 가로 띠(폭 ≥ 높이×3)이고 같은 폭이면 위아래 허용 | figure.mjs · pair.mjs | ✗ |
62
+ | R13b 틀 안 글자 0 | 틀 안(틀의 자손이거나 글리프 중심이 틀 안)에 HTML 글자 **0**. 빈 자리는 회색 점선이 말하고, 이름은 틀 위 칩(R13d)이 말한다. 예외는 원 번호 한 글자(①~⑳ — 읽는 순서) | figure.mjs | ✗ |
63
+ | R13c 표식 색 | 틀 안 표식은 **검정 실선** = 있는·바뀔 자리, **회색 점선** = 빠진 자리, **원 번호** = 읽는 순서만(이름표로 쓰지 않는다). 틀 안 유채색 표식(배경·테두리·원 번호 글자색) 0 — 유채색은 캡처 틀이 없는 상태 개념도에만. 캡처에 구워 넣은 배지는 기계가 못 보므로 `shot.mjs` 기본값(검정 원·흰 링)이 집행자다 | figure.mjs · shot.mjs 기본값 | ✗ |
64
+ | R13d 칩 이름표 | 비교 틀마다 틀 위(64px 안)에 **밝은 무채색 칩** 하나 — 칩 글자는 문서가 그 대상을 부르는 이름 그대로(기본 「그대로 두면」/「바꾸면」, 한눈표가 없으면 캡션의 이름). 칩 없음·검정·유채색 ⚠. `--caption "…"` 을 주면 칩 글자가 캡션에 없을 때 ⚠(alt·origin 은 대조 안 함). 칩 문구의 뜻과 도판 사이 대칭(선택지마다 같은 촬영 규격·같은 틀인가)은 기계가 못 가른다 | figure.mjs · 독자 검토 「도판」 관점 · 사람 눈 | ⚠ |
50
65
 
51
66
  ## 2. 사람 눈이 보는 것 (기계가 못 잰다)
52
67
 
@@ -55,16 +70,25 @@
55
70
  - 캡션이 「무엇을 보라」인가 — 그림이 이미 보여 주는 것을 되풀이하면 장식이다.
56
71
  - 라벨이 **자기 표식**을 가리키는가 — 기계는 거리만 잰다.
57
72
  - 한 그림에 **한 생각**인가 — 패널 둘까지.
73
+ - 선택지 도판들이 **같은 잣대**인가 — 한 선택지만 합성·실사·크게·먼저가 아닌가, 칩 이름이 캡션·표의 이름과 같은가(R13d 의 뜻 쪽).
58
74
  - 열람 화면을 **실제로 열어** 본다 — 반려는 늘 화면에서 났다(process.md 7단계 눈 검수).
59
75
 
60
76
  ## 3. 도판을 그리는 순서 (figure.mjs 와 짝)
61
77
 
62
- 1. **폭은 표시 폭 그대로**(760, 전폭이면 1030) 로 HTML 을 쓴다 — 소스 px = 표시 px 라 셈이 1:1 이 된다. 높이는 폭의 0.4~0.9배.
78
+ 1. **폭은 표시 폭 그대로**(760, 전폭이면 1030) 로 HTML 을 쓴다 — 소스 px = 표시 px 라 셈이 1:1 이 된다(어긋나면 R0). 높이는 폭의 0.4~0.9배.
63
79
  2. 글자는 라벨 17px 이상, 값(강조) 22px 굵게, 보조 15px. 행간 1.45~1.5. 회색은 `#595959`(7:1) 이하로 내려가지 않는다.
64
- 3. 뜻 색 셋을 먼저 정한다. 띠·막대는 **불투명**하게 칠하고 그 안 글자는 흰색 굵게(각 ≥ 4.5:1). 표식은 검정.
80
+ 3. 뜻 색 셋을 먼저 정한다. 띠·막대는 **불투명**하게 칠하고 그 안 글자는 흰색 굵게(각 ≥ 4.5:1). 표식은 검정 — 캡처 틀 안의 표식 문법은 R13c(검정 실선·회색 점선·원 번호).
65
81
  4. 문장은 쓰지 않는다. 라벨은 12자 안. 설명하고 싶은 것은 캡션 한 문장으로, 여러 값은 표로.
66
- 5. `node figure.mjs in.html out.png` — 기본값(scale auto → 1400px 출력)으로 돌려 ✗ 0 을 확인한다. 결과의 「provider 권고」대로 `assets[]` 에 적는다.
67
- 6. 발행 뒤 열람 화면을 캡처해 라벨 크기와 선명도를 눈으로 본다.
82
+ 5. 캡처를 앉히면 img 마다 `data-dpr`·`data-min-text` 를 적는다(R1c). 두 칸 비교 컷은 손으로 짜지 않고 `pair.mjs` 로 조립한다(R13a) — 전후 두 장은 `shot.mjs --hide … --after … --geom` 으로 찍어 좌표를 코드가 옮긴다.
83
+ 6. `node figure.mjs in.html out.png` — 기본값(scale auto → 1400px 출력)으로 돌려 ✗ 0 을 확인한다. ✗ 가 있으면 `out.png` 는 안 바뀌고 `<이름>.fail.png` 가 남는다. 결과의 「provider 권고」대로 `assets[]` 에 적는다.
84
+ 7. 발행 뒤 열람 화면을 캡처해 라벨 크기와 선명도를 눈으로 본다.
85
+
86
+ 하지 않는 것 둘.
87
+ - **관문 인자를 셸 변수에 담지 않는다** — zsh 는 `$VAR` 를 낱말로 가르지 않아 `--wide` 가 사라진다(figure.mjs 는 모르는 인자를 exit 2 로 거절한다). 여러 장은 `figure.mjs --batch plates.json`(인자는 문자열 배열) 한 번으로 굽고 예산 합계도 거기서 센다.
88
+ 매니페스트 한 줄 예: `[{"html":"a.html","out":"a.png","args":["--wide","--img-dpr","2"]}]` — 경로는 매니페스트 파일 기준이고 `args` 는 판정 인자만 받는다.
89
+ `--report report.json` 을 주면 provider 를 `assets[].file` 의 **파일 이름이 out 이름과 같을 때만** 대조하고, 아니면 캡처 유무로 추정해 「(추정)」을 찍는다.
90
+ `cwebp` 가 없으면 크기를 못 세므로 **exit 2 로 멈춘다** — 합계 0B 를 예산 통과로 읽지 않는다.
91
+ - **외부 크롭 도구로 자르지 않는다** — 백지를 낸 적이 있다(R0b). 자르기는 도판 HTML 틀(overflow·img 오프셋)이나 `shot.mjs --clip`(playwright clip)으로 한다.
68
92
 
69
93
  ## 4. 출처
70
94
 
@@ -1,44 +1,177 @@
1
- // 독자 검토 워크플로 — 발행 전에 원고를 네 관점으로 읽힌다(visual-first.md §7).
2
- // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다.
3
- // args = { file: "<report.json 절대경로>", reader: "<독자 한 줄: 누구이며 무엇을 결정하나>",
4
- // example: "<예시 한 명 한 줄: 번호·측정일·실제 값>", bitmapBudget: 3 }
5
- // 돌아오는 것: merged.findings[](where·problem·severity·rewrite·visual) 상위 10 + overall(작성자가 할 일 5줄).
6
- // 2026-09-08 앳플리 4차에서 이 검토가 용어 충돌(한 낱말 두 값)·수치 불일치·참조 오류·반올림 설명 오류를 잡았다.
1
+ // 독자 검토 워크플로 — 발행 전에 원고를 여덟 관점(상태 표가 있으면 아홉)으로 읽히고, 발견을 반박 검증해 거른 뒤 합친다(visual-first.md §7).
2
+ // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다. 배열·객체는 JSON 값 그대로 넘긴다(문자열로 싸면 한 덩어리가 된다).
3
+ // args = { file: "<report.json 절대경로>",
4
+ // reader: "<독자 한 줄: 누구이며 무엇을 하나 — 할 일이 둘이면 둘 다>",
5
+ // example: "<예시 한 명 한 줄: 번호·측정일·실제 값>", bitmapBudget: 3,
6
+ // scratch: "<측정 스크립트·풀어낸 도판을 둘 스크래치 절대경로>",
7
+ // plates: "<도판 HTML·PNG 작업 폴더(선택) — assets[].file 경로가 옮겨졌을 때 찾는 곳>",
8
+ // locked: ["<정해진 것 한 줄>", …], latest: "<사용자 최신 지적 원문>",
9
+ // sources: [{ name: "<자료 이름>", path: "<절대경로>", kind: "code|doc|pdf" }, …],
10
+ // decisions: "<0단계 결정 장부 decisions.json 경로(선택)>",
11
+ // 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 },
14
+ // questions[](작성자가 사람에게 물을 것), passed[](관점별로 재서 통과한 것 — 「안 봄」과 가른다),
15
+ // dropped[](버린 발견 + reasons — 사람이 되살릴 수 있게), jumps[](linear 관점의 뜀 지점 전부 — 0 이 목표),
16
+ // raw: { reads, verdicts, unverified } }
17
+ // merged.findings 앞에는 사실 틀림(class 사실틀림) 가운데 **Verify 가 참·부분으로 받은 것**이 합치지 않은 독립 항목으로 서고 개수 제한 밖이다.
18
+ // 미확인으로 판정된 사실 틀림은 합치는 쪽으로 가 problem 끝에 「(미확인)」이 붙는다. 뒤는 합친 상위 10.
19
+ // 못 연 도판이 있으면 merged.overall 머리 줄이 「⚠ 도판 N장 미열람」이다. severity<3·상한 초과·검증 실패는 반영하지 않고 raw.unverified 로.
20
+ // 2026-09-08 고객사 검수 보고 4차에서 이 검토가 용어 충돌(한 낱말 두 값)·수치 불일치·참조 오류·반올림 설명 오류를 잡았다.
7
21
  // 2026-09-11 실사례(고객사 결정 브리핑) 반려 둘에서 더한 것 — decider 관점에 「권고가 제목·열·색·요약에 배어 동의만 받는 틀인지」,
8
22
  // 새 관점 source(원본 대조: 링크를 실제로 열어 인용·쪽·값을 대조, 「흐리다」는 원본 해상도로 재측정). 원고에 확인 없이 「원본이 흐림」이 실렸었다.
9
- // 집행자: 이 워크플로의 agent 읽기(스크립트). 렌더러 ⚠ 는 없음 — 링크를 못 여는 환경이면 source 관점은 「미확인」으로 적고 사람의 눈으로 넘긴다.
23
+ // 2026-09-12 실사례(회신 없는 정황 보고) 반려에서 더한 것 — 새 관점 structure(PART 물음과 절 내용 대조).
24
+ // 네 관점 발견 55개가 「경과 / 원인」 두 PART 의 겹침을 0건 짚었고, 더하기 처방을 반영하자 PART 사이에 다시 서는 시각표 행이 8→12 로 늘었다.
25
+ // 그래서 합치기는 구조 발견(PART·절 합치기·빼기)을 앞에 두고, 부딪히면 빼기가 이긴다.
26
+ // 2026-09-14 실사례(고객사 3차 보고) 반려 「읽다가 시험보는 줄 알 것 같음 — 장을 넘나들면서 이해해야 되는 구조」에서 더한 것 —
27
+ // first-time 관점의 옛 처방 「막히는 곳은 같은 사실을 또 적지 말고 그 자리(건 번호·표 이름)를 가리켜라」와 structure 관점의 「빼기 → 대신 가리킬 건 번호」가
28
+ // 합쳐져 절마다 포인터가 서고(2판 교차 참조 62곳) 독자가 장을 오갔다. 순서를 고친 3판에도 선형 읽기 시험이 뜀 지점 45곳을 냈다 — 원인은 순서가 아니라
29
+ // 「뒤에서 정의되는 낱말」과 「앞에서 쓰는 뒤 절 번호」. 그래서 처방 규칙을 바꾸고(낱말은 첫 등장 자리에서 한 구절로, 뒤 절 번호는 앞에서 쓰지 않는다)
30
+ // 새 관점 linear(앞에서 뒤로 한 번 읽으며 뛰어야 하는 자리를 전부 적는다)를 더했다. 되풀이 금지(R12g)와 「그 자리에서 이해」는 다른 축이다.
31
+ // 2026-09-14 실사례(결정 브리핑 재발행) 반려 둘에서 더한 것 — 새 관점 figures(PNG 를 Read 로 열고 두 칸 틀을 잰다)·facts(내부 1차 자료)·state,
32
+ // Verify 단계와 JS 거름. 캡션만 읽은 검토가 틀 대칭 결함을 0건 짚었고, 검토자의 틀린 주장·정해진 것을 되돌리는 처방·
33
+ // 제작 방식·결정 날짜·판 이력을 더하라는 노이즈 처방이 거르지 않은 채 작성자에게 올라갔었다.
34
+ // 집행자: 이 워크플로의 agent 읽기(스크립트) + 거름 규칙(JS, 결정적). 렌더러 ⚠ 는 없음 — 링크·도판·코드를 못 여는 환경이면 그 판정은 「미확인」,
35
+ // 못 연 도판 수는 overall 머리에 박히고, questions[]·dropped[] 는 사람의 눈으로 넘긴다.
36
+ // PART·절 사이 되풀이도 렌더러·서버 dry_run 이 세지 않는다 — structure 관점이 유일한 기계 쪽 읽기다(readability R12g).
10
37
  export const meta = {
11
38
  name: 'report-reader-review',
12
- description: '보고서 원고(JSON)를 처음 읽는 사람·결정권자·정보 디자이너·원본 대조 네 관점으로 읽고 막히는 곳과 시각화할 곳을 합친다',
13
- phases: [{ title: 'Read', detail: '독자 4관점 병렬' }, { title: 'Merge', detail: '중복 제거·우선순위' }],
39
+ description: '보고서 원고(JSON)를 처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조·선형 읽기·도판·사실 대조(상태 표가 있으면 상태 대조까지) 관점으로 읽고, 발견을 반박 검증해 틀린 주장·정해진 것 되돌리기·노이즈 처방·포인터 처방을 거른 뒤 합친다',
40
+ phases: [
41
+ { title: 'Read', detail: '독자 8관점 병렬(상태 표가 있으면 9)' },
42
+ { title: 'Verify', detail: 'severity≥3 최대 12건 반박 · JS 거름' },
43
+ { title: 'Merge', detail: '사실 틀림 독립 · 나머지 상위 10 · 구조 빼기 우선' },
44
+ ],
14
45
  }
15
- const FILE = args.file
16
- const READER = args.reader || '고객사 기획 담당(비개발자). 판단 요청에 회신해야 한다'
17
- const EXAMPLE = args.example || '(예시 한 명 미지정 — 원고에서 찾아 쓰라)'
18
- const BUDGET = Number(args.bitmapBudget ?? 3)
19
- const CTX = `[읽기 전용. 파일을 수정하지 말 것. 한글로 답할 것.]
20
- 원고: ${FILE} (Lim 포털 관보 서식 보고서의 발행 JSON. sections[] 절, cases[] 건, open_items[] 판단 요청, summary, appendix, assets 도판)
46
+ const A = args || {}
47
+ const FILE = A.file
48
+ const READER = A.reader || '고객사 기획 담당(비개발자). 판단 요청에 회신해야 한다'
49
+ const EXAMPLE = A.example || '(예시 한 명 미지정 — 원고에서 찾아 쓰라)'
50
+ const BUDGET = Number(A.bitmapBudget ?? 3)
51
+ const SCRATCH = A.scratch || '(미지정 — 세션 스크래치 디렉토리 아래 reader-review/ 에 쓴다. 원고 폴더에는 쓰지 않는다)'
52
+ const PLATES = A.plates || '(미지정 — assets[].file 경로를 쓴다)'
53
+ const LOCKED = Array.isArray(A.locked) ? A.locked : (A.locked ? [String(A.locked)] : [])
54
+ const SOURCES = Array.isArray(A.sources) ? A.sources : []
55
+ const STATE = A.state || ''
56
+ const VERIFY_MAX = 12
57
+ const CTX = `[원고·도판·저장소 파일은 수정하지 않는다. 측정 스크립트·풀어낸 그림은 scratch 아래에만 쓴다. 한글로 답할 것.]
58
+ 원고: ${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}
60
+ scratch: ${SCRATCH}
21
61
  독자: ${READER}
22
62
  예시 한 명: ${EXAMPLE}
23
63
  도판 예산: 새 비트맵 그림은 ${BUDGET}장까지. 표·flow(4열 흐름도)·charts(막대)는 예산 밖이라 자유롭게.
24
- 작성자 기준: 「글은 직관적이고 흐름을 이해하기 쉽게, 보충 설명은 글이 아니라 그림 같은 시각 자료로. 핵심은 굵게, 상태는 칩, 색은 뜻으로」. 단 인라인 표기(굵게·칩·[글자](주소))는 운영 렌더러에 오르기 전에는 권하지 않는다(lim-portal-format §11-6 머리) — 그 전에는 tone·표·차트로.
64
+ 정해진 것(되돌리는 처방을 내지 않는다):
65
+ ${LOCKED.length ? LOCKED.map((s) => `- ${s}`).join('\n') : '(지정 없음)'}
66
+ 결정 장부: ${A.decisions || '(지정 없음)'}
67
+ 사용자 최신 지적(원문): ${A.latest || '(지정 없음)'}
68
+ 내부 1차 자료: ${SOURCES.length ? SOURCES.map((s) => `${s.name} [${s.kind}] ${s.path}`).join(' · ') : '(지정 없음 — 앱 동작·내부 수치 판정은 「미확인」)'}
69
+ ${STATE ? `상태 표: ${STATE}\n` : ''}작성자 기준: 「글은 직관적이고 흐름을 이해하기 쉽게, 보충 설명은 글이 아니라 그림 같은 시각 자료로. 핵심은 굵게, 상태는 칩, 색은 뜻으로」.
25
70
  표기: **굵게** · ==강조== · {{ok|칩}}(톤 ok·wait·bad·info·muted) · 차트 rows[].tone.`
26
- const FIND = { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: {
27
- where: { type: 'string', description: '절·건·항목 번호와 자리' },
28
- problem: { type: 'string', description: '독자가 왜 막히는가 — 한 문장' },
71
+ const FINDING = { type: 'object', properties: {
72
+ where: { type: 'string', description: '절·건·항목·도판 id 와 자리' },
73
+ problem: { type: 'string', description: '독자가 왜 막히는가, 또는 원고가 원본과 어떻게 다른가 — 한 문장' },
29
74
  severity: { type: 'integer', minimum: 1, maximum: 5 },
75
+ class: { type: 'string', enum: ['사실틀림', '구조', '읽기', '시각', '묻기'], description: '사실틀림=원고의 수치·인용·앱 동작·상태·그림↔본문이 원본과 다르다 · 구조=PART·절 합치기·빼기 · 읽기=용어·흐름·문장 · 시각=그림·표·흐름도로 바꿈 · 묻기=원고와 자료만으로 못 정해 작성자가 사람에게 물어야 한다(rewrite 에 물을 문장)' },
76
+ evidence: { type: 'string', description: '연 것·잰 것 — 파일:줄 · 쪽(행) · px 값 · 주소 · 다시 한 계산. 원고 글만 읽고 낸 판단이면 「추정 — 」으로 시작' },
30
77
  rewrite: { type: 'string', description: '글로 고친다면(개조식 2~4문장). 필요 없으면 빈 문자열' },
31
78
  visual: { type: 'string', description: '그림/표/flow/chart 로 바꾼다면 — 종류(bitmap|flow|table|chart|none) + 축·칸·숫자·라벨. 예시 한 명의 숫자로' } },
32
- required: ['where', 'problem', 'severity', 'rewrite', 'visual'] } }, overall: { type: 'string' } }, required: ['findings', 'overall'] }
79
+ required: ['where', 'problem', 'severity', 'class', 'evidence', 'rewrite', 'visual'] }
80
+ const PASSED = { type: 'array', description: '재서 통과한 것 — 안 본 것은 넣지 않는다', items: { type: 'object', properties: {
81
+ where: { type: 'string' }, check: { type: 'string', description: '무엇을 어떻게 쟀고 값이 얼마였나' } }, required: ['where', 'check'] } }
82
+ const FIND = { type: 'object', properties: { findings: { type: 'array', items: FINDING }, passed: PASSED, overall: { type: 'string' } }, required: ['findings', 'passed', 'overall'] }
83
+ const FIND_FIG = { type: 'object', properties: { ...FIND.properties, unopened: { type: 'array', description: '열지 못한 도판', items: { type: 'object', properties: {
84
+ id: { type: 'string' }, why: { type: 'string' } }, required: ['id', 'why'] } } }, required: [...FIND.required, 'unopened'] }
85
+ const VERDICT = { type: 'object', properties: {
86
+ verdict: { type: 'string', enum: ['참', '부분', '거짓', '미확인'] },
87
+ finding_basis: { type: 'string', enum: ['확인', '추정'], description: '발견의 evidence 가 원본·코드·잰 값에 닿으면 확인, 원고 글만 읽은 짐작이면 추정' },
88
+ checked: { type: 'string', description: '이번에 직접 연 원본과 자리 — 파일:줄 · 쪽 · px · 주소. 못 열었으면 까닭' },
89
+ touches_locked: { type: 'boolean' },
90
+ noise_kind: { type: 'string', enum: ['없음', '제작', '날짜경로', '판이력'] },
91
+ note: { type: 'string', description: '판정 까닭 한두 문장. 부분이면 맞는 쪽과 틀린 쪽' } },
92
+ required: ['verdict', 'finding_basis', 'checked', 'touches_locked', 'noise_kind', 'note'] }
93
+ 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'] }
33
96
  const LENSES = [
34
- ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳.'],
35
- ['decider', '결정권자 — 회신해야 한다. 판단 요청 각 항에서 「무엇을 고르는지(물음) / 두 길이 각각 무엇을 바꾸는지(그대로 두면 · 바꾸면) / 기한」이 3초 안에 읽히는지. 안 읽히는 항의 재구성안과, 항들을 한눈에 묶는 표·관문 흐름도. 수치·링크·건수가 절마다 어긋나는 곳(신뢰를 깨는 것). 권고가 제목·표 열·색(칩·톤)·요약면에 배어 독자가 동의만 하게 되는 틀(답정너)인지 — 배인 자리마다 짚고, 물음과 두 길 대칭으로 되돌린 재구성안.'],
97
+ ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳. 처방은 그 자리에서 이해되게 하는 것 — 낱말·규칙·예시 선언은 첫 등장 자리에서 한 구절(20자 안)로 풀고, 뒤 절의 번호·표 이름·§ 를 앞 절에서 쓰지 않는다(뒤 절이 앞 절을 되짚는 방향만). 「N절을 보라」「부록에 있다」는 처방이 아니다 — 그것이 시험지를 만든다(2026-09-14).'],
98
+ ['decider', '결정권자 — 회신해야 한다. 판단 요청 각 항에서 「무엇을 고르는지(물음) / 두 길이 각각 무엇을 바꾸는지(그대로 두면 · 바꾸면) / 기한」이 3초 안에 읽히는지. 안 읽히는 항의 재구성안과, 항들을 한눈에 묶는 표·관문 흐름도. 수치·링크·건수가 절마다 어긋나는 곳(신뢰를 깨는 것). 권고가 제목·표 열·색(칩·톤)·요약면에 배어 독자가 동의만 하게 되는 틀(답정너)인지 — 배인 자리마다 짚고, 물음과 두 길 대칭으로 되돌린 재구성안. 그리고 둘을 더 본다: (1) 독자의 **할 일마다**(위 「독자」 줄의 할 일 목록) 그것이 표지 meta · 요약면 현황 줄(notice) · 읽는 법(guide) 첫 물음에 서는지 대조한다 — 한 자리라도 빠지면 severity 4 로 짚고 어느 자리에 무엇을 넣을지 rewrite 에 적는다(요청문이 아니라 사실문으로). (2) 요약면 수치 칸(stats)끼리 **겹치는 집합**인지 — 한 수가 다른 수에 포함되면(전체 건 수와 그중 일부) 하나를 빼거나 라벨에 관계를 드러내는(「그중 …」) 재구성안을 적는다.'],
36
99
  ['visual', '정보 디자이너 — 글로 설명하는데 그림이면 한 번에 보일 곳. 원리·갈림·타임라인·분포·자리별 상태. 각 그림의 축·칸·숫자·라벨을 구체로. 표의 숫자를 문장으로 되풀이하는 문단, 굵게·칩·톤을 둘 자리.'],
37
- ['source', '원본 대조 — 원고가 가리키는 원본을 실제로 연다: 건 cases[].source_url, 도판 출처 줄 assets[].source_text 안의 주소·쪽, 부록 출처 목록과 본문 글자 안의 맨 주소·[글자](주소). assets[].origin 은 렌더되지 않는 제공자 기록이라 대조 대상이 아니다. 인용 문구·쪽(행)·값을 원문과 대조한다. 어긋나는 인용, 원문에 없는 문장, 쪽·절 번호 오기, 고객이 눌러도 안 열리는 링크를 발견으로. 「흐리다」「못 연다」는 주장은 원본 해상도로 재측정해(PDF 는 pdfimages -list 전체, 웹은 실제로 열어) 잰 값을 problem 에 적고, 못 열었으면 「미확인」이라 적는다 — 짐작으로 판정하지 않는다. rewrite 에는 원문 문구 그대로 + 원본 이름·쪽(행)·주소로 고친 출처 줄.'],
100
+ ['source', '원본 대조 — 고객이 누를 수 있는 원본과 원고 인용의 일치를 본다. 원고가 가리키는 원본을 실제로 연다: 건 cases[].source_url, 도판 출처 줄 assets[].source_text 안의 주소·쪽, 부록 출처 목록과 본문 글자 안의 맨 주소·[글자](주소). assets[].origin 은 렌더되지 않는 제공자 기록이라 대조 대상이 아니다. 인용 문구·쪽(행)·값을 원문과 대조한다. 어긋나는 인용, 원문에 없는 문장, 쪽·절 번호 오기, 고객이 눌러도 안 열리는 링크를 발견으로(원고가 원문과 다르면 class 사실틀림). 「흐리다」「못 연다」는 주장은 원본 해상도로 재측정해(PDF 는 pdfimages -list 전체, 웹은 실제로 열어) 잰 값을 evidence 에 적고, 못 열었으면 「미확인」이라 적는다 — 짐작으로 판정하지 않는다. 앱 동작·내부 수치·결정의 주체는 facts 관점 몫이라 여기서 판정하지 않는다 — 다른 문서에 그 말이 없다고 앱 동작을 거짓이라 하지 않는다. rewrite 에는 원문 문구 그대로 + 원본 이름·쪽(행)·주소로 고친 출처 줄.'],
101
+ ['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 에 적는다.'],
103
+ ['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
+ ['facts', '사실 대조 — FCG 내부 1차 자료(내부 1차 자료 목록)와 결정 장부로 원고의 수치·앱 동작·결정의 주체와 시점을 판정한다. 수치는 원고의 입력값에서 다시 계산한다(반올림·날짜 산술 포함). 앱 동작은 kind=code 파일을 열어 파일:줄로만 판정한다 — 다른 문서에 그 말이 없다는 것은 반증이 아니다. 결정은 누가·어디서 정했는지 결정 장부·정해진 것과 대조한다(주체 없는 「정한 사항」, 장부와 다른 차수·자리). 판정 못 하면 「미확인」이라 적는다. source 관점과의 경계: 고객이 누를 수 있는 원본(링크·쪽)과 인용의 일치는 source 몫이고, 여기는 고객에게 보이지 않는 내부 자료 몫이다. 원고가 틀린 발견은 class 사실틀림. 다시 계산해 맞은 수치·코드와 맞은 동작은 passed 에 파일:줄·계산식과 함께 적는다.'],
105
+ ...(STATE ? [['state', '상태 대조 — 상태 표를 연다. 원고에서 「반영·적용·고침·보이지 않음·빠짐」 같은 완료형 동사가 선 문장마다 상태 표의 실제 확인 수준(코드 · 시험판 · 실기기 · 운영)과 대조한다. 확인 수준보다 앞선 시제·단정(코드만 고쳤는데 앱에서 보이지 않는다고 쓴 문장)은 class 사실틀림으로 두고, rewrite 에 확인 수준에 맞춘 문장을 적는다. 수준과 맞는 문장은 passed 에.']] : []),
38
106
  ]
39
107
  phase('Read')
40
- const reads = (await parallel(LENSES.map(([k, inst]) => () => agent(`${CTX}\n[관점: ${k}] ${inst}\n원고 전체를 읽고 findings 를 8~14개, severity 로 정직하게 매겨라. overall 에 이 원고의 흐름이 어디서 끊기는지 3문장.`, { label: `read:${k}`, phase: 'Read', schema: FIND })))).filter(Boolean)
41
- log(`독자 관점 ${reads.length}/${LENSES.length} · 발견 ${reads.reduce((n, r) => n + r.findings.length, 0)}`)
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 })))
109
+ 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(', ')}` : ''}`)
112
+ 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(', ')})` : '')
114
+ if (figHead) log(figHead)
115
+
116
+ phase('Verify')
117
+ // 거를 대상 고르기 — 사실 틀림을 먼저, 그다음 severity. 상한 밖은 반영하지 않고 raw.unverified 로 남긴다(조용히 자르지 않는다).
118
+ const all = reads.flatMap((r) => r.findings.map((f, i) => ({ ...f, id: `${r.lens}-${i + 1}`, lens: r.lens })))
119
+ const rank = (f) => (f.class === '사실틀림' ? 10 : 0) + f.severity
120
+ // 같은 rank 안에서는 **관점마다 돌아가며** 뽑는다 — 안정 정렬만 쓰면 LENSES 앞 관점이 늘 이겨 뒤 관점(figures·facts)의 발견이
121
+ // 상한 밖으로 밀린다(2026-09-14 실측: 관점 일곱이 각 8~14건을 내 상한 초과가 거의 매번, 도판 결함이 늘 검증 밖이었다 — 이 보강의 동기가 그 누락이었다).
122
+ const LENS_AT = Object.fromEntries(LENSES.map(([k], i) => [k, i]))
123
+ const turn = new Map()
124
+ const seen = {}
125
+ all.filter((f) => f.severity >= 3).sort((a, b) => rank(b) - rank(a)).forEach((f) => {
126
+ const k = `${rank(f)}|${f.lens}`
127
+ seen[k] = (seen[k] || 0) + 1
128
+ turn.set(f, seen[k])
129
+ })
130
+ 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} 초과` }))]
133
+ log(`검증 ${toVerify.length}건 · 검증 밖 ${unverified.length}건(raw.unverified — 반영하지 않음)`)
134
+ const vs = await parallel(toVerify.map((f) => () => agent(`${CTX}\n[반박 검증 — 발견 ${f.id}] 아래 발견은 ${f.lens} 관점 검토자가 냈다. 검토자 주장도 틀린다. 기본 자세는 반박이다 — 발견이 가리킨 원본(코드 파일:줄 · PDF 쪽 · PNG · 주소)과 원고의 그 자리를 직접 열어 판정한다. 열지 못했으면 verdict 는 미확인이다. 원고 글만 읽고 참이라 하지 않는다.
135
+ verdict: 발견의 주장(problem)이 참/부분/거짓/미확인. class 가 묻기면 「원고와 자료만으로 정말 못 정하는가」를 판정한다 — 자료에 답이 있으면 거짓.
136
+ touches_locked: 처방(rewrite·visual)이 정해진 것이나 결정 장부의 확인된 결정을 되돌리면 true.
137
+ noise_kind: 처방이 본문·캡션·출처 줄에 제작 방식(합성·재현·캡처 도구·그린 방법)을 더하면 제작, 결정 날짜·결정 경로(몇 차·어느 회의에서)를 더하면 날짜경로, 판 이력(몇 판에서 바뀜)을 더하면 판이력, 아니면 없음. 원본 위치 한 구절(원본 이름·쪽·행·주소·건 §)이 빠졌다는 처방은 노이즈가 아니라 SKILL.md 5a 결함이다 — 없음.
138
+ ${JSON.stringify(f)}`, { label: `verify:${f.id}`, phase: 'Verify', schema: VERDICT })))
139
+ // JS 거름(결정적) — 거짓·정해진 것 되돌리기·노이즈 처방·추정뿐인 근거는 dropped(사유 포함), 묻기는 questions, 사실 틀림은 합치지 않는 독립 항목.
140
+ const verdicts = toVerify.map((f, i) => ({ id: f.id, ...(vs[i] || { verdict: '검증 실패' }) }))
141
+ const verdictOf = Object.fromEntries(verdicts.map((v) => [v.id, v.verdict]))
142
+ const dropped = [], questions = [], errors = [], candidates = []
143
+ toVerify.forEach((f, i) => {
144
+ 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 })
149
+ else if (f.class === '묻기') questions.push(g)
150
+ // 사실 틀림의 독립 자리(개수 제한 밖·맨 앞)는 **Verify 가 참·부분으로 받은 것**만이다.
151
+ // 미확인(근거를 못 연 것)까지 그 자리에 두면 확인되지 않은 주장이 개수 제한 없이 맨 앞에 서고 「(미확인)」 표시도 안 붙는다(2026-09-14 검증).
152
+ else if (f.class === '사실틀림' && (v.verdict === '참' || v.verdict === '부분')) errors.push(g)
153
+ else candidates.push(g)
154
+ })
155
+ log(`거름: 버림 ${dropped.length} · 묻기 ${questions.length} · 사실 틀림(참·부분) ${errors.length} · 합칠 것 ${candidates.length}`)
156
+ if (!dropped.length) log('버림 0 — 표본 3건 재반박 권함')
157
+
42
158
  phase('Merge')
43
- const merged = await agent(`${CTX}\n[합치기] 아래 네 관점의 발견을 같은 자리끼리 합치고(중복 제거), severity 합산으로 정렬해 상위 10개만 남겨라. 각 항목에 가장 구체적인 rewrite 와 visual 을 고르되, 비트맵은 ${BUDGET}장까지만 'bitmap' 으로 두고 나머지는 flow/table/chart 로 바꿔 적어라. overall 에는 「작성자가 지금 바로 할 일 순서」를 5줄로 — 용어 잠금과 수치·참조·원본 대조 오류가 그림보다 먼저다.\n${JSON.stringify(reads)}`, { label: 'merge', phase: 'Merge', schema: FIND })
44
- return { merged, raw: reads }
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 끝에 「(미확인)」을 남긴다.
160
+ 「사실 틀림」은 원고가 원본과 다른 곳(그림↔본문 반박 포함)이다 — 사실 대조에서 틀림·부분으로 판정된 발견은 다른 발견에 합쳐 넣지 않고 독립 항목으로 이미 앞에 서며 개수 제한 밖이다. findings 에 다시 적지 말고 overall 순서에만 반영하라. overall 에는 「작성자가 지금 바로 할 일 순서」를 5줄로 — 사실 틀림 고침이 맨 먼저, 그다음 구조(PART·절 합치기·빼기), 용어 잠금과 수치·참조·원본 대조, 그림은 마지막이다.
161
+ [사실 틀림 — 참고만]
162
+ ${JSON.stringify(errors)}
163
+ [합칠 것]
164
+ ${JSON.stringify(candidates)}`, { label: 'merge', phase: 'Merge', schema: MERGED }) : null
165
+ const pick = ({ where, problem, severity, class: c, evidence, rewrite, visual }) => ({ where, problem, severity, class: c, evidence, rewrite, visual })
166
+ const body = m ? m.overall : ((candidates.length || errors.length) ? '합치기 실패 — raw.reads 를 사람이 훑는다' : '반영할 발견 0 — passed·dropped 를 사람이 훑는다')
167
+ const merged = {
168
+ findings: [
169
+ ...errors.map((e) => ({ ids: [e.id], ...pick(e), verdicts: [`${e.id}:${e.verdict}`] })),
170
+ ...(m ? m.findings : []).map((x) => ({ ...x, verdicts: x.ids.map((id) => `${id}:${verdictOf[id] || '?'}`) })),
171
+ ],
172
+ overall: figHead ? `${figHead}\n${body}` : body,
173
+ }
174
+ const passed = reads.flatMap((r) => r.passed.map((p) => ({ lens: r.lens, ...p })))
175
+ // linear 관점의 뜀 지점은 상위 10·검증 상한과 별개로 전부 돌려준다 — 0 이 목표라 하나도 숨기지 않는다(linear-read-test.workflow.js 와 같은 셈).
176
+ const jumps = (reads.find((r) => r.lens === 'linear') || {}).findings || []
177
+ return { merged, questions, passed, dropped, jumps, raw: { reads, verdicts, unverified } }