genetics-mcp 2.9.1 → 2.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,161 +1,168 @@
1
- # 작업 절차
1
+ # 작업 절차 — 관보 기준 (2026-08-24 재작성)
2
2
 
3
- SKILL.md 는 "무엇을 만드는가"를 정하고, 이 문서는 **각 단계에서 실제로 무엇을 쓰고 무엇을 판단하는가**를 정한다.
4
- 데이터 모델·조판 규칙은 `content-pipeline.md`, 토큰 역할은 `design-system.md`,
5
- 검수 항목 전체는 `qa.md`, 편집기 마크업 계약은 `wysiwyg.md`, 폰트는 `font-embedding.md` 가 맡는다.
6
- 여기서는 그것들을 **어떤 순서로 어떤 판단을 거쳐 채우는가**만 다룬다.
3
+ SKILL.md 는 「무엇을 만드는가」를 정하고, 이 문서는 **각 단계에서 실제로 무엇을 쓰고 무엇을 판단하는가**를 정한다.
7
4
 
8
- 이 문서의 모든 인용과 수치는 최종 자산(`assets/*.py`, `assets/*.css`, `assets/editor.js`)과
9
- 실제로 돌아가는 예시 프로젝트에서 확인했다.
10
- 확인하지 못한 것은 맨 끝 「미확정」에 남겼다.
5
+ 칸 하나하나의 뜻은 `references/lim-portal-format.md`, 어휘·원고 규칙은 `references/content-pipeline.md` 가 맡는다.
6
+ **조판 규칙(활자 스케일·괘선·간격 격자·인쇄 페이지 문법·검사기 실행법)은 여기 안 적는다** — 원전은 `gazette-report` 스킬이다.
7
+ 두 벌로 두면 갈라지고, 갈라지면 어느 쪽이 참인지 아무도 모른다.
8
+
9
+ 이 문서의 수치와 동작은 전부 코드에서 확인했다.
10
+ 확인 못 한 것은 맨 끝 「미확정」에 남겼다.
11
11
 
12
12
  ---
13
13
 
14
14
  ## 0. 전체 흐름
15
15
 
16
- | 단계 | 하는 일 | 산출물 | 사용자에게 묻는가 | 자동 게이트 |
16
+ | 단계 | 하는 일 | 산출물 | 묻는가 | 자동 관문 |
17
17
  |---|---|---|---|---|
18
18
  | 0 | 독자 정의 | 독자 문장 1개 + 어휘 정책 | **반드시 묻는다** | — |
19
- | 1 | 자료 수집 | 원자료(티켓·로그·대화) | 소스·범위·"고객이 아는 참조"를 묻는다 | — |
20
- | 2 | 건별 정리 | `cases.json` | 안 묻는다. 단 "확인 필요"는 되돌린다 | 빌드 assert |
21
- | 3 | 등급 부여 | `cases.json` 의 `grade` | 등급 이름·개수만 묻는다 | QA 칩 개수 대조 |
22
- | 4 | 요약·확인 요청 | `report.json` 의 `summary`/`open_items`/`cta` | 공통 원인의 실재 여부를 묻는다 | 빌드 `§`·`forbid` assert |
23
- | 5 | 팔레트 | `report.json` 의 `palette` 한 줄 | 톤·브랜드색 사용 여부를 묻는다 | 빌드 팔레트 대비 assert |
24
- | 6 | 조립 | 단일 HTML | 원자료의 "확인 필요"만 묻는다 | `build.py` assert 4종 |
25
- | 7 | QA | 통과/실패 리포트 | 안 묻는다 | `qa.py` 13군, 실패 시 exit 1 |
26
- | 8 | 전달 | 고객본 `.html` | **발송 직전 사람 승인** | 내보내기 `confirm` |
27
-
28
- 새 프로젝트에서 손대는 파일은 `report.json` 과 `cases.json` **둘뿐**이다.
29
- `build.py`·`palette.py`·`report.css`·`editor.js` 는 **레포**(`private/builder/`)에 있고,
30
- `qa.py` 만 이 스킬 자산이다 — 넷은 렌더러라 원본이 레포 하나이고, `qa.py` 는 저작 도구라 여기 남는다.
31
- 다섯 모두 프로젝트 고유 데이터를 하나도 갖고 있지 않다.
32
-
33
- 명령은 두 줄이다.
19
+ | 1 | 자료 수집 | 원자료(티켓·로그·대화) | 소스·범위·「고객이 아는 참조」 | — |
20
+ | 2 | 건별 정리 | `cases` 배열 | 안 묻는다. 단 「확인 필요」는 되돌린다 | 발행 검증(그룹 정합) |
21
+ | 3 | 등급 부여 | `grades` + 건별 `grade` | 등급 이름·개수만 | 없음 — 사람이 본다 |
22
+ | 4 | 요약·확인 요청 | `summary` · `open_items` · `cta` | 공통 원인의 실재 여부 | **요약 근거 게이트 — 발행 거절** |
23
+ | 5 | **여백 주석 채우기** | 절마다 `marg{why,key,advice}` | 안 묻는다 | ⚠ 경고(거절 아님) |
24
+ | 6 | 조립 | `parts` + `sections` + 골격 칸 | 원자료의 「확인 필요」 | 발행 검증 + 렌더러 assert 2종 |
25
+ | 7 | 검수 | 통과/실패 판정 | 안 묻는다 | dry_run warnings + 검사기 8종 + **사람의 눈** |
26
+ | 8 | 전달 | 열람 링크 또는 `.html` 하나 | **공개 직전 사람 승인** | 초안이 기본값 |
27
+ | 9 | 발행 뒤 판정 | outline 지문 | 안 묻는다 | `get_report(outline: true)` |
28
+
29
+ 새 프로젝트에서 손대는 것은 **발행 데이터 하나**다(로컬 렌더라면 `report.json` + `cases.json`).
30
+ 렌더러(`build.py`·`gazette.py`·`sentbreak.py`·`boundary.py`·`gazette.css`)는 레포에만 있고 프로젝트 고유 데이터를 하나도 안 갖는다.
31
+
32
+ 로컬 렌더 명령은 한 줄이다.
34
33
 
35
34
  ```bash
36
- python3 ~/.claude/skills/genetics-report/assets/build.py report.json
37
- python3 ~/.claude/skills/genetics-report/assets/qa.py report.json # 실패 0 이어야 통과
35
+ python3 ~/.claude/skills/genetics-report/assets/render.py report.json
38
36
  ```
39
37
 
40
- `qa.py` 는 `report.json` 의 `out` 을 읽어 **빌드가 실제로 뱉은 파일**을 검사한다.
41
- 빌드 없이 QA 만 돌리면 이전 산출물을 검사하게 되므로 항상 둘을 붙여서 돌린다.
38
+ `render.py` 가 서버의 `REPORT_TEMPLATES` 를 읽어 `render`·`css`·폰트를 설정에 얹는다.
39
+ **`report.json` 에 `css`·`render`·`palette` 를 적지 마라** — 서버와 갈릴 수 있고, 갈리면 로컬에서 본 것과 고객이 본 것이 다르다.
42
40
 
43
- 예시 프로젝트 실측(18건): `523,464B` · 폰트 임베드 `300,544B → base64 400,728B` ·
44
- QA `통과 48 · 경고 1 · 실패 0`. 경고 1건은 아래 9.3 참조.
41
+ **`build.py` 를 직접 부르지 마라.**
42
+ `cfg.get('css', ['report.css'])`(`build.py:285-286`)로 떨어져 옛 서식 시트가 실린다.
43
+ 스타일이 안 맞는 문서가 검사를 통과해 「거짓 초록」이 된다.
44
+
45
+ 실측(스킬 동봉 예시 · 절 5 · 건 2): 폰트 임베드 1면 409,044B → base64 545,392B · 산출물 659,164B · ⚠ 0건.
45
46
 
46
47
  ---
47
48
 
48
- ## 1. 0단계 — 독자 정의
49
+ ## 1. 0단계 — 독자를 먼저 못 박는다
49
50
 
50
- **이 스킬에서 유일하게 반드시 사용자에게 확인해야 하는 단계다.** 나머지는 스스로 판단해도 된다.
51
+ **이 스킬에서 유일하게 반드시 사용자에게 확인해야 하는 단계다.**
52
+ 나머지는 스스로 판단해도 된다.
51
53
 
52
54
  ### 묻는 것
53
55
 
54
56
  | 질문 | 왜 이걸 묻는가 | 예시 답 |
55
57
  |---|---|---|
56
- | 이 문서를 **읽고 판정하는 사람**은 누구인가 — 직함이 아니라 *하는 일*로 | 직함("팀장")에서는 어휘 정책이 나오지 않는다. 행위("검증한다")에서는 나온다 | "고객사 검수 담당 — 우리 작업이 실제로 해결됐는지 확인하는 사람" |
57
- | 읽고 나서 **해야 할 행동**은 무엇인가 | `cta` 문구와 각 건의 `how_to_check`·`remaining` 칸이 여기서 결정된다 | "각 건을 한 번씩 열어 확인하고, 남은 판단 사항에 답을 준다" |
58
- | 문서에 실어도 되는 **참조 체계**는 무엇인가 | 독자가 확인할 수 없는 참조는 신뢰에 기여하지 않고 노이즈만 된다 | "본인이 제보한 원본 링크만. 내부 티켓 번호는 안 됨" |
59
- | 배포 범위 — 파일 전달인가, 인쇄·PDF 도 도는가 | 인쇄 규칙(`@media print`)과 링크 URL 인쇄 여부가 갈린다 | "파일 전달 + 인쇄 가능해야 함" |
58
+ | 이 문서를 **읽고 판정하는 사람**은 누구인가 — 직함이 아니라 *하는 일*로 | 직함(「팀장」)에서는 어휘 정책이 안 나온다. 행위(「검증한다」)에서는 나온다 | 「고객사 검수 담당 — 우리 작업이 실제로 해결됐는지 확인하는 사람」 |
59
+ | 읽고 나서 **해야 할 행동**은 무엇인가 | `cta`·`open_items`·건별 `how_to_check` 가 여기서 결정된다 | 「각 건을 한 번씩 열어 확인하고, 남은 판단 사항에 답을 준다」 |
60
+ | 문서에 실어도 되는 **참조 체계**는 무엇인가 | 독자가 확인할 수 없는 참조는 신뢰에 기여하지 않고 노이즈만 된다 | 「본인이 제보한 원본 링크만. 내부 티켓 번호는 안 됨」 |
61
+ | 전달 경로 — 포털 링크인가, 파일 하나인가, 인쇄도 도는가 | 포털은 셸이 문서 둘레에 요약·댓글을 놓는다. 파일 전달은 그것이 없다 | 「포털 링크 + 인쇄 가능해야 함」 |
60
62
 
61
63
  ### 이 답에서 곧바로 정해지는 것
62
64
 
63
- - `report.json` 의 `allow_external` — 세 번째 답이 그대로 QA 외부 리소스 예외 목록이 된다.
64
- - `report.json` 의 `forbid` — 독자가 검증할 수 없는 내부 어휘의 정규식 목록.
65
- - 절 구성(`sections`) — 독자가 바뀌면 절이 통째로 없어진다.
66
- 전 건 검수자에게는 `status` 절의 판정표가 핵심이지만, 임원 독자에게는 지표 밴드만 남기고 판정표를 빼는 게 맞다.
65
+ - **금지 어휘 목록** — 독자가 검증할 수 없는 내부 어휘와 식별자.
66
+ 로컬 렌더라면 `forbid` 정규식으로 적고, 포털 발행이라면 손으로 지킨다(7단계 참조).
67
+ - **PART 구성** — 독자가 바뀌면 읽기 순서가 바뀐다.
68
+ 검수자에게는 「지금 부탁드리는 것」이 PART 1 이지만, 사후 열람자에게는 「경과」가 앞이다.
69
+ - **절 구성(`sections`)** — 독자가 바뀌면 절이 통째로 없어진다.
70
+ 전 건 검수자에게는 `status` 절의 대조표가 핵심이지만, 결정권자 독자에게는 요약면 상태 막대만 남기고 대조표를 빼는 것이 맞다.
67
71
 
68
72
  | 규칙 | 근거 | 어기면 |
69
73
  |---|---|---|
70
- | 독자를 문장 하나로 확정하기 전에는 한 건도 쓰지 않는다 | 어휘 정책·절 구성·`forbid` 가 전부 여기서 파생된다 | 전 건 재집필. 18건이면 18건 전부 |
71
- | 독자를 "직함"이 아니라 "하는 일"로 받는다 | 하는 일이 정해져야 "이 사람이 확인할 수 있는가"를 매 문장에서 판정할 수 있다 | 매 건 즉흥 판단이 되고, 문서 안에서 어휘가 흔들린다 |
74
+ | 독자를 문장 하나로 확정하기 전에는 한 건도 쓰지 않는다 | 어휘 정책·PART·절 구성이 전부 여기서 파생된다 | 전 건 재집필. 18건이면 18건 전부 |
75
+ | 독자를 「직함」이 아니라 「하는 일」로 받는다 | 하는 일이 정해져야 「이 사람이 확인할 수 있는가」를 매 문장에서 판정할 수 있다 | 매 건 즉흥 판단이 되고 문서 안에서 어휘가 흔들린다 |
72
76
 
73
- 독자를 안 정하고 만들면 **개발 로그에 색만 칠한 물건**이 나온다.
74
- 예시 프로젝트에서 실제로 한 번 그렇게 만들었고, "개발팀 내부적인 작업 시나리오까지 섞어서 보고서로 보내고 있다"는 지적을 받았다.
77
+ 독자를 안 정하고 만들면 **개발 로그에 조판만 입힌 물건**이 나온다.
78
+ 실제로 한 번 그렇게 만들었고, 「개발팀 내부적인 작업 시나리오까지 섞어서 보고서로 보내고 있다」는 지적을 받았다.
75
79
 
76
80
  ---
77
81
 
78
82
  ## 2. 1단계 — 자료 수집
79
83
 
80
- ### 규칙
81
-
82
84
  | 규칙 | 근거 | 어기면 |
83
85
  |---|---|---|
84
- | 티켓 본문만 읽지 않는다. **코멘트·스레드까지** 읽는다 | 실제 조치 내용은 본문(notes)이 아니라 코멘트에 적히는 게 보통이다 | `fix` 칸이 통째로 빈다. 제목만 보고 쓴 건은 전부 "확인 필요"가 된다 |
85
- | 건이 많으면 N건씩 묶어 병렬로 읽는다 | 품질 문제가 아니라 속도 문제다. 예시 프로젝트는 3건×6묶음 | (느릴 뿐) |
86
- | 제보 출처 링크가 있으면 **그대로** 보존한다 | `cases.json` 의 `source_url` → 카드 상단 "제보 원본 보기 ↗" 링크 | 접수 건(`reported`)과 자체 발견 건(`own`)을 가르는 근거가 사라진다 |
87
- | 링크는 **독자가 아는 것만** 남긴다 | 그 도메인을 `report.json` 의 `allow_external` 에 적어야 QA 를 통과한다 | QA 외부 리소스 검사가 실패한다(단일 파일 원칙) |
86
+ | 티켓 본문만 읽지 않는다. **코멘트·스레드까지** 읽는다 | 실제 조치 내용은 본문이 아니라 코멘트에 적히는 것이 보통이다 | `fix` 칸이 통째로 빈다. 제목만 보고 쓴 건은 전부 「확인 필요」가 된다 |
87
+ | 건이 많으면 N건씩 묶어 병렬로 읽는다 | 품질이 아니라 속도 문제다 | (느릴 뿐) |
88
+ | 제보 출처 링크가 있으면 **그대로** 보존한다 | `cases[].source_url` → 건 필드표의 제보 원본 링크 | 접수 건과 자체 발견 건을 가르는 근거가 사라진다 |
89
+ | 링크는 **독자가 아는 것만** 남긴다 | 독자가 조회할 수 없는 주소는 확인 수단이 아니라 노이즈다 | 문서가 독자가 아니라 자기 조직을 향해 말한다 |
88
90
 
89
- 예시 프로젝트 실측: 18건 중 `source_url` 보유 9건 = 접수 건 9건과 정확히 일치.
90
- `allow_external` 은 `["figma.com"]` 한 줄이다.
91
+ `source_url` 은 `http(s)` 로 시작해야 한다.
92
+ 아니면 **발행이 거절된다**(`report_publish.php:194-197`).
91
93
 
92
- **묻는다**: 원자료 소스와 범위(기간·상태 필터), 어떤 링크가 "고객이 아는 참조"인지.
94
+ **묻는다**: 원자료 소스와 범위(기간·상태 필터), 어떤 링크가 「고객이 아는 참조」인지.
93
95
  **판단한다**: 병렬 분할, 필드 추출, 어느 티켓이 한 건으로 합쳐지는지.
94
96
 
95
97
  ---
96
98
 
97
99
  ## 3. 2단계 — 건별 정리 (한 건 = 한 레코드)
98
100
 
99
- `cases.json` 은 객체 배열이다. 실제 스키마(예시 프로젝트 18건 전수에서 확인):
101
+ `cases` 는 객체 배열이다.
100
102
 
101
103
  ```
102
- group "reported" | "own" — report.json 의 groups[].key 와 일치해야 한다
104
+ group groups[].key 중 하나여야 한다 — 아니면 발행 거절
103
105
  title 보고서 표제 (티켓 제목이 아니다 — 아래 참조)
104
- grade report.json 의 grades 키 중 하나
106
+ grade grades 키 중 하나
105
107
  symptom 무엇이 잘못 보였는가
106
108
  cause 왜 그랬는가 (한 문장)
107
109
  fix 이제 어떻게 되는가
108
- how_to_check 고객이 자기 화면에서 밟을 수 있는 순서 ← 필수
109
- remaining 우리가 못 닫은 것 (없으면 빈 문자열) ← 선택
110
- source_url 제보 원본 링크 (없으면 생략)
110
+ how_to_check 고객이 자기 화면에서 밟을 수 있는 순서
111
+ remaining 우리가 못 닫은 것 (없으면 생략)
112
+ source_url 제보 원본 링크 (없으면 생략. http(s) 필수)
111
113
  ```
112
114
 
113
- 필드 정의와 조판 규칙의 상세는 `content-pipeline.md` 로 넘긴다. 여기서 정하는 건 **집필 규율**이다.
115
+ 필드가 비어 있어도 렌더러는 안 죽는다(`build.py` 가 빈 문자열로 채운다).
116
+ **즉 빠뜨린 칸은 아무 데서도 안 잡힌다** — 사람이 세는 수밖에 없다.
114
117
 
115
118
  ### 작성 규칙 6개조
116
119
 
117
120
  > · 파일명·함수명·커밋해시·SQL·클래스명을 쓰지 마라. 고객은 모른다.
118
- > · "무엇이 잘못 보였는가 → 왜 그랬는가 → 이제 어떻게 되는가" 순서로 쓴다.
121
+ > · 「무엇이 잘못 보였는가 → 왜 그랬는가 → 이제 어떻게 되는가」 순서로 쓴다.
119
122
  > · 원인은 한 문장으로, 비유 없이, 사실만. 변명하지 마라.
120
123
  > · 고객이 **직접 확인할 수 있는 방법**이 있으면 한 줄 덧붙인다.
121
- > · 아직 안 끝난 게 있으면 숨기지 말고 "남은 것"으로 적는다.
122
- > · 과장 금지. "완벽히 해결" 대신 무엇을 확인했는지 쓴다.
124
+ > · 아직 안 끝난 게 있으면 숨기지 말고 「남은 것」으로 적는다.
125
+ > · 과장 금지. 「완벽히 해결」 대신 무엇을 확인했는지 쓴다.
123
126
  >
124
- > 근거가 없으면 지어내지 말고 그 필드를 **"확인 필요"** 로 남긴다.
127
+ > 근거가 없으면 지어내지 말고 그 필드를 **「확인 필요」** 로 남긴다.
125
128
 
126
129
  | 규칙 | 근거 | 어기면 |
127
130
  |---|---|---|
128
- | 서사 순서를 증상→원인→조치로 고정한다 | 카드 레이아웃이 이 순서를 물리적으로 강제한다 — `report.css` 의 `.dx`(진단 2열) → `.r-fix`(조치 전폭) → `.ck`(확인 2열) | 카드마다 순서가 달라 독자가 매번 읽는 법을 새로 배운다 |
129
- | 원인은 한 문장·비유 없이 | 비유는 사실 진술처럼 보이지만 검증할 수 없다 | 검수 독자에게 변명으로 읽힌다 |
130
- | 근거 없는 필드는 **"확인 필요"** 로 남기고 사용자에게 되돌린다 | 추측이 사실 진술의 활자로 인쇄되면 회복 불가능하다 | 발송 후에는 회수할 수 없다 |
131
- | `how_to_check` 는 **전 건 필수** | `qa.py` 구조 검사가 `class="row r-check"` 개수와 건 수를 대조한다. 한 건이라도 비면 실패 | 검수 요청이 아니라 통보가 된다 |
132
-
133
- 실측 확인: 18건 중 한 건의 `how_to_check` 를 비우고 빌드하면 QA 가 `❌ 확인 방법 17/18` 로 잡는다.
134
- 예시 프로젝트 최종 `cases.json` 에는 "확인 필요"가 0건이다 — 되돌려 전부 채운 뒤 조립에 들어갔다는 뜻이다.
131
+ | 근거 없는 필드는 「확인 필요」로 남기고 사용자에게 되돌린다 | 추측이 사실 진술의 활자로 인쇄되면 회복 불가능하다 | 발송 후에는 회수할 수 없다 |
132
+ | `how_to_check` 는 **전 건 필수** | 없으면 검수 요청이 아니라 통보다 | 기계가 안 잡는다. 사람이 건 수와 대조한다 |
135
133
 
136
134
  ### `title` 은 티켓 제목이 아니다
137
135
 
138
- 원자료 제목에는 `>` 구분자, `#2`, `[후속]`, `(심각)`, `버그?` 처럼 티켓에서만 뜻이 통하는 표기와
139
- `RN 앱 토큰 부활 경합` 같은 내부 용어가 섞여 있다. 전 건 제목을 **보고서 표제로 다시 쓴 뒤** `cases.json` 에 넣는다.
136
+ 원자료 제목에는 `>` 구분자, `#2`, `[후속]`, `(심각)` 처럼 티켓에서만 뜻이 통하는 표기와 내부 용어가 섞여 있다.
137
+ 전 건 제목을 **보고서 표제로 다시 쓴 뒤** 데이터에 넣는다.
138
+
139
+ > 예: 「RN 앱 토큰 부활 경합」 → 「로그아웃 직후 이전 로그인 상태로 되돌아가던 상황」
140
+
141
+ ### ⚠ `text_fix` 치환표는 관보에서 **안 돈다** (코드 확인 · 옛 문서와 어긋남)
140
142
 
141
- > 예: "RN 앱 토큰 부활 경합" → "로그아웃 직후 이전 로그인 상태로 되돌아가던 문제"
143
+ `text_fix` 는 `build.py:516` 에만 있고, 그 줄은 **옛 「수정 내역 보고」 경로 안**이다.
144
+ 관보 경로 `build_gazette()`(`build.py:421-454`)는 그 코드를 지나지 않고, `gazette.py` 에는 `text_fix` 라는 낱말이 아예 없다.
145
+ 서버는 그 키를 화이트리스트에 넣어 **받기만 한다**(`report_publish.php:45`) — 설정에 실어 두고 아무도 안 읽는다.
142
146
 
143
- **주의**: `report.json` 의 `text_fix` 치환은 `symptom·cause·fix·how_to_check·remaining` 다섯 필드에만 걸린다
144
- (`build.py` 의 `fields` 튜플). **`title` 은 치환을 거치지 않는다.** 제목은 처음부터 고객 언어로 써야 한다.
147
+ 실측(2026-08-24): `{"from":"레코드","to":"항목"}` 을 넣고 원고에 「레코드」를 심어 렌더했더니 **「레코드」가 그대로 산출물에 남았다.**
148
+
149
+ 그러므로 **고객 언어 치환은 원고에서 직접 한다.**
150
+ 치환 누락을 잡는 그물은 `forbid` 하나뿐이고, 그것도 로컬 렌더에서만 돈다(6단계).
145
151
 
146
152
  ---
147
153
 
148
154
  ## 4. 3단계 — 등급 부여 (독립 단계로 분리한다)
149
155
 
150
- 검수 독자가 가장 먼저 보는 축이다. "이걸 너희는 어디까지 확인했는가"에 답하는 장치다.
156
+ 검수 독자가 가장 먼저 보는 축이다.
157
+ 「이걸 너희는 어디까지 확인했는가」에 답하는 장치다.
151
158
 
152
159
  ### 분류 규율
153
160
 
154
- 각 건의 `fix` / `how_to_check` 본문을 읽고, **본문에 실제로 적혀 있는 근거만으로** 분류한다.
161
+ 각 건의 `fix`·`how_to_check` 본문을 읽고, **본문에 실제로 적혀 있는 근거만으로** 분류한다.
155
162
 
156
- 예시 프로젝트의 4단계(강한 순):
163
+ 예시(4단계, 강한 순):
157
164
 
158
- | 등급 | 뜻 (문서에 범례로 함께 실린다) | 붙는 조건 |
165
+ | 등급 | 뜻 (범례로 문서에 함께 실린다) | 붙는 조건 |
159
166
  |---|---|---|
160
167
  | 운영 환경 확인 | 운영 환경에서 실제로 동작시켜 결과까지 확인 | 실제 환경에서 재현·확인했다고 본문에 적혀 있음 |
161
168
  | 실데이터 대조 | 운영 데이터를 건별로 대조해 확인 | 데이터를 조회·대조했다고 적혀 있음 |
@@ -164,473 +171,477 @@ source_url 제보 원본 링크 (없으면 생략)
164
171
 
165
172
  | 규칙 | 근거 | 어기면 |
166
173
  |---|---|---|
167
- | **본문에 적힌 근거로만** 분류한다. 실제로 무엇을 했는지가 아니라 *문서가 주장할 수 있는 것*만 등급이 된다 | 독자는 문서 밖을 볼 수 없다 | 문서에 없는 작업이 등급의 근거가 되고, 등급은 자기 신고가 된다 |
168
- | 승급의 근거는 **본문의 짧은 원문 인용**(30자 이내)으로 스스로 확인한다 | 인용이 안 잡히면 그 등급을 주장할 근거가 본문에 없다는 뜻이다 | 근거 없는 승급을 막을 수단이 사라진다 |
174
+ | **본문에 적힌 근거로만** 분류한다 | 독자는 문서 밖을 볼 수 없다. 등급은 *문서가 주장할 수 있는 것*이지 실제로 한 일이 아니다 | 등급이 자기 신고가 된다 |
175
+ | 승급의 근거를 **본문의 짧은 원문 인용**(30자 이내)으로 스스로 확인한다 | 인용이 안 잡히면 그 등급을 주장할 근거가 본문에 없다는 뜻이다 | 근거 없는 승급을 막을 수단이 사라진다 |
169
176
  | **최약 등급이 기본값**이다 | 근거가 없으면 최약 등급 | 근거 없는 건이 중간 등급으로 흘러 등급 축 전체가 무의미해진다 |
170
- | 최약 등급의 정의문에 **한계를 명시**한다 | `report.json` 의 `grades["반영 확인"][1]` = `"…확인 (화면 재현은 미실시)"` | 등급 이름만으로는 무엇을 안 했는지 알 수 없다. 검수 독자는 "전부 완료"보다 "여기까지만 확인함"을 훨씬 신뢰한다 |
177
+ | 최약 등급의 정의문에 **한계를 명시**한다 | 등급 이름만으로는 무엇을 안 했는지 알 수 없다 | 검수 독자는 「전부 완료」보다 「여기까지만 확인함」을 훨씬 신뢰한다 |
171
178
  | 등급 부여를 **요약 집필과 다른 판단으로** 분리한다(가능하면 다른 에이전트) | 요약을 쓰는 사람이 등급도 매기면 자기 요약을 뒷받침하도록 등급이 끌려간다 | 요약과 등급 축이 서로를 증명하는 순환이 된다 |
172
179
 
173
- **등급 인용(evidence)은 최종 스키마에 남는 필드가 아니다.** `cases.json` 에는 `grade` 만 들어간다.
174
- 인용은 분류 근거를 스스로 검증하기 위한 **작업 중 산출물**이다. 문서에 발췌 인용을 넣지는 않는다 —
175
- 문장 중간에서 잘린 인용은 조치 본문과 중복이고 읽기만 방해한다.
180
+ 등급 인용은 **작업 중 산출물**이고 발행 데이터에 남지 않는다.
181
+ 문서에 발췌 인용을 넣지는 않는다 — 문장 중간에서 잘린 인용은 조치 본문과 중복이고 읽기만 방해한다.
176
182
 
177
- ### `grades` 선언
183
+ ### `grades` 는 **키 순서가 강도 순서**다
178
184
 
179
- `report.json` 의 `grades` 에는 **실제로 쓰이는 등급만** 선언하는 것이 좋다 — 검수자에게
180
- 쓰지도 않는 등급 정의를 읽히지 않기 위해서다. 다만 강제는 아니다. 범례는 건수가 0이 아닌
181
- 등급만 그리고(`build.py`: `for k in grades if mc.get(k)`), `qa.py` 는 `grades` 를 읽지 않으므로
182
- 안 쓰는 등급을 남겨 두어도 검사는 통과한다(실측 확인).
185
+ `gazette.py:80-83` 이 주석으로 「스킬 2단계가 계약」이라고 물고 있다.
186
+ 자리로 세기를 정한다 — JSON 객체의 키 순서가 그대로 강한 순이다.
183
187
 
184
- 실측 분포(예시 프로젝트 18건): 운영 환경 확인 10 · 실데이터 대조 4 · 반영 확인 3 · 재발 점검 적용 1.
188
+ ```
189
+ RANK_LVL = ['full', 'half', 'open', 'open'] # 대조표의 강도 표식
190
+ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
191
+ ```
192
+
193
+ 실측으로 나오는 두 가지 한계(`gazette.py:751-762`).
194
+
195
+ - **강도 표식은 3단까지만 갈린다.** 3번째와 4번째가 둘 다 `open` 이다.
196
+ - **5번째 등급부터는 전부 최약 표식으로 눌린다**(`table[-1]`). `grades` 에 없는 이름을 건에 적어도 같다.
197
+
198
+ 그래서 등급은 **4단 이하**로 짠다.
199
+ 더 필요하면 `grades[k]` 의 3·4번째 자리에 표식·배지 이름을 직접 준다(같은 함수의 `slot` 분기).
185
200
 
186
201
  **묻는다**: 등급 이름과 개수가 프로젝트에 맞는지.
187
- **협상 대상이 아닌 것**: "근거 없으면 최약 등급", "최약 등급 정의에 한계 명시".
202
+ **협상 대상이 아닌 것**: 「근거 없으면 최약 등급」, 「최약 등급 정의에 한계 명시」.
188
203
 
189
204
  ---
190
205
 
191
206
  ## 5. 4단계 — 요약·확인 요청 집필
192
207
 
193
- 건별 내용이 아니라 **문서의 판단 층**이다. 전부 `report.json` 안에 있다.
208
+ 건별 내용이 아니라 **문서의 판단 층**이다.
209
+
210
+ ### 문체
194
211
 
195
- ### 문체 규칙
212
+ 개조식이다 — `~함` / `~했음` / `~필요` / `~한 상황`.
213
+ 대응표는 `references/lim-portal-format.md` §1.
196
214
 
197
215
  > · 개발 용어(파일명·커밋해시·SQL·클래스명) 금지. **배포 단계 용어(스테이징/테스트환경) 금지.**
198
- > · 결론을 먼저 쓴다. 각 문단은 굵게 강조할 리드 문장 하나로 시작한다.
216
+ > · 결론을 먼저 쓴다. 각 문단은 리드 문장 하나로 시작한다.
199
217
  > · 과장 금지. 확인한 것과 확인하지 못한 것을 구분해서 쓴다.
200
- > · 전 건이 이미 조치·반영을 마쳤다면 **"해결했으니 검토해 달라"가 기본 입장**이다.
201
218
 
202
- ### `summary` — 4개의 `{lead, body}`
219
+ 문체를 안 바꾸면 낱말이 아니라 **조판의 문제**로 남는다 — 경어체 문장은 관보 문법 위에서 겉돈다.
220
+ `tone.mjs` 가 경어체 잔존을 센다.
203
221
 
204
- `lead` 는 결론 한 문장(`.ex-lead` 로 굵게 렌더), `body` 는 2~4문장. **건별 나열 금지.**
205
- 검수자가 첫 화면에서 판단을 끝낼 수 있어야 한다. 문단 골격:
222
+ ### `summary` — 4개의 `{lead, body, refs}`
223
+
224
+ `lead` 는 결론 한 문장, `body` 는 2~4문장.
225
+ **건별 나열 금지** — 검수자가 요약면에서 판단을 끝낼 수 있어야 한다.
206
226
 
207
227
  1. 접수된 N건이 어떤 성격이었고 지금 어떤 상태인가
208
228
  2. 확인 과정에서 함께 발견해 고친 것 중 영향이 컸던 것
209
- 3. 여러 건을 관통하는 **공통 원인**이 있다면 그것
229
+ 3. 여러 건을 관통하는 **공통 원인**이 있다면 그것 — **실재할 때만 쓴다.** 없는데 만들어 내면 그게 이 문서에서 가장 나쁜 슬롭이다
210
230
  4. 검토 요청 — 무엇을 봐 주면 되는지
211
231
 
212
- ③ 은 **실재할 때만 쓴다.** 없는데 만들어 내면 그게 이 문서에서 가장 나쁜 슬롭이다.
213
- 공통 원인이 실재할 때만 `report.json` 의 `figure` 로 도해(인라인 SVG)를 붙인다.
214
- 도해 색은 전부 `var(--…)` 토큰이어야 한다 — 인라인 SVG 는 사용자 정의 속성을 상속하므로
215
- `fill="var(--primary-bg)"` 가 그대로 동작하고, 팔레트를 갈아끼워도 도해가 따라온다.
232
+ ### 요약 근거 게이트 — 발행이 **거절**된다
216
233
 
217
- ### `open_items` — 3~6개의 `{title, body, refs[]}`
234
+ `report_publish.php:260-287` 이 짝을 이룬 두 검사를 건다.
218
235
 
219
- 건별 `remaining` 을 **성격별로 묶는다.** 건별로 다시 나열하지 않는다.
236
+ | 검사 | 무엇을 보는가 | 어기면 |
237
+ |---|---|---|
238
+ | 참조 존재 | 문단마다 근거 최소 하나 | 거절 |
239
+ | 참조 실재 | 그 번호가 실제로 있는 사례인가 | 거절 |
220
240
 
221
- > 이건 고객 숙제 목록이 아니라 **우리가 확정하지 못한 것을 밝히는** 절이다. 주어를 우리로 써라.
241
+ 근거는 두 모양 다 받는다 — `refs: ["3.1"]` 또는 본문 안의 `§3.1`.
242
+ 사례 번호는 입력에 없다 — `groups[].no` 아래로 1부터 서버가 **계산**한다.
222
243
 
223
- 이 한 줄이 문서의 성격을 바꾼다. 같은 데이터를 "고객이 해야 할 일"로 쓰면 책임 전가 문서가 되고,
224
- "우리가 아직 못 닫은 것"으로 쓰면 신뢰 문서가 된다. 그래서 `open_items[].title` 은 전부 1인칭 완전문이다 —
225
- `검증에 쓴 표본이 작습니다`, `같은 전화번호를 쓰는 계정의 주인은 저희가 판단할 수 없습니다`,
226
- `같은 원인으로 생긴 피해 범위를 끝까지 세지 못했습니다`.
244
+ > ⚠ **사례가 없는 문서는 `summary` 를 쓸 수 없다.**
245
+ > `cases` 가 비면 있는 번호가 0개라 모든 요약 문단이 「근거 없음」으로 거절된다.
246
+ > 요청·안내만 있는 문서라면 `summary` 를 비우고 요지를 `notice`·`bar_note`·`asks` 절 `lede` 로 옮긴다.
227
247
 
228
- | 규칙 | 근거 | 어기면 |
229
- |---|---|---|
230
- | 확인 요청 절의 주어는 **우리**다 | 위 규칙 | 마무리 못 한 쪽이 상대에게 일을 넘기는 문서가 된다 |
231
- | 결론 먼저, 문단당 리드 1문장 | `.ex-lead` 가 있어야 요약 그리드의 시각 위계가 선다 | 굵게 렌더할 것이 없어 4칸이 균질한 회색 덩어리가 된다 |
232
- | 확인한 것과 확인 못 한 것을 **문장 안에서 구분**한다 | 등급 축(3단계)과 본문이 어긋나면 안 된다 | 검수 독자는 그 어긋남을 가장 먼저 찾아낸다 |
233
- | 파괴적 동작은 **결과를 같이 적고, 두 곳에 중복**한다 | 예시: `[발송] 버튼은 실제로 고객에게 알림톡이 나가므로 누르지 말아 주시기 바랍니다` — `summary` 4번과 `cta` 두 곳 | 한 곳에만 있으면 훑어 읽는 독자가 놓친다. 이 중복은 실수가 아니라 설계다 |
248
+ ### `open_items` — 3~6개의 `{title, body, refs[]}`
234
249
 
235
- ### `cta` 는 유일한 raw HTML 필드
250
+ 건별 `remaining` 을 **성격별로 묶는다.**
251
+ 건별로 다시 나열하지 않는다.
236
252
 
237
- `build.py` 의 `asks` 분기는 `cta` 원소를 이스케이프하지 않고 `link()` 만 통과시킨다
238
- (`f'<p>{link(p)}</p>'`). 그래서 `<strong>` 을 직접 쓸 수 있고, **동시에 `<`·`&` 를 그냥 쓰면 마크업이 깨진다.**
239
- `figure` 로 지정한 파일도 통째로 삽입된다. 나머지 필드는 전부 `e()` 로 이스케이프된다.
253
+ > 이건 고객 숙제 목록이 아니라 **우리가 확정하지 못한 것을 밝히는** 절이다.
254
+ > 주어를 우리로 써라.
240
255
 
241
- ### `§` 는 원고 마커다
256
+ 이 한 줄이 문서의 성격을 바꾼다.
257
+ 같은 데이터를 「고객이 해야 할 일」로 쓰면 책임 전가 문서가 되고, 「우리가 아직 못 닫은 것」으로 쓰면 신뢰 문서가 된다.
258
+ 그래서 `open_items[].title` 은 전부 1인칭이다 — `검증에 쓴 표본이 작음`, `같은 전화번호를 쓰는 계정의 주인은 우리가 판단할 수 없음`.
259
+
260
+ | 규칙 | 어기면 |
261
+ |---|---|
262
+ | 확인 요청 절의 주어는 **우리**다 | 마무리 못 한 쪽이 상대에게 일을 넘기는 문서가 된다 |
263
+ | 확인한 것과 확인 못 한 것을 **문장 안에서 구분**한다 | 등급 축과 본문이 어긋나고, 검수 독자는 그 어긋남을 가장 먼저 찾아낸다 |
264
+ | 파괴적 동작은 결과를 같이 적고 **두 곳에 중복**한다(요약 + `asks`) | 한 곳에만 있으면 훑어 읽는 독자가 놓친다. 이 중복은 실수가 아니라 설계다 |
242
265
 
243
- 절 참조는 원고에 `§3.1` 로 쓴다. `build.py` 의 `link()` 가 `<a class="xref" href="#s3-1">3.1</a>` 로 바꾸며
244
- `§` 기호를 버린다(§ 는 법령·논문 기호이지 보고서 활자가 아니다). 검수자는 건과 건을 오가며 대조하므로
245
- 손으로 찾게 두면 안 된다.
266
+ ### `§` 는 원고 마커다
246
267
 
247
- **`link()` 를 통과하는 자리**: 절 `lede`, `summary` 의 `lead`/`body`, `open_items` 의 `body`/`refs`,
248
- `cta`, `note` 절 `paragraphs`, 그리고 `chunk()` 를 거치는 건별 5개 본문 필드.
249
- **통과하지 않는 자리**: 모든 제목(`title`, `open_items[].title`), 지표 라벨, 표지 메타.
268
+ 절 참조는 원고에 `§3.1` 로 쓴다.
269
+ 렌더러의 `link()` 가 앵커로 바꾸며 `§` 기호를 버린다 — `§` 는 법령·논문 기호이지 보고서 활자가 아니다.
250
270
 
251
- 제목에 `§` 를 쓰면 빌드가 중단된다(실측): `AssertionError: § 가 산출물에 남았다 — link() 를 거치지 않은 텍스트가 있다`.
271
+ `link()` 를 안 거친 자리에 `§n.n` 이 남으면 **렌더가 중단된다**(`build.py:390-393`).
272
+ 제목·라벨·표지 메타는 `link()` 를 안 지난다 — 거기 절 참조를 쓰지 마라.
252
273
 
253
274
  ---
254
275
 
255
- ## 6. 5단계 — 팔레트
276
+ ## 6. 5단계 — 여백 주석(`marg`)을 채운다
277
+
278
+ **여기가 관보의 본체다.**
279
+ 옛 절차의 이 자리에는 팔레트 고르기가 있었다.
280
+ 관보에는 팔레트가 없다(`build_gazette` 가 `styles(..., tokens=False)` 로 부른다 — `build.py:441`).
281
+ 고를 색이 없어진 자리를 **채울 문장**이 대신한다.
256
282
 
257
- **색을 손으로 고르지 않는다.** `report.json` 의 `palette` 한 줄만 정하면 나머지는 유도된다.
283
+ `marg` 가 비면 렌더러가 경고한다(`gazette.py:400-402`).
258
284
 
259
285
  ```
260
- "palette": "korean-navy"
286
+ 절 03 「처리 현황」에 여백 주석(marg.why/key/advice)이 없습니다 — 두 단 본문이 한 단으로 섭니다
261
287
  ```
262
288
 
263
- `palette.py` 의 `derive()` 가 팔레트 6색(primary·secondary·bg·surface·ink·mid)에서 문서용 토큰 21개를 유도하고,
264
- `audit()` 가 17쌍의 대비를 되잰다. `build.py` 가 그 결과를 `:root` 로 만들어 문서 맨 앞에 주입한다.
265
- `report.css` 는 색 토큰을 **하나도 선언하지 않는다** — 구조 스타일시트일 뿐이다.
289
+ 거절이 아니라 경고다.
290
+ **즉 안 채워도 문서는 나온다** — 그리고 그 문서는 「문서처럼 꾸민 웹페이지」로 되돌아간다.
291
+ 이 경고를 안고 고객에게 링크를 보내지 마라.
266
292
 
267
- 유도가 필요한 이유: doc-pptx 의 큐레이션 팔레트는 **발표 슬라이드용**이라 문서 본문 기준으로는 대비가 모자란다.
268
- Korean Navy 의 Gold `#C9A961` 은 흰 바탕에서 **2.25:1** 이라 글자로 쓰면 WCAG 를 통과하지 못한다.
269
- 그래서 같은 색을 용도별로 쪼갠다 — `--accent`(배경·괘선 전용) / `--accent-mid`(24px+ Bold, 3:1) /
270
- `--accent-ink`(작은 글자, 4.5:1) / `--accent-chip`+`--accent-on`(칩 배경과 그 위 글자, 쌍으로만 쓴다).
271
- 토큰 역할의 전모는 `design-system.md`.
293
+ ### 세 줄이 각각 무엇인가
272
294
 
273
- ### 무엇을 묻고 무엇을 판단하는가
295
+ | 칸 | 무엇을 적는가 | 문형 |
296
+ |---|---|---|
297
+ | `why` | 그 절을 **왜** 읽는가. 독자가 실제로 던질 질문 그대로 | `~하는가?` / `~인가?` (`~합니까?` 아님) |
298
+ | `key` | 이 절에서 하나만 가져간다면 그것 | 개조식 단문 |
299
+ | `advice` | 어디를 참조해야 하는지. 근거의 출처를 가리킨다 | 개조식 단문 |
274
300
 
275
- **묻는다**: 톤(격식 / 담백 / 임팩트), 고객사 브랜드색을 쓸 것인지.
276
- **판단한다**: 그 답을 어느 팔레트 키로 옮길지. 색 값 자체는 판단하지 않는다.
301
+ 셋 중 채운 것만 그려진다.
302
+ 하나도 없으면 `<aside class="marg">` 자체가 안 나오고 본문이 한 단이 된다.
277
303
 
278
- | 독자·성격 | 팔레트 키 |
304
+ ### 나쁜 `marg` 의 모양 넷
305
+
306
+ | 모양 | 왜 나쁜가 |
279
307
  |---|---|
280
- | 격식 보고·검수 요청·임원 열람 | `korean-navy` (기본) |
281
- | 일반 B2B·신뢰 | `claude-classic` |
282
- | 공문·법무·최대한 담백하게 | `claude-mono` |
283
- | 데이터·금융 | `claude-blue-calm` |
284
- | 한국 중견기업 B2B | `korean-brick` |
285
- | 헬스·교육 | `korean-sage` |
286
- | 임팩트 | `high-contrast` |
308
+ | 본문 첫 문장을 그대로 옮긴 `key` | 같은 것이 두 번 있게 된다. 여백 단은 요약란이 아니라 **다른 층위**다 |
309
+ | 「이 절에서는 …을 설명합니다」류의 `why` | 독자의 질문이 아니라 저자의 목차다. 독자는 그 질문을 던지지 않는다 |
310
+ | 어느 절에나 붙는 `advice` (「자세한 내용은 본문 참조」) | 가리키는 곳이 없으면 안 쓰느니만 못하다. **실제 절 번호**를 대라 |
311
+ | 채우려고 넣은 세 줄 | 세 칸을 다 채우는 것이 목표가 아니다. 할 말이 둘이면 둘만 쓴다 |
287
312
 
288
- `palette.py` 의 `PALETTES` 에는 9개가 들어 있고(위 7개 + `claude-coral`·`claude-green`),
289
- `python3 palette.py` 를 단독 실행하면 9개 전부의 감사 결과가 나온다. **9개 전부 OK 다**(실측).
290
- Dark Editorial 은 다크 배경 팔레트라 흰 바탕 인쇄 전제와 맞지 않아 제외돼 있다.
313
+ ### 절 종류별로 성격이 갈린다
291
314
 
292
- ### 브랜드색을 쓸 때
315
+ | kind | `why` 가 답하는 질문 | `key` 가 담는 것 |
316
+ |---|---|---|
317
+ | `asks` | 무엇을 조치해야 하는가? | 그중 가장 시급한 것 하나 |
318
+ | `summary` | 이번 회차는 결국 어떻게 됐는가? | 판단에 필요한 한 문장 |
319
+ | `status` | 어디까지 믿어도 되는가? | 등급 축이 뜻하는 것 |
320
+ | `cases` | 무엇을 고쳤는가? | 이 묶음의 성격(접수 건인가 자체 발견인가) |
321
+ | `note` | 무엇을 아직 모르는가? | 확인 못 한 범위 |
293
322
 
294
- `palette` 자리에 문자열 대신 6색 딕셔너리를 직접 넣을 수 있다.
323
+ ### 분량
295
324
 
296
- ```json
297
- "palette": { "primary":"#FFD400", "secondary":"#7A5CFF", "bg":"#FFFFFF",
298
- "surface":"#FAF9F5", "ink":"#141413", "mid":"#B0AEA5" }
299
- ```
325
+ 여백 주석 단은 좁다 — `--marg: 240px`(실측).
326
+ 한글로 19자/줄쯤이다.
327
+ **한 칸을 두 문장 넘게 쓰지 마라** — 절반 넘게 접힌다.
300
328
 
301
- `derive()` 가 명도만 낮춰 대비를 맞추므로 **색상·채도는 브랜드 그대로 살아남는다.**
302
- 위 형광 노랑 primary 도 그대로 통과한다(실측: 빌드 성공, QA 실패 0).
303
- 다만 `ink` 를 흐린 회색으로 주면 유도로 구제할 수 없어 **빌드가 중단된다**(실측):
329
+ 글줄 폭 근거와 자폭 실측치는 `gazette-report` 스킬에 있다.
330
+ 여기 옮겨 적지 않는다.
304
331
 
305
- ```
306
- AssertionError: 팔레트 대비 미달: [('본문 ink/paper', 3.45), ('본문 ink/surface', 3.38), …]
307
- ```
332
+ ---
308
333
 
309
- 이게 정상 동작이다. 대비 미달을 안고 문서가 나가는 경로는 없다.
334
+ ## 7. 6단계 — 조립
310
335
 
311
- | 규칙 | 근거 | 어기면 |
312
- |---|---|---|
313
- | 한 문서 한 팔레트 | `:root` 는 하나만 주입된다 | — |
314
- | **`--accent` 를 글자색으로 쓰지 않는다** | `qa.py` 가 `color:var(--accent)` 를 실패로 잡는다(하이픈 접두 속성은 제외하므로 `border-color` 는 정상) | 2.25:1. "좀 연하네"로만 보이고 대비 미달인 줄 모른다 |
315
- | `--mid` 는 괘선 전용 | 같은 검사가 `color:var(--mid)` 도 실패로 잡는다 | 2.3:1 |
316
- | 색을 손으로 추가하지 않는다 | 하드코딩 hex 는 QA 경고로 잡히고, 팔레트를 갈아끼울 때 그 색만 남는다 | 팔레트 교체가 반쪽이 된다 |
336
+ ### 골격 순서는 바꾸지 않는다
317
337
 
318
- 팔레트 교체는 실제로 한 줄이다. 예시 프로젝트를 `claude-mono`·`korean-brick`·`high-contrast` 로 바꿔
319
- 빌드·QA 를 돌리면 셋 다 `통과 48 · 경고 1 · 실패 0` 이다(실측).
338
+ `gazette.py:898-906` 이 순서를 고정한다.
320
339
 
321
- ---
340
+ ```
341
+ 표지 → 발행 이력 → 차례 → 이 문서 읽는 법 → 요약
342
+ → PART 간지 → 절 → … → 부록 → 판권지
343
+ ```
322
344
 
323
- ## 7. 6단계 — 조립
345
+ 칸 하나하나의 뜻과 예시는 `references/lim-portal-format.md`.
324
346
 
325
- `build.py` 가 하는 일만 알면 된다. 손댈 일은 거의 없다.
347
+ ### PART 를 먼저 가른다
326
348
 
327
- ### 문서 순서
349
+ `parts` 가 없으면 경고 뒤 절 전체가 한 묶음으로 선다(`gazette.py:843-847`).
350
+ 그러면 「먼저 읽을 것 / 경과 / 남은 것」이라는 **읽기 순서가 사라진다.**
328
351
 
329
- `report.json` 의 `sections` 배열 순서가 그대로 문서 순서다. `kind` 다섯 종류가 렌더 방식을 정한다.
352
+ 가르는 기준은 절의 성격이 아니라 **독자가 무엇을 해야 하는가**다.
330
353
 
331
- | `kind` | 렌더 | 읽는 설정 |
354
+ | PART | 독자의 몫 | 대개 들어가는 kind |
332
355
  |---|---|---|
333
- | `summary` | 요약 2열 그리드 + (있으면) 도해 | `summary`, `figure` |
334
- | `status` | 등급 범례 + 전 건 판정표 | `grades`, 전 건 |
335
- | `cases` | 건별 카드 | `groups[].key` 로 짝지어진 `cases` |
336
- | `asks` | CTA 박스 + 확인 요청 카드 2열 | `cta`, `open_items` |
337
- | `note` | 꼬리 문단 | `paragraphs` |
356
+ | 1 요청 | 지금 해야 할 것 | `asks` |
357
+ | 2 경과 | 읽고 확인할 것 | `summary` · `status` · `cases` |
358
+ | 3 남은 것 | 알아 둘 것 | `note` |
338
359
 
339
- 표지(`cover`)·지표 밴드(`stats`)·목차(`toc`)·꼬리말(`footer`)은 `sections` 밖에서 자동으로 붙는다.
340
- 목차는 좌측 고정 열이다 — 본문 위에 얹으면 웹앱처럼 보인다.
360
+ 절이 하나뿐인 묶음도 간지를 둔다 — 간지는 장식이 아니라 층위 표시다.
341
361
 
342
- 건 카드 내부는 전 건 고정 3단이다.
362
+ ### `kind` 는 다섯뿐이다
343
363
 
344
- ```
345
- 진단 2열 (증상 · 원인) → 조치 전폭 → 확인 2열 (확인 방법 · 확인 요청)
346
- ```
364
+ `summary` · `status` · `cases` · `asks` · `note`.
365
+ 그 밖의 값은 **서버가 거절한다**(`REPORT_RENDER_KINDS`, `report_publish.php:96·172-175`).
366
+ 거절하는 이유는 완성도가 아니라 손실이다 — 모르는 `kind` 의 절은 렌더러가 아무 말 없이 통째로 빠뜨린다.
367
+
368
+ `tone` 은 여섯이다(`you`·`deep`·`neutral`·`done`·`wait`·`hold`).
369
+ 목록 밖의 값은 **조용히 `neutral` 로 강등된다**(`gazette.py:801-802`) — 기계가 안 잡으니 눈으로 본다.
370
+ 생략하면 `kind` 가 정한다.
347
371
 
348
- 배치 자체가 "무엇이 보였나 → 왜 → 이제 어떻게 → 당신이 볼 것" 서사를 강제한다.
349
- 확인 요청이 없는 건은 `.ck.one` 으로 한 칸이 된다.
372
+ ### 렌더러가 자동으로 하는 것
350
373
 
351
- ### 절 번호는 데이터에서 계산된다
374
+ | 무엇 | 어디서 | 원고에서 할 일 |
375
+ |---|---|---|
376
+ | 문장 단위 줄바꿈 (`<span class="sn">`) | `sentbreak.py` + `boundary.py` 를 렌더러가 부른다 | **문장을 짧게 끊는다.** 낱말은 하나도 안 바뀐다 |
377
+ | `§3.1` → 앵커 링크 | `link()` | 절 참조를 `§n.n` 으로 적는다 |
378
+ | 사례 번호 계산 | `groups[].no` 아래로 1부터 | 번호를 손으로 적지 않는다 |
352
379
 
353
- `groups[].no` 아래로 1부터 매긴다 → `3.1`, `3.2`, … `4.1`.
354
- 어느 그룹에도 배정되지 않은 건이 있으면 빌드가 중단된다:
380
+ **긴 산문의 문단 분절은 자동으로 안 된다.**
381
+ 옛 경로의 `chunk()` 는 관보 렌더러에 안 넘어간다(`build.py:440` 이 `{'e', 'link'}` 만 준다).
382
+ 문단은 원고에서 끊는다.
355
383
 
356
- ```
357
- AssertionError: 그룹에 배정되지 않은 건이 있다: 17/18 — cases 의 group 값이 ['reported', 'own'] 중 하나여야 한다
358
- ```
384
+ ### 관보에 자리가 없는 칸
359
385
 
360
- 지표 밴드·범례·판정표의 숫자도 전부 데이터에서 센다(`Counter(c['grade'] for c in cases)`).
361
- **손으로 적은 숫자는 반드시 본문과 어긋나고, 그 어긋남을 독자가 먼저 찾는다.**
362
- `stats` 의 값만은 사람이 적으므로(`report.json`), 여기만은 건 수와 맞는지 눈으로 확인한다.
386
+ `chapters`(챕터 오프닝) · `figure`(요약 도형) · `principles` · `logic` · `assets` 중 `role != "logo"` 인 것.
387
+ 전부 **세고 버린다**(`gazette.py:878-896`).
388
+ 보낸 사람이 실린 줄 알면 안 되므로 ⚠ 로 소리 내어 센다.
363
389
 
364
- ### 자동으로 일어나는 텍스트 변환 3종
390
+ `assets[].brief` 를 주면 **서버가 진짜로 그린다** — 크레딧이 타고 렌더러가 버린다.
391
+ 자세한 것은 `references/illustrations.md`.
365
392
 
366
- | 변환 | 무엇을 하는가 | 어디에 적는가 |
393
+ ### 방어선
394
+
395
+ | 층 | 무엇을 잡는가 | 결과 |
367
396
  |---|---|---|
368
- | `text_fix` | 개발 서술 → 고객 서술. 문자열 짝 치환 | `report.json` 의 `text_fix` 배열 |
369
- | `link()` | `§3.1` → 그 절로 가는 앵커, `§` 제거 | 자동 |
370
- | `chunk()` | 긴 산문 분절 + `(1)(2)` 열거 목록화 | 자동 |
397
+ | 서버 검증 | 모르는 `kind` · 없는 그룹 · 없는 PART · 요약 근거 · `source_url` 모양 · 목록/객체 모양 | **발행 거절** |
398
+ | 렌더러 assert | `link()` 를 안 거친 `§n.n` · `forbid` 정규식 적중 | **렌더 중단(파일 안 씀)** |
399
+ | 렌더러 ⚠ | `marg` 없음 · `parts` 없음 · 그룹 미배정 건 · 자리 없는 그림 | 경고만 |
371
400
 
372
- `chunk()` 의 판정 규칙(`build.py` 의 `chunk()` docstring 에 근거가 있다):
401
+ ### ⚠ `forbid` 는 **로컬 렌더에서만** 돈다 (코드 확인)
373
402
 
374
- - `(N)` 마커가 2개 이상이면 열거 후보. **단 마커 바로 뒤에 조사(`은는이가을를도와과의에서로`)가 붙으면 참조**로 보고 목록화하지 않는다 — "(1)은 새 일지를 열 때…"를 쪼개면 조사만 남고 문장이 부서진다.
375
- - `(3)(4)` 처럼 번호가 붙어 나오면 두 항목을 함께 가리키는 한 덩어리로 합친다.
376
- - 열거가 아니면서 `길이 > 180자` 이고 `문장 > 2개` 면 두 문장 단위로 `<p>` 분절. 문장 분리는 한국어 종결어 기준(`(?<=다\.)`, `(?<=요\.)`, `(?<=오\.)`).
403
+ `guard()`(`build.py:394-396`)가 `cfg['forbid']` 를 읽어 산출물을 훑고, 걸리면 assert 로 죽는다.
404
+ 실측(2026-08-24): 금지어 「티켓」을 원고에 심고 `render.py` 를 돌리면 `AssertionError: 금지 표현 유출: 티켓 → ['티켓']` 으로 멈추고 파일이 안 써진다.
377
405
 
378
- ### `text_fix` 작성 규칙
406
+ 그런데 포털 발행에서는 이 그물이 없다.
407
+ `forbid` 는 `REPORT_CLIENT_KEYS` 에 없어서 `report_client_fields()` 가 **깎아 낸다**(`report_publish.php:76` 이 「서버만 정한다」고 명시).
408
+ 서버는 그 값을 따로 안 채운다 — 빈 채로 렌더러에 닿는다.
379
409
 
380
- 프로젝트마다 다르므로 `report.json` 에 적는다. **규칙 자체가 프로젝트 산출물이다.**
381
- 예시 프로젝트는 24쌍이고 그중 3쌍이 통째 삭제(`"to": ""`)다. 유형:
410
+ 포털 발행이 최종 경로라도 **발행 전에 같은 데이터를 로컬로 한 번 구워서 `forbid` 그물을 통과시켜라.**
411
+ 그것이 지금 유일한 자동 어휘 관문이다.
382
412
 
383
- | 유형 | 원문(내부) | 치환 후(고객본) |
413
+ ---
414
+
415
+ ## 8. 7단계 — 검수
416
+
417
+ 관문은 셋이고, 셋 다 다른 것을 본다.
418
+
419
+ | 층 | 무엇을 보는가 | 통과 기준 |
384
420
  |---|---|---|
385
- | 검수 환경 용어 | `테스트 매장의 테스트 고객` | `검수용 매장·고객` |
386
- | 배포 용어 | `2026-08-09 운영 배포를 마쳤고` | `2026년 8월 9일 운영 환경에 반영했고` |
387
- | 티켓 운영 | `별도 티켓으로 분리해 처리했습니다` | `§4.3에 따로 정리했습니다` (문서 안 참조로) |
388
- | 내부 산출물 규모 | `저장 경로 6곳을 수정했고` | `저장이 일어나는 모든 경로를 수정했고` |
389
- | 내부 검증 산출물 | `자동 검증 2건을 추가했습니다` | `같은 문제가 다시 나타나면 곧바로 걸러지도록 상시 점검을 걸어 두었습니다` |
390
- | 앱 배포 경로 | `다음 스토어 빌드부터` | `다음 앱 업데이트부터` |
391
- | 설계 검토 이력 | `처음 검토안 중 '…'은 …위험이 있어 제외했습니다.` | **삭제(`""`)** |
392
- | 화면에 영향 없는 내부 잔여 | `이 밖에 지금은 쓰이지 않는 잔여 기능 정리가 별도 건으로 남아 있습니다(화면 동작에는 영향 없음).` | **삭제(`""`)** |
421
+ | `dry_run` warnings | 무엇이 **안 실렸는가** | ⚠ 0건 |
422
+ | 검사기 8종 | 조판이 **성립했는가** | 각 검사기의 0건 |
423
+ | 사람의 눈 | 이 문서가 **이 독자에게 맞는가** | 판단 |
393
424
 
394
- > **금지: 의미를 바꾸는 치환.** 24쌍은 전부 어휘 교체 또는 삭제이지 사실 변경이 아니다.
395
- > "저장 경로 6곳" → "저장이 일어나는 모든 경로"는 **같은 사실의 다른 세는 단위**다(6곳이 전부였다).
396
- > 이 선을 넘으면 치환표가 아니라 은폐가 된다.
425
+ ### `dry_run` 이 실질적 유일한 품질 관문이다
397
426
 
398
- 치환은 **단순 문자열 `replace`** 다. 정규식이 아니고, 원문이 한 글자라도 다르면 조용히 안 걸린다.
399
- 그래서 `forbid` 가 따로 있다 — 치환이 빠진 것을 잡는 그물이다.
427
+ `publish_report(dry_run: true)` 는 렌더까지 돌리고 아무것도 저장하지 않는다.
428
+ 돌아오는 `warnings` 는 세 곳에서 모인다(`report_publish.php:1100-1108`).
400
429
 
401
- ### 조립기의 방어선 4종 — 전부 빌드 중단(assert)
430
+ 1. **렌더러가 센 것** — `marg` 없음 · `parts` 없음 · 그룹 미배정 건 · 자리 없는 그림. 이것이 먼저 온다.
431
+ 2. **입고 검사** — 렌더 산출물이 발행 규격을 지켰는가.
432
+ 3. **그림 관문** — 자산 재사용 등.
402
433
 
403
- | # | 검사 | 메시지 |
404
- |---|---|---|
405
- | 1 | 그룹 미배정 건 | `그룹에 배정되지 않은 건이 있다: N/M` |
406
- | 2 | 팔레트 대비 미달 | `팔레트 대비 미달: [...]` |
407
- | 3 | `§` 잔존 | `§ 가 산출물에 남았다 — link() 를 거치지 않은 텍스트가 있다` |
408
- | 4 | `forbid` 정규식 적중 | `금지 표현 유출: <패턴> → [...]` |
434
+ 2026-08-18 이전에는 이 값이 **한 번도 도구에 도달하지 않았다**(서버가 `error_log` 로만 흘렸다) — 에이전트는 「경고 없음」으로 읽었고 확인할 방법이 없었다.
435
+ 지금은 dry_run 과 실제 발행 응답 **양쪽에** 실린다.
409
436
 
410
- **`forbid` 는 조립기와 QA 가 같은 목록을 읽는다** — 둘 다 `report.json` 의 `forbid` 를 쓴다.
411
- 목록을 두 곳에 복사해 두면 한쪽만 고쳐져 조립기는 조용하고 QA 만 터지거나, 그 반대가 된다.
412
- 예시 프로젝트의 `forbid`:
437
+ ⚠ 가 하나라도 있으면 그것부터 고친다.
438
+ 경고를 안고 고객에게 링크를 보내지 마라.
413
439
 
414
- ```json
415
- ["1217\\d{12}", "[Aa]sana", "운영 배포", "별도 티켓", "자동 검증 \\d건"]
416
- ```
440
+ ### 검사기 8종
417
441
 
418
- 첫 항목이 티켓 번호다. **내부 식별자 유출은 경고가 아니라 빌드 실패로 다룬다** —
419
- 독자가 확인할 수 없는 식별자가 인쇄되면 발송 후에 회수할 수 없다.
442
+ `~/.claude/skills/gazette-report/scripts/` 에 있다.
443
+ **실행법과 설계 원리는 `gazette-report` 스킬에 있다** — 여기 두 벌로 두지 않는다.
420
444
 
421
- **묻는다**: 원자료에 남은 "확인 필요" 필드, 고객만 아는 고유명사 표기.
422
- **판단한다**: 위 변환 전부.
445
+ | 파일 | 무엇을 잡는가 |
446
+ |---|---|
447
+ | `kcheck.mjs` | 대비(알파·조상 opacity 합성) · 제목 레벨 · 표 접근명 · 앵커 · 7폭 넘침 |
448
+ | `read.mjs` | 한글 자폭 실측 글자/줄 · 행간 · 본문 대비 |
449
+ | `tone.mjs` | 경어체 잔존 · 늘어짐 지표 · 한 문장 한 줄 |
450
+ | `fontchk.mjs` | 렌더된 활자 크기·굵기 분포 |
451
+ | `flexcheck.mjs` | flex/grid 안에서 문장이 토막 나는 곳 |
452
+ | `cssaudit.sh` | 활자 스케일 · 간격 격자 · 표 마감 · 미봉책 |
453
+ | `pdfcheck.sh` | 실제 PDF 쪽 구조 |
454
+ | `contentdiff.sh` | 조판이 본문·id·앵커를 바꿨는지 |
423
455
 
424
- ---
456
+ **0표본 통과는 통과가 아니다.**
457
+ 표본 수를 함께 본다 — 잴 것이 없어서 0건인지, 재서 0건인지가 다르다.
425
458
 
426
- ## 8. 7단계 — QA
459
+ **재는 판이 실제 판과 다르면 인과가 거짓으로 잡힌다.**
460
+ 로컬 검증은 `render.py` 산출물로 한다(서버와 같은 서식·같은 폰트).
461
+ `build.py` 를 직접 부른 산출물로 재면 옛 시트가 실려 거짓 초록이 나온다.
427
462
 
428
- ```bash
429
- python3 ~/.claude/skills/genetics-report/assets/qa.py report.json
430
- ```
463
+ ### 자동 검사가 다 통과해도 **반드시 눈으로 연다**
431
464
 
432
- 실패가 하나라도 있으면 `exit 1`. 경고는 판단해서 넘긴다.
433
- 검사 대상은 `<body>` 이후 `<script>` 이전 구간(본문)이고, 고객본 원칙·외부 리소스·색 검사는 문서 전체를 본다 —
434
- **편집기 소스를 본문으로 오인하지 않기 위해서**다.
465
+ 자동 검사는 **안 보이는 결함**을 잡고, 눈은 **보이는 결함**을 잡는다.
466
+ 둘은 겹치지 않는다.
435
467
 
436
- ### 13군
468
+ 실제로 자동 검사를 다 통과한 문서에서 눈으로만 잡힌 것들.
437
469
 
438
- | # | 검사 | 판정 |
439
- |---|---|---|
440
- | 1 | 미치환 플레이스홀더 `{...}` | 실패 |
441
- | 2 | 가로 스크롤 유발 고정 px 폭 | 경고 |
442
- | 3 | 색 대비 17쌍 — **문서에 박힌 `:root` 를 읽어 잰다** | 실패 |
443
- | 4 | `--mid`·`--accent` 가 글자색으로 샜는가 | 실패 |
444
- | 4b | 팔레트 밖 하드코딩 hex / rgba 리터럴 | 경고 |
445
- | 5 | 폰트 화이트리스트 | 경고 |
446
- | 6 | `@font-face` 존재 + `format("woff2")` | 실패 |
447
- | 7 | 구조 일관 — 전 건이 절번호·증상·원인·조치·확인방법·등급칩을 갖는가 | 실패 |
448
- | 8 | 목차 항목 수·판정표 행 수 = 건 수 | 실패 |
449
- | 9 | 죽은 절 참조 | 실패 |
450
- | 10 | 고객본 원칙 — `§` 0건 + `forbid` 전체 | 실패 |
451
- | 11 | 카피 게이트 — 대시 대비 헤드라인 + AI 슬롭 어휘 | 실패 |
452
- | 12 | `lang` 선언 · `<img>` alt · 외부 리소스 | 실패 |
453
- | 13 | **고객본 내보내기 시뮬레이션** | 실패 |
454
-
455
- 3번이 요점이다. 빌드가 유도한 값이 아니라 **실제로 문서에 나간 `:root` 값**을 다시 잰다.
456
- 유도기를 잘못 고쳐도 산출물에서 잡힌다.
457
-
458
- 13번은 `editor.js` 의 `serialize(customer=true)` 와 같은 규칙으로 문서를 잘라 보고,
459
- `data-rte-ui`·`showSaveFilePicker`·`syncVerdict`·`data-report-edit`·`data-internal="true"` 가
460
- **하나도 남지 않는지** 확인한다. 크기도 원본의 98% 미만이어야 한다(실제로 안 지워졌으면 크기가 안 준다).
461
- 빌드가 편집 레이어에 표식을 안 붙이면 내보내기 코드는 **조용히 아무것도 못 지운다** — 그래서 기계로 검사한다.
462
-
463
- ### 프로젝트마다 채워야 하는 유일한 항목
464
-
465
- `report.json` 의 `forbid` 와 `allow_external` 뿐이다.
466
- 나머지(대비 쌍, 슬롭 어휘, 구조 검사, 대시 게이트)는 스킬에 고정돼 있다.
467
-
468
- ### 자동 검수가 통과해도 반드시 브라우저로 연다
469
-
470
- `qa.py` docstring 이 못박고 있다 — 사람이 봐야 하는 것(톤·독자 적합성·실제 렌더)은 이 검사기가 대신하지 못한다.
471
- 실제로 자동 검수를 다 통과한 문서에서 눈으로만 잡힌 것들: **안 보이는 배지, 빈 목록 항목,
472
- 한국어 낱말 중간 줄바꿈, 편집기가 판정표를 지워버리는 버그.**
473
- 시각 검수 목록은 `qa.md`.
474
-
475
- ### 남아 있는 경고 1건
476
-
477
- 예시 프로젝트에서 `⚠️ rgb/rgba 리터럴 2건` 이 뜬다. `report.css` 의
478
- 표지 `dl` 구분선 `rgba(255,255,255,.22)` 와 `.r-open .xref` 밑줄 `rgba(111,87,33,.35)` 다.
479
- 앞의 것은 어느 팔레트에서나 Primary 위 흰 반투명이라 문제없지만, **뒤의 것은 Korean Navy 의 골드에서
480
- 유래한 갈색 리터럴이라 팔레트를 바꿔도 그 색이 남는다.** 밑줄 1px 이라 대비 기준과는 무관하고
481
- 현재는 경고로 넘기고 있다. 팔레트를 바꿔 쓸 때 눈으로 한 번 볼 자리다.
470
+ - **안 보이는 배지** — 대비는 통과하는데 시각적으로 바탕에 묻혔다.
471
+ - **빈 목록 항목** — 데이터가 빈 문자열이라 마크업은 정상이고 화면에는 빈 줄만 남았다.
472
+ - **한국어 낱말 중간 줄바꿈** — `keep-all` 이 안 걸린 자리. 기계는 넘침만 보고 낱말 경계는 안 본다.
473
+ - **편집기가 대조표를 지워버리는 버그** — 렌더는 정상이고 저장 뒤에 사라졌다.
474
+ - **문서 안 편집 층의 흔적** — 관보는 안 싣지만(`cfg['editor'] = False`), 옛 데이터를 물려받으면 스위치만 남는다.
475
+
476
+ ### 사람의 눈 검수 목록
477
+
478
+ 브라우저로 열고 **인쇄 미리보기까지** 연다.
479
+ 관보는 인쇄되는 문서다 — 화면만 보면 절반만 본 것이다.
480
+
481
+ | 자리 | 무엇을 보는가 |
482
+ |---|---|
483
+ | 표지 | 대제목 첫 행이 한 줄에 서는가. `title_sub` 에 문장이 들어가 조판이 무너지지 않았는가 |
484
+ | 표지 우상단 | 고객사 마크 자리가 비어 있지 않은가(로고 없으면 활자로 서 있는가) |
485
+ | 표지 가운데 | 빈 마당이 그대로인가. **채우지 마라** — 그것이 문법이다 |
486
+ | 차례 | 항목 수가 절 수와 맞는가. 죽은 링크가 없는가 |
487
+ | 이 문서 읽는 법 | 질문형이 `~합니까?` 로 새지 않았는가 |
488
+ | 요약면 | `notice.state` 가 실제 상태와 맞는가. 상태 막대 라벨이 무엇을 세는지 `bar_note` 가 밝히는가 |
489
+ | PART 간지 | 인쇄에서 각각 1쪽 단독으로 서는가 |
490
+ | 절 첫 화면 | 두 단으로 서는가. 한 단이면 `marg` 가 빈 것이다 |
491
+ | 여백 주석 | 본문 첫 문장의 복사본이 아닌가. 셋을 채우려고 넣은 줄이 없는가 |
492
+ | 건 필드표 | 증상→원인→조치→확인 방법 순서가 전 건 같은가 |
493
+ | 대조표 | 등급 표식이 강한 순으로 서는가. 5번째 등급이 눌려 있지 않은가 |
494
+ | 본문 전체 | 경어체 잔존 0건인가(`습니다|십시오|합니다|입니다|됩니다|주세요`) |
495
+ | 본문 전체 | 낱말 중간에서 줄이 바뀐 곳이 없는가 |
496
+ | 본문 전체 | 독자가 조회할 수 없는 식별자가 남아 있지 않은가(포털 발행이면 이 그물이 없다 — 여기가 마지막 관문이다) |
497
+ | 부록 | 본문에서 실제로 참조되는 것만 있는가 |
498
+ | 판권지 | 문서가 여기서 닫히는가. 꼬리 카드가 붙어 있으면 구버전이다 |
499
+ | 인쇄 미리보기 | 절마다 새 쪽에서 시작하는가. 표가 쪽 경계에서 잘리지 않는가 |
482
500
 
483
501
  ---
484
502
 
485
503
  ## 9. 8단계 — 전달
486
504
 
487
- 전달물은 `.html` 파일 하나다. 상대는 더블클릭만 하면 된다.
505
+ 서식은 서버가 정하므로 고를 것이 없다.
506
+ 경로만 고른다.
488
507
 
489
- 마스터 파일은 `<body data-report-edit="on">` 이고 편집 레이어를 품고 있다.
490
- 고객본은 하단 바의 **`고객본 내보내기`** 버튼으로 **별도 파일**(`<원본이름>_고객본.html`)로 뽑는다.
508
+ ### 포털 발행 (기본)
491
509
 
492
- | 규칙 | 근거 | 어기면 |
493
- |---|---|---|
494
- | 최종 발송 직전에 **사람이 버튼을 누른다**(= 승인 행위) | `exportCustomer()` 의 `confirm` 이 제거 내역을 숫자로 명시한다: `내부 전용 블록 N개가 제거됩니다 / 편집 기능이 파일에서 완전히 삭제됩니다 / 원본은 그대로 둡니다` | 내부 전용 블록이 남았는지 사람이 확인할 마지막 기회가 사라진다 |
495
- | 고객본은 별도 파일, **원본은 건드리지 않는다** | 같은 `confirm` 문구 | 내부 검토본이 사라져 다음 회차에 다시 만들어야 한다 |
496
- | 문구 수정 요청은 **다시 빌드하지 말고** 문서에서 고친다 | `저장`(Cmd+S)이 **이 파일 자체**에 덮어쓴다. 사본을 만들면 어느 파일이 최신인지 알 수 없다 | `report.json` 과 HTML 이 갈라진다 |
497
- | 편집기로 손댄 뒤 내보냈다면 **결과 파일을 열어 본다** | QA #13 은 빌드 산출물에서 시뮬레이션한 것이지 사람이 편집한 뒤의 파일이 아니다 | 절번호·판정표가 어긋난 채 나갈 수 있다(편집기가 재생성하지만 확인은 사람이 한다) |
510
+ `publish_report` 에 데이터를 준다 — `tenant`·`slug` 필수.
511
+ 서버가 렌더하므로 **HTML 을 만들지 않는다.**
498
512
 
499
- 편집기의 상세(마크업 계약, 내부 전용 블록, 저장 제스처 제약)는 `wysiwyg.md`.
500
- 절차상 알아야 할 것은 하나다 — **고객에게 가는 파일에는 내부 도구가 물리적으로 없다.**
501
- `build.py` 가 편집 레이어를 `<style data-rte-src>` / `<script data-rte-src>` 로 감싸고
502
- 앞뒤에 `<!-- REPORT-EDITOR:BEGIN -->` / `<!-- REPORT-EDITOR:END -->` 주석을 출력하며,
503
- `serialize(true)` 가 정확히 그 표식을 찾아 들어낸다. 꺼두는 것만으로는 흔적이 남는다.
513
+ **기본은 초안이다.**
514
+ 도구가 `publish` 를 명시적으로 참으로 주지 않는 한 고객에게 안 보인다.
515
+ 확인한 뒤 `release_report(id)` 로 연다 — 그것이 사람의 승인 행위다.
504
516
 
505
- **묻는다**: 최종 승인, 파일명 규칙.
506
-
507
- ---
517
+ 수정은 `get_report` → 데이터 수정 → `update_report`.
518
+ **같은 링크가 새 내용을 가리킨다.**
508
519
 
509
- ## 10. 카피 게이트 3종의 보고서 적용
520
+ 포털 셸이 문서 **둘레**에 놓는 것(검토 회신·핵심 요약·진척률 바)은 저작 대상이 아니다.
521
+ 문서 안에 「댓글로 알려 주세요」를 쓰면 같은 것이 두 번 있게 된다.
510
522
 
511
- doc-pptx 7단계는 슬라이드 카피의 구조적 슬롭 3종을 의무 FAIL 로 규정한다.
512
- 보고서에서는 **1·3번은 그대로 살리고 2번만 조건부로 완화한다.**
523
+ ### 단일 HTML
513
524
 
514
- | # | 패턴 | 보고서 적용 | 기계 검사 |
515
- |---|---|---|---|
516
- | 1 | 대시 대비 헤드라인 "X — Y" | **예외 없이 금지** | `qa.py` #11 — `h1/h2/h3` 만 스캔해 ` — ` 또는 ` – ` 발견 시 실패 |
517
- | 2 | 조사·체언 종결 조각문 | **레지스터로 갈라서 적용** (아래) | 없음 — 사람이 본다 |
518
- | 3 | "A에서 B로" 전환 공식 | 헤딩에서는 금지. 본문은 **수치와 측정 주체가 있을 때만** | 없음 — 사람이 본다 |
525
+ ```bash
526
+ python3 ~/.claude/skills/genetics-report/assets/render.py report.json
527
+ ```
519
528
 
520
- ### 게이트 2 — 왜 완화하는가, 어디까지 완화하는가
529
+ 파일 하나를 그대로 보낸다.
530
+ 상대는 더블클릭만 하면 된다.
521
531
 
522
- 보고서의 목차 항목과 건 제목은 **명사구가 한국어 보고서의 관례**다.
523
- "…던 문제", "처리 현황", "확인 요청 드리는 사항" — 전부 게이트 2에 걸린다.
524
- 그대로 적용하면 목차가 `사전상담 링크에서 다른 고객의 정보가 열렸습니다` 같은 문장 나열이 되어
525
- 길이가 들쭉날쭉해지고 **색인으로서 기능을 잃는다.**
532
+ 관보 문서에는 **문서 안 편집 층이 없다**(`cfg['editor'] = False`, `build.py:439`).
533
+ 편집기의 블록 모델이 옛 마크업에 묶여 있어 관보 위에서는 차례를 빈 값으로 덮는다 — 고장이 아니라 파괴다.
534
+ 문구 수정은 데이터를 고쳐 다시 굽는다.
526
535
 
527
- 그래서 카피를 두 레지스터로 나눈다.
536
+ **묻는다**: 최종 승인, 파일명 규칙.
528
537
 
529
- **A. 색인용 라벨 — 체언 종결 허용**
538
+ ---
530
539
 
531
- - `h2` 절 표제: `요약` / `처리 현황` / `접수하신 9건` / `함께 발견해 처리한 9건` / `확인 요청 드리는 사항`
532
- - `h3` 건 제목(`cases[].title`): `사전상담 링크에서 다른 고객의 정보가 열리던 문제` …
533
- - 표 헤더·태그·범례: `증상` `원인` `조치` `확인 방법` `확인 요청`
540
+ ## 10. 9단계 — 발행 뒤 판정
534
541
 
535
- **B. 주장용 카피 — 완전문 강제**
542
+ 브라우저를 안 열고도 「무엇을 냈는가」를 확인하는 창구다.
536
543
 
537
- - `summary[].lead`: `접수해 주신 9건은 모두 조치를 마치고 운영에 반영했습니다.`
538
- - `open_items[].title`: **같은 `h3` 인데 완전문이다** — `검증에 쓴 표본이 작습니다`
544
+ ```
545
+ get_report(id: N, outline: true)
546
+ ```
539
547
 
540
- 즉 **HTML 태그 레벨이 기준이 아니다.** 판별 기준은 하나다.
548
+ ### v2 지문 — 블록 목록에 이 일곱이 있어야 한다
541
549
 
542
- > 그 줄이 "어디에 있는지"를 알려주면 **라벨**이고, "무엇이 사실인지"를 주장하면 **문장**이다.
543
- > 라벨은 명사구로 짧게, 주장은 서술어를 끝까지 붙여 책임을 진다.
550
+ | 블록 | 무엇 |
551
+ |---|---|
552
+ | `header.cover` | 표지 |
553
+ | `section.hist` | 발행 이력 |
554
+ | `nav.toc` | 차례 |
555
+ | `section.guide` | 이 문서 읽는 법 |
556
+ | `section.pt` | PART 간지 |
557
+ | `aside.marg` | 여백 주석 단 — **절 수만큼 있어야 한다** |
558
+ | `div.colophon` | 판권지 |
544
559
 
545
- 체언 종결이 위험한 진짜 이유는 문법이 아니라 **책임 회피**다.
546
- "성공의 열쇠 — 자동화"는 아무도 아무것도 주장하지 않는다. `검증에 쓴 표본이 작습니다` 는 우리가 주장하고 책임진다.
547
- 라벨은 애초에 주장이 아니므로 이 위험이 없다. 그래서 면제된다.
560
+ `figure.eof-endcard` 가 보이면 **구버전**이다.
561
+ 관보는 꼬리 카드를 안 붙인다(`REPORT_EOF_TEMPLATES = []`, `report_publish.php:1249`).
548
562
 
549
- **세칙 둘**
563
+ `aside.marg` 개수가 절 수보다 적으면 그만큼의 절이 한 단으로 서 있다는 뜻이다.
550
564
 
551
- - 라벨 레지스터에서도 **종결형을 통일**한다. 예시 프로젝트의 건 제목은 전부 `…던 문제` / `…(방침 확정)` / `…정리` 로 맞춰져 있다. *통일되지 않은 명사구 나열*이 슬롭처럼 보이는 것이지 명사구 자체가 문제가 아니다.
552
- - **조사로 끝나는 조각문은 어느 레지스터에서도 금지**("…의", "…에서"). 이건 라벨도 문장도 아니다.
565
+ ### ⚠ 머리줄의 `절 0 · 건 0 · 장 0 · 그림 0` 은 **정상값**이다
553
566
 
554
- ### 대시에 관한 중요한 구분
567
+ MCP 가 찍는 요약 줄은 **옛 서식의 셈**만 읽는다(`mcp/genetics.mjs:2718-2726`).
568
+ 그 값들은 `h2.sec` · `.case` · `.ch-open` · `<img>` 를 세는데, 관보는 그중 무엇도 안 쓴다.
569
+ 실측(예시 문서): 절 0 · 건 0 · 장 0 · 그림 0 이고 실제로는 `ask` 1 · `sheet` 2 · `board` 1 이었다.
555
570
 
556
- 문서 본문에는 대시가 살아 있다 — 도해 출처 `해당 절 — 3.7 · 3.8 · 3.9 · 4.7`,
557
- 범례 정의 `반영 확인 — 수정이 운영에 반영된 것까지 확인`.
558
- **대시를 문서에서 추방한 게 아니라 헤딩에서만 추방했다.**
559
- 라벨-값 구분자로서의 대시(`레이블 — 값`)는 캡션·마이크로카피에서 정상 활자다.
560
- 금지 대상은 *대비 수사*로서의 대시다. 검사기를 `h1/h2/h3` 로 한정한 것이 이 구분의 구현이다.
561
- (주의: 검사는 **공백을 낀** ` — ` 만 잡는다. `X—Y` 처럼 붙여 쓰면 통과하므로 사람이 본다.)
571
+ **0 을 보고 「문서가 비었다」로 읽지 마라.**
572
+ 판정은 그 아래 **블록 목록**으로 한다.
562
573
 
563
- ### 4번째 게이트 — 보고서에만 있는 것
574
+ 서버는 관보 셈(`ask`·`sheet`·`board`·`sum`·`part` 등)을 실제로 계산해 `?outline=1` 에 실어 준다.
575
+ **MCP 요약 줄이 그것을 안 찍을 뿐이다** — 지금 도구로는 블록 목록으로만 본다.
564
576
 
565
- `qa.py` #10, `report.json` 의 `forbid`. **고객본 어휘 게이트**다.
577
+ ### 빈 블록
566
578
 
567
- 슬라이드 카피 게이트는 "AI 처럼 들리는가"를 본다.
568
- 고객 보고서에서는 "**내부처럼 들리는가**"가 훨씬 자주 무너진다.
569
- `qa.py` 의 AI 슬롭 어휘 목록(`혁신적인`, `차세대`, `재정의하는`, `한 차원 높은`, `최적화된 솔루션`,
570
- `완벽한 경험`, `새로운 패러다임`, `고도화된`)은 스킬에 고정이고, `forbid` 만 프로젝트가 채운다.
579
+ 응답 끝의 `⚠ 빈 블록 N건` 은 **렌더러가 조각을 빠뜨렸다는 뜻**이다.
580
+ 데이터 문제가 아니라 렌더러 문제이므로, 그대로 두지 말고 원인을 찾는다.
571
581
 
572
582
  ---
573
583
 
574
584
  ## 11. 같은 사실이 두 문서로 갈리는 지점
575
585
 
576
- 독자 정의가 왜 첫 단추인지를 보여주는 실제 사례다. 사실은 하나도 바뀌지 않았다.
586
+ 내부 기록 ↔ 고객 문서 대조표는 SKILL.md 0단계에 있다.
587
+ 여기서는 그 표를 만든 **원리 셋**만 적는다.
577
588
 
578
- | 사실(변하지 않음) | 내부 개발 로그 | 고객 보고서 |
579
- |---|---|---|
580
- | 배포 파이프라인을 통과했다 | "2026-08-09 운영 배포를 마쳤고" | "2026년 8월 9일 운영 환경에 반영했고" |
581
- | 회귀 테스트를 2건 추가했다 | "자동 검증 2건을 추가했습니다" | "같은 문제가 다시 나타나면 곧바로 걸러지도록 상시 점검을 걸어 두었습니다" |
582
- | 저장 경로 6곳을 고쳤다 | "저장 경로 6곳을 수정했고" | "저장이 일어나는 모든 경로를 수정했고" |
583
- | 티켓을 분리했다 | "별도 티켓으로 분리해 처리했습니다" | "4.3에 따로 정리했습니다" (문서 안 참조) |
584
- | 설계 대안 A를 검토 후 기각했다 | 기각 사유까지 기록 | **한 줄도 싣지 않는다** |
585
- | 스토어 빌드 이후 반영된다 | "다음 스토어 빌드부터" | "다음 앱 업데이트부터" |
586
- | 검수용 계정/매장을 썼다 | "테스트 매장의 테스트 고객" | "검수용 매장·고객" |
587
- | 화면 재현 없이 반영만 확인했다 | "코드 확인" | "반영 확인 — 수정이 운영에 반영된 것까지 확인 (화면 재현은 미실시)" |
588
- | 티켓 번호 `1217296144360763` | 식별자로 씀 | **싣지 않는다** |
589
-
590
- 관통하는 원리 셋:
591
-
592
- 1. **독자가 검증할 수 없는 참조는 싣지 않는다.** 티켓 번호·내부 시스템명은 확인 수단이 없어 노이즈만 된다. 반대로 제보 원본 링크는 독자가 자기 제보를 대조할 수 있으므로 남긴다.
593
- 2. **우리 조직의 내부 구조를 드러내는 명사는 전부 치환 대상이다.** 배포·티켓·테스트·경로 수 — 전부 "우리 팀이 어떻게 일하는가"이지 "당신 문제가 어떻게 됐는가"가 아니다.
594
- 3. **약한 근거는 숨기지 말고 이름을 붙여 노출한다.** 최약 등급을 지우는 게 아니라 정의를 달아 보여준다.
589
+ 1. **독자가 검증할 수 없는 참조는 싣지 않는다.**
590
+ 티켓 번호·내부 시스템명은 확인 수단이 없어 노이즈만 된다.
591
+ 반대로 제보 원본 링크는 독자가 자기 제보를 대조할 수 있으므로 남긴다.
592
+ 2. **우리 조직의 내부 구조를 드러내는 명사는 전부 바꿀 대상이다.**
593
+ 배포·티켓·테스트·경로 수 — 전부 「우리 팀이 어떻게 일하는가」이지 「당신 문제가 어떻게 됐는가」가 아니다.
594
+ 3. **약한 근거는 숨기지 말고 이름을 붙여 노출한다.**
595
+ 최약 등급을 지우는 게 아니라 정의를 달아 보여준다.
595
596
 
596
597
  독자가 바뀌면 구조까지 바뀐다.
597
598
 
598
599
  | 독자에서 나온 결정 | 구현 | 다른 독자였다면 |
599
600
  |---|---|---|
600
- | 전 건을 검증한다 | `status` 절의 전 건 판정표 | 임원 독자였다면 판정표를 빼고 지표 밴드만 남긴다 |
601
- | 건과 건을 오가며 대조한다 | 절 참조가 전부 클릭 링크(`link()`) | 순차 낭독용 문서였다면 링크는 불필요하다 |
602
- | 우리 작업의 완결성을 본다 | 확인 요청 절이 "고객 숙제"가 아니라 "우리가 확정 못 한 것" | 계약 정산 문서였다면 같은 데이터가 "잔여 작업 목록"이 된다 |
603
- | 실제로 화면을 눌러본다 | 위험 행동 경고를 요약과 CTA 두 곳에 중복 | 읽기만 하는 독자였다면 이 문장 자체가 불필요하다 |
601
+ | 전 건을 검증한다 | `status` 절의 전 건 대조표 | 결정권자 독자였다면 대조표를 빼고 상태 막대만 남긴다 |
602
+ | 건과 건을 오가며 대조한다 | 절 참조가 전부 앵커 링크 | 순차 낭독용 문서였다면 링크는 불필요하다 |
603
+ | 우리 작업의 완결성을 본다 | `asks` 절이 「고객 숙제」가 아니라 「우리가 확정 못 한 것」 | 계약 정산 문서였다면 같은 데이터가 「잔여 작업 목록」이 된다 |
604
+ | 실제로 화면을 눌러본다 | 위험 행동 경고를 요약과 `asks` 두 곳에 중복 | 읽기만 하는 독자였다면 이 문장 자체가 불필요하다 |
604
605
 
605
606
  ---
606
607
 
607
608
  ## 12. 새 프로젝트 체크리스트
608
609
 
609
- | # | 채울 것 | 어디에 | 어느 단계에서 |
610
+ | # | 채울 것 | 어디에 | 단계 |
610
611
  |---|---|---|---|
611
612
  | 1 | 독자 문장 1개 (하는 일로) | (문서 아님 — 작업 전제) | 0 |
612
- | 2 | "고객이 아는 참조" 도메인 | `report.json` `allow_external` | 0 |
613
- | 3 | 금지 어휘·식별자 정규식 | `report.json` `forbid` | 0·6 |
614
- | 4 | 절 구성과 순서 | `report.json` `sections` + `groups` | 0 |
615
- | 5 | 건별 9개 필드 | `cases.json` | 1·2 |
616
- | 6 | 보고서 표제로 재작성한 전 건 제목 | `cases.json` `title` | 2 |
617
- | 7 | 등급 이름·클래스·정의문 (최약 등급에 **한계 명시**) | `report.json` `grades` | 3 |
618
- | 8 | 요약 4칸 · 확인 요청 3~6개 · CTA | `report.json` `summary`/`open_items`/`cta` | 4 |
619
- | 9 | 지표 밴드 숫자 | `report.json` `stats` | 4 |
620
- | 10 | 공통 원인 도해 (**실재할 때만**) | `figure.html` + `report.json` `figure` | 4 |
621
- | 11 | 팔레트 키 (또는 브랜드 6색) | `report.json` `palette` | 5 |
622
- | 12 | 고객 언어 치환쌍 | `report.json` `text_fix` | 6 |
623
- | 13 | 표지·꼬리말·메타 | `report.json` `kicker`/`title`/`subtitle`/`meta`/`footer` | 6 |
624
-
625
- 건드리지 않는 것: `build.py`, `palette.py`, `report.css`, `editor.js`, `editor.css`, `qa.py`, `subset_font.py`.
626
- 폰트를 바꾸거나 커버리지를 조정할 때만 `font-embedding.md` 를 편다.
613
+ | 2 | 금지 어휘·식별자 정규식 | `forbid` (로컬 렌더에서만 돈다) | 0·6 |
614
+ | 3 | PART 구성 | `parts` | 0·6 |
615
+ | 4 | 절 구성과 순서 | `sections` + `groups` | 0·6 |
616
+ | 5 | 건별 필드 | `cases` | 1·2 |
617
+ | 6 | 보고서 표제로 재작성한 전 건 제목 | `cases[].title` | 2 |
618
+ | 7 | 등급 이름·정의문 (**4단 이하** · 최약 등급에 한계 명시) | `grades` | 3 |
619
+ | 8 | 요약 4문단 + **문단마다 근거** | `summary` (+ `refs`) | 4 |
620
+ | 9 | 확인 요청 3~6개 (주어는 우리) | `open_items` | 4 |
621
+ | 10 | **절마다 여백 주석** | `sections[].marg` | 5 |
622
+ | 11 | 표지·발행 이력·활용법 | `doc_no`·`client`·`title`·`title_sub`·`subtitle`·`meta`·`issuer`·`history`·`guide` | 6 |
623
+ | 12 | 요약면 | `notice`·`bar`·`bar_note`·`stats` | 6 |
624
+ | 13 | 부록·판권지 | `appendix`·`colophon` | 6 |
625
+ | 14 | 고객사 로고 (있으면) | `assets` — `role:"logo"` 하나만 실린다 | 6 |
626
+
627
+ 건드리지 않는 것: `build.py`·`gazette.py`·`gazette.css`·`sentbreak.py`·`boundary.py`.
628
+ `render.py` 가 사본이 생기면 멈춘다(`FORKABLE`) — 2026-08-18 에 나흘 만에 887줄이 갈라졌고 그동안 고객 문서가 서식 없이 나갔기 때문이다.
627
629
 
628
630
  ---
629
631
 
630
632
  ## 13. 미확정
631
633
 
632
- 1. **`open_items` 개수 3~6 의 근거.** 예시 프로젝트가 6개이고 2열 그리드가 짝수에서 깔끔하다는 것 외에, 상한·하한을 정한 실측 근거는 없다.
633
- 2. **등급 4단이 최적인가.** 예시 프로젝트에서 4단이 잘 동작했다는 것만 확인했다. 건이 100건을 넘거나 성격이 다른 프로젝트에서 3단/5단이 나은지는 검증하지 않았다.
634
- 3. **`.r-open .xref` 의 rgba 리터럴**(9절 마지막). 토큰으로 바꿔야 하는지, 밑줄 1px 이라 그대로 두는 게 맞는지 판단하지 않았다. 현재는 QA 경고로 남아 있다.
635
- 4. **`qa.py` #13 의 `data-internal` 제거 정규식**(`<[^>]*\bdata-internal="true".*?</\w+>`)은 중첩 요소에서 실제 `serialize()` 와 다르게 동작할 수 있다. 예시 프로젝트에는 내부 전용 블록이 0개라 이 경로가 실제로 검증되지 않았다.
636
- 5. **팔레트 톤 질문을 `AskUserQuestion` 으로 강제할지.** 이 문서는 "톤만 묻는다"로 사양화했으나, 도구를 써서 묻도록 강제한 실행 기록은 없다.
634
+ 1. **`open_items` 개수 3~6 의 근거.**
635
+ 상한·하한을 정한 실측 근거는 없다.
636
+ 2. **등급 4단이 최적인가.**
637
+ 4단이 표식의 한계라는 것은 코드로 확인했다.
638
+ 3단이 나은 프로젝트가 있는지는 검증하지 않았다.
639
+ 3. **관보 셈(`ask`·`sheet`·`board`·`marg` 등)을 MCP `outline` 요약 줄이 안 찍는 것.**
640
+ 서버는 계산해서 실어 준다.
641
+ 도구를 고칠지, 블록 목록으로 판정하는 지금 방식을 유지할지 판단하지 않았다.
642
+ 4. **포털 발행에 `forbid` 그물이 없는 것.**
643
+ 서버가 의도적으로 뺀 키인지(파일을 읽는 키와 같은 취급), 관보 전환에서 빠진 것인지 확인 못 했다.
644
+ 현재 처방은 「발행 전에 로컬로 한 번 굽는다」이고, 이것은 우회이지 해결이 아니다.
645
+ 5. **`text_fix` 를 서버가 계속 받는 것.**
646
+ 관보 렌더러가 안 읽는데 화이트리스트에 남아 있다.
647
+ 지울지, 관보 경로에 붙일지 판단하지 않았다.