genetics-mcp 2.9.0 → 2.9.2

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,272 +1,710 @@
1
- # LIM 웹 리포트 서식 (`template: "lim-report"`)
1
+ # Lim 포털 서식 — 관보 데이터 모델 원전
2
2
 
3
- 포털 발행 산출 중 **LIM 브랜드 계약을 따르는 판**이다. 포털의 다른 문서와 데이터 모델은
4
- 같고, 그 위에 **팔레트 `lim` · 챕터 오프닝 · 번호형 전체 목차 · 일러스트 · EOF 엔드카드**가 얹힌다.
3
+ 포털에 발행하는 문서는 **한국 정부간행물(관보) 문서 문법**으로 조판된다.
4
+ 서식 이름은 `lim-report` 하나이고, 고를 일이 없다 — 서버가 정한다.
5
5
 
6
- 정본 계약: 포털 저장소의 `DESIGN-lim-report.md`.
6
+ **이 문서가 데이터 모델의 원전이다.** 어떤 칸이 있고, 무엇을 뜻하고, 없으면 무슨 일이 나는가.
7
+ 조판 규칙(활자 스케일·괘선·간격 격자·인쇄 페이지 문법·검사기 8종)은 여기 안 적는다 —
8
+ `gazette-report` 스킬이 그 원전이고, 두 벌로 두면 갈라진다.
7
9
 
8
- > 이 문서의 예시 값(고객사명·건수·본문 문장)은 **한 실사례의 값**이다. 규칙은 도메인과 무관하다.
9
- 이 문서는 그 계약을 **스킬 사용자가 실제로 무엇을 채워야 하는가**로 옮긴 것이다.
10
- 계약과 어긋나면 계약이 이긴다.
11
-
12
- > **언제 이 서식을 쓰는가.** 포털로 나가는 문서는 전부 이 서식이다 — 고를 것이 없다.
13
- > `template` 을 생략해도 서버가 이것으로 낸다(`REPORT_TEMPLATE_DEFAULT`). 팔레트도
14
- > 계약이 정한 4색이라 `palette` 를 보내도 서버가 `lim` 으로 덮는다.
10
+ > **왜 갈렸는가.** 옛 서식은 표지·요약·건 카드에 챕터 오프닝과 일러스트를 붙인 웹 문서였다.
11
+ > 「웹페이지 같다 / 방향성만 보인다」는 반려를 반복해서 받았고, 원인이 색이나 여백이 아니라
12
+ > **페이지 문법이 없다는 것**으로 좁혀졌다.
13
+ > 카드를 아무리 다듬어도 문서가 되지 않는다.
15
14
 
16
15
  ---
17
16
 
18
- > **만드는 길이 둘이다.** MCP·API 로 `template: "lim-report"` 를 주거나,
19
- > 관리 화면 **새 문서**에서 「Lim 웹 리포트」를 고른다(2026-08-16 추가 — 그전에는 API 뿐이었다).
17
+ ## 0. 제일 먼저 — 이 셋이 없으면 관보가 아니다
20
18
 
21
- ## 1. 서식을 켜는 두 줄
19
+ | 없으면 | 무슨 일이 나는가 |
20
+ |---|---|
21
+ | `sections[].marg` (여백 주석 단) | 두 단 본문이 **한 단으로 선다.** 「문서처럼 꾸민 웹페이지」로 되돌아간다 |
22
+ | `parts` (PART 층위) | 절만 늘어선다. 「먼저 읽을 것 / 경과 / 남은 것」이라는 **읽기 순서**가 사라진다 |
23
+ | 개조식 톤 | 낱말이 아니라 조판의 문제로 남는다. 경어체 보고서는 관보 문법 위에서 겉돈다 |
22
24
 
23
- ```json
24
- { "template": "lim-report", "palette": "lim" }
25
- ```
25
+ 이 셋은 **거절 사유가 아니다** — 렌더러는 비어도 문서를 낸다(초안을 막지 않는다).
26
+ 대신 ⚠ 로 세어 준다(`gazette.py` 의 `marg()` · `build()`).
27
+ 경고가 있는 채로 고객에게 링크를 보내지 마라.
26
28
 
27
- `template` 은 서버가 스타일시트·폰트를 고르는 열쇠다(Spoqa Han Sans Neo 정적 3벌).
28
- `palette: "lim"` 은 표지를 **paper 아키타입**(흰 바탕 + Ink 활자)으로 만든다 —
29
- 계약 1절이 검정 전면과 문단 단위 역상을 금지하기 때문이다.
29
+ **왜 marg 가 본체인가.**
30
+ Tufte 의 여백 노트가 근거다 — 참조를 문서 끝이 아니라 참조된 자리 **옆**에 둔다.
31
+ 독자가 본문 흐름을 잃지 않고 「왜 읽는가 · 하나만 가져간다면 · 어디를 참조하나」를 받는다.
32
+ 그 단이 없으면 남는 것은 한 단짜리 글줄이고, 그것이 곧 웹페이지다.
30
33
 
31
34
  ---
32
35
 
33
- ## 2. 챕터 오프닝 — `chapters[]`
36
+ ## 1. 서식은 하나뿐이고 서버가 정한다
34
37
 
35
- 절 앞에 펼쳐지는 여는 글이다. 계약 4.2절의 순서가 그대로 렌더된다:
36
- **번호·짧은 제목·예상 시간 → 큰 제목 → 핵심 문장 → 일러스트 → 이 장에서 다룰 내용.**
38
+ `report_publish.php:851` 의 `REPORT_TEMPLATES` 에 `lim-report` 하나뿐이다.
39
+
40
+ | 서버가 정하는 것 | 값 |
41
+ |---|---|
42
+ | `render` | `gazette` — `build.py` 의 렌더 경로를 가른다 |
43
+ | `css` | `['gazette.css']` |
44
+ | `font` | Pretendard GOV · `fonts/pretendard-gazette.woff2` · **wght `300 700` 가변 1면** |
45
+ | `reads` (지문) | `build.py` · `gazette.py` · `boundary.py` · `sentbreak.py` |
46
+
47
+ - 폰트는 **한 면**이다(실측 409,044B).
48
+ 정적 3면이 아니고 400–700 도 아니다 — 300 이 필요한 것은 표지 둘째 행과 간지 숫자가 가는 굵기라서다.
49
+ - `palette.py` 는 지문 목록에서 **일부러 빠졌다**(`report_publish.php:885-888`).
50
+ 관보는 팔레트 토큰을 안 쓴다 — `build_gazette` 가 `styles(..., tokens=False)` 로 부른다(`build.py:441`).
51
+ **발행에 팔레트 인자가 없다.**
52
+ - `cfg['editor'] = False`(`build.py:439`) — 문서 안 편집 레이어를 안 싣는다.
53
+ - `REPORT_EOF_TEMPLATES = []`(`report_publish.php:1249`) — 꼬리 카드가 안 붙는다.
54
+
55
+ ### 로컬 렌더는 `render.py` 로 한다
37
56
 
38
- ```json
39
- "chapters": [{
40
- "id": "ch-01",
41
- "no": "01",
42
- "short_title": "핵심 진단",
43
- "reading_minutes": 3,
44
- "title_lines": ["계산보다 먼저", "목표 설정을", "바로잡아야 합니다"],
45
- "lead": "현재 방식은 도달해야 할 몸 상태가 아니라 지금 속한 칸의 바로 옆 칸을 목표로 삼습니다.",
46
- "asset": "chapter-01",
47
- "topics": ["반복된 핵심 패턴", "확인한 8건", "변경 영향 91.7%"]
48
- }]
57
+ ```
58
+ python3 assets/render.py <report.json>
49
59
  ```
50
60
 
51
- 절과는 `sections[].chapter` 로 잇는다.
61
+ - `render.py` 가 `templates_from_server()` 로 서버 등록부를 읽어 cfg 에 얹는다 — `render`·`css`·폰트가 따라온다.
62
+ - **`report.json` 에 `css` · `render` · `palette` 를 적을 필요가 없다.**
63
+ 적으면 서버와 갈릴 수 있다.
64
+ - **`build.py` 를 직접 부르지 마라.**
65
+ `cfg.get('css', ['report.css'])`(`build.py:285-286`)로 떨어져 옛 서식 시트가 실린다.
66
+ 스타일 없는 문서가 대비 검사를 통과해 「거짓 초록」이 된다.
67
+ - 로컬 전용 칸이 둘 있다: `out`(출력 파일명) · `forbid`(금칙 패턴).
68
+ 둘 다 발행 창구가 깎아 낸다 — 서버는 `out` 을 스스로 `report.html` 로 박는다(`report_publish.php:691`).
69
+
70
+ ---
71
+
72
+ ## 2. 문서 골격 — 9요소. 순서는 렌더러가 고정한다
52
73
 
53
- ```json
54
- "sections": [{ "no": "01", "id": "sec-1", "name": "핵심 진단", "kind": "summary", "chapter": "ch-01" }]
55
74
  ```
75
+ 표지 → 발행 이력 → 차례 → 이 문서 읽는 법 → 요약
76
+ → PART 간지 → 절(간지 + 두 단 본문) → … → 부록 → 판권지
77
+ ```
78
+
79
+ `gazette.py` 의 `build()` 가 이 순서로 조립한다.
80
+ 데이터로 바꿀 수 없다.
81
+ 인쇄하면 표지가 1쪽 단독, PART 간지가 각각 1쪽 단독, 절마다 새 쪽에서 시작한다.
82
+
83
+ | # | 가구 | 마크업 | 데이터 | 없으면 |
84
+ |---|---|---|---|---|
85
+ | 1 | 표지 | `header.cover` | `doc_no` · `client` · `assets[logo]` · `kicker` · `title` · `title_sub` · `intro`/`subtitle` · `meta` · `issuer` | 그 조각만 빠진다. 넷 다 비면 표지 자체가 안 선다 |
86
+ | 2 | 발행 이력 | `section.hist` | `history[]` | 이력면이 안 선다 |
87
+ | 3 | 차례 | `nav.toc` | `parts` + `sections` + `appendix` (자동) | 절이 하나도 없을 때만 안 선다 |
88
+ | 4 | 이 문서 읽는 법 | `section.guide` | `guide[]` | 활용법면이 안 선다 |
89
+ | 5 | 요약 | `section.sum` | `notice` · `bar[]` · `bar_note` · `stats[]` | 요약면이 안 선다 |
90
+ | 6 | PART 간지 | `section.pt` | `parts[]` | **경고** — 한 묶음으로 자동 생성된다 |
91
+ | 7 | 절 | `section.part` + `aside.marg` | `sections[]` | 절이 없으면 본문이 없다 |
92
+ | 8 | 부록 | `section.apx` | `appendix[]` | 부록면이 안 선다 |
93
+ | 9 | 판권지 | `footer.eof` > `div.colophon` | `colophon` (없으면 `title`·`title_sub`·`doc_no`·`meta`·`footer` 로 세운다) | 문서가 안 닫힌다 |
94
+
95
+ 문서 **바깥**(main 밖)에 머리띠(`header.topbar`)와 색인 레일(`nav.rail`)이 선다.
96
+ 둘 다 `sections`·`parts` 에서 자동으로 나온다 — 저작 대상이 아니다.
97
+
98
+ ---
56
99
 
57
- ### 반드시 알아야 할 것 넷
100
+ ## 3. 최상위 필드 — 전량
58
101
 
59
- | 항목 | 규칙 | 왜 |
102
+ `REPORT_CLIENT_KEYS`(`report_publish.php:31-67`)가 화이트리스트다.
103
+ **여기 없는 키는 조용히 버려진다** — 보낸 쪽은 실린 줄 안다.
104
+
105
+ ### 3.1 표지·머리
106
+
107
+ | 칸 | 뜻 | 없으면 |
108
+ |---|---|---|
109
+ | `title_tag` | `<title>` — 브라우저 탭·링크 미리보기 | 빈 제목으로 나간다 |
110
+ | `description` | `<meta name="description">` | 빈 값으로 나간다 |
111
+ | `kicker` | 표지 머리말(작은 글자 한 줄) | 그 줄만 빠진다 |
112
+ | `title` | 대제목 **첫 행**(굵게). 판권지 제목·러닝 푸터·자동 PART 이름의 폴백이기도 하다 | 표지 대제목이 안 선다 |
113
+ | `title_sub` | 대제목 **둘째 행**(가늘게 — 회차·부제). 판권지 부제 폴백 | 둘째 행만 빠진다 |
114
+ | `subtitle` | 표지 한 문단 요약(대제목 **아래**) | 요약문이 없다 |
115
+ | `intro` | 같은 자리. **`subtitle` 보다 먼저 쓰인다**(`gazette.py:169`) | — |
116
+ | `doc_no` | 표지 문서번호 스트립. 판권지 부제 폴백 | 문서번호 칸이 빠진다 |
117
+ | `client` | 표지 우상단 고객사명. 러닝 푸터 폴백 | 로고도 없으면 우상단이 빈다 |
118
+ | `issuer` | 발행처 스트립. 문자열 또는 목록(마지막 항목이 강조된다) | `doc_type`·`cover_date` 로 세운다 |
119
+ | `doc_type` | `issuer` 폴백 조각 | — |
120
+ | `cover_date` | `issuer` 폴백 조각 + 요약면 제목 옆 「기준」 날짜 | 기준일 표기가 빠진다 |
121
+ | `meta` | 표지 4칸 격자 `[{label,value}]`. 판권지 `rows` 폴백 | 격자가 안 선다 |
122
+
123
+ **대제목 둘째 행에 문장을 넣지 마라.**
124
+ 표제 register 라 행간이 좁고, 문장을 넣으면 조판이 무너진다(`read.mjs` 가 잡는다).
125
+ 한 문단 요약의 자리는 `intro`(또는 `subtitle`)이고 대제목 **아래**다.
126
+
127
+ **표지 가운데의 빈 마당은 미완성이 아니라 문법이다.**
128
+ 채우지 마라.
129
+
130
+ ### 3.2 본문 구조
131
+
132
+ | 칸 | 뜻 | 없으면 |
60
133
  |---|---|---|
61
- | `title_lines` | 줄바꿈을 **데이터가 정한다** | 브라우저에 맡기면 핵심 명사만 떨어져 나간다(계약 3절) |
62
- | `name` vs `title_lines` | `name` 은 목차용 **짧은 이름**, `title_lines` 는 본문의 **큰 제목** | 계약 4.1절이 목차를 「번호·짧은 제목·한 줄 설명」으로 정의한다. 둘은 다른 정보다 |
63
- | `lead` 와 `sections[].lede` | **둘 다 쓰지 말 것** | 챕터가 `lead` 를 가지면 렌더러가 절 도입문을 안 낸다(핵심 문장은 하나다). 다르면 경고를 찍는다 |
64
- | 챕터를 하나라도 주면 | **번호형 전체 목차가 자동으로 선다** | 계약 4.1절 고정 순서. 따로 넣을 것이 없다 |
134
+ | `parts` | PART 묶음 `[{no,title,tone,lede,questions[],roman}]` | **경고** 후 절 전체를 한 묶음으로 자동 생성 |
135
+ | `sections` | 절 목록. 4절 참조 | 본문이 없다 |
136
+ | `groups` | 건 묶음 `[{key,no,name}]` | `cases` 가 어디에도 안 실린다(**거절**) |
137
+ | `cases` | 건 목록. 3.5절 참조 | `kind:"cases"` 절이 빈다 |
138
+ | `summary` | 요약 문단 `[{lead,body,refs[]}]` | `kind:"summary"` 절이 빈다 |
139
+ | `open_items` | 요청 항목 `[{title,priority,body,refs[]}]` | `kind:"asks"` 절이 빈다 |
140
+ | `cta` | 요청 절 머리 강조문(문자열 목록) | 강조문이 안 선다 |
141
+ | `grades` | 확인 등급 사전. 7절 참조 | 대조표 범례와 등급 표식이 안 선다 |
142
+
143
+ ### 3.3 요약면·머리띠
144
+
145
+ | 칸 | 뜻 | 없으면 |
146
+ |---|---|---|
147
+ | `notice` | 현황 줄 **객체** `{state,text}` | 현황 줄이 안 선다 |
148
+ | `bar` | 상태 막대 `[{value,label,tone}]` | 막대가 안 선다 |
149
+ | `bar_alt` | 막대의 대체 텍스트(`role="img"` 의 이름) | 「전체 분포」로 선다 |
150
+ | `bar_note` | 막대 아래 각주 한 줄 | 각주가 빠진다 |
151
+ | `stats` | 수치 칸 `[{value,unit,label,accent,href,go}]` | 수치 칸이 안 선다 |
152
+ | `remain` | 머리띠 우측 판독부 수치(요청 잔여) | `open_items` 개수로 자동. 그것도 0이면 판독부가 빠진다 |
65
153
 
66
- `index_line` 을 주면 전체 목차의 한 줄 설명이 되고, 없으면 `lead` 를 쓴다.
154
+ ### 3.4 꼬리·인쇄·문서 설정
67
155
 
68
- ### 챕터에 `asset` 을 붙이면 **데스크톱에서 2열로 갈린다** (2026-08-16 구현)
156
+ | 칸 | 뜻 | 없으면 |
157
+ |---|---|---|
158
+ | `appendix` | 부록 `[{no,title,rows[],note}]` | 부록면이 안 선다 |
159
+ | `colophon` | 판권지 **객체** `{title,sub,rows[],mark,badge,note,tail}` | 다른 칸에서 세운다(2절 표) |
160
+ | `footer` | 판권지 `note` 폴백(문자열 목록) | — |
161
+ | `running_footer` | 인쇄 러닝 푸터의 뒷조각(`title · running_footer`) | `client` 로 대신한다 |
162
+ | `wide` | 문서 폭 CSS 값(`--wide`) | 시트 기본값 |
163
+ | `marg_width` | 여백 주석 단 폭 CSS 값(`--marg`) | 시트 기본값 |
164
+ | `labels` | 기본 라벨 덮어쓰기 `{키: 글자}` | 렌더러 기본 라벨 |
165
+ | `text_fix` | 문자열 치환 `[{from,to}]`(`build.py:516`) | 치환 없음 |
166
+ | `lang` | `<html lang>`. 모양 검사 후 아니면 `ko` | `ko` |
167
+ | `assets` | 그림. **`role:"logo"` 하나만 실린다**(11절) | 고객사명을 활자로 세운다 |
168
+
169
+ `labels` 로 덮을 수 있는 키(`gazette.py` 의 `self.L(...)` 전량):
170
+ `apx` `as_of` `ask` `bar_alt` `basis` `client` `col_grade` `col_state` `col_title` `cta`
171
+ `doc_no` `go` `guide` `hist` `hist_date` `hist_no` `hist_note` `marg` `nograde` `open`
172
+ `pt_ask` `refs` `remain` `remain_unit` `source` `source_row` `sum` `toc` `urgent`
173
+
174
+ ### 3.5 하위 목록의 칸
175
+
176
+ | 어디 | 칸 | 비고 |
177
+ |---|---|---|
178
+ | `history[]` | `no` · `date` · `note` | `no` 를 비우면 1부터 매긴다 |
179
+ | `guide[]` | `q`(질문) · `a[]`(▶ 지시 목록) | `a` 안에는 `<b>` 같은 인라인 태그가 **안 통한다**(이스케이프된다) |
180
+ | `parts[]` | `no` · `title` · `tone` · `lede` · `questions[]` · `roman` | `no` 가 `sections[].part` 가 가리키는 열쇠다 |
181
+ | `notice` | `state`(`critical`\|`info`\|`success`) · `text` | 밖의 값은 `info` 로 강등 |
182
+ | `bar[]` | `value` · `label` · `tone` | **tone 은 `done`\|`wait` 둘뿐이다** — 밖이면 `done` 으로 강등 |
183
+ | `stats[]` | `value` · `unit` · `label` · `accent` · `href` · `go` | 첫 칸만 `accent: true` |
184
+ | `meta[]` | `label` · `value` | `[k, v]` 배열도 받는다 |
185
+ | `open_items[]` | `title` · `priority`(`must` 면 「가장 시급」) · `body` · `refs[]` | `refs` 는 **첫 하나만** 「근거」 단추가 된다 |
186
+ | `cases[]` | `group` · `title` · `grade` · `symptom` · `cause` · `fix` · `how_to_check` · `remaining` · `source_url` · `source_text` | 번호(`anchor`)는 **렌더러가 매긴다** |
187
+ | `appendix[]` | `no` · `title` · `rows[{label,value}]` · `note` | `rows` 가 비면 그 항목이 통째로 빠진다 |
188
+ | `colophon` | `title` · `sub` · `rows[]` · `mark` · `badge` · `note` · `tail` | 넷(`title`·`rows`·`mark`·`note`)이 다 비면 판권지가 안 선다 |
189
+ | `grades{}` | `키: [클래스, 설명, lvl?, tag?]` | 7절 |
190
+
191
+ `cases[].remaining` 이 있으면 그 건은 대조표에서 **미결**로 선다.
192
+ 그 행이 독자에게 넘어가는 몫이라 다른 행과 시각적으로 갈린다.
69
193
 
70
- 961px 이상에서 `<main>` 이 격자가 되어 **글은 한쪽, 그림은 반대쪽**에 서고, 다음 챕터는
71
- 좌우가 뒤집힌다(01 좌·02 우·03 좌…). 그 아래 폭에서는 예전처럼 한 열이다.
194
+ ---
72
195
 
73
- 저작할 때 알아야 할 것은 셋이다.
196
+ ## 4. 절 단위 키 — 전량
74
197
 
75
- | | 내용 |
76
- |---|---|
77
- | 그림 비율 | 반폭(≈310–390px)에 서므로 **가로로 긴 그림**이 맞다. 세로로 긴 그림은 글 열보다 훨씬 길어져 여백이 생긴다 |
78
- | 그림 높이 | 그림이 걸치는 세로 길이는 **그 챕터의 글 조각 수**로 정해진다(머리말+제목+`lead`+`topics`). `topics` 를 빼면 그림이 그만큼 짧아진다 |
79
- | 표식 | `data-side`·`ch-span`·`ch-colend` 는 **렌더러가 붙인다.** 데이터에 적지 않는다 |
198
+ ```json
199
+ {
200
+ "kind": "asks",
201
+ "part": "1",
202
+ "name": "지금 확인이 필요한 것",
203
+ "short": "확인 요청",
204
+ "sub": "수신 측 조치 사항의 전부",
205
+ "lede": "총 건수 표시 1건에 대해 확인 필요.",
206
+ "marg": {
207
+ "why": "무엇을 확인해야 하는가?",
208
+ "key": "이 절이 조치 사항의 전부임. 나머지 절은 근거.",
209
+ "advice": "각 건의 「근거」 단추는 04·05절 상세로 연결됨."
210
+ }
211
+ }
212
+ ```
80
213
 
81
- 교차는 챕터 **순서**로 정해진다 — 특정 장을 왼쪽에 두고 싶다고 지정할 수 없다.
214
+ | 칸 | 뜻 | 없으면 |
215
+ |---|---|---|
216
+ | `kind` | 본문 장치를 정한다. 다섯뿐(5절) | **발행 거절** |
217
+ | `part` | 이 절이 속할 PART 의 `no` | 첫 묶음으로 떨어진다. 없는 번호를 가리키면 **거절** |
218
+ | `name` | 절 제목 | 제목이 빈다 |
219
+ | `label` | 절 머리의 번호 글자 | 「제N절」 |
220
+ | `short` | 색인 레일에 쓰는 짧은 이름 | `name` 을 쓴다 |
221
+ | `sub` | 차례에 붙는 한 줄 부제 | 차례에 부제가 없다 |
222
+ | `lede` | 절 머리의 도입 한두 문장 | 도입 스트립이 빠진다 |
223
+ | `marg` | 여백 주석 단 `{why,key,advice}` | **경고** — 두 단 본문이 한 단으로 선다 |
224
+ | `tone` | 장 색. 여섯(6절) | `kind` 가 정한다 |
225
+ | `group` | `kind:"cases"` 가 실을 그룹의 `key` | 그 절이 빈다. 없는 key 면 **거절** |
226
+ | `count` | 절 머리·차례의 개수 배지 | `cases`·`asks` 는 자동으로 센다. 그 밖은 배지 없음 |
227
+ | `count_unit` | 배지 단위 | 「건」 |
228
+ | `board_title` | `kind:"status"` 대조표의 캡션 | `name` 을 쓴다 |
229
+ | `board_hint` | 캡션 아래 보조 한 줄 | 보조 줄이 빠진다 |
230
+ | `paragraphs` | `kind:"note"` 강조 박스의 항목(문자열 목록) | 그 절이 빈다 |
231
+ | `limit_label` | 강조 박스의 머리 글자 | `name` 을 쓴다 |
232
+ | `note` | `kind:"status"` 에서 표 아래 한 줄 | 그 줄이 빠진다 |
233
+ | `flow` | 의존 흐름도 한 개(11-2절). `kind` 와 무관하다 | 도해가 빠진다 |
234
+ | `plates` | 절 본문 도판의 `assets[].id` 목록(11-1절) | 도판이 빠진다 |
235
+
236
+ ### ⚠ `no` 와 `id` 는 **안 읽힌다**
237
+
238
+ 절 번호와 앵커는 **배열 순서**가 정한다 — `sections[i]` 가 `ch = "%02d" % (i+1)` · `id = "sec-(i+1)"`.
239
+ `sections[].no`·`sections[].id` 를 적어도 렌더러가 안 본다.
240
+ 예시 데이터가 `"no": "01"` 을 적고 있는 것은 사람이 읽기 위한 것이다 —
241
+ **순서와 다르게 적으면 문서와 어긋난 원고가 된다.**
242
+
243
+ ### ⚠ `groups[].no` 는 그 그룹을 싣는 절의 순번과 같아야 한다
244
+
245
+ `§n.n` 마커의 앞 숫자가 **두 곳에서 다른 뜻**이다.
246
+
247
+ - 서버의 요약 근거 게이트는 `groups[].no` 로 센다(`report_publish.php:249-256`).
248
+ - 렌더러의 앵커는 **절 순번**으로 만든다(`gazette.py` 의 `_link` → `#case-%02d-%d`).
249
+
250
+ 둘이 어긋나면 게이트는 통과하는데 링크가 헛짚는다.
251
+ 그러니 `groups[].no` 를 그 그룹을 싣는 `kind:"cases"` 절의 순번과 같게 맞춘다.
252
+ (예시 데이터: 그룹 `no` 가 `4`·`5` 이고, 그 절이 4번째·5번째 절이다)
82
253
 
83
254
  ---
84
255
 
85
- ## 2b. 포털이 문서 **둘레**에 더해 주는 것 (저작 대상 아님)
256
+ ## 5. `kind` 다섯 — 그 밖은 발행이 거절된다
86
257
 
87
- 발행물 안에 쓰지 말아야 할 것을 알기 위해 적는다. 아래는 전부 포털이 준다.
258
+ `REPORT_RENDER_KINDS = ['summary','status','cases','asks','note']`(`report_publish.php:96`).
259
+ 모르는 `kind` 는 절이 통째로 빠지므로 **입구에서 거절한다.**
88
260
 
89
- | 무엇 | 어디에 | 저작자가 할 일 |
261
+ | kind | 무엇이 그려지는가 | 어디서 데이터를 가져오는가 |
90
262
  |---|---|---|
91
- | 진척률 바 | 발행물 최상단(5px, 스크롤 따라감) | 없음. 자동이고 인쇄에는 안 나온다 |
92
- | 상단 바 | 포털 셸(72px · 고객사 / 문서 제목 / 도구) | 없음 |
93
- | 검토 회신 | 포털 셸의 우측 패널 | **문서 안에 「댓글로 알려 주세요」 같은 문구를 쓰지 말 것.** 회신 자리는 이미 있다 |
94
- | 핵심 요약 | 포털 셸의 팝업(누르면 생성) | 없음. 다만 **근거가 `source_json` 원문뿐**이라 절 제목·`lead`·`open_items` 가 부실하면 요약도 부실하다 |
263
+ | `asks` | 머리 강조문 + 요청 항목 목록(위 2px 괘선이 가른다) | `cta[]` · `open_items[]` |
264
+ | `cases` | 건 필드표(증상·원인·조치·확인 방법·미결·원본) | 그 절의 `group` 에 속한 `cases[]` |
265
+ | `status` | 대조표(4괘선 + 확인 강도 표식) | **전체** `cases[]` · 절의 `board_title`·`board_hint`·`note` |
266
+ | `summary` | 요지 블록(리드 + 본문 + 해당 절) | `summary[]` |
267
+ | `note` | 강조 박스(미확인 사항 등) | 절의 `paragraphs[]` · `limit_label` |
95
268
 
96
- 핵심 요약은 **발행 데이터가 있는 문서에만** 뜬다(MCP·API·편집기 서식 발행분). 파일로 올린
97
- 문서에는 단추가 아예 안 나온다 — 요약할 원문이 없어서다.
269
+ `asks` 와 `summary` 는 **문서에 하나만 두는 것이 맞다** — 둘 다 최상위 목록을 통째로 그린다.
270
+ 둘을 두 절에 걸면 같은 목록이 두 번 실린다.
98
271
 
99
272
  ---
100
273
 
101
- ## 3. 일러스트 — `assets[]`
274
+ ## 6. `tone` 여섯 — 색은 뜻으로 고른다
275
+
276
+ 생략하면 `kind` 가 정한다.
277
+ 밖의 값은 **조용히 `neutral` 로 강등된다**(경고 없음).
278
+
279
+ | tone | 뜻 | 기본으로 붙는 kind |
280
+ |---|---|---|
281
+ | `you` | 독자 몫 | `asks` |
282
+ | `deep` | 요지 | `summary` |
283
+ | `neutral` | 허브 — 상태에 안 속함 | `status` |
284
+ | `done` | 끝난 것 | `cases` |
285
+ | `wait` | 대기 | — |
286
+ | `hold` | 유보 — 색을 채우지 않고 파선으로 | `note` |
287
+
288
+ `parts[].tone` 은 넷만 받는다: `you` · `neutral` · `wait` · `hold`.
289
+ 안 주면 첫 PART 는 `you`, 마지막은 `wait`, 가운데는 `neutral` 로 자동으로 붙는다.
290
+
291
+ **PDF 의 파스텔을 베끼지 마라.**
292
+ 색은 그 문서가 이미 쓰는 상태색을 승격한 것이다.
293
+
294
+ ---
102
295
 
103
- **바이트를 직접 싣지 않는다.** `source` 에 원본 주소만 주면 MCP 가 받아와 규격대로 굳혀 보낸다.
296
+ ## 7. `grades` — 키 순서가 강도 순서다 (계약)
104
297
 
105
298
  ```json
106
- "assets": [{
107
- "id": "hero", "role": "hero", "provider": "higgsfield",
108
- "alt": "…", "origin": "higgsfield:gpt_image_2:<job-id>",
109
- "source": "https://…/hf_….png", "required": true
110
- }]
299
+ "grades": {
300
+ "운영 환경 확인": ["m-live", "운영 환경에서 실제로 동작시켜 결과까지 확인함"],
301
+ "실데이터 대조": ["m-data", "운영 데이터를 건별로 대조해 확인함. 화면 재현은 미실시"]
302
+ }
111
303
  ```
112
304
 
113
- `mime`·`width`·`height` 는 **적지 않는다** — MCP 가 인코딩 결과에서 읽어 채운다.
114
- 운영 문서가 1400×934 그림을 1400×1050 이라 선언해 브라우저가 세로로 12.4% 늘여 그린 적이 있다.
115
- 선언을 안 받으면 그 결함이 구조적으로 안 생긴다.
305
+ - **사전의 키 순서가 강한 것부터다.**
306
+ `gazette.py:80-83` 이 「스킬 2단계가 계약」이라고 주석으로 물고 있고, 그 순서로 표식을 매긴다.
307
+ - 자리 → 표식: `['full','half','open','open']`(강도 점) · `['live','data','done','soon']`(태그 색).
308
+ - 세 번째·네 번째 칸에 이름을 주면 그것이 이긴다 — `["m-live", "설명", "full", "live"]`.
309
+ - 첫째·둘째 칸(클래스·설명)은 관보 렌더러가 **안 읽는다**.
310
+ 설명은 `appendix` 의 판정 기준 표에 다시 적는다 — 예시 데이터가 그렇게 한다.
311
+ - 등급이 없는 건은 「확인 안 함」으로 선다(`labels.nograde` 로 바꾼다).
116
312
 
117
- ### 서버가 거절하는 것 (계약 7.1절)
313
+ 순서를 바꾸면 **문서 전체의 강도 표식이 통째로 뒤집힌다.**
314
+ 등급을 추가할 때는 어디에 끼우는지가 곧 그 등급의 뜻이다.
118
315
 
119
- | 조합 | 판정 |
120
- |---|---|
121
- | `role: hero`·`chapter` 인데 `provider ≠ higgsfield` | **거절** |
122
- | `provider: deterministic` 인데 `role ≠ figure` | **거절** |
123
- | `provider: user-provided` 인데 `origin` 이 빔 | **거절** |
124
- | `provider: sample-exception` | **거절** (회귀 기준물 전용) |
125
- | 선언한 `mime`/`width`/`height` 가 실물과 다름 | **거절** |
126
- | 장당 80,000B · 합계 400,000B 초과 | **거절** |
127
- | `alt` 누락 | 경고(발행은 된다) |
316
+ ---
317
+
318
+ ## 8. 요약 근거 게이트 — 발행이 **거절**된다
319
+
320
+ `summary[]` 의 문단마다 근거로 삼은 **사례 번호**를 최소 하나 밝혀야 한다.
321
+
322
+ - 두 검사가 짝이다: **참조 존재**(문단마다 하나 이상) · **참조 실재**(그 번호가 실제로 있어야).
323
+ - 두 모양을 다 센다: `refs: ["4.1"]` 과 본문 안 `§4.1`.
324
+ - `§4.1` 은 **원고 마커**다 — 렌더에서 `04-1` 로 가는 링크가 되고 `§` 기호는 사라진다.
325
+ - 인라인(`§n.n`)이 편집기 왕복에서 확실히 살아남는 쪽이다.
326
+ 둘 다 적어 두는 편이 안전하다.
327
+
328
+ 참조 표기는 세 모양을 받는다: `4.1` · `04-1` · `#sec-3`.
329
+ 보이는 글자는 **`04-1` 로 정규화된다** — 문서의 다른 자리(건 번호·차례·대조표)와 같은 표기라서다.
330
+ `#apx-1` 같은 앵커도 받지만 보이는 글자가 `apx-1` 이 된다 —
331
+ 부록을 가리킬 때는 `refs` 대신 본문 글자로 「부록 1」이라 적는 편이 낫다.
128
332
 
129
- EOF 엔드카드만 `user-provided` 가 정상값이고, `origin` 에 제공자를 적는다.
333
+ ---
130
334
 
131
- > **꼬리 카드는 대개 안 준다**(2026-08-19). 서버가 기본 카드를 끼우므로 `assets[]` 에
132
- > `role: eof` 를 넣을 이유는 **이 문서만의 카드가 있을 때뿐**이다. 그때도 규격이 다르다 —
133
- > 무손실(PNG 또는 무손실 WebP) · **줄이지 않고 원본 크기** · 장당 상한 200,000B
134
- > (일러스트의 80,000B 가 아니다). 기본 카드는 포털 **설정** 화면에서 관리자가 올린다.
335
+ ## 9. 거절 조건 — 발행이 막힌다 (422)
135
336
 
136
- ### 예산 실측 (2026-08-16, GPT Image 2 7장)
337
+ `report_data_validate()` 가 잡는 것뿐이다.
338
+ **완성도는 안 본다** — 여기서 막는 것은 「모양이 달라서 통째로 안 실리는」 것이다.
137
339
 
138
- | | |
340
+ | 무엇 | 왜 |
341
+ |---|---|
342
+ | `sections[].kind` 가 다섯 밖 | 절이 통째로 빠진다 |
343
+ | `kind:"cases"` 절이 없는 `group` 을 가리킴 | 그 절이 빈다 |
344
+ | `cases[].group` 이 없는 그룹 | 그 건이 어디에도 안 실린다 |
345
+ | `sections[].part` 가 없는 PART 번호 | 조용히 첫 묶음으로 자리를 옮긴다 |
346
+ | `summary` 문단에 근거 표기 0개 | 독자가 근거를 찾아 헤맨다 |
347
+ | `summary` 가 없는 사례 번호를 근거로 댐 | 같음 |
348
+ | `sections`·`cases`·`summary` 가 목록이 아님 | `{}` 를 보내면 뒤에서 날 TypeError 가 샌다 |
349
+ | `history`·`guide`·`parts`·`appendix`·`bar` 가 목록이 아님 | 같음 |
350
+ | `notice`·`colophon` 이 객체가 아님 | 같음 |
351
+ | `cases[].source_url` 이 http(s) 가 아님 | 안 실린다. `javascript:` 를 겸해서 막는다 |
352
+ | `assets[]` 의 모르는 `role`·`provider`, 매직바이트 불일치, data URI 아님 | 위조·무언의 소실 |
353
+ | `assets[]` 크기 초과 — 장당 80,000B · 합계 400,000B | 서버가 못 견딘다 |
354
+
355
+ ## 10. 경고 조건 — 발행은 되지만 빠진다 (⚠)
356
+
357
+ `dry_run` 의 `warnings` 가 **실질적 유일한 품질 관문이다.**
358
+ 렌더러가 센 것이 먼저 오고, 그 뒤에 규격 게이트와 그림 관문이 붙는다.
359
+
360
+ | ⚠ | 무엇이 안 실렸는가 |
139
361
  |---|---|
140
- | 자산 합계 | 109,964B / 400,000B (**27%**) |
141
- | 발행물 | 약 1,036,000B / 1,600,000B (**65%**) |
362
+ | 여백 주석이 없습니다 | 그 절의 `aside.marg` — 두 단 본문이 한 단으로 선다 |
363
+ | PART 묶음이 없어 한 묶음으로 세웠습니다 | 읽기 순서 |
364
+ | 절에 실을 내용이 없습니다 | `kind` 가 가리키는 데이터가 비었다 |
365
+ | 그룹에 배정되지 않은 건 N개 | `cases[].group` 이 어긋났다(그룹이 아예 없을 때) |
366
+ | 그림 N장은 놓을 자리가 없어 | `logo`·`figure` 밖의 `role`(`hero`·`chapter`·`eof`) |
367
+ | 도판 N장이 어느 절에도 걸리지 않아 | `sections[].plates` 에 그 id 를 안 적었다 — 11-1절 |
368
+ | 절 …의 도판 「…」를 assets 에서 못 찾았습니다 | `plates` 가 없는 `assets[].id` 를 가리킨다 |
369
+ | 도판 「…」에 그림 바이트가 없습니다 | `file`·`source` 없이 이름만 왔다 |
370
+ | `chapters`·`figure`(**최상위 칸**) 는 자리가 없어 | 옛 서식 전용 칸. `assets[].role:"figure"` 와 다른 것이다 |
371
+ | 절 …의 `principles`/`logic` 은 자리가 없어 | 같음 |
372
+ | `wide`/`marg_width` 값이 길이 단위가 아니라 | CSS 값 검사 |
373
+
374
+ 경고가 하나라도 있으면 **그것부터 고친다.**
375
+ 경고를 남긴 채 고객에게 링크를 보내지 마라.
376
+
377
+ ---
378
+
379
+ ## 11. 안 실리는 것
380
+
381
+ **`assets` 는 `role:"logo"` 와 `role:"figure"` 둘이다.**
382
+ `data:image/` 로 시작하는 값이어야 읽는다(`source`·`file` 을 주면 MCP 가 굳혀 넣는다).
383
+ `hero` · `chapter` · `eof` 는 세어서 버려진다 — 12-1절 참조.
384
+
385
+ ⚠ **`assets[].brief` 를 주지 마라.**
386
+ `brief` 가 있고 `data` 가 없으면 서버가 **role 을 안 가리고** 그리기 작업을 만든다.
387
+ 크레딧은 타고 문서에는 안 나온다 — 자세한 것은 `references/illustrations.md`.
142
388
 
143
- **흰 여백을 잘라내지 말 것.** 트림하면 오히려 **커진다**(7장 109,964B → 162,354B, +48%).
144
- WebP 가 균일한 흰 면을 거의 공짜로 압축하는데, 잘라내면 같은 상자에 그림이 더 크게 들어가
145
- 디테일 픽셀이 늘기 때문이다. 투명 배경은 3배가 된다(311,856B) — 문서 배경이 어차피 White 라 이득도 없다.
389
+ **`chapters` · `principles` · `logic` 은 관보에 자리가 없다.**
390
+ 세고 버린다.
391
+
392
+ **꼬리 카드가 없다.**
393
+ 문서는 판권지로 닫고 그 뒤에 아무것도 붙이지 않는다.
394
+
395
+ **표지 가운데의 빈 마당은 그림 자리가 아니라 여백이다.**
396
+ 채우지 마라.
397
+ 도판의 자리는 표지가 아니라 **절 본문**이다 — 12-1절.
398
+
399
+ **문서 안에 스크립트가 없다.**
400
+ 열람 CSP 가 `default-src 'none'` 이고 `script-src` 를 따로 열지 않는다(`view.php:236-242`).
401
+ 인라인 스크립트를 여는 분기는 **문서 안 편집기가 실제로 들어 있을 때만** 열리는데,
402
+ 관보 렌더러는 그 레이어를 아예 안 싣는다(`build.py:439·442`) — 그 분기에 걸릴 일이 없다.
403
+
404
+ 그래서 머리띠는 「지금 어디인가」를 주장하지 않는다.
405
+ 번호 칸은 전부 앵커 링크이고, 우측 판독부에는 문서 전체의 사실(요청 잔여 N건)만 적힌다.
406
+ 알 수 없는 것을 적으면 거짓말이라서다.
146
407
 
147
408
  ---
148
409
 
149
- ## 4. 요약 근거 게이트 — 발행이 **거절**된다
410
+ ## 11-1. 도판 — 관보에도 그림 문법이 있다
411
+
412
+ **「관보는 비트맵을 안 싣는다」는 서술이 한동안 여기 있었다. 틀린 것이었다.**
413
+ 조판 원전(`gazette-report` 스킬)은 `그림 5-1` 캡션을 표지·차례·간지와 나란히 관보 가구로
414
+ 규정한다(그 SKILL.md 3·105·416줄). 표지 가운데를 비워 두는 것과, 근거로 실린 화면을 문서에서
415
+ 몰아내는 것은 다른 이야기다. **검수 보고에서 확인 화면을 못 싣게 하면 그 문서는 「보여 주는」
416
+ 수단을 통째로 잃는다.**
417
+
418
+ ### 무엇을 어디에 적는가
419
+
420
+ | 자리 | 칸 |
421
+ |---|---|
422
+ | 그림 자체 | `assets[]` 에 `role: "figure"` · `id` · `alt`(접근성 계약) |
423
+ | 제공자 | `provider: "user-provided"` 이면 **`origin` 이 필수다** — 누가 제공했는지. 비우면 서버가 발행을 거절한다(「출처를 못 적는 자산은 고객 문서에 싣지 않습니다」) |
424
+ | 그림 바이트 | `file`(**로컬 절대경로**) 또는 `source`(허용 호스트 https). 둘 다 주면 `file` 이 이긴다 |
425
+ | 캡션 | `assets[].caption` — 번호 뒤에 붙는다. **번호는 적지 않는다**(자리가 매긴다) |
426
+ | 출처 | `assets[].source_text` — 언제·어디서 얻은 화면인가 |
427
+ | 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠) |
428
+
429
+ 번호는 **그림 〈절 순번〉-〈그 절에서의 순서〉** 다. 절 4의 둘째 도판이면 `그림 4-2` 이고,
430
+ 앵커는 `#fig-04-2` 다. 데이터로 바꿀 수 없다 — 절 번호와 같은 규약이다.
431
+
432
+ ### 도형과 실촬 화면은 인코딩이 갈린다
433
+
434
+ `provider` 가 그 갈림을 이미 들고 있다.
435
+
436
+ | provider | 인코딩 | 왜 |
437
+ |---|---|---|
438
+ | `deterministic` | **무손실** WebP | 평면 색과 선이라 무손실이 작고 선이 안 뭉갠다 |
439
+ | `user-provided` | 화질 사다리 82 → 72 → 62 → 52 | 화면 캡처는 사진에 가까워 무손실이 예산을 몇 배로 넘긴다. 글자가 읽히는 것이 목적이라 `-sharp_yuv` 를 끝까지 유지한다 |
440
+
441
+ 최저 화질에서도 장당 80,000B 를 넘으면 거절된다. 그때 줄일 것은 **화질이 아니라 담은 화면 수**다 —
442
+ 여러 장을 한 판에 몰아 넣으면 어차피 문서에서 글자가 안 읽힌다.
150
443
 
151
- 계약 5.6.1절: 요약의 **모든 문단**은 근거로 삼은 **사례 번호**를 최소 하나 밝혀야 한다.
444
+ ### 캡션에 무엇을 쓰는가
445
+
446
+ **이 화면에서 무엇을 보라는 것인지**를 쓴다.
447
+ 본문이 이미 말한 것을 되풀이하면 도판이 장식이 된다.
448
+ 그리고 `alt` 는 캡션의 사본이 아니다 — 캡션은 「무엇을 보라」이고 `alt` 는 「무엇이 보이는가」다.
449
+
450
+ ## 11-2. 흐름도 — 「A 가 되면 B 가 되고 C 가 된다」
451
+
452
+ **이것도 한동안 없었다.** 조판(`gazette.css` 의 `.flow*` 30규칙)은 추출로 들어와 있었는데
453
+ 그 클래스를 붙이는 코드가 없었고, 그래서 이 문서가 「도해가 필요하면 flow 장치로」라고
454
+ 안내하면서 실제로는 표현할 길이 없었다. 도해가 필요한 절이 전부 불릿 목록으로 눌려 나갔다.
455
+ (2026-08-24, 원전 `FI-soft/report-v2` 대조에서 드러남.)
152
456
 
153
457
  ```json
154
- "summary": [{
155
- "lead": "개선안을 운영 데이터에 적용하면 이용자 91.7%의 안내가 달라집니다.",
156
- "body": "방향이 뒤집히는 분이 30.5%(§4.2), 근육 목표가 없어지는 분이 16.8%(§3.2)입니다."
157
- }]
458
+ "flow": {
459
+ "caption": "세 건이 연결되는 순서",
460
+ "heads": ["건", "서버가 준비할 것", "앱 (준비 완료)", "그러면"],
461
+ "rows": [
462
+ { "no": "5.1",
463
+ "cells": [
464
+ { "text": "조회 주소 개설", "path": "/wp-content/plugins/kboard/api.php",
465
+ "note": "표시할 게시판 번호 확정" },
466
+ { "text": "위젯이 신규 도메인을 보도록 전환 완료",
467
+ "note": "표시 건수 안드로이드 5 · 아이폰 4 로 사양과 이미 일치" },
468
+ { "text": "홈 화면 위젯에 게시글 목록이 채워짐" } ] }
469
+ ]
470
+ }
158
471
  ```
159
472
 
160
- | 함정 | 사실 |
473
+ **열은 넷으로 고정이다.** CSS 의 `grid-template-columns` 가 「좌표 4.6em + 세 칸」으로 박혀
474
+ 있고, 칸 사이 화살표도 첫 두 칸의 `::after` 로 그린다. 그래서 `heads` 는 **정확히 4개**여야
475
+ 하고 각 행의 `cells` 는 **정확히 3개**여야 한다. 어긋나면 ⚠ 로 세고 잘라 붙인다.
476
+
477
+ **칸 성격은 데이터가 고르지 않고 자리가 정한다.**
478
+
479
+ | 자리 | 조판 | 무엇을 담는가 |
480
+ |---|---|---|
481
+ | `cells[0]` | 강조 면(노란 배경·굵게) | **해야 할 것** — 아직 안 된 것, 이 문서가 요청하는 것 |
482
+ | `cells[1]` | 흐린 글자 | **이미 된 것** — 배경, 우리 쪽에서 끝난 것 |
483
+ | `cells[2]` | 본문 글자 | **그러면** — 앞의 둘이 맞물렸을 때의 결과 |
484
+
485
+ 데이터가 클래스 이름을 고르게 두면 서식이 데이터를 따라다닌다. 그래서 안 받는다.
486
+ 읽는 순서가 「해야 할 것 → 이미 된 것 → 결과」와 안 맞으면 **열 제목을 그 순서로 바꿔 적는다** —
487
+ 도해를 비틀지 말고 말을 맞춘다.
488
+
489
+ | 칸 | 뜻 |
161
490
  |---|---|
162
- | 「절 번호」가 아니다 | `§n.n` 은 **사례 번호**다. `{그룹 no}.{그 그룹 안 순번}` 으로 **계산**된다. `sections[].no`("01")를 적으면 거절된다 |
163
- | `refs[]` 와 인라인 중 무엇을 쓰나 | **인라인 `§` 를 권장.** 둘 다 근거로 세지만, 인라인은 본문 필드라 편집기 왕복에서 확실히 살아남는다 |
164
- | `§` 없는 맨 숫자 | 근거로 안 센다. `문제(3.1)` 는 죽은 활자이고 `문제(§3.1)` 는 링크가 된다 |
491
+ | `caption` | 그림 제목. 번호(`그림 5-1`)는 **자리가 매긴다** — 적지 않는다 |
492
+ | `heads[0]` | 좌표 열의 머리(「건」·「요청」·「구간」 등) |
493
+ | `rows[].no` | 좌표. `4.1`·`04-1` 모양이면 그 사례로 **링크가 된다**. 그 밖의 글자는 그냥 글자다 |
494
+ | `cells[].text` | 칸 본문. `§N.M` 상호참조가 링크로 산다 |
495
+ | `cells[].note` | 칸의 둘째 줄(작은 글자). 조건·한정을 담는다 |
496
+ | `cells[].path` | 경로·기술 문자열. 별도 활자로 서고 `/` 앞에서 끊어 읽는다 |
165
497
 
166
- ---
498
+ **그림 번호는 도판과 한 셈을 쓴다** — 한 절에 흐름도와 도판이 같이 있으면 본문 순서대로
499
+ `그림 N-1`·`그림 N-2` 가 된다. 따로 세면 같은 번호가 두 번 나와 상호참조가 무엇을 가리키는지
500
+ 알 수 없게 된다.
501
+
502
+ ## 12. 가르치는 문서 — 기술보고서·인수인계·포스트모템
503
+
504
+ 독자가 **판정**하는 문서(검수 보고)와 **배우는** 문서는 뼈대가 다르다.
505
+ 같은 서식, 다른 서사다.
506
+
507
+ ### 12.0 이 절이 생긴 사건
508
+
509
+ 어느 기술보고서(2026-08-14)에서, 검수 보고 뼈대(요약→판정표→건별 카드→확인 요청)에
510
+ 타이포·여백·색만 갈아입힌 판을 냈다가 반려됐다.
511
+ 지적은 정확히 **심슨의 역설**이었다.
512
+
513
+ > **부분(활자·색·여백)마다 좋아졌는데 전체(문서가 읽히는 방식)는 그대로였다.**
514
+
515
+ > 베스트셀러 교과서는 같은 내용을 더 예쁘게 실어서 팔리는 게 아니다.
516
+ > **가르치는 구조 자체가 다르다. 구조가 제품이다.**
517
+
518
+ 그러므로 이 모드에서 **스타일 작업은 마지막이다.**
519
+ 먼저 물을 것은 하나다 — 이 문서는 무엇을 어떤 순서로 가르치는가.
520
+ 그 답이 PART 구성이 된다.
521
+
522
+ ### 12.1 ⚠ `kind:"lesson"` 은 없다
523
+
524
+ 옛 문서가 `kind:"lesson"` 과 `blocks[]`(`concept`·`example`·`recap`·`code`·`figure`)를 규정했다.
525
+ **그 스키마는 존재하지 않는다.**
526
+
527
+ - `kind` 는 다섯뿐이고 그 밖은 **서버가 발행 단계에서 거절한다**(5절·9절).
528
+ - `sections[].blocks` 는 렌더러가 안 읽는다 — 경고도 없이 사라진다.
529
+ - `data-recall` 재호출 팝오버도 없다.
530
+ `gazette.css`·`gazette.py` 어디에도 없고, 스크립트가 없으니 설 수 없다.
531
+
532
+ 가르치는 문서도 **PART 구성과 다섯 kind 로 푼다.**
533
+
534
+ ### 12.2 모드 판별 — 독자가 누구인가
167
535
 
168
- ## 5. 출처 링크 문구 — `cases[].source_text`
536
+ | 독자의 일 | 모드 | 뼈대 |
537
+ |---|---|---|
538
+ | 판정한다 (검수·회신·결재) | 검수 보고 | 요약 → 대조표 → 건별 필드표 → 확인 요청 |
539
+ | 배운다 (유지보수·인수·온보딩·사후분석) | **가르치는 문서** | 학습목표 → 원리 → 붕괴 → 처방 → 적용 → 연습 |
540
+
541
+ 트리거: 기술보고서 · 인수인계 · 온보딩 문서 · 포스트모템 ·
542
+ 「다음 사람이 읽고 이해해야」 · 「왜 이렇게 됐는지 설명하는 문서」.
543
+
544
+ ### 12.3 서사 공식 — 의존 순서로 가르친다
545
+
546
+ **각 장은 앞 장 없이는 성립하지 않아야 한다.**
547
+ 그래야 읽는 순서가 학습 곡선이 된다.
548
+ **병렬 나열(어디서 읽어도 같은 문서)이 되는 순간 이 모드가 아니다.**
549
+
550
+ | 단계 | 역할 | 관보에서 무엇으로 서는가 |
551
+ |---|---|---|
552
+ | 학습목표 | 「다 읽고 나면 할 수 있는 것」 2~4개. **문서의 성패 기준**이라고 명시한다 | `guide[]` 의 질문 → ▶ 지시 · PART 1 간지의 `questions[]` |
553
+ | 원리 | 시스템이 성립하는 핵심 명제 **하나** | `kind:"note"` 절 하나 — `limit_label` 이 명제 이름, `paragraphs` 가 정의 |
554
+ | 붕괴 | 그 원리가 깨졌던 구조. **실제 사건**을 단계 풀이로 | `kind:"cases"` 절 — `symptom`→`cause`→`fix`→`how_to_check` 가 「왜」의 사슬이다 |
555
+ | 처방 | 붕괴를 **구조적으로 불가능하게** 만든 결정 | 같은 건의 `fix` 행, 또는 `kind:"note"` 절 하나 |
556
+ | 적용 | 원리가 실제 작업에 적용된 사례들 | 다른 `group` 의 `kind:"cases"` 절 |
557
+ | 한눈에 | 지금까지 다룬 것의 강도 대조 | `kind:"status"` 절 |
558
+ | 연습 | 수정·운영 전 스스로 답할 물음 | `kind:"asks"` 절 — 12.5절 |
559
+ | 대장 | 판정 기준 · 환경 · 재현 명령 | `appendix[]` |
560
+
561
+ **원리가 개념 박스 하나로 압축되지 않으면 아직 원리를 못 찾은 것이다.**
562
+ 박스가 셋이면 원리가 아니라 목록이다.
169
563
 
170
- 넷이 전부 "제보 원본 보기" 면 계약 10절 「목적을 알 수 있는 문구」를 못 지킨다.
564
+ 「더 좋은 검사를 붙였다」가 아니라 **「그 부류가 존재할 수 없게 했다」**로 처방을 프레임한다.
565
+ 적용 사례가 원리를 되짚지 않고 혼자 서면 그것은 본문이 아니라 부록감이다.
566
+
567
+ ### 12.4 본문 / 대장 분리
568
+
569
+ **본문은 가르치고, 대장은 참조를 받친다.**
570
+ 건별 사실(재현 명령·수치 표·환경)은 `appendix[]` 로 내린다.
571
+ **건별 카드를 본문에 두면 병렬 나열로 되돌아간다.**
572
+
573
+ 본문이 대장을 가리키는 길은 인라인 `§n.n` 참조 하나다 — 팝오버는 없다.
574
+ 그래서 참조 앞뒤에 **무엇을 확인하러 가는지**를 글자로 적어 준다.
575
+
576
+ ### 12.5 연습문제 — `kind:"asks"` 를 다시 읽는다
577
+
578
+ `open_items` 를 확인 요청이 아니라 **물음–답–복습 지침**으로 쓴다.
171
579
 
172
580
  ```json
173
- { "source_url": "…", "source_text": "상담 원본 · 체성분 양호자 증량 문의" }
581
+ {"title": "골든 지문이 어긋났을 때, 왜 새 결과로 갱신하면 안 되는가?",
582
+ "body": "(답) 참조 구조가 자기 인증이 되기 때문임. … 답이 바로 안 나오면 05절을 다시 읽을 것.",
583
+ "refs": ["5.1"]}
174
584
  ```
175
585
 
176
- 인쇄본에서는 URL 이 **안 펼쳐진다**(계약 8절) — 이용자 식별자가 들어간 관리 화면 주소가
177
- 지면에 찍히던 것을 막은 규칙이다. 화면에서는 이 문구가 링크로 보인다.
586
+ - `title` 은 **완전한 물음문**.
587
+ 「골든 지문 관리」 같은 명사구는 심각도를 감춘다.
588
+ - `body` 는 답을 준다 — 시험이 아니라 자기 점검이다.
589
+ 끝에 「N절을 다시 읽을 것」 복습 지침을 단다.
590
+ - `cta` 는 이 문서 전체에서 독자가 지킬 **단 하나의 행동 규칙**이다.
591
+
592
+ ### 12.6 검수 항목
593
+
594
+ - [ ] 장 순서를 뒤섞어도 문서가 성립하는가?
595
+ **성립하면 실패다** — 병렬 나열이라는 뜻이다
596
+ - [ ] **각 절의 `marg.key` 만 이어 읽어도** 문서 요지가 재구성되는가?
597
+ - [ ] 본문에 건별 사실(재현 명령·수치 표)이 남아 있는가?
598
+ 대장(`appendix`)으로 내릴 것
599
+ - [ ] 붕괴 절의 각 행에 「왜」가 있는가?
600
+ `cause` 없는 `fix` 는 풀이가 아니라 표다
601
+ - [ ] 연습문제 답에 복습 지침이 있는가?
602
+ - [ ] 스타일부터 만지고 있지 않은가?
603
+ **절 구성이 확정되기 전의 CSS 작업은 전부 낭비다**
604
+
605
+ ### 12.7 근거
606
+
607
+ - **신호 이론(SARA)** — 장치 문법을 일관되게 유지하면 독자가 레이아웃 해석을 멈추고 내용만 해석한다.
608
+ 관보의 `marg` 세 칸(Why·Key Point·Advice)이 문서 끝까지 같은 자리에 서는 이유다.
609
+ - **Worked examples** — 완성된 풀이 제시가 실증적으로 기억을 향상시킨다.
610
+ `kind:"cases"` 의 필드표가 그 자리다.
611
+ - **여백 노트(Tufte)** — 참조를 문서 끝이 아니라 **참조된 자리 옆**에.
612
+ 이것이 관보 `marg` 단의 근거다.
613
+ - **측정폭** — 긴 행은 시선 복귀 비용이다.
614
+ 구체 수치와 한글 글줄 폭 근거는 `gazette-report` 스킬에 있다.
178
615
 
179
616
  ---
180
617
 
181
- ## 6. 발행 전 자기 점검
618
+ ## 13. 판정법 — 어느 서식으로 나가는지 두 곳에서 잰다
182
619
 
183
- - [ ] 요약 문단마다 `§n.n` 이 최소 하나 있고, 그 번호가 **실재**하는가
184
- - [ ] hero·챕터 자산의 `provider` 가 전부 `higgsfield` 이고 `origin` 에 job 식별자가 있는가
185
- - [ ] `mime`·`width`·`height` 를 **안 적었는가**(MCP 가 채운다)
186
- - [ ] 챕터 `lead` 와 절 `lede` 를 **둘 다 쓰지 않았는가**
187
- - [ ] `title_lines` 로 줄바꿈을 정했는가
188
- - [ ] 출처 링크 문구가 서로 다른가
189
- - [ ] 챕터 그림이 **가로로 긴** 비율인가(2열에서 반폭에 선다)
190
- - [ ] 문서 안에 「댓글로」·「의견 주세요」 같은 문구가 없는가(회신 자리는 포털이 준다)
620
+ ### 13.1 배포 여부 — 서버가 v2 인가
191
621
 
192
- 발행 뒤에는 `get_report` 로 되읽어 자산 `provider`·`origin`·크기와 요약 근거를 눈으로 확인한다.
622
+ `sections[].part` 에 **없는 PART 번호**를 하나 넣고 `dry_run` 을 태운다.
193
623
 
194
- ---
624
+ - **422 로 거절되면 v2** — 「절 N 이(가) 없는 PART '…' 를 가리킵니다」.
625
+ - 통과하면 그 서버의 검증기에 관보 칸 검사가 아직 없다.
195
626
 
196
- ## 7. 운영 환경 제약 (렌더러를 고칠 때만)
627
+ ### 13.2 렌더 결과 — 문서가 v2 로 섰는가
197
628
 
198
- | 제약 | 내용 |
199
- |---|---|
200
- | 파이썬 | 운영은 **3.6.8**. f-string 중괄호 식 안 **역슬래시 금지**(3.12+ 문법), walrus·match 금지 |
201
- | 스타일시트 | 주석에도 `§n.n` 표기를 쓰지 말 것 — 절참조 게이트가 발행을 죽인다. 「5.1절」로 쓴다 |
202
- | 인라인 `<svg>` | 편집기 일반 문서 경로의 입고 검사가 거절한다. 화살표는 문자(`↘`)나 CSS 로 |
203
- | 편집기 | 챕터 오프닝 조각은 `<main>` **직계 형제**여야 한다. 감싸는 요소를 두면 절 이동이 남의 오프닝을 딸려 옮긴다 |
204
-
205
- 로컬 파이썬이 3.12+ 면 첫 줄 함정이 **로컬에서 안 잡힌다.** 실제로 그 한 줄이 운영 발행을
206
- 통째로 멈춘 적이 있다(2026-08-16). `tests/builder.test.py` 의 호환성 묶음이 그것을 읽어서 잡는다.
207
-
208
- ## 절 앞에 세우는 두 블록 (2026-08-18)
209
-
210
- 정본이 쓰던 서식인데 데이터 칸이 없어 못 만들던 둘이다. 이제 **쓰면 그려진다.**
211
- 안 쓰면 안 그려진다 — 없는 문구를 지어내지 말 것.
212
-
213
- ### `logic` — 대조 도해 (「무엇을 바꾸는가」가 한 장면일 때)
214
-
215
- ```jsonc
216
- { "kind": "summary",
217
- "logic": {
218
- "kicker": "CORE LOGIC",
219
- "title_lines": ["‘다음 칸’이 아니라", "‘적정 상태’를 목표로 바꿉니다"],
220
- "lede": "계산식이 아니라 목표로 삼는 도착점을 바꾸는 비교입니다.",
221
- "models": [
222
- { "label": "현재 방식", "flow": ["현재 상태", "인접 등급", "상한"], "body": "…" },
223
- { "label": "개선 방식", "flow": ["현재 상태", "적정 체중", "그 상태"], "body": "…" }
224
- ] } }
225
- ```
629
+ `get_report(id, outline: true)` 를 부른다.
630
+ `<main>` 안의 **가구 블록만** 돌아온다(항목은 안 실린다).
226
631
 
227
- 첫 모델이 「현재」, 그다음이 「개선」으로 그려진다 — **자리로 정한다.**
228
- `flow` 의 **마지막 칸이 강조**된다(도착점). 단계 수는 자유다.
632
+ - **v2** — `header.cover` · `section.hist` · `nav.toc` · `section.guide` · `section.sum` ·
633
+ `section.pt` · `section.part` · `aside.marg` · `section.apx` · `div.colophon`
634
+ - **구버전** — `figure.eof-endcard` · `div.ch-open` · `h2.sec`
229
635
 
230
- **언제 쓰나** — 바꾸는 것이 *값*이 아니라 *구조*일 때. 「A 를 B 로 고쳤다」는 건 카드에
231
- 쓰고, 「도착점 자체가 달라진다」처럼 두 방식을 나란히 놓아야 이해되는 것만 여기 쓴다.
636
+ ⚠ 요약 줄의 「절 N · 건 N · 장 N · 그림 N」은 **옛 서식의 셈이다.**
637
+ 관보 문서에서는 전부 0으로 나오는 것이 정상이다 — 「문서가 비었다」가 아니라 다른 것으로 서 있다는 뜻이다.
638
+ 판정은 **블록 목록**으로 한다.
232
639
 
233
- ### `principles` — 3열 카드 (규칙이 셋 안팎으로 나뉠 때)
640
+ `aside.marg` 가 하나도 없으면 두 단 본문이 한 단으로 선 것이다 — 그 서식의 본체가 빠졌다.
641
+ `⚠ 빈 블록` 이 뜨면 렌더러가 조각을 빠뜨린 것이다.
234
642
 
235
- ```jsonc
236
- { "kind": "note",
237
- "paragraphs": ["긴 설명은 그대로 여기에."],
238
- "principles": [["적정 체중", "…"], ["체지방 목표", "…"], ["근육 유지", "…"]] }
239
- ```
643
+ ---
240
644
 
241
- **번호는 주지 않는다** — 자리로 매겨진다. 순서를 바꾸면 번호도 따라온다.
242
- 긴 문단은 `paragraphs` 에 그대로 두고, 카드에는 **짧은 제목 + 한두 문장**만 넣는다.
243
- 카드가 문단의 요약이 되면 같은 말을 두 번 하는 문서가 된다.
645
+ ## 14. 톤 — 개조식. 이것부터 바꾼다
244
646
 
245
- **언제 쓰나** — 원칙·기준처럼 **병렬로 읽히는 규칙**일 때. 순서가 뜻을 가지면(1단계→2단계)
246
- 카드가 아니라 문단이 맞다.
647
+ 논문·과제·공문 문체다.
648
+ `~합니다 / ~해 주세요 / ~했습니다` **아님**.
247
649
 
248
- ## 발행 절차 — 초안으로 내고, 보고, 연다 (2026-08-18)
650
+ | 쓰지 마라 | 써라 |
651
+ |---|---|
652
+ | 인증서를 설치해 주셔야 합니다 | 인증서 설치 **필요** |
653
+ | 확인했습니다 | 확인**함** |
654
+ | 반영되었습니다 | 반영**됨** |
655
+ | 거부합니다 | 거부하는 **상황** |
656
+ | 제거하겠습니다 | 제거 **예정** |
657
+ | 적어 두었습니다 | 기재**함** |
249
658
 
250
- `publish_report` 는 **기본이 초안**이다. 만들어도 고객에게는 안 보인다.
659
+ 질문형(여백 주석의 Why, 활용법 라벨)은 `~하는가?` / `~인가?`.
660
+ `~합니까?` 아님.
251
661
 
252
- ```
253
- publish_report(… ) → 초안. 응답이 「아직 고객에게 보이지 않습니다」라고 말한다
254
- ↓ 눈으로 확인
255
- release_report(id) → 이때부터 고객 목록·열람에 뜬다
256
- ```
662
+ **「고객 언어로 바꾼다」는 규칙은 그대로다.**
663
+ 개조식은 문체이지 내용이 아니다 — 내부 용어를 고객 용어로 바꾸는 일(0단계)을 건너뛰는 핑계가 되지 않는다.
257
664
 
258
- 왜 이렇게 바뀌었나: 전에는 만드는 순간 고객이 볼 수 있었고 **물릴 방법이 없었다.**
259
- 도구 설명의 「초안으로 올려 두고」는 「빈 칸이 있어도 발행이 막히지 않는다」는 뜻이었는데
260
- 읽는 쪽은 「고객에게 아직 안 보인다」로 읽는다 — 그 둘은 결과가 정반대다.
665
+ ### 한 문장 한 줄
261
666
 
262
- | 도구 | 언제 |
263
- |---|---|
264
- | `publish_report(dry_run: true)` | 쓰기 전에 경고만 보고 싶을 때. 아무것도 저장하지 않는다 |
265
- | `publish_report(publish: true)` | 확인이 끝났고 바로 열어도 될 때 |
266
- | `release_report(id)` | 초안을 고객에게 열 때 |
267
- | `unpublish_report(id, reason)` | 잘못 낸 것을 물릴 때. **지우지 않고 감춘다** — 이력은 남는다 |
268
- | `update_report(dry_run: true)` | 이미 나간 링크를 덮기 전에 한 번 볼 때 |
667
+ **렌더러가 한다.**
668
+ 원고는 문장을 이어 붙여 써도 된다 — `sentbreak.py` 가 `boundary.py` 를 물고 와서
669
+ 문장마다 `<span class="sn">` 을 새기고, 이어지는 줄만 내어쓴다.
670
+ 낱말은 하나도 안 바뀐다.
269
671
 
270
- 경고(`warnings`)는 이제 응답에 실린다. 그전에는 서버 로그로만 흘러 **에이전트가 볼 수
271
- 없었고**, 그래서 「경고 없음」으로 읽혔다.
672
+ 원고에서 할 일은 하나다: **문장을 짧게 끊어라.**
673
+ 글줄 폭을 넘으면 접히고, 좁은 여백 주석 단에서는 절반 넘게 접힌다.
674
+ 폭의 실측값은 `gazette-report` 스킬에 있다.
675
+
676
+ ---
677
+
678
+ ## 15. 포털이 문서 **둘레**에 더해 주는 것 (저작 대상 아님)
679
+
680
+ 검토 회신(댓글) · 핵심 요약 · 진척률 바 · 상단 바는 포털 셸이 문서 밖에 놓는다.
681
+ 문서 안에 「댓글로 알려 주세요」를 쓰거나 요약을 흉내 내면 같은 것이 두 번 있게 된다.
682
+
683
+ 읽기 진행률 바도 문서에 안 싣는다 — 셸이 이미 놓고, 채울 스크립트도 없다.
684
+
685
+ ---
272
686
 
687
+ ## 16. 발행 전 자기 점검 · 운영 제약
688
+
689
+ 1. `dry_run: true` 로 먼저 태운다.
690
+ **⚠ 가 하나라도 있으면 그것부터 고친다.**
691
+ 2. `get_report(outline: true)` 로 가구 9개가 다 섰는지 본다(13.2).
692
+ 3. 돌아온 열람 주소를 **실제로 연다** — 절 누락 · 여백 주석 · 표 마감 · 인쇄 미리보기.
693
+ 자동 검사가 통과해도 눈으로 연다.
694
+ 안 보이는 배지 · 빈 목록 항목 · 낱말 중간 줄바꿈은 눈으로만 잡힌다.
695
+ 4. 톤: `습니다|십시오|합니다|입니다|됩니다|주세요` 가 본문에 **0건**인가.
696
+ 5. 확인 못 한 범위를 적었는가.
697
+
698
+ 조판 검사기 8종(`kcheck.mjs` · `read.mjs` · `tone.mjs` · `fontchk.mjs` · `flexcheck.mjs` ·
699
+ `cssaudit.sh` · `pdfcheck.sh` · `contentdiff.sh`)의 실행법은 `gazette-report` 스킬에 있다.
700
+ 여기 두 벌로 두지 않는다.
701
+
702
+ ### 렌더러를 고칠 때만
703
+
704
+ - 운영 python3 는 **3.6.8** 이다.
705
+ f-string 의 중괄호 식 안에 역슬래시를 넣으면 SyntaxError — 로컬이 3.12+ 면 조용히 통과하고 운영에서만 터진다.
706
+ - `gazette.css` 는 **추출물이다.**
707
+ 손으로 고치지 마라 — `gazette-report` 스킬의 `extract.sh` 로 다시 뽑아 덮는다.
708
+ - 문서마다 달라지는 것(장 색 · PART 색 · 러닝 푸터 · 판형)은 `gazette.py` 의 `tail_css()` 가 낸다.
709
+ - 렌더러의 원본은 레포 하나다(`private/builder/`).
710
+ 스킬에 사본을 두지 마라 — `render.py` 가 `FORKABLE` 목록으로 막고 있다.