genetics-mcp 2.24.0 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NATIVE-CODEX-SESSION.md +182 -0
- package/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/aarch64-unknown-linux-musl/gcell +0 -0
- package/bin/manifest.json +5 -5
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/codex-session-cli.mjs +82 -0
- package/codex-session.mjs +353 -0
- package/genetics.mjs +399 -61
- package/package.json +5 -2
- package/skills/gazette-report/scripts/tone.mjs +15 -6
- package/skills/gazette-report/scripts/tone.test.mjs +39 -0
- package/skills/genetics-report/SKILL.md +37 -22
- package/skills/genetics-report/assets/mcp-call.mjs +61 -10
- package/skills/genetics-report/assets/patch.py +237 -22
- package/skills/genetics-report/assets/render.py +49 -1
- package/skills/genetics-report/references/content-pipeline.md +3 -4
- package/skills/genetics-report/references/edition-review.workflow.js +55 -13
- package/skills/genetics-report/references/font-embedding.md +1 -1
- package/skills/genetics-report/references/lim-portal-format.md +34 -17
- package/skills/genetics-report/references/process.md +9 -7
- package/skills/genetics-report/references/reader-review.workflow.js +13 -5
- package/skills/genetics-report/references/visual-first.md +6 -6
- package/skills/gazette-report/scripts/package-lock.json +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genetics-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.26.0",
|
|
4
4
|
"description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
|
|
5
5
|
"author": "주식회사 LIM (LIM Inc.)",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"genetics.mjs",
|
|
13
|
+
"codex-session.mjs",
|
|
14
|
+
"codex-session-cli.mjs",
|
|
15
|
+
"NATIVE-CODEX-SESSION.md",
|
|
13
16
|
"LICENSE",
|
|
14
17
|
"skills",
|
|
15
18
|
"bin"
|
|
@@ -18,7 +21,7 @@
|
|
|
18
21
|
"node": ">=18"
|
|
19
22
|
},
|
|
20
23
|
"scripts": {
|
|
21
|
-
"test": "node smoke.mjs && node append.test.mjs && node task-results-stdio.test.mjs && node notify.test.mjs && node schedule.test.mjs && node windows.test.mjs && bash ../cell/tests/cellcycle.sh",
|
|
24
|
+
"test": "node smoke.mjs && node append.test.mjs && node codex-session.test.mjs && node task-results-stdio.test.mjs && node notify.test.mjs && node schedule.test.mjs && node windows.test.mjs && bash ../cell/tests/native-codex-mail.sh && bash ../cell/tests/cellcycle.sh && python3 ../tests/report-patch.test.py",
|
|
22
25
|
"test:task-results": "node task-results.test.mjs && node task-results-stdio.test.mjs",
|
|
23
26
|
"prepack": "find skills -name __pycache__ -type d -prune -exec rm -rf {} + 2>/dev/null; bash ../cell/build-all.sh",
|
|
24
27
|
"prepublishOnly": "npm test",
|
|
@@ -53,15 +53,24 @@ export function analyze(raw0) {
|
|
|
53
53
|
// 떨어지지 않으려고 「회신 주시면 됨」 같은 하대로 돌아간다 — 2026-09-03 에 그렇게 반려됐다.
|
|
54
54
|
// ⑵ 「」 안은 원문 인용이다(SKILL 11 — 인용은 원문 문구 그대로). 앱 문구 「다시 입력해 주세요」를 인용한
|
|
55
55
|
// 표가 경어체로 잡히면 인용을 비틀게 된다. 요청 자리 밖(문단·표·요약)의 우리 문장은 그대로 센다.
|
|
56
|
-
// 2026-09-26 — ⑴ 과 같은 까닭으로 나머지 요청 자리도 뺀다: 머리 강조문(`aside.urgent` = cta), PART I 간지(`section.pt
|
|
56
|
+
// 2026-09-26 — ⑴ 과 같은 까닭으로 나머지 요청 자리도 뺀다: 머리 강조문(`aside.urgent` = cta), PART I 간지(첫 `section.pt`),
|
|
57
57
|
// 판권지 회신 안내(`p.colophon__note`). 요청 자리는 사실 문장까지 하십시오체다(genetics-report SKILL 5·5a) — 여기서 세면
|
|
58
58
|
// 본보기가 그 자리를 개조식으로 남겨 두게 되고, 에이전트는 그 본보기를 따른다(2026-09-26 A/B 에서 판단 항목 10/14 문장이 명사형).
|
|
59
|
-
// PART I 간지는 그 PART 가 요청·결정 PART 일 때(PART
|
|
59
|
+
// PART I 간지는 그 PART 가 요청·결정 PART 일 때(PART I 의 절 안에 요청 절 `ol.asks` 가 있을 때)만 요청 자리다 — 태그를 지우기 전에 뺀다.
|
|
60
60
|
// 배우는 문서·정황 공유 문서의 PART I(원리·경과)은 요청 자리가 아니므로 센다.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
// PART I 은 **문서 순서로** 가린다(2026-09-29, 서버 report_tone.php 와 같은 자) — 렌더러는 PART 마다 「간지 → 그 PART 의
|
|
62
|
+
// 절」 순서로 세우므로(gazette.py Doc.build()) 첫 간지부터 다음 간지 전까지가 PART I 이다. no 로 가리면 no 가 「1」이
|
|
63
|
+
// 아닌 문서(「I」)를 놓치고, 두 PART 가 같은 no 를 가지면(렌더러는 뒤 PART 가 절을 가져간다) 엉뚱한 간지를 뺀다.
|
|
64
|
+
// 단 첫 PART 에 제목도 절도 없으면 렌더러가 그 간지를 안 세운다 — 그때 첫 간지는 PART II 의 것이다. 차례는 PART 를
|
|
65
|
+
// 빠짐없이 순서대로 싣으므로(`li.toc__part`) 첫 간지의 no 가 차례 첫 PART 의 no 와 같을 때만 PART I 로 본다(차례가 없는 조각은 자리로).
|
|
66
|
+
const pts = [...raw.matchAll(/<section class="pt"[^>]*data-part-no="([^"]*)"[^>]*>/g)];
|
|
67
|
+
const toc1 = (raw.match(/<li class="toc__part" data-part-no="([^"]*)"/) || [])[1];
|
|
68
|
+
let rawReq = raw;
|
|
69
|
+
if (pts.length && (toc1 === undefined || pts[0][1] === toc1)
|
|
70
|
+
&& /class="asks"/.test(raw.slice(pts[0].index, pts.length > 1 ? pts[1].index : raw.length))) {
|
|
71
|
+
const end = raw.indexOf('</section>', pts[0].index);
|
|
72
|
+
if (end > 0) rawReq = raw.slice(0, pts[0].index) + ' ' + raw.slice(end + '</section>'.length);
|
|
73
|
+
}
|
|
65
74
|
const politeTxt = rawReq
|
|
66
75
|
.replace(/<p class="ask__body"[^>]*>[\s\S]*?<\/p>/g, ' ')
|
|
67
76
|
.replace(/<aside class="urgent"[^>]*>[\s\S]*?<\/aside>/g, ' ')
|
|
@@ -163,6 +163,45 @@ test('경어체 — 요청 PART 의 간지·강조문·판권지 회신 안내
|
|
|
163
163
|
assert.equal(pos(analyze(learn).polite.length), 1);
|
|
164
164
|
});
|
|
165
165
|
|
|
166
|
+
test('경어체 — PART I 은 문서 순서로 첫 간지다, no 가 「1」이 아니어도(2026-09-29 · 서버 report_tone.php 와 같은 자)', () => {
|
|
167
|
+
// no 가 「I」인 요청 PART — 전에는 data-part-no="1" 만 보아 이 간지를 요청 자리로 못 알아보고 셌다.
|
|
168
|
+
// 요청 PART 안이어도 여백 주석은 기록 자리다 — 간지만 빼고 절까지 들어내면(뮤테이션) 여기서 0 이 되어 죽는다.
|
|
169
|
+
const reqI = '<section class="pt" id="pt-I" data-part-no="I"><p class="pt__lede">확인을 부탁드리는 사항입니다.</p></section>'
|
|
170
|
+
+ '<section class="part" id="sec-1" data-chapter="01" data-part-no="I">'
|
|
171
|
+
+ '<aside class="marg"><p class="marg__t">10월 2일까지 회신 부탁드립니다.</p></aside>'
|
|
172
|
+
+ '<ol class="asks"><li class="ask"><p class="ask__body">회신 부탁드립니다.</p></li></ol></section>'
|
|
173
|
+
+ '<section class="pt" id="pt-II" data-part-no="II"><p class="pt__lede">경과를 적음.</p></section>'
|
|
174
|
+
+ '<section class="part" id="sec-2" data-chapter="02" data-part-no="II"><p>경과 하나를 다룸.</p></section>';
|
|
175
|
+
const r = analyze(reqI);
|
|
176
|
+
assert.equal(pos(r.polite.length), 1, r.polite.join(' | '));
|
|
177
|
+
assert.equal(r.polite[0].includes('10월 2일까지'), true, r.polite.join(' | '));
|
|
178
|
+
// 대조군 — 요청 절이 둘째 PART 에 있으면 첫 간지는 기록 자리다 → 센다. 뮤테이션: 요청 목록이 어느 PART 에 있든 첫 간지를 빼면 여기서 죽는다.
|
|
179
|
+
// 요청 절이 둘째 PART 에 있으면 첫 간지도, 요청 PART 인 둘째 간지도 센다 — 요청 자리는 PART I 간지뿐이다.
|
|
180
|
+
// 뮤테이션: 「요청 목록 앞의 간지」를 빼면 둘째 간지가 빠져 1 이 되어 죽는다.
|
|
181
|
+
const askInII = '<section class="pt" id="pt-I" data-part-no="I"><p class="pt__lede">원리를 먼저 설명합니다.</p></section>'
|
|
182
|
+
+ '<section class="part" id="sec-1" data-chapter="01" data-part-no="I"><p>원리 하나를 다룸.</p></section>'
|
|
183
|
+
+ '<section class="pt" id="pt-II" data-part-no="II"><p class="pt__lede">아래 항목에 회신 부탁드립니다.</p></section>'
|
|
184
|
+
+ '<section class="part" id="sec-2" data-chapter="02" data-part-no="II">'
|
|
185
|
+
+ '<ol class="asks"><li class="ask"><p class="ask__body">회신 부탁드립니다.</p></li></ol></section>';
|
|
186
|
+
assert.equal(pos(analyze(askInII).polite.length), 2);
|
|
187
|
+
// 첫 PART 에 제목도 절도 없으면 렌더러가 그 간지를 안 세운다 — 첫 간지는 PART II 의 것이라 요청 자리가 아니다 → 센다.
|
|
188
|
+
// 차례(`li.toc__part`)는 PART 를 빠짐없이 싣는다. 뮤테이션: 차례 대조 없이 첫 간지를 빼면 0 이 되어 죽는다.
|
|
189
|
+
const noPt1 = '<ol class="toc"><li class="toc__part" data-part-no="1"></li><li class="toc__part" data-part-no="2"></li></ol>'
|
|
190
|
+
+ '<section class="pt" id="pt-2" data-part-no="2"><p class="pt__lede">아래 항목에 회신 부탁드립니다.</p></section>'
|
|
191
|
+
+ '<section class="part" id="sec-1" data-chapter="01" data-part-no="2">'
|
|
192
|
+
+ '<ol class="asks"><li class="ask"><p class="ask__body">회신 부탁드립니다.</p></li></ol></section>';
|
|
193
|
+
assert.equal(pos(analyze(noPt1).polite.length), 1);
|
|
194
|
+
// 두 PART 가 같은 no 를 가지면 렌더러는 뒤 PART 가 절을 가져간다 — 요청 목록은 셋째 간지 밑에 서고 첫 간지는 기록 자리다 → 센다.
|
|
195
|
+
// 뮤테이션: no 로 가리면(data-part-no="1" 이 둘) 첫 간지를 빼서 여기서 죽는다.
|
|
196
|
+
const dupNo = '<section class="pt" id="pt-a" data-part-no="1"><p class="pt__lede">원리를 먼저 설명합니다.</p></section>'
|
|
197
|
+
+ '<section class="pt" id="pt-b" data-part-no="2"><p class="pt__lede">경과를 적음.</p></section>'
|
|
198
|
+
+ '<section class="part" id="sec-1" data-chapter="01" data-part-no="2"><p>경과 하나를 다룸.</p></section>'
|
|
199
|
+
+ '<section class="pt" id="pt-c" data-part-no="1"><p class="pt__lede">요청 두 항목.</p></section>'
|
|
200
|
+
+ '<section class="part" id="sec-2" data-chapter="02" data-part-no="1">'
|
|
201
|
+
+ '<ol class="asks"><li class="ask"><p class="ask__body">회신 부탁드립니다.</p></li></ol></section>';
|
|
202
|
+
assert.equal(pos(analyze(dupNo).polite.length), 1);
|
|
203
|
+
});
|
|
204
|
+
|
|
166
205
|
test('양성 표본 수 — 0표본 통과 방지', () => {
|
|
167
206
|
console.log(` 양성 표본 ${positives}건 (0 이면 이 시험은 아무것도 안 잰 것)`);
|
|
168
207
|
assert.equal(positives >= 28, true, String(positives));
|
|
@@ -48,17 +48,17 @@ version: "4.0.0"
|
|
|
48
48
|
| 3 등급 | `grades` · 건별 `grade` | — | 4단 이하 · 최약 등급이 기본 | 사람 | 등급 이름·개수 |
|
|
49
49
|
| 4~6 원고 | `report.json` · 코드 작업을 녹이면 원고 밖 상태 표(반드시 지키는 것 3) | — | 요약 근거 · 요지 칸 수(4~6단계) · 절마다 `marg` | 서버 요약 근거 게이트(거절) · 사람 | 공통 원인이 실재하는가 |
|
|
50
50
|
| 6 도판 굽기 | 도판 HTML → PNG · `plates.json` | `node assets/figure.mjs --batch plates.json --report report.json` · 두 칸 비교는 `shot.mjs --after` → `pair.mjs` | exit 0(✗0 · 합계 2,400,000B 안) | `figure.mjs` R0~R10·R13 · `pair.mjs` · 칩 문구의 뜻은 사람 눈 | — |
|
|
51
|
-
| 7 로컬 렌더 | 단일 HTML | `python3 assets/render.py report.json`(소속처 문서면 `--tenant <slug>` — 원고에 `tenant` 가 있으면 생략) | ⚠0 · `forbid` 적중 0 | `gazette.py` `text_scan()`·`brief_checks()`·`tally_scan()` · `build.py guard()` | — |
|
|
51
|
+
| 7 로컬 렌더 | 단일 HTML | `python3 assets/render.py report.json`(소속처 문서면 `--tenant <slug>` — 원고에 `tenant` 가 있으면 생략) | ⚠0 · `forbid` 적중 0 · 단일 HTML 로 전달할 판이면 「보관 그림 N장」 줄 0(그 HTML 은 그림 자리가 상자인 검토본 — 원고에 `out` 이 있으면 그 줄이 ⚠ 로 선다) | `gazette.py` `text_scan()`·`brief_checks()`·`tally_scan()` · `build.py guard()` · `render.py kept_stand_ins()` | — |
|
|
52
52
|
| 7 dry_run | 경고 목록 | `publish_report`/`update_report` 에 `dry_run: true` | 작성자가 고칠 수 있는 ⚠0 — 못 없애는 경고는 까닭을 핸드오프에 | `report_publish.php report_build_checked()`(렌더러·입고·그림·문체 · 금칙어는 `report_publish.php report_forbid_warnings()`) · `update_report` 는 `comments.php comment_anchor_forecast()`(회신 핀 자리) · 경고 기준선 대조는 사람 | — |
|
|
53
|
-
| 7 독자 검토 | 렌더본 텍스트(원고 폴더 밖 빈 폴더) · 워크플로 결과(`merged`·`questions`·`dropped`·`jumps`·`names` — 판 갱신 검토는 `kept`·`refuted`·`weak`·`unverified`) | `python3 assets/render.py report.json --text <원고 폴더 밖 빈 폴더>/본문.txt` → `references/reader-review.workflow.js`(`args.text` 필수, 0.5단계 7행) · 구조를 안 바꾼 낱말 손질 판은 `references/edition-review.workflow.js`(역시 `args.text`) | 사실 틀림 반영 · 선형 읽기 뜀(`jumps`) 가운데 심각도 3 이상 0건 · 만든 이름(`names`) 0건 — 못 없앤 것은 까닭을 핸드오프에 · 결과 머리 「검토 미완」 없음(있으면 다시 돌림) · 「⚠ 도판 N장 미열람」 없음 · 반영 뒤 로컬 렌더·dry_run 다시 | 워크플로 관점 + JS 거름 · `questions`·`dropped`·`refuted`·`rx`(처방 다시) 는 사람 | `questions[]` |
|
|
53
|
+
| 7 독자 검토 | 렌더본 텍스트(원고 폴더 밖 빈 폴더) · 워크플로 결과(`merged`·`questions`·`dropped`·`jumps`·`names` — 판 갱신 검토는 `kept`·`refuted`·`weak`·`unverified`) | `python3 assets/render.py report.json --text <원고 폴더 밖 빈 폴더>/본문.txt` → `references/reader-review.workflow.js`(`args.text` 필수, 0.5단계 7행) · 구조를 안 바꾼 낱말 손질 판은 `references/edition-review.workflow.js`(역시 `args.text`) | 사실 틀림 반영 · 선형 읽기 뜀(`jumps`) 가운데 심각도 3 이상 0건 · 만든 이름(`names`) 0건 — 못 없앤 것은 까닭을 핸드오프에 · 결과 머리 「검토 미완」 없음(있으면 다시 돌림) · 「⚠ 도판 N장 미열람」 없음 — 다만 미열람·「서버 보관」 줄에 찍힌 id 가 모두 원고에서 `data` 가 「(생략 — NB)」 표식인 장이면, 사람이 열람 주소에서 그 장을 열어 보고 id 를 핸드오프에 적으면 통과다(그 밖의 미열람은 다시 돌린다) · 반영 뒤 로컬 렌더·dry_run 다시 | 워크플로 관점 + JS 거름 · `questions`·`dropped`·`refuted`·`rx`(처방 다시) 는 사람 | `questions[]` |
|
|
54
54
|
| 7 조판 검사 | 검사기 출력 | `gazette-report` 검사기 8종(단일 HTML 전달일 때) | 각 0건 · **표본 수를 함께 본다**(0표본 통과는 통과가 아니다) | 검사기 — 실행법은 `gazette-report` 스킬 | — |
|
|
55
55
|
| 7 사람 눈 | — | 인쇄 미리보기까지 연다 | `references/process.md` 「7단계 — 검수」 사람의 눈 표 | 사람 | — |
|
|
56
|
-
| 8 초안 발행·수정 | 초안 링크 | 새 문서 `publish_report` · 고치면 8단계 「수정 절차」 | 같은 링크 · 사람이 고친 초안을 모르고 덮지 않음 |
|
|
57
|
-
| 9 발행 뒤 대조 | 블록 목록 | `get_report(id, outline: true)` | `aside.marg` 수 = 절 수 · 옛 서식 블록 0 · 빈 블록 0 | 사람(블록 목록을 읽음) — 대조 기계 없음 | — |
|
|
56
|
+
| 8 초안 발행·수정 | 초안 링크 · 고친 판이면 받은 판 JSON 과 `edits.json`(작업 폴더, 발행 안 함) | 새 문서 `publish_report` · 고치면 8단계 「수정 절차」 | 같은 링크 · `patch.py --freeze` 가 `edits.json` 을 씀(받은 판 사본에 두 번째 바꿈 0) · 교체와 그 앞 dry_run 에 받은 판의 `body_rev`(`get_report` 머리줄 값)를 실음 · 사람이 고친 초안을 모르고 덮지 않음 | `patch.py`(옛 값이 다르면 멈춤 · 굳힐 때 멱등 검사) · `update_report` 의 `body_rev` 잠금(받은 뒤 본문 판이 바뀌었으면 서버 409) · 편집기에 저장만 된 사람 초안은 판을 안 올려 잠금에 안 걸림 — 목록의 「덮음」을 사람이 본다 | 사람이 고친 초안이면 덮어도 되는가 |
|
|
57
|
+
| 9 발행 뒤 대조 | 블록 목록 · 고친 판이면 되읽은 JSON(`body_rev` 를 파일 이름에) | `get_report(id, outline: true)` · `get_report(id)` | `aside.marg` 수 = 절 수 · 옛 서식 블록 0 · 빈 블록 0 | 사람(블록 목록을 읽음) — 대조 기계 없음 | — |
|
|
58
58
|
| 9 열람 확인 | 캡처 | 열람 주소를 연다 | 칩·굵게·링크가 원문 글자가 아니라 서식으로 섬 · 도판 쌍·판단 절이 섬 | 사람 눈 — 못 열면 까닭을 핸드오프에 | — |
|
|
59
|
-
| 9a 첨삭 메모 반영 | 반영된 판 · 닫힌 메모 · 까닭을 단 답글 | `list_reports` → `list_notes` →
|
|
60
|
-
| 9b 검토 회신 반영 | 회신마다 「어느 물음의 답인가」 대조 · 반영된 판 · 반영 표시 | **개정 전에** `list_notes(id)`(검토 회신이 본문과 함께 실린다) →
|
|
61
|
-
| 10 공개 직전 날짜 | `
|
|
59
|
+
| 9a 첨삭 메모 반영 | 반영된 판 · 닫힌 메모 · 까닭을 단 답글 | `list_reports` → `list_notes` → 8단계 「수정 절차」 → `resolve_note`·`reply_note`(9a단계) | 미반영 메모 0 — 남은 것은 전부 답글로 까닭이 달림 | 사람이 메모를 닮 · 서버는 미반영 수만 셈(`open_notes`, 우리 팀에게만) | 사람이 「메모 해놨다」고 할 때만 돈다 — 무엇을 고칠지 되묻지 않음 |
|
|
60
|
+
| 9b 검토 회신 반영 | 회신마다 「어느 물음의 답인가」 대조 · 반영된 판 · 반영 표시 | **개정 전에** `list_notes(id)`(검토 회신이 본문과 함께 실린다) → 8단계 「수정 절차」로 반영 → `resolve_note(id, reply, rev)`(9b단계) | 열린 회신 0 — 남은 것은 사람에게 넘긴 까닭이 있음 · 원고에 「회신 없음」이라고 쓴 자리가 회신과 어긋나지 않음 | 서버는 열린 회신 수(`open_replies`)를 목록·되읽기·교체 응답에 싣고, 열린 회신의 문장이 바뀌면 교체 경고로 말함 · 본문 읽기와 대조는 사람·에이전트 | 고객에게 보이는 답글 문안(에이전트는 달지 않는다) |
|
|
61
|
+
| 10 공개 직전 날짜 | 받은 판 JSON 과 `edits.json`(날짜 세 칸) | 8단계 「수정 절차」로 한 번 — 받은 판에서 날짜 세 칸만 고쳐 `body_rev` 를 실어 교체(날짜만 고치는 이 판은 독자 검토·판 갱신 검토 없이 로컬 렌더·dry_run 만) | 작성일 세 자리가 한 값이고 공개하는 날(3c) | `gazette.py` `date_scan()` ⚠(세 자리가 다르면) · 공개하는 날인지는 사람 | 공개일 |
|
|
62
62
|
| 10 공개 | 공개된 링크 | `release_report(id)` | 사용자 승인 | 사람(승인 행위) | **반드시** 공개 승인 |
|
|
63
63
|
|
|
64
64
|
## 렌더러는 레포에 하나뿐이다
|
|
@@ -103,14 +103,14 @@ version: "4.0.0"
|
|
|
103
103
|
`~하여 주시기 바랍니다 / ~부탁드립니다 / ~하겠습니다` 로 맺는다. 자세한 것은 아래 5·5a.
|
|
104
104
|
경어체 `습니다|십시오|합니다|입니다|됩니다|주세요` 0건은 **요청 자리 밖에만** 거는 기준이다.
|
|
105
105
|
- ⚠ **톤 게이트는 경고이지 거절이 아니다.** `report_tone.php report_tone_warnings()` 가 `dry_run` 경고로 셀 뿐 발행을 막지 않는다.
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
서버와 로컬 `tone.mjs` 는 같은 요청 자리 넷(판단 항목 본문·`cta`·판권지 회신 안내·요청 PART 의 PART I 간지 도입문·물음)과 「」 인용을 빼고 센다(2026-09-29 — 요청 절 여백 주석·PART 제목은 센다).
|
|
107
|
+
읽는 법(`guide`)의 요청 한 줄만 자리로 못 가려 두 경로 모두 센다 — 그 한 줄의 경어체 경고는 **의도이므로 고치지 않는다.** 간지에서 나면 요청 PART I 이 아닌 간지이니 개조식으로 고친다.
|
|
108
108
|
이 경고를 하드 규칙으로 오인해 종결을 비튼 것이 **거듭된 반려의 원인**이다(경위: `references/history.md` §5).
|
|
109
109
|
- `tenant` 는 키가 가리키는 고객사와 일치해야 한다(오발행 방지 가드). 값은 소속처 slug 우선·이름 폴백 — 8단계의 `slug` 는 **보고서** slug 라 다른 것이다.
|
|
110
110
|
- `sections[].kind` 는 `summary|status|cases|asks|note` 다섯 가지뿐 — 그 밖의 값은 절이 통째로 사라지므로 서버가 거절한다.
|
|
111
111
|
- `cases[].grade` 는 `grades` 에 정의된 이름이어야 한다.
|
|
112
112
|
- **`update_report` 는 같은 주소를 덮어쓴다** — 이미 고객에게 나간 링크가 새 내용을 가리키므로, 발행 후 수정은 재발행이 아니라 update 로 한다. 기존 문서는 `list_reports`/`get_report` 로 받아 고쳐 되돌려 보낸다.
|
|
113
|
-
- **그림 계약**: `update_report` 에서 `assets` 를 **빼고** 보내면 서버가 저장된 그림을 그대로
|
|
113
|
+
- **그림 계약**: `update_report` 에서 `assets` 를 **빼고** 보내면 서버가 저장된 그림을 그대로 유지한다 — 캡션·출처 줄·alt 같은 도판 글 칸도 저장값 그대로다. 글 칸을 고쳤으면 `assets` 를 `get_report` 표식째 목록으로 보낸다(서버가 id 로 바이트를 잇고 글 칸은 보낸 값을 쓴다). 전부 지우려면 `assets: []` 를 명시하고 `sections[].plates`·`chapters[].asset` 에서도 그 id 를 빼야 한다 — 가리키는 곳이 없는 id 는 422. 한 장만 바꾸려면 그 id 에 새 `source`/`file`, 나머지는 `get_report` 가 준 표식 그대로.
|
|
114
114
|
- 초안 상태로 올려 두고 다듬어도 발행은 막히지 않는다. 단 초안 `update_report` 도 **본문 판(`body_rev`)** 을 올리고 사용자가 그 판을 읽으므로 **독자 검토를 통과한 본문 판만 올린다** — 다듬는 중간 판은 로컬 렌더·dry_run 으로 본다. 사용자가 「바로 반영」을 지시하면 먼저 올리고 검토는 곧바로 뒤따라 돌려 다음 판에 반영한다(2026-09-15 사용자 결정).
|
|
115
115
|
**발행 이력 행(`history`)은 다른 것이다** — 공개 전 수정은 이력 행을 만들지 않는다(3c).
|
|
116
116
|
- 내용 설계 규칙(아래 0~3단계, 고객 언어 치환, 카피 게이트)은 **전달 경로와 무관하게 동일하게** 적용된다 — 서버가 렌더한다고 해서 원고 품질 단계를 건너뛰지 않는다.
|
|
@@ -323,10 +323,23 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
|
|
|
323
323
|
**기본은 초안이다.** 도구가 `publish` 를 참으로 주지 않는 한 고객에게 안 보인다.
|
|
324
324
|
공개 직전 날짜를 맞춘 뒤(3c) `release_report(id)` 로 연다 — 그것이 사람의 승인 행위다(체크리스트 10단계).
|
|
325
325
|
|
|
326
|
-
**수정 절차** —
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
326
|
+
**수정 절차** — 올라 있는 판에서 출발한다. **같은 링크가 새 내용을 가리킨다.**
|
|
327
|
+
|
|
328
|
+
1. `list_reports` 줄에 「덮음: …」(사람의 편집 초안·사람이 발행한 본문)이 있으면 덮기 전에 묻는다 — 문서 번호를 들었어도 이 목록으로 본다(`get_report` 머리는 사람의 편집 초안을 안 보인다). 받은 판은
|
|
329
|
+
`node assets/mcp-call.mjs --no-mail --json get_report '{"id":<id>,"tenant":"<slug>"}' <파일>` 로 작업 폴더에 받는다 — 파일에는 JSON 한 덩어리만 쓰고 머리·꼬리 줄은 화면에 보인다(`patch.py`·`render.py` 는 머리줄 붙은 파일을 못 읽는다). 파일 이름에 화면에 보인 `body_rev` 를 넣는다 — 그 머리줄 값이 「받은 판」이고, 파일 안 JSON 에는 판이 없다(옛 저장본에 `body_rev` 칸이 메아리로 남은 문서가 있어 되읽기가 떼고 준다). 지난 교체 뒤 되읽어 둔 판이 있어도 다시 받는다 — `body_rev` 는 내용이 같아도 오르니, 그 사이 누가 고쳤는지는 둔 판과 내용으로 견준다.
|
|
330
|
+
2. 고칠 자리를 draft 에 적는다 — `set` 은 `new` 만, `replace` 는 옛 조각·새 조각. `python3 assets/patch.py <1의 파일> draft.json --freeze edits.json` 이 `set` 의 `old` 를 1의 파일에서 채우고, 그 사본에 두 번 돌려 멱등일 때만 `edits.json` 을 쓴다. 새로 받은 판으로 굳히지 않는다.
|
|
331
|
+
3. 새 판은 1의 파일을 복사해 `python3 assets/patch.py <새 판> edits.json` 으로 만든다 — `patch.py` 는 준 파일을 제자리에서 고친다. 새 판 이름에 예상 판번호를 쓰지 않는다(`rev` 번호는 서버에서 받은 판에만).
|
|
332
|
+
4. `python3 assets/render.py <새 판> --text <원고 폴더 밖 빈 폴더>/본문.txt` 로 새 판을 그대로 굽는다(렌더 사본을 따로 짓지 않는다) — 보관 그림은 자리 상자로 서고 그 HTML 은 검토본이다. ⚠0 을 본다. 바꾸거나 더한 그림처럼 `file`·`source` 로 준 장은 로컬에서 「그림 바이트가 없습니다」 ⚠ 로 빠지고 그 캡션도 본문.txt 에서 빠진다 — 그 줄은 빼고 읽는다.
|
|
333
|
+
5. 교체 인자는 새 판으로 만든다. `patch.py` 가 「도판 칸을 고친 편집」을 알리지 않았으면 `assets` 를 뺀다(서버가 저장된 도판을 그대로 잇는다). 알렸으면(캡션·출처 줄·alt 를 고쳤거나 그림을 바꿈) `assets` 를 새 판 그대로 싣는다 —
|
|
334
|
+
표식은 표식째 두면 서버가 id 로 바이트를 잇고 글 칸은 보낸 값을 쓴다. 그림을 바꿀 장은 `set assets[id=…]` 에 객체를 통째로(`file` 포함) 준다(`file` 칸만 `set` 하면 없는 칸이라 멈춘다). 늘 싣지 않는 까닭: 목록으로 보내면 아직 그리는 중인 그림 자리가 처음부터 다시 걸린다.
|
|
335
|
+
→ `update_report(dry_run: true, body_rev: <받은 판>)` — 받은 뒤 본문 판이 바뀌었으면 여기서 409 다(6단계의 409 절차로). 「이 서버는 받은 판 잠금을 모릅니다」 줄이 나오면 교체가 판을 견주지 않는다는 뜻이니 교체 전에 사용자에게 알린다.
|
|
336
|
+
→ 판 갱신 검토(`edition-review.workflow.js` — `edits` 에 `edits.json`, `backup` 에 1의 파일, `file` 에 3의 새 판, `text` 에 4의 본문.txt, `patch` 에 2·3단계에서 쓴 그 `patch.py` 의 절대경로, 수치를 고친 판이면 `canon` 에 정본을 가리키는 한 줄 — 파일과 자리). 구조를 바꾼 판이면 독자 검토다.
|
|
337
|
+
6. 교체는 `update_report` 에 받은 판의 `body_rev` 를 실어 한다(1에서 머리줄에 보이고 파일 이름에 넣은 값 — 시험 실행이 알려 주는 「지금 판」이 아니다) — 받은 뒤 본문 판이 바뀌었으면(다른 교체 · 편집기 발행 · 그림이 앉아 다시 구운 것) 서버가 409 로 멈추고 아무것도 안 쓴다(2026-09-30 받은 판 잠금).
|
|
338
|
+
409 면 새로 받은 판을 1의 파일 자리에 두고(이름에 새 `body_rev`) `edits.json` 을 다시 적용해 3~5 를 다시 돈다(`backup` 도 그 판) — `set`·`replace` 자리는 남이 고쳤으면 멈추지만, 줄 op 는 남의 고침을 못 가린다(`drop_rows`·`remove_row` 는 라벨만 보고 남이 고친 행도 빼고, `append_row`·`insert_row` 는 남이 붙인 행 곁에 또 붙인다). 그래서 다시 적용하기 전에 옛 1의 파일과 새로 받은 판을 diff 해 그 표부터 본다.
|
|
339
|
+
교체되면 출력에 「본문 판 body_rev=N」(새 판)이 찍혔는지 본다 — 없으면 판을 견주지 않고 덮인 것이니(잠금을 모르는 옛 서버이거나, `body_rev` 인자를 모르는 옛 MCP 가 떠 있다) 되읽어 1의 파일과 견준다.
|
|
340
|
+
→ 9단계 두 행 → 되읽은 JSON 을 새 `body_rev` 로 둔다.
|
|
341
|
+
|
|
342
|
+
그림은 서버가 보관한다 — `assets` 를 빼고 보내면 그림도 도판 글 칸(캡션·출처 줄·alt)도 저장된 그대로, `assets: []` 면 삭제(plates 에서도 빼야 422 가 안 난다), 바꿀 장만 새 `source`/`file`(나머지는 표식 그대로).
|
|
330
343
|
|
|
331
344
|
### 9단계 — 발행 뒤 판정: 블록 목록으로 본다
|
|
332
345
|
|
|
@@ -349,10 +362,10 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
|
|
|
349
362
|
|
|
350
363
|
| 순서 | 도구 | 할 일 |
|
|
351
364
|
|---|---|---|
|
|
352
|
-
| 1 | `list_reports` | 줄 끝에 `첨삭 메모 N건 미반영` 이 붙은 문서를 찾는다. 문서 번호를
|
|
365
|
+
| 1 | `list_reports` | 줄 끝에 `첨삭 메모 N건 미반영` 이 붙은 문서를 찾는다. 문서 번호를 들었어도 그 줄의 「덮음: …」은 본다(사람의 편집 초안은 이 목록만 알린다) |
|
|
353
366
|
| 2 | `list_notes(id)` | 미반영 메모를 읽는다. `anchor.path`(예 `case:k7m2p9/fix`)가 **고칠 칸**, `anchor.quote_exact` 가 사람이 **본 글**이다. 답글은 사람이 덧붙인 설명이다 |
|
|
354
|
-
| 3 |
|
|
355
|
-
| 4 |
|
|
367
|
+
| 3 | 8단계 「수정 절차」 1~5 | 받은 판을 받아 두고, 메모마다 고칠 칸을 draft 에 적어 `--freeze` 로 굳혀 새 판을 만들고, 로컬 렌더 · dry_run(받은 판의 `body_rev` 를 실어) · 판 갱신 검토까지 돈다 |
|
|
368
|
+
| 4 | 8단계 「수정 절차」 6 | 메모를 모두 반영한 판을 **한 번에** `update_report(body_rev: …)` 로 보낸다. `uid` 는 받은 값 그대로 남는다(`patch.py` 는 고친 칸만 바꾼다) — 흘리면 메모가 자리를 잃는다. 초안은 초안 그대로 남는다 |
|
|
356
369
|
| 5 | `resolve_note(id, note, rev)` | **고친 뒤에만.** 반영이 실린 `body_rev` 를 함께 준다. 먼저 부르면 미반영 목록에서 빠져 다시 안 보인다 |
|
|
357
370
|
| 6 | `reply_note(id, note, body)` | 못 고치거나 다르게 본 메모는 **닫지 말고** 그 메모에 이유를 답글로 단다 |
|
|
358
371
|
| 7 | `remember_case`·`remember_decision`(메모가 반려일 때) | 반려를 팀 기억에 남길 때 「스킬 반영: `references/history.md` §n / 미반영(까닭)」 한 줄을 함께 적는다. 반려가 작업하며 지은 이름을 짚었으면 그 이름과 독자 화면의 원래 이름을 그 반려 결정(`remember_decision`)의 `rejected_names` 칸에 `{name, use}` 로 적는다(흔한 낱말이면 함께 설 낱말을 `near` 에) — 같은 소속처의 발행·모의 발행·로컬 렌더가 그 목록으로 센다. 확인은 `list_rejected_names`. 운영 반영 확인 전(2026-09-26) — 서버 배포 전에는 이 칸이 안 적힌다(응답이 「이 서버는 아직 그 칸을 모른다」고 말한다) |
|
|
@@ -367,7 +380,7 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
|
|
|
367
380
|
|---|---|---|
|
|
368
381
|
| 1 | `list_notes(id)` | 검토 회신이 **본문과 함께** 실린다. `[회신 N]` 줄의 자리(「판단 항 「…」」·건·절·도판)가 **어느 물음에 대한 말인지**, 그 아래가 누가(고객/우리) 무슨 말을 했는지다. 답글은 뿌리 밑에 붙는다 |
|
|
369
382
|
| 2 | (대조) | 회신마다 원고와 맞춰 본다 — 이미 답이 온 판단 항을 **열린 항으로 다시 묻고 있지 않은가**, 원고가 「회신에 언급이 없어 미결」이라고 쓴 자리가 회신과 어긋나지 않는가, 회신의 낱말과 정본(고객 문서)의 낱말이 갈리면 정본 낱말로 맞췄는가 |
|
|
370
|
-
| 3 | `
|
|
383
|
+
| 3 | 8단계 「수정 절차」 1~6 | 반영한다(받은 판 → draft · `--freeze` → 새 판 → 로컬 렌더 · dry_run(`body_rev` 를 실어) · 판 갱신 검토 → `update_report(body_rev: …)`). 답이 온 판단 항은 닫힌 항 표로 옮기고 출처에 회신 날짜를 적는다. 교체 경고의 「열린 검토 회신 …」 줄은 회신의 본문과 함께 온다 — 그 회신을 이 판이 반영한 것이면 그대로 발행한다 |
|
|
371
384
|
| 4 | `resolve_note(id, reply, rev)` | **반영한 뒤에만.** 표시해도 회신은 고객에게 그대로 보인다. 문서에 반영할 것이 없는 회신(인사·이미 답한 물음)도 읽었으면 표시한다 |
|
|
372
385
|
| 5 | `remember_case`·`remember_decision`(회신이 반려일 때) | 9a단계 7행과 같다 — 팀 기억에 「스킬 반영: `references/history.md` §n / 미반영(까닭)」 한 줄, 작업하며 지은 이름을 짚은 반려면 그 반려 결정의 `rejected_names` 칸에 그 이름과 원래 이름을 적는다 |
|
|
373
386
|
|
|
@@ -390,6 +403,8 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
390
403
|
눈은 "보이는 결함"을 잡는다. 실제로 자동 검수를 다 통과한 문서에서 눈으로만 잡힌 것들:
|
|
391
404
|
안 보이는 배지, 빈 목록 항목, 한국어 낱말 중간 줄바꿈.
|
|
392
405
|
|
|
406
|
+
**보관 그림 상자가 있는 HTML 은 전달본이 아니다** — `render.py` 가 「보관 그림 N장」을 말했으면 그 파일은 검토본이다. 전달본은 그림 바이트(`data:`)가 실린 원고로 굽는다.
|
|
407
|
+
|
|
393
408
|
파일 하나를 그대로 보낸다. 상대는 더블클릭만 하면 된다.
|
|
394
409
|
|
|
395
410
|
### 7단계 — 조판 검사: `gazette-report` 스킬의 8종
|
|
@@ -406,15 +421,15 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
406
421
|
|
|
407
422
|
| 파일 | 역할 |
|
|
408
423
|
|---|---|
|
|
409
|
-
| `assets/render.py` | `report.json` → 단일 HTML. **서버 등록부에서 서식을 읽어** 레포 `private/builder/build.py` 를 부른다 — 여기 사본을 두지 마라. `--text <출력.txt>` 는 렌더가 성공한 뒤 화면에 보이는 글만 화면 순서대로 텍스트로 뽑는다(그림 자리 「[그림]」 · 표 칸 「 \| 」 · `origin`·alt·스크립트 없음) — 독자 검토의 입력이고, 원고 폴더 안에 쓰면 ⚠ 가 뜬다. `--tenant <slug>`(없으면 원고의 `tenant`)는 그 소속처가 반려한 이름을 옆 `mcp-call.mjs` 로 MCP `list_rejected_names` 를 불러 받아 함께 센다 — 못 받으면 ⚠ 한 줄을 내고 반려 이름만 빼고 굽는다(`references/lim-portal-format.md` §10-1) |
|
|
410
|
-
| `assets/patch.py` | 원고 **부분 고치기(멱등)** — `python3 assets/patch.py report.json edits.json`. 회차마다 고치는 파이썬을 새로 짓지 말고 **데이터(edits.json)만** 남긴다. op
|
|
424
|
+
| `assets/render.py` | `report.json` → 단일 HTML. **서버 등록부에서 서식을 읽어** 레포 `private/builder/build.py` 를 부른다 — 여기 사본을 두지 마라. `--text <출력.txt>` 는 렌더가 성공한 뒤 화면에 보이는 글만 화면 순서대로 텍스트로 뽑는다(그림 자리 「[그림]」 · 표 칸 「 \| 」 · `origin`·alt·스크립트 없음) — 독자 검토의 입력이고, 원고 폴더 안에 쓰면 ⚠ 가 뜬다. `get_report` 로 받은 원고도 그대로 굽는다(받을 때는 `mcp-call.mjs --json` — 머리줄이 붙으면 못 읽는다) — `out` 이 없으면 원고 이름 옆 `.html` 로 쓰고, 도판(figure) 표식은 서버 기록 치수의 자리 상자로 그린다(`render.py kept_stand_ins()` — ⚠ 대신 「보관 그림 N장」 한 줄, 그 HTML 은 검토본). 로고 표식은 표지에서 고객사 이름 활자로 선다. 원고에 `out` 이 있으면(작성 원고 — 전달 경로) 그 한 줄은 ⚠ 로 선다. `--tenant <slug>`(없으면 원고의 `tenant`)는 그 소속처가 반려한 이름을 옆 `mcp-call.mjs` 로 MCP `list_rejected_names` 를 불러 받아 함께 센다 — 못 받으면 ⚠ 한 줄을 내고 반려 이름만 빼고 굽는다(`references/lim-portal-format.md` §10-1) |
|
|
425
|
+
| `assets/patch.py` | 원고 **부분 고치기(멱등)** — `python3 assets/patch.py report.json edits.json`. 회차마다 고치는 파이썬을 새로 짓지 말고 **데이터(edits.json)만** 남긴다. op 일곱: `set`·`replace`·`drop_rows`·`remove_row`·`append_row`·`insert_row`·`forbid`(칸과 뜻은 머리 주석 op 표). 표 줄은 번호 대신 첫 칸 글자로 가리킨다 — `rows[label=…]` 는 앞 op 가 줄을 끼우거나 빼도 같은 줄을 찾고, 그 글자인 줄이 하나가 아니면 멈춘다(2026-09-30 — 9/29 수정 판에서 통째로 바꾼 표 여섯을 줄 op 로 다시 써도 공개본과 같았다). 그 줄의 **첫 칸을 바꾸는** 편집은 `[label=…]` 로 적지 않고 번호 경로로 적는다 — 첫 실행 뒤 그 글자가 없어져 두 번째 실행이 줄을 못 찾으므로 돌리기 전에 멈춘다. 배열 원소는 `assets[id=…]` 처럼 `id` 로도 가리키고(끝자리여도 된다), 없는 칸은 만들지 않고 멈춘다. 두 번 돌려 「바꿈 0」이 계약이고, 값이 `old` 도 `new` 도 아니면 **멈춘다**(그 사이 누가 고쳤다는 뜻). `set` 의 `old` 는 손으로 옮기지 않는다 — draft 에 `new` 만 적고 `python3 assets/patch.py <받은 판> draft.json --freeze edits.json` 이 받은 판에서 채운다. 받은 판 사본에 두 번 돌려 첫째 건너뜀 0 · 둘째 바꿈 0 일 때만 OUT 을 쓰고, 줄을 끼우거나 뺀 표의 칸을 번호로 가리키는 `old` 없는 `set` 은 굳히지 않으며(`[label=…]` 로 가리킨다), 적용에 `old` 없는 `set` 이 오면 멈춘다. 편집이 도판 칸(`assets…`)을 고치면 「교체 인자에 assets 를 표식째 싣는다」를 알린다(`patch.py freeze()` — 2026-09-29 수정 판에서 긴 표 행의 `old` 를 스크래치 스크립트로 채웠던 자리). 회차 스크립트가 쌓여 고객사 문서 폴더가 플랫폼이 되던 것을 멈추려고 들였다(2026-09-22 — 한 폴더 2,563줄·다른 폴더 4,285줄 실측) |
|
|
411
426
|
| `assets/xlsx-plate.py` | 스프레드시트 칸을 **근거 도판**으로 — `python3 assets/xlsx-plate.py 파일.xlsx 시트 B40:E48 out.png --html wrap.html`. 원본은 읽기만 하고 사본에서 인쇄 범위를 잘라 LibreOffice → PDF → PNG. ⚠ **LibreOffice 가 시스템 폰트를 못 보는 기계가 있다** — 그때 한글만 빠진 그림이 조용히 나가므로, 렌더 뒤 PDF 에 한글이 실렸는지 스스로 확인하고 없으면 멈춘다(2026-09-22 macOS 실측). 나온 PNG 는 감싸개 HTML 로 `figure.mjs` 관문을 지나야 도판이 된다 |
|
|
412
427
|
| `assets/subset_font.py` | 폰트 서브셋 생성 (보통 실행 불필요) |
|
|
413
428
|
| `assets/example/report.json` · `cases.json` | 관보 필드를 전부 채운 본보기. 새 문서는 여기서 베낀다 |
|
|
414
429
|
| `assets/diagram.mjs` | Archify 도해를 관보 `diagrams[].svg` 로 굳힌다 — `render <type> <ir.json> <out.svg> --wide --min-text 16 --tight`(Archify deliver → 계산된 스타일을 속성으로 → 클래스·스크립트·애니메이션 걷음 → 작은 글자 키움·빈 띠 조임·겹침 측정) · `export <html> <out.svg>` · `inspect <svg>`. 렌더러와 같은 잣대(바이트 250KB · viewBox · 표시 글자 15px)로 재고 ✗ 면 exit 3. Archify 자체는 동봉하지 않는다(`references/diagrams.md` §1) |
|
|
415
430
|
| `assets/figure.mjs` · `shot.mjs` · `mockshot.mjs` · `pair.mjs` | 도판 도구 — 도판 HTML 을 굽고 가독성·틀 문법 관문(R0~R10·R13)을 잰다, `--batch plates.json` 은 일괄 판정과 예산 합계, `--spec` 은 계약 상수(`figure.mjs`) · 웹 화면을 검정 원 번호 표식과 함께 찍고 `--after` 로 블록을 숨긴 전후 두 장을 같은 구간으로 찍는다(`shot.mjs`) · 고객 앱 화면 도안을 2배 밀도로 찍는다(`mockshot.mjs`, `references/visual-first.md` 「화면 도안」) · 전후 두 컷을 좌우 같은 틀·같은 배율로 조립하고 판정까지 찍는다(`pair.mjs`, 본보기 `assets/example/plate-pair.html`) |
|
|
416
431
|
| `references/linear-read-test.workflow.js` | 선형 읽기 **실측** — 처음 읽는 사람 셋 + 결정권자가 앞에서 뒤로 한 번 읽으며 뛰어야 하는 자리를 전부 센다(`args: {text, file, reader, round}` — `text`(필수)는 `render.py --text` 로 뽑은 렌더본 텍스트이고 읽는 넷은 이 파일 하나만 읽는다. `file`(필수)은 그 렌더본을 뽑은 `report.json` 이고 렌더본이 원고보다 새로운지 재는 데만 쓴다 — 읽는 넷에게는 주지 않는다. 둘 중 하나가 없거나 렌더본이 비었거나 원고보다 오래되면 읽기 전에 멈춘다. 재측정은 `round` 를 바꿔야 캐시를 안 탄다). 뜀 0 이 목표. 독자 검토(`reader-review.workflow.js`)의 linear 관점이 같은 읽기를 하나로 한다 |
|
|
417
|
-
| `references/edition-review.workflow.js` | **판 갱신 검토** — 구조는 그대로이고 낱말·출처 줄·여백 주석만 고친 판의 **변경 자리**(edit-N.py 의 put/sub
|
|
432
|
+
| `references/edition-review.workflow.js` | **판 갱신 검토** — 구조는 그대로이고 낱말·출처 줄·여백 주석만 고친 판의 **변경 자리**(`assets/patch.py` 의 편집 목록 `edits.json` — 옛 `edit-N.py` 의 put/sub 호출도 받는다)를 규칙 대조·사실 대조로, 새 판 렌더본 전체를 선형 독자로 읽고 심각도 3 이상은 진단과 처방을 따로 반박한다(`args: {file, backup, edits, doc, text}`, 편집 스크립트면 `edits` 대신 `edit_script` · `patch` 는 편집 목록을 굳힌 그 patch.py 의 경로(기본 스킬 설치본 — 없거나, 그 목록을 못 읽는 판이거나, 새 판에 편집이 아직 적용돼 있지 않으면 시작 전 검사에서 멈춘다) · `canon` 은 수치 편집을 대조할 정본을 가리키는 한 줄(안 주면 검토자가 `doc` 글·`handoff` 에서 찾고, 어디에도 없으면 「정본 없음」으로 적는다) · `renderer` 는 규칙 관점과 반박이 읽을 렌더러 — 결과 `kept`(진단이 거짓 아님, 처방이 규칙에 걸리면 `rx`)·`refuted`(진단 거짓)·`weak`·`unverified`(상한 밖·검증 실패), 실패가 있으면 `head` 「검토 미완」). 사실 관점이 백업↔새 판 JSON diff 에 바뀐 자리가 모두 기록에 있는지(줄 수가 아니라 자리로) 검산한다 — 편집 목록이면 백업 사본에 `patch.py` 를 적용한 결과가 새 판과 같은지와 수치를 바꾼 편집 전부, 편집 스크립트면 수치 무변경. 구조가 바뀐 판은 `reader-review.workflow.js`. 낱말 손질 판을 검토 없이 발행하자 다음 판 8자리 손질이 생겼다(2026-09-15) |
|
|
418
433
|
| `assets/example/report-briefing.json` · `cases-briefing.json` | 「결정한다(R&D 브리핑)」 모드 본보기 — 물음 제목 · 두 길 대칭 한눈표 · 선택지 전치 행렬 · FCG 의견 표 한 장 · 바꾸는 길의 위험 · 고른 길별 일정 · `cta` 기본 `[]` |
|
|
419
434
|
| `assets/example/report-briefing-designs.json` · `cases-briefing-designs.json` | 「결정한다」 도안 결재형 본보기 — 세 길 · 전폭 선택지 도판(자리 표시) · 조건표 · 선택지마다 한 행인 의견 표 · 판단 항목 둘(물음 + 「… 확인 요청」) · 확인 프로필 표(`references/lim-portal-format.md` §12.3b) |
|
|
420
435
|
| 폰트 | **레포** `private/builder/fonts/pretendard-gazette.woff2` — 가변 1면 |
|
|
@@ -448,7 +463,7 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
448
463
|
**결정 주체** — 결정권이 고객에게 있는 항은 고객이 정한 기록이 있고 그 항이 원문에서 실제로 닫혀 있을 때만(「정해야 함·차기」로 남아 있지 않을 때) 사실로 배경에 쓴다.
|
|
449
464
|
기록이 없거나 FCG·내부 대화에서 정했으면 사실로 쓰지 않고 결정 물음이나 FCG 의견 절로 둔다. FCG 권한 안의 작업 결정(작업 순서·일정)은 「FCG 작업」으로 사실 서술한다.
|
|
450
465
|
본문에는 결정 출처·날짜를 적지 않는다 — 확인은 결정 장부(0단계 물음 4)에서 한다. 집행자: 사람 눈(0a) · 독자 검토 facts 관점 · 주체 없는 결정 명사(「정한 사항」·「결정된 사항」·「결정임」)는 `text_scan()` 과정 어휘 ⚠.
|
|
451
|
-
3c. **공개 전 수정은 판이 아니다.** `history` 는 공개할 판 한 행(「최초 발행」)이고, `meta` 의 작성 · 판권지 작성 · `history` 마지막 `date` 는 **한 값**이며 공개하는 날로 맞춘다(체크리스트 10단계 — 공개 직전
|
|
466
|
+
3c. **공개 전 수정은 판이 아니다.** `history` 는 공개할 판 한 행(「최초 발행」)이고, `meta` 의 작성 · 판권지 작성 · `history` 마지막 `date` 는 **한 값**이며 공개하는 날로 맞춘다(체크리스트 10단계 — 공개 직전 8단계 「수정 절차」로 한 번).
|
|
452
467
|
공개 뒤 내용이 바뀐 update 에만 `history` 행을 더한다. 문서 밖 결정의 날짜는 본문에 적지 않는다. `cover_date` 는 요약면 「기준」 날짜(수치의 기준일)라 이 셋과 대조하지 않는다.
|
|
453
468
|
집행자: `gazette.py` `date_scan()` ⚠ — 세 자리를 년·월·일로 견줘 다르면 센다(두 경로 · `lim-portal-format.md` §10-1). 공개하는 날인지와 `history` 행 수는 사람(체크리스트 행) · 공개 때 날짜 안내(`release_report`)는 아직 기계가 없다(경위: `references/history.md` §12).
|
|
454
469
|
4. **확인 못 한 범위를 반드시 적는다.** 감추면 다음 보고서까지 못 믿게 된다.
|
|
@@ -13,17 +13,25 @@
|
|
|
13
13
|
//
|
|
14
14
|
// ## 쓰는 법
|
|
15
15
|
//
|
|
16
|
-
// node assets/mcp-call.mjs [--no-mail] <도구 이름> <인자.json> [결과.txt]
|
|
16
|
+
// node assets/mcp-call.mjs [--no-mail] [--json] <도구 이름> <인자.json> [결과.txt]
|
|
17
17
|
//
|
|
18
18
|
// node assets/mcp-call.mjs update_report args-dry.json out-dry.txt
|
|
19
19
|
// node assets/mcp-call.mjs get_report '{"id":<id>,"tenant":"<tenant>"}' out.txt # 작은 인자는 인라인도 받는다
|
|
20
|
+
// node assets/mcp-call.mjs --no-mail --json get_report '{"id":<id>,"tenant":"<tenant>"}' report.rev28.json # 받은 판 — JSON 만
|
|
21
|
+
//
|
|
22
|
+
// `--json` — 도구 출력에서 **줄 머리에서 시작하는 첫 JSON 값**만 결과 파일(없으면 표준 출력)에 쓰고, 머리줄·꼬리 줄(판번호·
|
|
23
|
+
// 그림 보관 안내·메일·세계 소식)은 표준 오류로 보인다. get_report 출력을 그대로 쓰면 patch.py·render.py 가 JSON 을 못 읽는다
|
|
24
|
+
// (머리만 떼도 꼬리 줄이 남는다 — 2026-09-30 적대 검토). JSON 이 없거나 도구가 오류를 내면 종료 1 이고 **결과 파일을 쓰지 않는다**
|
|
25
|
+
// (같은 이름으로 받아 둔 판을 오류 글로 덮지 않는다 — 까닭은 표준 오류로 보인다).
|
|
20
26
|
//
|
|
21
27
|
// `--no-mail` — 프로그램이 한 번 읽고 버리는 호출용(render.py·발행 직전 훅). 부르는 쪽 세션의 신원을 자식에게
|
|
22
28
|
// 안 물려준다. 물려주면 genetics.mjs 가 그 세션으로 알고 도구를 부를 때마다 그 세션 앞 메일을 긷고
|
|
23
29
|
// 「받음」 도장(/api/mail/ack)을 찍는데, 이 파일은 응답을 쓰고 서버를 죽이므로 그 메일은 아무도 못 읽는다.
|
|
24
30
|
// 옵션이 없으면 지금과 같다(부모 env 를 그대로 물려준다).
|
|
25
31
|
//
|
|
26
|
-
// 서버
|
|
32
|
+
// 서버 위치(정본은 이 줄 — 다른 문서는 여기를 가리킨다): `GENETICS_MCP` → 이 파일의 자리(저장소 안의 스킬이면 ../../../genetics.mjs) → 없으면
|
|
33
|
+
// `npx -y genetics-mcp`(게시본). `GENETICS_MCP` 가 없는 파일을 가리키면 종료 2 로 멈춘다 — 다음 자리로 넘어가지 않는다.
|
|
34
|
+
// npm 설치본은 스킬을 ~/.claude/skills 로 **복사**해 깔아 옆에 genetics.mjs 가 없다 — 그 기계에서도 같은 명령이 돈다(2026-09-30 적대 검토).
|
|
27
35
|
// 종료 코드: 0 성공 · 1 도구·프로토콜 오류 · 2 쓰는 법 틀림.
|
|
28
36
|
import { spawn } from 'node:child_process';
|
|
29
37
|
import fs from 'node:fs';
|
|
@@ -32,17 +40,20 @@ import { fileURLToPath } from 'node:url';
|
|
|
32
40
|
|
|
33
41
|
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
34
42
|
// assets/ → genetics-report/ → skills/ → mcp/genetics.mjs
|
|
35
|
-
const
|
|
43
|
+
const LOCAL = path.resolve(HERE, '../../../genetics.mjs');
|
|
44
|
+
const SERVER = process.env.GENETICS_MCP || (fs.existsSync(LOCAL) ? LOCAL : null);
|
|
45
|
+
const SERVER_CMD = SERVER ? ['node', [SERVER]] : ['npx', ['-y', 'genetics-mcp']];
|
|
36
46
|
|
|
37
47
|
const argv = process.argv.slice(2);
|
|
38
48
|
const noMail = argv.includes('--no-mail');
|
|
39
|
-
const
|
|
49
|
+
const jsonOnly = argv.includes('--json');
|
|
50
|
+
const [tool, argsArg, outPath] = argv.filter((a) => a !== '--no-mail' && a !== '--json');
|
|
40
51
|
if (!tool || !argsArg) {
|
|
41
|
-
console.error('쓰기: node mcp-call.mjs [--no-mail] <도구 이름> <인자.json|인라인 JSON> [결과.txt]');
|
|
52
|
+
console.error('쓰기: node mcp-call.mjs [--no-mail] [--json] <도구 이름> <인자.json|인라인 JSON> [결과.txt]');
|
|
42
53
|
process.exit(2);
|
|
43
54
|
}
|
|
44
|
-
if (!fs.existsSync(
|
|
45
|
-
console.error(`✗ MCP 서버를 못 찾았습니다: ${
|
|
55
|
+
if (process.env.GENETICS_MCP && !fs.existsSync(process.env.GENETICS_MCP)) {
|
|
56
|
+
console.error(`✗ MCP 서버를 못 찾았습니다: ${process.env.GENETICS_MCP}\n GENETICS_MCP=/경로/genetics.mjs 로 알려 주세요.`);
|
|
46
57
|
process.exit(2);
|
|
47
58
|
}
|
|
48
59
|
// 인자는 파일이 기본이다 — 큰 원고가 이 도구의 존재 이유다. 인라인은 작은 호출의 편의.
|
|
@@ -68,11 +79,15 @@ function noMailEnv() {
|
|
|
68
79
|
return env;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
const srv = spawn(
|
|
82
|
+
const srv = spawn(SERVER_CMD[0], SERVER_CMD[1], { stdio: ['pipe', 'pipe', 'inherit'], ...(noMail ? { env: noMailEnv() } : {}) });
|
|
72
83
|
srv.on('error', (e) => { console.error(`✗ 서버를 못 띄웠습니다: ${e.message}`); process.exit(1); });
|
|
73
84
|
|
|
74
85
|
let buf = '';
|
|
75
86
|
const pending = new Map();
|
|
87
|
+
// 파이프는 64KiB 조각으로 온다 — 조각마다 따로 글자로 풀면 경계에 걸친 한글 한 글자가 U+FFFD(�) 둘~셋으로 바뀐다(종료 0 · JSON 도 읽힌다).
|
|
88
|
+
// 받은 판이 새 판의 출발점이라 그 글자가 그대로 발행 인자에 실린다(2026-09-30 검토 — 9/14~9/22 의 받은 사본 7벌에 실제로 남아 있었다).
|
|
89
|
+
// 스트림에 디코더를 달아 조각 경계를 넘겨 푼다.
|
|
90
|
+
srv.stdout.setEncoding('utf8');
|
|
76
91
|
srv.stdout.on('data', (d) => {
|
|
77
92
|
buf += d;
|
|
78
93
|
let i;
|
|
@@ -104,6 +119,42 @@ const text = (r.result?.content || []).map((c) => c.text || '').join('\n');
|
|
|
104
119
|
const out = r.error
|
|
105
120
|
? 'ERROR ' + JSON.stringify(r.error)
|
|
106
121
|
: (r.result?.isError ? 'TOOL_ERROR\n' : '') + text;
|
|
107
|
-
if (outPath) fs.writeFileSync(outPath, out); else process.stdout.write(out + '\n');
|
|
108
122
|
srv.kill();
|
|
109
|
-
|
|
123
|
+
const failed = Boolean(r.error || r.result?.isError);
|
|
124
|
+
// 표준 출력으로 낼 때는 **쓰기가 끝난 뒤에** 끝낸다 — 곧바로 exit 하면 파이프로 받는 쪽(셸 파이프 · 파이썬 capture_output)이 64KiB 에서
|
|
125
|
+
// 잘린 출력을 종료 0 과 함께 받는다(2026-09-30 검토). 파일로 쓸 때는 동기 쓰기라 그대로 끝낸다.
|
|
126
|
+
const finish = (s, code) => {
|
|
127
|
+
if (outPath) { fs.writeFileSync(outPath, s); process.exit(code); }
|
|
128
|
+
process.stdout.write(s, () => process.exit(code));
|
|
129
|
+
};
|
|
130
|
+
if (!jsonOnly) finish(outPath ? out : out + '\n', failed ? 1 : 0);
|
|
131
|
+
else if (failed) {
|
|
132
|
+
// 오류면 결과 파일을 건드리지 않는다 — 같은 이름으로 받아 둔 판을 「TOOL_ERROR …」 글로 덮지 않게. 까닭은 화면(표준 오류)에 보인다.
|
|
133
|
+
console.error(`✗ 도구가 오류를 냈습니다 — 결과 파일은 쓰지 않았습니다\n${out}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
} else {
|
|
136
|
+
const got = firstJson(text);
|
|
137
|
+
if (!got) { console.error(`✗ 도구 출력에 JSON 이 없습니다 — ${text.split('\n')[0].slice(0, 200)}`); process.exit(1); }
|
|
138
|
+
const rest = (got.before + got.after).trim();
|
|
139
|
+
if (rest) console.error(rest); // 머리·꼬리 줄(판번호 등)은 사람 눈에 — 결과 파일에는 JSON 만
|
|
140
|
+
finish(got.json + '\n', 0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** 줄 머리에서 시작하는 첫 JSON 값(객체·배열) — 문자열 안의 괄호는 안 센다. 못 읽으면 다음 후보로 넘어간다. */
|
|
144
|
+
function firstJson(s) {
|
|
145
|
+
for (let i = 0; i < s.length; i++) {
|
|
146
|
+
if ((s[i] !== '{' && s[i] !== '[') || (i > 0 && s[i - 1] !== '\n')) continue;
|
|
147
|
+
let depth = 0, inStr = false, esc = false;
|
|
148
|
+
for (let j = i; j < s.length; j++) {
|
|
149
|
+
const ch = s[j];
|
|
150
|
+
if (inStr) { if (esc) esc = false; else if (ch === '\\') esc = true; else if (ch === '"') inStr = false; continue; }
|
|
151
|
+
if (ch === '"') inStr = true;
|
|
152
|
+
else if (ch === '{' || ch === '[') depth++;
|
|
153
|
+
else if ((ch === '}' || ch === ']') && --depth === 0) {
|
|
154
|
+
const json = s.slice(i, j + 1);
|
|
155
|
+
try { JSON.parse(json); return { json, before: s.slice(0, i), after: s.slice(j + 1) }; } catch { break; }
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|