genetics-mcp 2.19.2 → 2.20.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/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/aarch64-unknown-linux-musl/gcell +0 -0
- package/bin/manifest.json +4 -4
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/genetics.mjs +131 -156
- package/package.json +2 -2
- package/skills/genetics-report/SKILL.md +29 -4
- package/skills/genetics-report/assets/example/report-briefing.json +3 -3
- package/skills/genetics-report/references/history.md +18 -0
- package/skills/genetics-report/references/lim-portal-format.md +46 -11
- package/skills/genetics-report/references/readability.md +2 -2
- package/skills/genetics-report/assets/example/plate-pair.html +0 -25
|
Binary file
|
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
3
|
-
"aarch64-unknown-linux-musl": "
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
5
|
-
"x86_64-unknown-linux-musl": "
|
|
2
|
+
"aarch64-apple-darwin": "1db100bb09a127f3895fbe833dd494072cb831a89b90b6044dbcd0d3b0d2eb00",
|
|
3
|
+
"aarch64-unknown-linux-musl": "731f44a4ae57e624312e0c7e76def96d977cbf18993f87677913bb6af7537caf",
|
|
4
|
+
"x86_64-apple-darwin": "a1b59a18e1a7f1d79085213d11f7370a433d21742f36536db6c7aa65f8e0b5aa",
|
|
5
|
+
"x86_64-unknown-linux-musl": "e3f0f1394760b39435dd7fa8af6e992cdc43d345236b7472bed8cf99a6939fd2"
|
|
6
6
|
}
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -1426,6 +1426,25 @@ const REPORT_DATA = {
|
|
|
1426
1426
|
how_to_check: { type: 'string' },
|
|
1427
1427
|
remaining: { type: 'string', description: '고객 확인이 필요한 사항(선택)' },
|
|
1428
1428
|
source_url: { type: 'string', description: 'http(s) 제보 원본 링크(선택)' },
|
|
1429
|
+
quotes: {
|
|
1430
|
+
type: 'array',
|
|
1431
|
+
description: '**이 건에 묶인** 원문 인용 — 절의 `quotes` 와 같은 모양({text, lang?, source_text, source_url?, verdict?, translation?}). '
|
|
1432
|
+
+ '건 필드표 바로 아래 그 건 안에 선다. 건이 여럿인 절에서 근거는 절이 아니라 건에 묶는다 — '
|
|
1433
|
+
+ '절에 두면 건 목록 뒤에 한꺼번에 서서 어느 건의 근거인지 캡션 글자로만 이어진다(2026-09-17).',
|
|
1434
|
+
items: { type: 'object' },
|
|
1435
|
+
},
|
|
1436
|
+
tables: {
|
|
1437
|
+
type: 'array',
|
|
1438
|
+
description: '**이 건에 묶인** 자료표 — 절의 `tables` 와 같은 모양({caption, cols[], rows[[]], align?, note?}). '
|
|
1439
|
+
+ '번호는 절과 한 셈(건의 것이 먼저).',
|
|
1440
|
+
items: { type: 'object' },
|
|
1441
|
+
},
|
|
1442
|
+
plates: {
|
|
1443
|
+
type: 'array',
|
|
1444
|
+
description: '**이 건에 묶인** 도판의 `assets[].id` 목록. 건 안이라 늘 본문 단 폭이다 — 전폭(wide) 자산은 절 `plates` 에. '
|
|
1445
|
+
+ '그림 번호는 절과 한 셈(건의 것이 먼저). 없는 id 는 발행 거절.',
|
|
1446
|
+
items: { type: 'string' },
|
|
1447
|
+
},
|
|
1429
1448
|
uid: {
|
|
1430
1449
|
type: 'string',
|
|
1431
1450
|
description: '이 건의 **불변 이름**(영소문자·숫자 6자). 안 주면 서버가 발급한다. '
|
|
@@ -1832,6 +1851,28 @@ function mySessionId() {
|
|
|
1832
1851
|
return process.env.CLAUDE_CODE_SESSION_ID || cursorActiveComposer();
|
|
1833
1852
|
}
|
|
1834
1853
|
|
|
1854
|
+
/**
|
|
1855
|
+
* 이 세션에 **훅 레인**이 있는가 — MCP 가 관측·메일 폴을 대신 돌 이유가 있는지를 가른다(자기 일정 표 `SCHEDULE`).
|
|
1856
|
+
*
|
|
1857
|
+
* 설계(cell/README 「훅 없이 관측한다」 · 결정 2026-09-17): 저장소 관측은 세포 훅의 일이고, MCP 의 20초
|
|
1858
|
+
* `gcell observe` 는 **훅이 없는 클라이언트(Codex)만을 위한 예외**다. 그런데 클라이언트를 안 가리고 모든 창에서
|
|
1859
|
+
* 돌아, 창 26개가 20초마다 세포를 둘씩 띄웠다(초당 2.6 fork — 2026-09-17 실측).
|
|
1860
|
+
*
|
|
1861
|
+
* 판정은 **부팅 때부터 있는 사실**로 한다: Claude Code 는 env, Cursor 는 활성 대화 원장으로 자기를 안다 —
|
|
1862
|
+
* 둘 다 세포 훅이 거는 클라이언트다. 둘 다 없으면(Codex·미지) 훅이 없다. `initialize` 의 clientInfo 는 뜬 뒤에야
|
|
1863
|
+
* 오고 이름이 실측되지 않아 판정에 안 쓴다(그 줄에서 로그로만 남긴다). `GENETICS_HOOK_LANE=0|1` 은 시험·수동 강제.
|
|
1864
|
+
* 한 번 참이면 끝까지 참이다(세션이 훅을 잃지 않는다). 거짓은 60초만 기억한다 — Cursor 원장은 창이 뜬 뒤에 생긴다.
|
|
1865
|
+
*/
|
|
1866
|
+
let hookLaneMemo = { v: false, at: 0 };
|
|
1867
|
+
function hookLane() {
|
|
1868
|
+
if (cfg.GENETICS_HOOK_LANE === '1') return true;
|
|
1869
|
+
if (cfg.GENETICS_HOOK_LANE === '0') return false;
|
|
1870
|
+
if (hookLaneMemo.v) return true;
|
|
1871
|
+
if (Date.now() - hookLaneMemo.at < 60_000) return false;
|
|
1872
|
+
hookLaneMemo = { v: !!(process.env.CLAUDE_CODE_SESSION_ID || cursorActiveComposer()), at: Date.now() };
|
|
1873
|
+
return hookLaneMemo.v;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1835
1876
|
function gitHere() {
|
|
1836
1877
|
try {
|
|
1837
1878
|
const r = spawnSync('git', ['-C', process.cwd(), 'rev-parse', '--abbrev-ref', 'HEAD'],
|
|
@@ -2324,17 +2365,11 @@ function transcriptHead(file) {
|
|
|
2324
2365
|
const cut = from > 0 ? text.indexOf('\n') : -1;
|
|
2325
2366
|
const head = jsonlHead(from > 0 ? (cut < 0 ? '' : text.slice(cut + 1)) : text);
|
|
2326
2367
|
|
|
2327
|
-
// 줄 수 — 늘어난 만큼만 센다.
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
lineCount = was.head.lines + countNl(grow);
|
|
2333
|
-
} else {
|
|
2334
|
-
const all = Buffer.alloc(st.size);
|
|
2335
|
-
fs.readSync(fd, all, 0, all.length, 0);
|
|
2336
|
-
lineCount = countNl(all);
|
|
2337
|
-
}
|
|
2368
|
+
// 줄 수 — 늘어난 만큼만 센다. 처음 보는 파일·줄어든 파일은 처음부터 세되 **파일을 통째로 올리지 않는다**
|
|
2369
|
+
// (2026-09-17: 38MB 전사를 `Buffer.alloc(size)` 로 올려 바이트 루프로 세던 자리가 부팅·압축 뒤의 버스트였다).
|
|
2370
|
+
const lineCount = (was && st.size >= was.size)
|
|
2371
|
+
? was.head.lines + countNlRange(fd, was.size, st.size)
|
|
2372
|
+
: countNlRange(fd, 0, st.size);
|
|
2338
2373
|
head.lines = lineCount;
|
|
2339
2374
|
headCache.set(file, { size: st.size, mtimeMs: st.mtimeMs, head });
|
|
2340
2375
|
return head;
|
|
@@ -2366,9 +2401,18 @@ function jsonlHead(text) {
|
|
|
2366
2401
|
}
|
|
2367
2402
|
return { id: '', ms: 0 };
|
|
2368
2403
|
}
|
|
2369
|
-
|
|
2404
|
+
/** `[from, to)` 구간의 개행 수 — 1MB 조각으로 읽어 `indexOf` 로 센다. 메모리는 파일 크기와 무관하게 1MB 다. */
|
|
2405
|
+
function countNlRange(fd, from, to) {
|
|
2406
|
+
const NL_CHUNK = 1024 * 1024;
|
|
2370
2407
|
let n = 0;
|
|
2371
|
-
|
|
2408
|
+
if (to <= from) return 0;
|
|
2409
|
+
const buf = Buffer.allocUnsafe(Math.min(NL_CHUNK, to - from));
|
|
2410
|
+
for (let pos = from; pos < to;) {
|
|
2411
|
+
const got = fs.readSync(fd, buf, 0, Math.min(buf.length, to - pos), pos);
|
|
2412
|
+
if (got <= 0) break;
|
|
2413
|
+
for (let i = buf.indexOf(0x0a); i !== -1 && i < got; i = buf.indexOf(0x0a, i + 1)) n++;
|
|
2414
|
+
pos += got;
|
|
2415
|
+
}
|
|
2372
2416
|
return n;
|
|
2373
2417
|
}
|
|
2374
2418
|
|
|
@@ -3775,8 +3819,6 @@ async function beatTick() {
|
|
|
3775
3819
|
// 옮겨진 세션은 resolve 가 새 자리를 준다(심박 프레임은 그대로 — §4.5).
|
|
3776
3820
|
try { await resolveMoved(reply); } catch { /* 다음 바퀴에 다시 */ }
|
|
3777
3821
|
try { await autoUploadRound(reply, recs, cands); } catch { /* 한 바퀴가 실패해도 심박은 계속 뛴다 */ }
|
|
3778
|
-
// 두뇌가 짚은 모순을 긷는다 — 실패해도 심박을 안 멈춘다(안 오는 것이 고장은 아니다).
|
|
3779
|
-
try { await brainPull(); } catch { /* 그물·자격 문제는 다음 바퀴에 다시 */ }
|
|
3780
3822
|
} finally {
|
|
3781
3823
|
beatRunning = false;
|
|
3782
3824
|
}
|
|
@@ -3801,7 +3843,6 @@ async function beatTick() {
|
|
|
3801
3843
|
// 시험은 20초를 못 기다린다. **갈아 끼울 수 있게** 둔다(`GCELL_STATE_DIR` 과 같은 규율) —
|
|
3802
3844
|
// 다만 하한을 둬서 실수로 0.01초가 들어가 기계를 태우는 일은 막는다.
|
|
3803
3845
|
const OBSERVE_EVERY_MS = Math.max(200, Number(cfg.GENETICS_OBSERVE_MS) || 20_000);
|
|
3804
|
-
let observeTimer = null;
|
|
3805
3846
|
|
|
3806
3847
|
function observeTick() {
|
|
3807
3848
|
const { bin } = cellCheck();
|
|
@@ -3834,27 +3875,19 @@ function observeTenant() {
|
|
|
3834
3875
|
} catch { return ''; }
|
|
3835
3876
|
}
|
|
3836
3877
|
|
|
3837
|
-
function startObserve() {
|
|
3838
|
-
if (cfg.GENETICS_NO_BEAT) return; // 심박을 끄는 사람은 이것도 안 원한다
|
|
3839
|
-
observeTick();
|
|
3840
|
-
observeTimer = setInterval(observeTick, OBSERVE_EVERY_MS);
|
|
3841
|
-
observeTimer.unref?.(); // 창을 닫으면 노드가 남지 않게
|
|
3842
|
-
}
|
|
3843
|
-
|
|
3844
3878
|
/*
|
|
3845
|
-
* ──
|
|
3879
|
+
* ── 메일은 **심박에 얹지 않는다** (2026-08-25 운영 첫 왕복 실측) ─────────────────
|
|
3846
3880
|
* 전에는 beatTick 꼬리에 얹었는데, beatTick 은 색인에 올린 세션이 없으면 **일찍 빠져나간다**
|
|
3847
3881
|
* (`if (!recs.length) return`). cell/README 가 observeTick 에 대해 경고한 바로 그 무늬를
|
|
3848
|
-
* 밟았다 — 새로 뜬 세션은 색인 밖이라 메일을 영영 안 길었다.
|
|
3882
|
+
* 밟았다 — 새로 뜬 세션은 색인 밖이라 메일을 영영 안 길었다.
|
|
3883
|
+
*
|
|
3884
|
+
* 긷는 자리는 둘이다(자기 일정 표, 2026-09-17): 훅 레인이 있는 세션은 **도구를 부를 때**(60초 상한) —
|
|
3885
|
+
* 설계의 도구응답 레인 그대로다(DESIGN-mail §6.3 「세션이 도구를 불러야 붙는다 — 깨우지 못한다」).
|
|
3886
|
+
* 훅이 없는 세션만 60초 타이머로 긷는다. 어느 쪽이든 서버는 사람 토큰만 받는다.
|
|
3849
3887
|
*/
|
|
3850
|
-
|
|
3851
|
-
function startMailPoll() {
|
|
3852
|
-
if (cfg.GENETICS_NO_BEAT) return;
|
|
3888
|
+
function mailPollReady() {
|
|
3853
3889
|
const cred = credential();
|
|
3854
|
-
|
|
3855
|
-
mailPull().catch(() => {});
|
|
3856
|
-
mailTimer = setInterval(() => { mailPull().catch(() => {}); }, MAIL_POLL_MS);
|
|
3857
|
-
mailTimer.unref?.();
|
|
3890
|
+
return !!(BASE && cred && cred.kind === 'user');
|
|
3858
3891
|
}
|
|
3859
3892
|
|
|
3860
3893
|
function startBeat() {
|
|
@@ -5124,116 +5157,6 @@ function cellNotice(why) {
|
|
|
5124
5157
|
].join('\n');
|
|
5125
5158
|
}
|
|
5126
5159
|
|
|
5127
|
-
/* ── ★★ 백신 받기 — 두뇌가 짚은 **모순**을 이 세션 눈앞에 놓는다 ──────────────
|
|
5128
|
-
*
|
|
5129
|
-
* 사용자의 진단(2026-08-24): 여러 세포가 한 군체를 다룰 때 **정상적으로 일하는 사람이
|
|
5130
|
-
* 두뇌에겐 바이러스가 된다.** 그러면 두뇌가 그 세션에게 **되돌려 줄 줄 알아야** 한다.
|
|
5131
|
-
*
|
|
5132
|
-
* ── 왜 세포(gcell)가 아니라 여기서 긷나 ────────────────────────────────────
|
|
5133
|
-
* 세포도 같은 장부를 긷지만 `kind != "write"` 를 **버린다**(`cell/src/server.rs:254`).
|
|
5134
|
-
* 통과시켜도 `root.join(ref)` 로 ref 를 경로 취급해 **없는 파일**을 「최근에 바뀐 것」으로
|
|
5135
|
-
* 만든다 — 그 파일의 주석이 바로 그 해악을 경고한다. 세포를 다시 굽는 것은 별건이라,
|
|
5136
|
-
* 검토는 이 자리에서 **직접** 긷는다(사람 자격으로 열리는 창구다).
|
|
5137
|
-
*
|
|
5138
|
-
* ── 처음 한 바퀴에 옛날 것을 읊지 않는다 ────────────────────────────────────
|
|
5139
|
-
* 커서가 없으면 지난 것을 전부 「지금 일」로 읽는다. 그래서 첫 바퀴는 **커서만 맞추고**
|
|
5140
|
-
* 최근 10분 것만 말한다. 커서는 색인에 적어 두므로 이 프로세스가 죽었다 살아나도 안 잃는다.
|
|
5141
|
-
*/
|
|
5142
|
-
const BRAIN_POLL_MS = 120_000; // 굽기는 분 단위다 — 20초 심박마다 두드릴 이유가 없다
|
|
5143
|
-
const BRAIN_FRESH_MS = 600_000; // 첫 바퀴에 「방금 것」으로 볼 나이
|
|
5144
|
-
const BRAIN_PAGE = 200; // 서버 상한(WORLD_PAGE_MAX)과 같다
|
|
5145
|
-
let brainAt = 0;
|
|
5146
|
-
let brainCursor = null; // null = 아직 색인에서 안 읽었다
|
|
5147
|
-
const brainQueue = []; // 아직 안 말한 모순
|
|
5148
|
-
|
|
5149
|
-
async function brainPull() {
|
|
5150
|
-
const mine = mySessionId();
|
|
5151
|
-
if (!mine) return;
|
|
5152
|
-
const e = indexRead()[mine];
|
|
5153
|
-
if (!e?.tenant || !e?.tenant_id) return; // 아직 어느 소속처인지 모른다
|
|
5154
|
-
const cred = credential();
|
|
5155
|
-
// **키로는 세계 장부를 못 연다** — 서버가 사람 토큰만 받는다(`api.php` 의 world 라우트).
|
|
5156
|
-
if (!cred || cred.kind !== 'user') return;
|
|
5157
|
-
if (Date.now() - brainAt < BRAIN_POLL_MS) return;
|
|
5158
|
-
brainAt = Date.now();
|
|
5159
|
-
|
|
5160
|
-
const first = brainCursor === null;
|
|
5161
|
-
if (first) brainCursor = Number(e.brain_cursor || 0);
|
|
5162
|
-
const cwd = relHome(process.cwd());
|
|
5163
|
-
const fresh = [];
|
|
5164
|
-
for (let page = 0; page < 10; page++) {
|
|
5165
|
-
const r = await api('GET', '/api/world?tenant=' + encodeURIComponent(tenantArgOf(e))
|
|
5166
|
-
+ '&since=' + brainCursor + '&cwd=' + encodeURIComponent(cwd) + '&limit=' + BRAIN_PAGE);
|
|
5167
|
-
const evs = Array.isArray(r?.events) ? r.events : [];
|
|
5168
|
-
for (const ev of evs) if (ev?.kind === 'review' && ev?.ref) fresh.push(ev);
|
|
5169
|
-
if (typeof r?.cursor === 'number' && r.cursor > brainCursor) brainCursor = r.cursor;
|
|
5170
|
-
if (!r?.truncated) break;
|
|
5171
|
-
}
|
|
5172
|
-
// 빈 폴링은 수신이 아니다 — 실제로 온 것이 있을 때만 도장을 찍는다.
|
|
5173
|
-
// 커서는 소속 칸을 안 건드린다(§5.1 P1-a) — 읽어 둔 `e` 가 그 사이 낡았을 수 있다.
|
|
5174
|
-
indexPatch(mine, { brain_cursor: brainCursor }, fresh.length ? 'rx' : '');
|
|
5175
|
-
|
|
5176
|
-
const now = Date.now();
|
|
5177
|
-
for (const ev of fresh) {
|
|
5178
|
-
if (!brainFresh(ev, first, now)) continue;
|
|
5179
|
-
brainQueue.push(String(ev.ref));
|
|
5180
|
-
}
|
|
5181
|
-
}
|
|
5182
|
-
|
|
5183
|
-
/**
|
|
5184
|
-
* 이 사건을 **지금 일로 말해도 되나.**
|
|
5185
|
-
*
|
|
5186
|
-
* 커서가 있는 바퀴(=이 프로세스가 이미 따라잡은 뒤)라면 내려온 것은 정의상 새것이라 전부 참.
|
|
5187
|
-
* **첫 바퀴만** 나이를 본다 — 커서가 0이면 지난 것이 통째로 내려오는데, 그것을 읊으면
|
|
5188
|
-
* 이미 끝난 이야기를 **지금 하는 일로** 읽는다. 그게 이 물건이 막으려던 바로 그 병이다.
|
|
5189
|
-
*
|
|
5190
|
-
* ⚠ 시각을 못 읽으면 **안 말한다.** 모르는 것을 「방금」으로 치는 쪽이 더 위험하다.
|
|
5191
|
-
*/
|
|
5192
|
-
function brainFresh(ev, first, now) {
|
|
5193
|
-
if (!first) return true;
|
|
5194
|
-
const at = brainAtMs(ev);
|
|
5195
|
-
return at > 0 && now - at <= BRAIN_FRESH_MS;
|
|
5196
|
-
}
|
|
5197
|
-
|
|
5198
|
-
/**
|
|
5199
|
-
* 서버가 준 `at` 을 밀리초로. **서버는 시간대 표식 없는 UTC 로 준다.**
|
|
5200
|
-
*
|
|
5201
|
-
* ⚠⚠ 맨몸 `Date.parse("2026-08-24 01:50:30")` 은 그 문자열을 **로컬 시각**으로 읽는다.
|
|
5202
|
-
* 서버는 `world_at()` 에서 `gmdate('Y-m-d H:i:s')` 로 쓰므로 실제로는 UTC 다
|
|
5203
|
-
* (서버 스스로도 그것을 안다 — `world_delta.php` 가 화면에 쓸 때 `…substr(at,11,5) . 'Z'`).
|
|
5204
|
-
* 그래서 **KST(+9) 기계에서는 방금 난 모순이 9시간 낡은 것**으로 보인다.
|
|
5205
|
-
* 첫 바퀴는 나이를 보고 버리므로 백신이 통째로 사라지고, **커서는 그대로 전진해**
|
|
5206
|
-
* 그 사건들은 이 세션에서 영영 안 온다. 조용한 손실이다.
|
|
5207
|
-
*
|
|
5208
|
-
* 2026-08-24 실측: 같은 하네스에 서버 꼴을 넣으면 백신 0건, `Z` 를 붙이면 1건.
|
|
5209
|
-
* 내 첫 PoC 가 이걸 놓친 이유는 픽스처가 `toISOString()` 이라 **실제로 깨지는 그 꼴을
|
|
5210
|
-
* 비켜 갔기** 때문이다 — 픽스처는 서버가 진짜 주는 글자여야 한다.
|
|
5211
|
-
*
|
|
5212
|
-
* ⚠ 시각을 못 읽으면 **0**. 모르는 것을 「방금」으로 치는 쪽이 더 위험하다.
|
|
5213
|
-
*/
|
|
5214
|
-
const BRAIN_AT_NAIVE = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})$/;
|
|
5215
|
-
function brainAtMs(ev) {
|
|
5216
|
-
const s = String(ev?.at || '').trim();
|
|
5217
|
-
const m = BRAIN_AT_NAIVE.exec(s);
|
|
5218
|
-
return (m ? Date.parse(`${m[1]}T${m[2]}Z`) : Date.parse(s)) || 0;
|
|
5219
|
-
}
|
|
5220
|
-
|
|
5221
|
-
/**
|
|
5222
|
-
* 큐에 쌓인 모순을 **한 번씩만** 말한다.
|
|
5223
|
-
*
|
|
5224
|
-
* ⚠ **지시를 안 붙인다.** 실측 2026-08-22: 「…라고 말하라」를 실었더니 모델이 *「가짜 시스템」*
|
|
5225
|
-
* 이라며 거부했다 — 옳은 거부다. 서버도 `world_event_clean()` 으로 지시형을 막는다.
|
|
5226
|
-
* 여기서는 **어디를 보면 되는지**만 덧붙인다(사실).
|
|
5227
|
-
*/
|
|
5228
|
-
function brainSays() {
|
|
5229
|
-
if (!brainQueue.length) return '';
|
|
5230
|
-
const n = brainQueue.length;
|
|
5231
|
-
const lines = brainQueue.splice(0, n).map((t) => ` · ${t}`);
|
|
5232
|
-
return '두뇌가 이 폴더의 지식에서 모순을 짚었다:\n' + lines.join('\n')
|
|
5233
|
-
+ '\n전문은 포털 지식 화면의 검토 목록에 있다. search_cases 로 그 주제를 찾으면'
|
|
5234
|
-
+ ' 「⚠ 상태:」가 붙은 장이 어느 것인지 보인다.';
|
|
5235
|
-
}
|
|
5236
|
-
|
|
5237
5160
|
/* ── ★★ 메일 레인 — 두뇌·다른 세션·사람이 **나를 지목해** 보낸 것 ──────────
|
|
5238
5161
|
*
|
|
5239
5162
|
* 설계는 `DESIGN-mail.md`, 규칙은 서버의 `lib/mail.php`(순수), 표는 051·052.
|
|
@@ -5553,21 +5476,15 @@ function notesSays() {
|
|
|
5553
5476
|
|
|
5554
5477
|
function worldAppend(result) {
|
|
5555
5478
|
/*
|
|
5556
|
-
*
|
|
5557
|
-
*
|
|
5558
|
-
*
|
|
5559
|
-
|
|
5560
|
-
/*
|
|
5561
|
-
* ★★ 메일이 **가장 앞**이다. 백신은 주소 없는 방송이고 델타는 배경인데, 메일은
|
|
5562
|
-
* 누군가가 **나를 지목해** 보낸 것이다. 그리고 만료가 있어 늦으면 되돌아간다 —
|
|
5563
|
-
* 셋 중 유일하게 **시간이 걸려 있는** 것이라 순서에서 앞선다.
|
|
5479
|
+
* ★★ 메일이 **가장 앞**이다. 델타는 배경인데, 메일은 누군가가 **나를 지목해** 보낸 것이다.
|
|
5480
|
+
* 그리고 만료가 있어 늦으면 되돌아간다 — **시간이 걸려 있는** 것이라 순서에서 앞선다.
|
|
5481
|
+
* (두뇌 백신 줄은 2026-09-17 에 걷었다 — 서버가 kind='review' 를 더는 안 만들어
|
|
5482
|
+
* 2026-08-25 부터 사문이었고, 120초마다 /api/world 를 열 쪽씩 두드리던 폴도 함께 없앴다.)
|
|
5564
5483
|
*/
|
|
5565
5484
|
const mm = mailSays();
|
|
5566
5485
|
if (mm) result.content.push({ type: 'text', text: mm });
|
|
5567
|
-
const v = brainSays();
|
|
5568
|
-
if (v) result.content.push({ type: 'text', text: v });
|
|
5569
5486
|
// ★ 소속처 미등록은 **델타보다 앞**이다. 델타가 비어도(조용한 구간) 나가야 하고,
|
|
5570
|
-
// 그것이 안 풀리면 위의
|
|
5487
|
+
// 그것이 안 풀리면 위의 메일도 이 저장소에서는 영영 안 온다.
|
|
5571
5488
|
const tn = tenantNotice();
|
|
5572
5489
|
if (tn) result.content.push({ type: 'text', text: tn });
|
|
5573
5490
|
/*
|
|
@@ -5829,6 +5746,12 @@ async function callTool(name, args = {}) {
|
|
|
5829
5746
|
if (x.overwrites && x.overwrites !== 'none') bits.push(OVERWRITES[x.overwrites] || `덮음: ${x.overwrites}`);
|
|
5830
5747
|
// 옛 서버와도 물린다 — 그쪽은 아직 거절하던 판이다.
|
|
5831
5748
|
else if (x.can_write === false) bits.push('API 로 수정 불가(옛 서버)');
|
|
5749
|
+
// 미반영 첨삭 메모 — 사람이 「메모 해놨다」고만 해도 어느 문서인지 목록에서 보인다.
|
|
5750
|
+
// 서버가 모르는 판(칸 없음)·볼 자격 없음(null)이면 아무것도 안 찍는다. 기억해 두면 뒤 도구 꼬리가 다시 알린다.
|
|
5751
|
+
if (Number.isInteger(x.open_notes)) {
|
|
5752
|
+
notesSeen(Number(x.id), x.open_notes);
|
|
5753
|
+
if (x.open_notes > 0) bits.push(`첨삭 메모 ${x.open_notes}건 미반영`);
|
|
5754
|
+
}
|
|
5832
5755
|
const tag = bits.length ? ` [${bits.join(' · ')}]` : '';
|
|
5833
5756
|
// slug 를 싣는다. 중복 slug 는 409 로 잘 막히지만 **미리 알 수가 없었다** —
|
|
5834
5757
|
// 목록에 이름표가 없어 새 이름을 고를 때 참고할 것이 없었다(2026-08-18 검토).
|
|
@@ -6696,6 +6619,10 @@ async function handle(line) {
|
|
|
6696
6619
|
* initialize 의 clientInfo 로만 온다. 놓치면 다시 물어볼 자리가 없다.
|
|
6697
6620
|
*/
|
|
6698
6621
|
CLIENT = req.params?.clientInfo || null;
|
|
6622
|
+
// 어느 레인으로 떴는가 — clientInfo 이름은 아직 판정에 안 쓴다(실측 전). 이 줄이 그 실측이다.
|
|
6623
|
+
try {
|
|
6624
|
+
process.stderr.write(`genetics: lane=${hookLane() ? 'hooked' : 'hookless'} client=${CLIENT?.name || '?'}/${CLIENT?.version || '?'} sid=${mySessionId() ? 'yes' : 'no'}\n`);
|
|
6625
|
+
} catch { /* 로그는 최선만 */ }
|
|
6699
6626
|
result = {
|
|
6700
6627
|
protocolVersion: req.params?.protocolVersion || '2024-11-05',
|
|
6701
6628
|
// ★ `listChanged` 를 **선언해야** 알림을 보낼 자격이 생긴다. 명세상 안 알리고
|
|
@@ -6744,6 +6671,10 @@ async function handle(line) {
|
|
|
6744
6671
|
} else if (req.method === 'tools/list') {
|
|
6745
6672
|
result = { tools: toolsNow() };
|
|
6746
6673
|
} else if (req.method === 'tools/call') {
|
|
6674
|
+
lastToolAt = Date.now(); // 유휴 조절기의 시계(자기 일정 표)
|
|
6675
|
+
// 도구응답 레인의 긷기 — **기다리지 않는다**(도구 지연은 그대로). 60초 상한은 mailPull 안의 mailAt 검사.
|
|
6676
|
+
// 도구가 망을 도는 동안 도착한 통은 이번 응답 꼬리에, 아니면 다음 응답에 실린다.
|
|
6677
|
+
if (hookLane() && mailPollReady()) mailPull().catch(() => {});
|
|
6747
6678
|
const text = await callTool(req.params?.name, req.params?.arguments || {});
|
|
6748
6679
|
result = { content: [{ type: 'text', text }] };
|
|
6749
6680
|
worldAppend(result);
|
|
@@ -7208,6 +7139,52 @@ function ensureSkillOnBoot() {
|
|
|
7208
7139
|
}
|
|
7209
7140
|
}
|
|
7210
7141
|
|
|
7142
|
+
/*
|
|
7143
|
+
* ══ 자기 일정 표 — 이 프로세스가 **스스로** 도는 일의 전부 (2026-09-17) ══════════════════
|
|
7144
|
+
*
|
|
7145
|
+
* 의도(오너 확정): **MCP 는 세션의 심박과 도구 응답만 맡는다. 저장소 관측은 세포 훅의 것이다.**
|
|
7146
|
+
* 타이머는 이 표에만 적는다 — 표가 없어서 08-23~09-15 에 타이머가 하나씩 얹혔고(관측·메일·두뇌),
|
|
7147
|
+
* 어느 것도 클라이언트를 안 가리고 유휴에도 안 멈췄다. 새 일을 얹으려면 이 표에 행을 더하고
|
|
7148
|
+
* `lane`(누구에게 도는가)·`idle`(조용할 때 어떻게 하는가)을 적는다. 기존 틱에 얹지 않는다.
|
|
7149
|
+
*
|
|
7150
|
+
* lane 'all' = 모든 클라이언트 · 'hookless' = 훅 레인이 없는 클라이언트만(`hookLane()` 이 거짓 — Codex)
|
|
7151
|
+
* idle 'never' = 늘 같은 주기(심박 — 살아 있음이 신호다, 서버는 90초 침묵을 죽음으로 본다)
|
|
7152
|
+
* 'slow' = 마지막 도구 호출 뒤 IDLE_AFTER_MS 가 지나면 IDLE_EVERY_MS 에 한 번만. 도구를 부르면 곧 복귀
|
|
7153
|
+
*
|
|
7154
|
+
* 타이머가 아닌 것(심박 바퀴 안에서 사건·시각으로 도는 것): resolve(10분 TTL·사건 기반) · 판 비교(30분) ·
|
|
7155
|
+
* 되찾기 훑기(5분) · 자동 올리기(90초 하한, 동의 관문). 메일은 훅 레인이 있으면 도구 호출 때 긷는다(60초 상한).
|
|
7156
|
+
*/
|
|
7157
|
+
const IDLE_AFTER_MS = Math.max(1000, Number(cfg.GENETICS_IDLE_AFTER_MS) || 30 * 60_000);
|
|
7158
|
+
const IDLE_EVERY_MS = Math.max(1000, Number(cfg.GENETICS_IDLE_EVERY_MS) || 5 * 60_000);
|
|
7159
|
+
let lastToolAt = Date.now();
|
|
7160
|
+
const SCHEDULE = [
|
|
7161
|
+
{ name: 'beat', every: BEAT_EVERY_MS, lane: 'all', idle: 'never', start: startBeat,
|
|
7162
|
+
why: '세션이 살아 있음을 서버에 알린다 — 생사 판정은 서버 심박이 소유한다(결정 2026-08-27)' },
|
|
7163
|
+
{ name: 'observe', every: OBSERVE_EVERY_MS, lane: 'hookless', idle: 'slow', tick: observeTick,
|
|
7164
|
+
why: '훅이 없는 클라이언트(Codex)의 전사를 세포가 밖에서 훑게 한다 — 훅이 있으면 훅이 한다' },
|
|
7165
|
+
{ name: 'mail', every: MAIL_POLL_MS, lane: 'hookless', idle: 'slow', tick: () => { mailPull().catch(() => {}); },
|
|
7166
|
+
ready: mailPollReady,
|
|
7167
|
+
why: '훅 레인이 없는 세션의 편지함 — 훅 레인이 있으면 도구 호출 때 긷는다' },
|
|
7168
|
+
];
|
|
7169
|
+
|
|
7170
|
+
function startSchedule() {
|
|
7171
|
+
if (cfg.GENETICS_NO_BEAT) return; // 심박을 끄는 사람은 나머지도 안 원한다
|
|
7172
|
+
for (const row of SCHEDULE) {
|
|
7173
|
+
if (row.start) { row.start(); continue; }
|
|
7174
|
+
if (row.ready && !row.ready()) continue;
|
|
7175
|
+
const run = () => {
|
|
7176
|
+
if (row.lane === 'hookless' && hookLane()) return;
|
|
7177
|
+
const now = Date.now();
|
|
7178
|
+
if (row.idle === 'slow' && now - lastToolAt > IDLE_AFTER_MS && now - (row.lastRun || 0) < IDLE_EVERY_MS) return;
|
|
7179
|
+
row.lastRun = now;
|
|
7180
|
+
row.tick();
|
|
7181
|
+
};
|
|
7182
|
+
run();
|
|
7183
|
+
row.timer = setInterval(run, row.every);
|
|
7184
|
+
row.timer.unref?.(); // 창을 닫으면 노드가 남지 않게
|
|
7185
|
+
}
|
|
7186
|
+
}
|
|
7187
|
+
|
|
7211
7188
|
if (IS_MAIN) {
|
|
7212
7189
|
ensureCellOnBoot(); // 세포가 먼저다 — 스킬보다 이쪽이 「무엇을 못 하는가」를 정한다
|
|
7213
7190
|
ensureSkillOnBoot();
|
|
@@ -7216,9 +7193,7 @@ if (IS_MAIN) {
|
|
|
7216
7193
|
* 동안」이라, 시작·끝을 따로 알릴 필요가 없다. 자격이나 주소가 없으면 조용히 안 뛴다 —
|
|
7217
7194
|
* 꺼진 것이지 고장이 아니다(그림 자격이 없을 때와 같은 태도).
|
|
7218
7195
|
*/
|
|
7219
|
-
|
|
7220
|
-
startMailPoll(); // 메일 폴은 심박의 조기 반환에 매이지 않는다
|
|
7221
|
-
startObserve(); // 훅이 안 걸리는 도구(Codex)도 관측을 올린다 — 위 머리말 참조
|
|
7196
|
+
startSchedule();
|
|
7222
7197
|
process.stdin.setEncoding('utf8');
|
|
7223
7198
|
process.stdin.on('data', (chunk) => { buf += chunk; pump(); });
|
|
7224
7199
|
process.stdin.on('end', () => { pump(true); stopBeat().catch(() => {}); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genetics-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.20.0",
|
|
4
4
|
"description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
|
|
5
5
|
"author": "주식회사 LIM (LIM Inc.)",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"node": ">=18"
|
|
19
19
|
},
|
|
20
20
|
"scripts": {
|
|
21
|
-
"test": "node smoke.mjs && node notify.test.mjs && bash ../cell/tests/cellcycle.sh",
|
|
21
|
+
"test": "node smoke.mjs && node notify.test.mjs && node schedule.test.mjs && bash ../cell/tests/cellcycle.sh",
|
|
22
22
|
"prepack": "find skills -name __pycache__ -type d -prune -exec rm -rf {} + 2>/dev/null; bash ../cell/build-all.sh",
|
|
23
23
|
"prepublishOnly": "npm test",
|
|
24
24
|
"release": "bash release.sh"
|
|
@@ -50,6 +50,7 @@ version: "4.0.0"
|
|
|
50
50
|
| 8 초안 발행·수정 | 초안 링크 | 새 문서 `publish_report` · 고치면 8단계 「수정 절차」 | 같은 링크 · 사람이 고친 초안을 모르고 덮지 않음 | 사람 — `update_report` 에 `body_rev` 잠금 칸이 아직 없음 | 사람이 고친 초안이면 덮어도 되는가 |
|
|
51
51
|
| 9 발행 뒤 대조 | 블록 목록 | `get_report(id, outline: true)` | `aside.marg` 수 = 절 수 · 옛 서식 블록 0 · 빈 블록 0 | 사람(블록 목록을 읽음) — 대조 기계 없음 | — |
|
|
52
52
|
| 9 열람 확인 | 캡처 | 열람 주소를 연다 | 칩·굵게·링크가 원문 글자가 아니라 서식으로 섬 · 도판 쌍·판단 절이 섬 | 사람 눈 — 못 열면 까닭을 핸드오프에 | — |
|
|
53
|
+
| 9a 첨삭 메모 반영 | 반영된 판 · 닫힌 메모 · 까닭을 단 답글 | `list_reports` → `list_notes` → `update_report` → `resolve_note`·`reply_note`(9a단계) | 미반영 메모 0 — 남은 것은 전부 답글로 까닭이 달림 | 사람이 메모를 닮 · 서버는 미반영 수만 셈(`open_notes`, 우리 팀에게만) | 사람이 「메모 해놨다」고 할 때만 돈다 — 무엇을 고칠지 되묻지 않음 |
|
|
53
54
|
| 10 공개 직전 날짜 | `report.json` | `update_report` 한 번(날짜만 고치는 이 판은 독자 검토 없이 로컬 렌더·dry_run 만) | 작성일 세 자리가 한 값이고 공개하는 날(3c) | `gazette.py` `date_scan()` ⚠(세 자리가 다르면) · 공개하는 날인지는 사람 | 공개일 |
|
|
54
55
|
| 10 공개 | 공개된 링크 | `release_report(id)` | 사용자 승인 | 사람(승인 행위) | **반드시** 공개 승인 |
|
|
55
56
|
|
|
@@ -335,6 +336,21 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
|
|
|
335
336
|
**「그림 N(표지마크 M)」은 진짜 셈이다.** 표지 마크는 고객사에 로고가 등록돼 있으면 **서버가 넣으므로** `M = 1` 이 정상이고 본문 도판은 `N − M` 이다.
|
|
336
337
|
**`그림 0` 을 합격 기준으로 삼지 마라** — 도판을 실은 문서에서 그 기준은 자기 근거를 지운다.
|
|
337
338
|
|
|
339
|
+
### 9a단계 — 첨삭 메모 반영: 「메모 해놨다」를 들으면
|
|
340
|
+
|
|
341
|
+
사람은 초안을 읽으면서 머리 도구줄 **「첨삭 메모」**로 자리를 집어 고칠 곳을 적는다. 우리 팀(admin·editor)만 달고 보며, **고객에게는 공개 뒤에도 안 보인다**(검토 회신 핀과 다른 점). 사람이 「메모 해놨다」「첨삭 달아 놨다」고 하면 아래 순서로 반영한다 — **무엇을 고칠지 대화창에서 되묻지 않는다.** 메모가 이미 말한다.
|
|
342
|
+
|
|
343
|
+
| 순서 | 도구 | 할 일 |
|
|
344
|
+
|---|---|---|
|
|
345
|
+
| 1 | `list_reports` | 줄 끝에 `첨삭 메모 N건 미반영` 이 붙은 문서를 찾는다. 문서 번호를 들었으면 건너뛴다 |
|
|
346
|
+
| 2 | `list_notes(id)` | 미반영 메모를 읽는다. `anchor.path`(예 `case:k7m2p9/fix`)가 **고칠 칸**, `anchor.quote_exact` 가 사람이 **본 글**이다. 답글은 사람이 덧붙인 설명이다 |
|
|
347
|
+
| 3 | `get_report(id)` | 발행 데이터를 받는다. 머리 두 줄의 정본이 발행물이면 덮기 전에 무엇을 덮는지 본다 |
|
|
348
|
+
| 4 | `update_report(dry_run: true)` → `update_report` | 메모마다 그 칸을 고쳐 **한 번에** 보낸다. `uid` 는 받은 값 그대로 되돌린다 — 흘리면 메모가 자리를 잃는다. 초안은 초안 그대로 남는다 |
|
|
349
|
+
| 5 | `resolve_note(id, note, rev)` | **고친 뒤에만.** 반영이 실린 `body_rev` 를 함께 준다. 먼저 부르면 미반영 목록에서 빠져 다시 안 보인다 |
|
|
350
|
+
| 6 | `reply_note(id, note, body)` | 못 고치거나 다르게 본 메모는 **닫지 말고** 그 메모에 이유를 답글로 단다 |
|
|
351
|
+
|
|
352
|
+
`anchor_state` 가 `unmoored`·`reparented` 면 문서가 바뀌어 자리를 잃은 메모다 — `quote_exact` 로 본문에서 찾아 고치고, 못 찾으면 답글로 묻는다. 끝나면 사람에게 **한 줄로** 알린다: 반영 N건 · 답글로 되물은 N건 · 문서 링크.
|
|
353
|
+
|
|
338
354
|
### 8단계′ — 전달: 단일 HTML (Lim 밖 고객·메일 첨부)
|
|
339
355
|
|
|
340
356
|
```bash
|
|
@@ -470,7 +486,8 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
470
486
|
`role: figure` · `provider: user-provided` · `origin`(누가 제공했는지 — **비우면 거절된다**)
|
|
471
487
|
으로 두고 `file` 에 로컬 절대경로를 주면
|
|
472
488
|
화질을 계단으로 내려 예산(장당 300,000B) 안에 앉힌다. `sections[].plates` 에 id 를
|
|
473
|
-
적어야 자리가 선다. 캡션은 **이 화면에서
|
|
489
|
+
적어야 자리가 선다. 캡션은 **이 화면에서 독자가 볼 자리**를 명사형으로 적는다(「8.0 행의 두 날짜와 8.4 행의 종료일 참고」) —
|
|
490
|
+
「…을 보라」 같은 해라체 명령은 어디에도 없다(반드시 지키는 것 12 · 렌더러 ⚠) —
|
|
474
491
|
본문이 이미 말한 것을 되풀이하면 도판이 장식이 된다.
|
|
475
492
|
다만 `hero`·`chapter`·`eof` 는 자리가 없다. 표지의 빈
|
|
476
493
|
마당은 미완성이 아니라 문법이고, 문서는 판권지로 닫는다(꼬리 카드가 없다).
|
|
@@ -511,13 +528,20 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
511
528
|
「사진」(스냅샷)·「단추」(버튼)는 물건을 비유로 부른 것이다 — 둘 다 결정권자에게 뜻이 닿지 않는다.
|
|
512
529
|
같은 버릇을 소설 연구가 잰 적이 있다: 감정을 이름으로 부르지 않고 몸으로만 보여 주는 비율이 사람 글 29 %, AI 글 8 %(StoryScope, arXiv 2604.03136 —
|
|
513
530
|
소설 연구이므로 수치는 유추일 뿐, 규칙의 근거는 2026-09-17 반려다). 결정 문서는 직접 부르는 쪽이 맞다.
|
|
514
|
-
|
|
531
|
+
**공급사 용어도 같다** — AWS 의 Extended Support 를 「연장 지원료」로 직역하면 「지원해 주는 돈」으로 읽힌다(같은 날 반려). 요금·기능은 그것이 무엇인지로 부르고(「구버전 유지 요금」),
|
|
532
|
+
공급사 이름은 첫 등장 자리나 용어표에 한 번만 붙인다. **이 스킬의 규칙 문장도 원고 말투가 아니다** — 캡션 규칙 「무엇을 보라는 것인지」를 베껴 캡션이 「…을 보라.」가 됐다.
|
|
533
|
+
독자에게 말 거는 자리는 하십시오체, 기록 자리는 개조식이고, 해라체 명령은 어디에도 없다.
|
|
534
|
+
집행자: `gazette.py` `text_scan()` ⚠ 「선택지를 「길」로 부른 곳」·「해라체 명령이 N곳」(두 경로 — 운영 반영 확인 2026-09-17) · 비유·조어·직역 일반은 **없음 — 사람의 눈**(경위: `references/history.md` §13).
|
|
515
535
|
13. **우리 쪽 보안·운영 실수와 자격증명 취급 사실을 문서에 적지 않는다 — 구두로 전한다.**
|
|
516
536
|
「비밀번호가 메신저로 오갔음」「키를 메일로 보냈음」은 교체 권고의 근거로 옳아도 고객 문서의 자리가 아니다 —
|
|
517
537
|
박제되고 책임 소재를 글로 남긴다. 권고는 사용자에게 채팅으로만, 문서에는 그 사건의 원인·조치·확인만(`references/content-pipeline.md` 유형 L).
|
|
518
538
|
경계: 그 사건 범위 안의 조언(심사에 넘긴 시험 계정을 「외부에 알려진 것으로 봄」)은 남긴다.
|
|
519
539
|
집행자: `gazette.py` `text_scan()` ⚠ 「자격증명 노출」 — 자격증명 명사와 노출 경로·동사(메신저·메일·평문·오갔·보냈…)가 한 문장 30자 안에 같이 선 곳만. 명사 하나(「비밀번호 재설정」)는 안 셈 ·
|
|
520
540
|
「우리 실수」 일반(「늦게 알았음」 류)은 **없음 — 사람 눈 · 독자 검토 decider 관점**(경위: `references/history.md` §14).
|
|
541
|
+
14. **건이 둘 이상인 절의 표·도판·인용은 절이 아니라 그 건에 묶는다** — `cases[].tables`·`plates`·`quotes`(`references/lim-portal-format.md` §11-10).
|
|
542
|
+
절 칸에 두면 렌더러가 건 목록 **뒤**에 한꺼번에 세워, 독자는 건 다섯을 글로만 읽고 나서 표 다섯·그림 넷을 만나고 어느 건의 근거인지는 캡션 글자로만 잇는다.
|
|
543
|
+
절 칸은 절 전체에 걸치는 근거(조치 전후 대조표)만. 전폭 도판은 건 안에 못 서니 절에 둔다(건에 두면 ⚠).
|
|
544
|
+
집행자: 렌더러 `gazette.py` `case_evidence()` — 자리를 만들 뿐 「절에 둔 것이 사실은 한 건의 것」인지는 **사람 눈**(경위: `references/history.md` §15).
|
|
521
545
|
|
|
522
546
|
## 흔한 실패 모드
|
|
523
547
|
|
|
@@ -552,7 +576,7 @@ doc-pptx 의 슬롭 3종 중 **대시 대비 헤드라인**("복붙에서 위임
|
|
|
552
576
|
| 학생 보고서투 · 완충어 · 근거 없는 총평 · 단독 부정 종결 | `tone.mjs` S1/S2 | 로컬 렌더 산출물 |
|
|
553
577
|
| 금칙 값(고객사·지역·제품명·티켓) | `build.py` `forbid` | **로컬 렌더만** — 포털 발행은 `report_publish.php report_client_fields()` 가 깎아 낸다 |
|
|
554
578
|
| 개발 식별자(3 — 대상 목록은 `lim-portal-format.md` §10-1) | `gazette.py` `text_scan()` ⚠ | **두 경로 다** |
|
|
555
|
-
| 자격증명 명사 + 노출 경로·동사 근접 · 우리 실수 서술(13번) | `gazette.py` `text_scan()` ⚠ 「자격증명 노출」 — 명사 하나(「비밀번호 재설정」)는 안 셈 · 「우리 실수」 일반은 **없음 — 사람 눈** | **두 경로 다**
|
|
579
|
+
| 자격증명 명사 + 노출 경로·동사 근접 · 우리 실수 서술(13번) | `gazette.py` `text_scan()` ⚠ 「자격증명 노출」 — 명사 하나(「비밀번호 재설정」)는 안 셈 · 「우리 실수」 일반은 **없음 — 사람 눈** | **두 경로 다** |
|
|
556
580
|
| 판단 항목·확인 요청에 기한 없음 · `asks` 절 없음 · 선택지 표에 근거 등급 없음 · `asks` 절 표의 동일값 열(브리핑) | `gazette.py` `brief_checks()` ⚠ | **두 경로 다** |
|
|
557
581
|
| 본문 절의 회차 서술(「이번 회차…」) | `gazette.py` `round_talk_scan()` ⚠ | **두 경로 다** |
|
|
558
582
|
| 본문의 진행 상태 어휘(정해짐·회신 대기·N판) · 개수로 상태를 세는 표 제목 · 주체 없는 결정 명사(3b) | `gazette.py` `text_scan()` 과정 어휘 ⚠ | **두 경로 다** |
|
|
@@ -560,7 +584,8 @@ doc-pptx 의 슬롭 3종 중 **대시 대비 헤드라인**("복붙에서 위임
|
|
|
560
584
|
| 권고 낱말(제목·표 열·절 이름·여백 주석·요약면)(10번) | `gazette.py` ⚠ **W1** — 이름에 「의견」이 든 절은 안 셈 | **두 경로 다** |
|
|
561
585
|
| 고객·제3자 자료에서 낸 도판(`user-provided`)의 출처 줄에 원본 위치(쪽·행·주소·건 §) 누락(11번) | `gazette.py` ⚠ **W2** | **두 경로 다** |
|
|
562
586
|
| 도판 출처 줄의 제작 설명·그림 번호 · 출처 줄 길이(0.5단계 5a행) | `gazette.py` `text_scan()` ⚠ — 캡션은 안 봄 | **두 경로 다** |
|
|
563
|
-
| 선택지를 「길」로 부른 곳(스킬 장치 이름의 유출, 12번) | `gazette.py` `text_scan()` ⚠ —
|
|
587
|
+
| 선택지를 「길」로 부른 곳(스킬 장치 이름의 유출, 12번) | `gazette.py` `text_scan()` ⚠ — 「막대 길이」「길게」는 안 세고 「어느 길이」·열 머리 「길」은 셈. 「사진」「단추」 같은 비유는 **없음 — 사람의 눈** | **두 경로 다** |
|
|
588
|
+
| 해라체 명령(「…을 보라」「…하라」, 12번) | `gazette.py` `text_scan()` ⚠ — 「보라색」「정할 것」은 안 셈. 공급사 용어 직역(「지원료」)은 **없음 — 사람의 눈** | **두 경로 다** |
|
|
564
589
|
| 건 행 이름(`labels`) 낱말이 다른 절 부제·표 제목·열 머리에 다시 섬 · 선택지 기호 한 글자에 이름 둘(0.5단계 3행) | `gazette.py` `text_scan()` ⚠ · 뜻이 같은지는 사람 눈 | **두 경로 다** |
|
|
565
590
|
| 그림·표 번호 가리킴 — 없는 번호 · 조사 · 서지 못한/서지 않는 칸의 토큰 · `plates` 순서(0.5단계 6행) | `gazette.py` `text_scan()`·`build()` ⚠ — 번호 셈은 `fig_order()` 한 곳 | **두 경로 다** |
|
|
566
591
|
| 괄호 안 부연 · 흐름도 칸 `note` · 도판 캡션 문장 수(readability R11) | `gazette.py` `text_scan()` ⚠ — 한 줄 45자·요약·도입문 문장 수는 사람 눈 | **두 경로 다** |
|
|
@@ -249,7 +249,7 @@
|
|
|
249
249
|
"tone": "neutral",
|
|
250
250
|
"short": "선택지",
|
|
251
251
|
"sub": "같은 칸으로 견준 세 선택지",
|
|
252
|
-
"lede": "검토한 세 선택지를 같은 칸으로 견줌. 열이
|
|
252
|
+
"lede": "검토한 세 선택지를 같은 칸으로 견줌. 열이 선택지이고 행이 견줄 것이며, 어느 쪽이 맞는지 판정하는 열은 두지 않음.",
|
|
253
253
|
"marg": {
|
|
254
254
|
"why": "세 선택지는 각각 무엇을 얻고 잃는가?",
|
|
255
255
|
"key": "방식 B 는 우리 자료 규모에서 정확도가 꺾인 사례가 있음.",
|
|
@@ -347,9 +347,9 @@
|
|
|
347
347
|
"sub": "사실과 갈라 둔 의견",
|
|
348
348
|
"lede": "03·04절의 사실을 보고 우리회사가 맞다고 보는 쪽. 의견이며, 정하는 것은 01절 회신임.",
|
|
349
349
|
"marg": {
|
|
350
|
-
"why": "우리회사는 어느
|
|
350
|
+
"why": "우리회사는 어느 쪽이 맞다고 보는가?",
|
|
351
351
|
"key": "의견은 이 표 한 장에만 둠.",
|
|
352
|
-
"advice": "회신 뒤 일정은 고른
|
|
352
|
+
"advice": "회신 뒤 일정은 고른 선택지가 정함. 이 의견은 그 일정과 별개임."
|
|
353
353
|
},
|
|
354
354
|
"tables": [
|
|
355
355
|
{
|
|
@@ -134,6 +134,10 @@
|
|
|
134
134
|
규칙의 근거는 반려 원문이다.
|
|
135
135
|
처방 — 반드시 지키는 것 12(물건은 이름으로), 0.5단계 3행에 「스킬의 장치 이름도 장치 이름」, 본보기 JSON 의 「길」 19곳을 「선택지」·「쪽」으로 고침,
|
|
136
136
|
렌더러 `text_scan()` 에 「선택지를 「길」로 부른 곳」 ⚠(RX_GIL — 「길이」「길게」「길다」 제외) 과 시험 2건.
|
|
137
|
+
같은 날 두 번 더 — ① 발행 뒤 블록 목록에 「어느 길이」「지나는 길과」·열 머리 「길」이 남아 RX_GIL 을 넓힘(선택지를 세는 앞말 뒤의 「길이」·이든/과/와·칸 하나가 「길」).
|
|
138
|
+
② 「지원료라는 단어가 요금을 지원해준다는 느낌으로 읽히지 않겠음?」·「"…7월 31일을 보라." 이거 싸가지 없지 않겠어?」 — AWS 의 Extended Support 를 「연장 지원료」로 직역한 것과,
|
|
139
|
+
캡션 규칙 문장 「무엇을 보라는 것인지」를 캡션 말투로 베낀 것. 둘 다 스킬 문장의 유출이라 규칙 문장을 고쳤다(캡션 = 독자가 볼 자리, 명사형 · 공급사 용어는 그것이 무엇인지로) 하고
|
|
140
|
+
렌더러에 해라체 명령 ⚠(RX_CMD) 과 시험 2건을 더했다. 공급사 용어 직역은 기계 없음 — 사람 눈.
|
|
137
141
|
|
|
138
142
|
## 14. 「비밀번호가 메신저로 오갔음」 (2026-09-17) — SKILL 반드시 지키는 것 13 · process.md 원리 4 · content-pipeline 유형 L · 카피 게이트 새 행
|
|
139
143
|
|
|
@@ -145,3 +149,17 @@
|
|
|
145
149
|
렌더러 `RX_CRED`(자격증명 명사 + 노출 경로·동사가 한 문장 30자 안에 같이 선 곳만 — 명사 하나는 안 셈) ⚠ 과 §10-1 행, 시험 2건(★ + 대조군).
|
|
146
150
|
실측(문장 11개): 노출 문장 4/4 잡음 · 정상 문장 6/7 통과 · 오탐 1(「메일 발송 기능이 계정 정보와 무관」 — 경고라 사람이 넘긴다).
|
|
147
151
|
「우리 실수」 일반(「늦게 알았음」 류)은 기계가 못 재 사람 눈과 독자 검토 decider 관점에 둔다.
|
|
152
|
+
운영 반영 확인(2026-09-17): 서버 배포(HEAD 2301f523) 뒤 운영 `dry_run` 탐침이 「자격증명 노출 서술이 1곳 … 「비밀번호가 메신저」」를 냈다 — 두 경로 다 산다. genetics-mcp 2.19.2 로 게시.
|
|
153
|
+
|
|
154
|
+
## 15. 「이 스킬 병신이라서 이딴 허접한 관보가 탄생하는 거임?」 (2026-09-17) — 렌더러 건별 근거 · SKILL 반드시 지키는 것 14 · lim-portal-format §11-10
|
|
155
|
+
|
|
156
|
+
HUnion 사후 보고 초안 182, 3절(건 다섯). 사용자가 브라우저에서 집은 것은 3절 머리의 인용 「Guideline 2.1(a) … Unable to connect」 —
|
|
157
|
+
파생 앱(3.5 건)의 거절문인데 절 인용이라 건 목록 뒤에 서서 3.1 건의 거절문처럼 읽혔고, 출처 줄에 어느 앱인지·날짜(9. 16.)도 틀렸다.
|
|
158
|
+
검토해 보니 스킬이 아니라 렌더러였다: `gazette.py Doc.chapter()` 가 배치 순서의 유일한 정본이고 상수·설정·시험이 없으며,
|
|
159
|
+
cases 절의 DOM 은 간지 → 여백 주석 → 건 전부 → 인용 → 자료표 → 차트 → 흐름도 → 도해 → 도판 → (그리드 밖) 전폭. 모든 첨부는
|
|
160
|
+
절(`c['sec']`)에서만 읽고 건(`sheets_block`)은 여덟 칸만 읽었다. 문서 §11-8 「인용은 절 본문 맨 앞」도 코드와 달랐다(kind 블록 뒤).
|
|
161
|
+
운영 초안 182 의 본문 텍스트로 잰 순서: 건 03-1~03-5 → 인용 → 표 3-1~3-5 → 그림 3-1~3-4 — 표 다섯의 주인은 3.1·3.2·3.3·3.1·3.5 건.
|
|
162
|
+
처방 — 렌더러 `case_evidence()`(건 `quotes`·`tables`·`plates` 를 건 article 안에 인용 → 표 → 도판 순으로, 번호는 절과 한 셈·건이 먼저),
|
|
163
|
+
절 인용을 문서대로 kind 블록 앞으로, `_plate_ids()`·`_tables_of()` 로 경고 스캔(세로 긴 도판·앞 절 가리킴·표 번호·안 걸린 도판)이 건의 것도 세게,
|
|
164
|
+
건 안의 전폭 자산 ⚠, 서버 `report_data_validate()` 건 `plates` 게이트, `api.php` 자리 잡은 도판 셈, MCP 스키마 세 칸, CSS `.sheet__ev`,
|
|
165
|
+
시험 「건별 근거」. 집필 규칙은 SKILL 14 · §4 주석 · §11-1 「놓을 절」 · §11-10. 편집기는 이 칸을 아직 모른다(절 `quotes`·`tables` 와 같은 처지).
|
|
@@ -189,7 +189,7 @@ python3 assets/render.py <report.json>
|
|
|
189
189
|
| `stats[]` | `value` · `unit` · `label` · `accent` · `href` · `go` | 첫 칸만 `accent: true` |
|
|
190
190
|
| `meta[]` | `label` · `value` | `[k, v]` 배열도 받는다 |
|
|
191
191
|
| `open_items[]` | `title` · `priority`(`must` 면 「가장 시급」) · `body` · `refs[]` | `refs` 는 **첫 하나만** 「근거」 단추가 된다 |
|
|
192
|
-
| `cases[]` | `group` · `title` · `grade` · `symptom` · `cause` · `fix` · `how_to_check` · `remaining` · `source_url` · `source_text` · **`uid`** | 번호(`anchor`)는 **렌더러가 매기고**, `uid` 는 **서버가
|
|
192
|
+
| `cases[]` | `group` · `title` · `grade` · `symptom` · `cause` · `fix` · `how_to_check` · `remaining` · `source_url` · `source_text` · **`uid`** · `quotes` · `tables` · `plates` | 번호(`anchor`)는 **렌더러가 매기고**, `uid` 는 **서버가 발급한다**. `quotes`·`tables`·`plates` 는 **그 건에 묶인 근거**(11-10절) — 절의 같은 칸과 모양이 같고 건 필드표 바로 아래 선다 |
|
|
193
193
|
| `appendix[]` | `no` · `title` · `rows[{label,value}]` · `note` | `rows` 가 비면 그 항목이 통째로 빠진다 |
|
|
194
194
|
| `colophon` | `title` · `sub` · `rows[]` · `mark` · `badge` · `note` · `tail` | 넷(`title`·`rows`·`mark`·`note`)이 다 비면 판권지가 안 선다 |
|
|
195
195
|
| `grades{}` | `키: [클래스, 설명, lvl?, tag?]` | 7절 |
|
|
@@ -265,6 +265,9 @@ python3 assets/render.py <report.json>
|
|
|
265
265
|
| `quotes` | 원문 인용 목록(11-8절). `kind` 와 무관하다 | 인용이 빠진다 |
|
|
266
266
|
| `diagrams` | 도해(인라인 SVG) 목록(11-9절). `kind` 와 무관하다 | 도해가 빠진다 |
|
|
267
267
|
|
|
268
|
+
`kind:"cases"` 절에서 **건이 둘 이상이면 표·도판·인용은 절이 아니라 건에 묶는다**(`cases[].tables`·`plates`·`quotes`, 11-10절).
|
|
269
|
+
절 칸에 두면 건 목록 **뒤**에 한꺼번에 서서 어느 건의 근거인지 캡션 글자로만 이어진다. 절 칸은 절 전체에 걸치는 근거의 자리다.
|
|
270
|
+
|
|
268
271
|
### ⚠ `no` 와 `id` 는 **안 읽힌다**
|
|
269
272
|
|
|
270
273
|
절 번호와 앵커는 **배열 순서**가 정한다 — `sections[i]` 가 `ch = "%02d" % (i+1)` · `id = "sec-(i+1)"`.
|
|
@@ -396,8 +399,9 @@ python3 assets/render.py <report.json>
|
|
|
396
399
|
| 절에 실을 내용이 없습니다 | `kind` 가 가리키는 데이터가 비었다 |
|
|
397
400
|
| 그룹에 배정되지 않은 건 N개 | `cases[].group` 이 어긋났다(그룹이 아예 없을 때) |
|
|
398
401
|
| 그림 N장은 놓을 자리가 없어 | `logo`·`figure` 밖의 `role`(`hero`·`chapter`·`eof`) |
|
|
399
|
-
| 도판 N장이 어느 절에도 걸리지 않아 | `sections[].plates` 에 그 id 를 안 적었다 — 11-1절 |
|
|
400
|
-
| 절 …의 도판 「…」를 assets 에서 못 찾았습니다 | `plates` 가 없는 `assets[].id` 를 가리킨다 |
|
|
402
|
+
| 도판 N장이 어느 절에도 걸리지 않아 | `sections[].plates` 나 `cases[].plates` 에 그 id 를 안 적었다 — 11-1절·11-10절 |
|
|
403
|
+
| 절 …의 도판 「…」를 assets 에서 못 찾았습니다 | `plates` 가 없는 `assets[].id` 를 가리킨다(건 `plates` 면 「건 NN-n 의 도판」) |
|
|
404
|
+
| 건 NN-n 에 묶인 도판 「…」는 전폭(wide)인데 건 안이라 본문 단 폭으로 섭니다 | `cases[].plates` 가 `wide:true` 자산을 가리킨다 — 건 안은 두 단 그리드라 전폭이 못 선다. 절 `plates` 로 옮기거나 `wide` 를 끈다(11-10절) |
|
|
401
405
|
| 도판 「…」에 그림 바이트가 없습니다 | `file`·`source` 없이 이름만 왔다. 로컬 `render.py` 는 `file` 을 바이트로 굳히지 않아 `file` 이 있어도 뜬다 — 로컬에서는 이 줄을 빼고 나머지 ⚠ 를 읽는다 |
|
|
402
406
|
| `chapters`·`figure`(**최상위 칸**) 는 자리가 없어 | 옛 서식 전용 칸. `assets[].role:"figure"` 와 다른 것이다 |
|
|
403
407
|
| 절 …의 `principles`/`logic` 은 자리가 없어 | 같음 |
|
|
@@ -450,7 +454,7 @@ python3 assets/render.py <report.json>
|
|
|
450
454
|
| ⚠ | 문턱 | 처방 |
|
|
451
455
|
|---|---|---|
|
|
452
456
|
| 절 N 「이름」이 글로만 서 있습니다(M자) | 산문 400자 이상인데 표·흐름도·차트·도판·인용·도해 0 | 그 절의 요지를 표·흐름도·차트·도판·인용·도해 중 하나로 세운다(§11-1~11-9) |
|
|
453
|
-
|
|
|
457
|
+
| %s 인용 %d 에 출처 줄이 없습니다 — 원본 위치 한 구절(주소·쪽·행)을 적습니다 | `quotes[]` 에 `source_text`·`source_url` 둘 다 없다(`%s` 는 「절 N」 또는 「건 NN-n」) | 원문의 위치 한 구절(주소·쪽·행)을 적는다(§11-8) |
|
|
454
458
|
| 절 %s 도해 「%s」의 글자가 표시 %dpx 로 섭니다 — %dpx 이상이 되게 wide 로 두거나 노드를 줄입니다 | 도해 SVG 의 가장 작은 font-size × (표시 폭 ÷ viewBox 폭) < 15px(readability R1 과 같은 잣대) | `wide: true` 로 전폭에 세우거나 노드·라벨을 줄인다(§11-9) |
|
|
455
459
|
| 절 %s 도해 「%s」가 %d바이트입니다 — 상한 %d바이트, 노드를 줄이거나 도판(PNG)으로 싣습니다 | `diagrams[].svg` 가 250,000B 초과 | 도해를 나누거나 PNG 도판으로 |
|
|
456
460
|
| 절 %s 도해 「%s」에 viewBox 가 없거나 svg 가 아닙니다 — 폭을 알 수 없어 싣지 않습니다 | 루트 `<svg>` 에 viewBox 가 없다 | `diagram.mjs export` 로 굳힌 SVG 를 넣는다(§11-9) |
|
|
@@ -472,10 +476,11 @@ python3 assets/render.py <report.json>
|
|
|
472
476
|
| 앞 절이 뒤 절을 가리키는 곳이 %d개입니다(%s) — 낱말·규칙은 첫 등장 자리에서 풀고, 뒤 절이 앞 절을 되짚는 방향만 둡니다(readability R12h) | 절 i(배열 순서, 1부터)의 산문 — `sections[].name·sub·lede·paragraphs` · `tables[].caption·cols·rows·note` · `flow.caption·note`·`rows[].no`(건 좌표 꼴 `^§?\d{1,2}[-.]\d{1,2}$` 의 앞 숫자)·`cells[].text·note` · `marg(why/key/advice)` · 그 절이 `kind:"cases"` 면 같은 그룹 `cases[].symptom·cause·fix·how_to_check·remaining` · `kind:"asks"` 면 `cta[]`·`open_items[].title·body` · `sections[].plates` 가 가리키는 `assets[].caption·source_text` — 에서 정규식 `§\s*(\d+)\.\d+` 와 `(?<![\d.])(\d{1,2})절` 의 앞 숫자가 i 보다 큰 것을 센다. 「N차 … 제N절」(남의 문서의 절 번호, 예 「검수 보고 4차 제5절」)은 건너뛴다. `summary[].lead·body` 와 `refs[]` 는 세지 않는다 — 요약 근거 게이트(§8)가 `refs` 로 서고 「해당 절」·「근거」 단추가 되므로 뜀이 아니다. `%s` 는 자리와 걸린 낱말 최대 3개 | 낱말·규칙·예시 선언은 **첫 등장 자리에서 한 구절**로 풀고, 뒤 절 번호 대신 그 자리에서 뜻이 서는 말을 둔다. 근거 건은 `refs` 로만. 뒤 절이 앞 절을 되짚는 방향(05절에서 「03절 표」)은 둔다(readability R12h · SKILL 0.5단계 7행) |
|
|
473
477
|
| 표 「%s」의 「%s」 열은 모든 행이 같습니다 — 차이를 못 나르는 열은 주석으로 | `doc_type` 에 「브리핑」 · `kind:"asks"` 절의 `tables[]` 가운데 행이 셋 이상인 표에서, 첫 열을 뺀 한 열이 모든 행에서 같은 값(빈 칸 아님)이다. asks 밖 절(일정 표 「누가」 열 등)·행 둘 이하·빈 칸뿐인 열은 안 센다 | 그 열을 빼고 ※ 주석 한 줄로(visual-first §5). 선택지를 가르는 열만 한눈표에 둔다(§12.3b 대칭 불변식) |
|
|
474
478
|
| 건 행 이름 「%s」 낱말이 %d곳에도 쓰였습니다(%s) — 한 낱말은 한 장치에만, 다른 자리는 다른 낱말로 | 데이터가 준 `labels` 의 `symptom`·`cause`·`fix`·`check` 값이 `kind≠cases` 절의 `sub`·`tables[].caption`·`tables[].cols` 에 낱말로 선다(뒤에 조사 하나까지 같은 낱말 — 「판단력」·「판단이다」는 다른 낱말). 괄호 안은 자리 이름 최대 3개. `labels` 를 안 준 기본 이름·건 절 자신·절 이름·도입문·산문은 안 본다 | 의견 절의 열·부제는 「의견」으로(§3.5 · visual-first §3) |
|
|
475
|
-
| 선택지를 「길」로 부른 곳이 %d곳 있습니다(%s 「%s」 …) — 「길」은 스킬의 장치 이름입니다. 원고에서는 선택지를 그것이 실제로 무엇인지로 부릅니다 | 글자 자리 전부(과정 어휘와 같은 자리 — `history`·`labels`·`origin` 등 제외)에서 정규식 `(
|
|
479
|
+
| 선택지를 「길」로 부른 곳이 %d곳 있습니다(%s 「%s」 …) — 「길」은 스킬의 장치 이름입니다. 원고에서는 선택지를 그것이 실제로 무엇인지로 부릅니다 | 글자 자리 전부(과정 어휘와 같은 자리 — `history`·`labels`·`origin` 등 제외)에서 정규식 `RX_GIL`(gazette.py) — 「두 길의」「복제본 길」「고른 길별」「지나는 길과」, 앞말이 선택지를 세는 말인 「어느 길이」「두 길이」, 칸 하나가 「길」인 열 머리는 걸리고 「막대 길이」「길게」「길다」는 안 걸린다 | 선택지는 그것이 실제로 무엇인지(고객이 부르는 이름)로 — 「복제본 전환」「직접 업그레이드」「현행 유지」. 「길」은 스킬 문서 안의 장치 이름일 뿐이다(SKILL 반드시 지키는 것 12) |
|
|
480
|
+
| 해라체 명령이 %d곳 있습니다(%s 「%s」 …) — 독자에게 말 거는 자리는 하십시오체, 기록 자리는 개조식입니다. 캡션·확인 방법은 「… 참고」「…에서 확인」처럼 명사형으로 | 글자 자리 전부(「길」과 같은 자리)에서 정규식 `RX_CMD`(gazette.py) — 낱말 끝 「보라·하라·열라·누르라·찾으라·읽으라·두라·말라·가라·보시오·하시오」 뒤에 마침표·공백·닫는 따옴표가 오거나 글이 끝나면 센다. 「보라색」(한글이 이어짐)·「정할 것」(명사구)은 안 센다 | 캡션·확인 방법은 명사형(「… 참고」「…에서 확인」), 독자에게 말 거는 자리는 하십시오체(5a). 경위: 캡션 규칙 「무엇을 보라는 것인지」가 캡션이 됐다(2026-09-17) |
|
|
476
481
|
| 선택지 기호 %s 에 이름이 %d개입니다(「%s」·「%s」) — 기호는 한 목록에만 붙이고 이름은 표마다 같게 | 모든 표에서 한 축(열 머리 줄, 또는 행 첫 칸 열)마다 `^([A-Z])[\s·:.)]+(\S.*)$` 꼴을 모으고, 그 축의 기호가 A 부터 차례(둘 이상)일 때만 선택지 정의로 친다. 문서 전체에서 한 글자에 이름이 둘 이상이면 센다(짧은 이름과 긴 이름이 갈린 것도). 산문(「방식 A」)·A 부터 차례가 아닌 축(부분을 가리키는 행)·괄호 부연 차이는 안 센다 | A·B·C 는 한 목록에만. 같은 선택지는 표마다 같은 이름(visual-first §3) |
|
|
477
482
|
| 개발 식별자가 %d곳에 있습니다(%s) — 고객은 조회할 수 없습니다. 커밋·파일·도구·시험 건수는 원고 밖 상태 표에만 | 글자 자리 전부를 주소를 지운 뒤(부록 포함, `history`·`labels`·`origin`·`file`·`source_url`·`href`·`data` 와 `uid`·`id`·`plates`·`refs` 제외) 정규식으로 센다 — 16진 해시(a~f 와 숫자가 섞인 7~64자) · 코드 파일(`.ts`·`.tsx`·`.js`·`.jsx`·`.mjs`·`.py`·`.php`·`.kt`·`.swift`·`.java`) · `src/`·`lib/` · 개발 도구(`tsc`·`eslint`·`prettier`·`jest`·`pytest`·`playwright`·`git`) · `N/N 통과` · 커밋·브랜치·머지·풀 리퀘스트·스테이징. 괄호 안은 「자리 「낱말」」 최대 3개 | 확인 수준은 이용자가 확인할 수 있는 동작으로(content-pipeline §4.2 유형 B). 「나머지」·「머지않아」·「푸시 알림」·숫자만인 번호·날짜·본문 주소·대문자로 시작하는 제품 이름(「Node.js」)은 안 걸린다 |
|
|
478
|
-
| 자격증명 노출 서술이 %d곳에 있습니다(%s) — 우리 쪽 취급 사실은 문서가 아니라 구두로. 권고 문장만 남기고 경로 서술은 지웁니다 | 글자 자리 전부(개발 식별자와 같은 제외 칸)에서 자격증명 명사(비밀번호·패스워드·암호(「암호화」 제외)·접속/계정/로그인 정보·자격 증명·API/비밀/인증서 키·토큰)와 노출 경로·동사(메신저·카카오톡·카톡·채팅·문자·메일·이메일·슬랙·평문·공유·전달·주고받·오갔·보냈·받았·남겼)가 **한 문장 30자 안에 같이** 선 곳
|
|
483
|
+
| 자격증명 노출 서술이 %d곳에 있습니다(%s) — 우리 쪽 취급 사실은 문서가 아니라 구두로. 권고 문장만 남기고 경로 서술은 지웁니다 | 글자 자리 전부(개발 식별자와 같은 제외 칸)에서 자격증명 명사(비밀번호·패스워드·암호(「암호화」 제외)·접속/계정/로그인 정보·자격 증명·API/비밀/인증서 키·토큰)와 노출 경로·동사(메신저·카카오톡·카톡·채팅·문자·메일·이메일·슬랙·평문·공유·전달·주고받·오갔·보냈·받았·남겼)가 **한 문장 30자 안에 같이** 선 곳 | 그 문장을 지우고 권고(「교체 권장」)만 남긴다 — 취급 사실은 구두로. 「비밀번호 재설정 기능」·「암호화 키」·「푸시 알림이 문자로」처럼 명사 하나나 경로 하나만 선 문장은 안 걸린다 |
|
|
479
484
|
| 없는 그림·표 번호를 %d곳에서 가리킵니다(%s) — 그림은 {{그림:id}} 로 가리키면 순서가 바뀌어도 따라갑니다 | 글자 자리의 `(그림·표) N-M`(이어 쓴 `· ~ ,` 번호까지 풀어서, `~` 는 같은 절이면 범위)이 렌더러의 번호 셈(§11-1 순서)에 없다. 「」 인용 안·`history`·`origin` 과 앞이 한글인 것(「지표 1-2」)은 건너뛴다 | 그림은 `{{그림:<id>}}` 토큰으로(§11-6). 다른 문서의 번호는 「」 로 인용 |
|
|
480
485
|
| 번호 뒤 조사가 맞지 않는 곳이 %d개입니다(「%s」 → 「%s」) — 끝 숫자의 소리로 고르거나 {{그림:id\|은}} 으로 | 마지막 번호 뒤 조사(`으로·은·는·이·가·을·를·과·와·로`)가 끝 숫자의 받침과 어긋난다 — 받침 있음 0·1·3·6·7·8, 없음 2·4·5·9, 「으로」는 0·3·6 만 | 조사를 고치거나 `{{그림:id\|은}}` |
|
|
481
486
|
| 그림 가리킴 토큰 %d곳이 번호로 서지 못합니다(%s) — plates 에 걸린 assets[].id 나 flow:<절 번호>, 조사는 은·이·을·과·으로 짝으로 | `{{그림:…}}` 의 id 가 번호 셈에 없거나(plates 에 안 걸림) 조사가 짝 밖이다. 토큰은 글자 그대로 남는다 | id 를 plates 에 걸거나 조사를 짝 안에서 |
|
|
@@ -555,7 +560,7 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
|
|
|
555
560
|
| 그림 바이트 | `file`(**로컬 절대경로**) 또는 `source`(허용 호스트 https). 둘 다 주면 `file` 이 이긴다 |
|
|
556
561
|
| 캡션 | `assets[].caption` — 번호 뒤에 붙는다. **번호는 적지 않는다**(자리가 매긴다). **한 문장으로** — 마침표가 둘이면 문장 분리가 캡션을 줄로 가르고 렌더러가 ⚠ 로 센다(부연은 괄호·가운뎃점으로). 정직성은 끝의 한 낱말(「— 도안」·「— 재현」·「— 합성」)이면 된다. 판정 칩은 렌더러가 문장 블록 밖에 붙인다(2026-09-09) |
|
|
557
562
|
| 출처 | `assets[].source_text` — 독자가 원본에 닿는 **위치 한 구절**(쪽·행·주소·건 § 가운데 하나, readability R12e). 제작 메타는 `origin` 에만, 바깥 원본이 없는 우리 도안·흐름은 비운다 |
|
|
558
|
-
| 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠). 본문 단 도판을 먼저, 전폭을 뒤에 적는다(순서가 번호와 갈리면 ⚠) |
|
|
563
|
+
| 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠). 본문 단 도판을 먼저, 전폭을 뒤에 적는다(순서가 번호와 갈리면 ⚠). 한 건의 근거인 화면은 절이 아니라 **그 건**에 적는다 — `cases[].plates`(11-10절). 건 안은 늘 본문 단 폭이라 전폭 자산은 절에 둔다(건에 두면 ⚠) |
|
|
559
564
|
| 전폭 | `assets[].wide: true` — 두 단 밖, 절 끝에 문서 폭(readability §1 기준 창의 전폭)으로 선다. 그 절의 그림 번호는 본문 단 도판 뒤다 |
|
|
560
565
|
|
|
561
566
|
**전폭(`wide: true`)은 렌더러 `7f1cc25` 부터 그리고, 2026-09-12 운영 배포(`3a9fe3f`)에 들어 있다.** 두 단 밖에 전폭 폭으로 서고
|
|
@@ -565,7 +570,7 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
|
|
|
565
570
|
→ 그런 화면을 근거로 실어야 하면 **글자를 도판에만 맡기지 말고 부록에 전사(轉寫)하라.**
|
|
566
571
|
도판은 「그 화면이 실재한다」를 맡고, 읽히는 것은 부록이 맡는다.
|
|
567
572
|
|
|
568
|
-
**그림 번호의 순서**는 절마다 차트 → 흐름도 → 본문 단 도판(`plates` 순) → 전폭 도판이다(`gazette.py Doc.fig_order()` 한 벌 —
|
|
573
|
+
**그림 번호의 순서**는 절마다 건에 묶인 도판(건 순 · `cases[].plates` 순) → 차트 → 흐름도 → 본문 단 도해 → 본문 단 도판(`plates` 순) → 전폭 도해 → 전폭 도판이다(`gazette.py Doc.fig_order()` 한 벌 —
|
|
569
574
|
그리는 쪽·가리킴 토큰·번호 대조 ⚠ 가 같은 셈을 읽는다). 표는 따로 센다. 셈은 서버 기준이고 **바이트와 무관**하다 —
|
|
570
575
|
그림 바이트가 없어 로컬에서 안 그려지는 도판도 자리를 차지하므로 로컬 번호가 운영 번호와 같다. `assets` 에 없는 id 는 세지 않는다.
|
|
571
576
|
본문에서 그림을 가리킬 때는 번호를 손으로 적지 않고 `{{그림:<id>}}` 로 쓴다(§11-6 — ⚠ **운영 반영 확인 전**이라 배포 뒤 열람 DOM 확인 전에는 쓰지 않는다).
|
|
@@ -598,9 +603,9 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
|
|
|
598
603
|
|
|
599
604
|
### 캡션에 무엇을 쓰는가
|
|
600
605
|
|
|
601
|
-
**이 화면에서
|
|
606
|
+
**이 화면에서 독자가 볼 자리**를 명사형으로 쓴다(「8.0 행의 두 날짜와 8.4 행의 종료일 참고」). 「…을 보라」 같은 해라체 명령은 어디에도 없다 — 렌더러가 ⚠ 로 센다.
|
|
602
607
|
본문이 이미 말한 것을 되풀이하면 도판이 장식이 된다.
|
|
603
|
-
그리고 `alt` 는 캡션의 사본이 아니다 — 캡션은
|
|
608
|
+
그리고 `alt` 는 캡션의 사본이 아니다 — 캡션은 「독자가 볼 자리」이고 `alt` 는 「무엇이 보이는가」다.
|
|
604
609
|
|
|
605
610
|
## 11-2. 흐름도 — 「A 가 되면 B 가 되고 C 가 된다」
|
|
606
611
|
|
|
@@ -877,7 +882,8 @@ AWS 요금 조사(2026-09)에서 트래픽·로그·DB 용량이 8월 1일 앞
|
|
|
877
882
|
| `translation` | 우리말 풀이 한 문장(선택). 우리 글이므로 표기를 푼다 |
|
|
878
883
|
|
|
879
884
|
인용은 절 본문의 **맨 앞**에 선다(원문이 가장 강한 근거이고 표·도해는 그것을 줄인 것). `kind` 와 무관하다.
|
|
880
|
-
번호는 없다 — 그림·표가 아니다.
|
|
885
|
+
번호는 없다 — 그림·표가 아니다. (2026-09-17 까지 렌더러는 주석과 달리 kind 블록 **뒤**에 붙였다 — 같은 날 코드를 이 문장에 맞췄다.)
|
|
886
|
+
한 건에 딸린 인용(그 건의 거절문·회신)은 `cases[].quotes` 로 **그 건 안**에 둔다(11-10절) — 절에 두면 첫 건의 것처럼 읽힌다.
|
|
881
887
|
|
|
882
888
|
## 11-9. 도해 — Archify 를 흡수한다 (2026-09-16)
|
|
883
889
|
|
|
@@ -918,6 +924,35 @@ AWS 요금 조사(2026-09)에서 트래픽·로그·DB 용량이 8월 1일 앞
|
|
|
918
924
|
관문(⚠, §10-1): 바이트 250,000 초과 · viewBox 없음 · **가장 작은 글자 × (표시 폭 ÷ viewBox 폭) < 15px**(readability R1, `svg` 길에만).
|
|
919
925
|
`diagram.mjs` 가 같은 잣대로 먼저 재므로 렌더러 ⚠ 는 마지막 그물이다. 그림 번호는 차트 → 흐름도 → 도해 → 도판 순.
|
|
920
926
|
|
|
927
|
+
## 11-10. 건에 묶는 근거 — `cases[].quotes` · `tables` · `plates` (2026-09-17)
|
|
928
|
+
|
|
929
|
+
건이 여럿인 절에서 표·도판·인용을 절 칸에 두면 렌더러는 건 목록을 다 그린 **뒤**에 인용 → 표 → 그림을 한꺼번에 세운다
|
|
930
|
+
(`gazette.py Doc.chapter()` 의 순서). HUnion 사후 보고(초안 182)에서 건 다섯 뒤에 인용 하나·표 다섯·그림 넷이 서서,
|
|
931
|
+
3.5 건의 애플 거절 인용이 3.1 건의 거절문처럼 읽혀 반려됐다. 근거는 **그 건의 필드표 바로 아래** 서야 한다.
|
|
932
|
+
|
|
933
|
+
```json
|
|
934
|
+
"cases": [
|
|
935
|
+
{ "group": "c", "title": "파생 앱도 같은 원인으로 탈락함", "grade": "해외 측정 확인",
|
|
936
|
+
"symptom": "…", "cause": "…", "fix": "…", "how_to_check": "…",
|
|
937
|
+
"quotes": [{ "text": "Guideline 2.1(a) … Unable to connect", "source_text": "파생 앱 심사 메시지 · App Store Connect · 2026. 9. 16.", "verdict": "직접 확인" }],
|
|
938
|
+
"tables": [{ "caption": "파생 앱 사이트 해외 측정", "cols": ["경로", "국내", "미국 두 곳"], "rows": [["첫 화면", "{{ok|실제 페이지}}", "{{bad|검사 화면}}"]] }],
|
|
939
|
+
"plates": ["apple-403"] }
|
|
940
|
+
]
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
| 칸 | 모양 | 어디에 서나 |
|
|
944
|
+
|---|---|---|
|
|
945
|
+
| `cases[].quotes` | 절의 `quotes` 와 같다(11-8절) | 그 건 article 안, 필드표 아래 첫 자리 |
|
|
946
|
+
| `cases[].tables` | 절의 `tables` 와 같다(11-3절) | 인용 다음. 표 번호는 절과 **한 셈** — 건의 표가 먼저 |
|
|
947
|
+
| `cases[].plates` | `assets[].id` 목록(11-1절) | 표 다음. 그림 번호도 절과 한 셈 — 건의 도판이 먼저. **늘 본문 단 폭** — 전폭(`wide`) 자산은 절 `plates` 에(건에 두면 ⚠ 로 세고 본문 단 폭으로 그린다) |
|
|
948
|
+
|
|
949
|
+
- 절 칸(`sections[].tables`·`plates`·`quotes`)은 **절 전체에 걸치는 근거**의 자리로 남는다 — 조치 전후 대조표처럼 여러 건이 함께 읽는 표.
|
|
950
|
+
- 없는 id 를 `cases[].plates` 에 적으면 서버가 발행을 거절한다(절 `plates` 와 같은 게이트). 「어느 절에도 안 걸린 도판」 ⚠ 는 건 `plates` 도 자리로 센다.
|
|
951
|
+
- `{{그림:<id>}}` 토큰은 건 도판도 가리킨다(번호 셈이 한 벌).
|
|
952
|
+
- 편집기(`lim-report` 서식)는 절의 `quotes`·`tables` 와 마찬가지로 이 칸을 아직 모른다 — 편집기로 고쳐 발행하면 빠진다(발행 데이터 정본일 때만 산다).
|
|
953
|
+
|
|
954
|
+
집행자: 렌더러 `gazette.py Doc.case_evidence()` · 서버 `report_publish.php report_data_validate()`(건 `plates` 게이트) · 시험 `tests/builder.test.py` 「건별 근거」.
|
|
955
|
+
|
|
921
956
|
## 12. 판정 밖의 두 모드 — 가르치는 문서 · R&D 브리핑
|
|
922
957
|
|
|
923
958
|
독자가 **판정**하는 문서(검수 보고)와 **배우는** 문서, **결정하는** 문서는 뼈대가 다르다.
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
| R11 본문·표 | 문단 사이 ≥ 1.5em · 한 줄 한글 14~45자 · 헤더 ≥ 본문 ×1.5 굵게 · 문단 ≤ 220자 · 산문 참조 번호 ≤ 3 | gazette.css 토큰 · gazette-report `read.mjs` · §10-1 | ⚠ |
|
|
43
43
|
| R11a 문장 | **한 문장 ≤ 60자**(공백 제외). 관보는 한 문장을 한 줄로 세우므로 문장 길이가 곧 줄 길이 — 본문 단은 한 줄에 45자, 60자면 1.4줄. 판단 항목·건처럼 가장 많이 읽는 자리는 **45자 안**으로 써 한 줄에 세운다. 접히는 문장은 마침표에서 둘로 끊는다(실측: 고객사 결정 브리핑의 앞선 판에서 45~57자 문장 20개가 한 번씩 접혔음) | gazette.py `text_scan`(60자) · 45자는 사람 눈 | ⚠ |
|
|
44
44
|
| R11b 표 칸 | 칸 ≤ **20자**(4열 이하) · ≤ **12자**(5열 이상) — 열 폭 130~180px 에서 두 줄. 설명은 ※ 주석·본문으로. 흐름도 칸 ≤ 20자·note ≤ 12자(칸 폭 200px), 조건은 note. 칸에 넣던 숫자 나열은 **행으로 푼다**(「A 구간 1,234 · B 구간 5,678 …」 한 칸 → 구간마다 한 행) | gazette.py `text_scan`(칸·흐름도 칸·note) · 숫자 나열은 사람 눈 | ⚠ |
|
|
45
|
-
| R11c 도판 글 | 캡션 ≤ **60자** 한 문장(
|
|
45
|
+
| R11c 도판 글 | 캡션 ≤ **60자** 한 문장(독자가 볼 자리만, 명사형 — 문장 수도 센다) · 출처 줄 ≤ **40자**, 원본 위치 한 구절만(R12e) | gazette.py `text_scan` | ⚠ |
|
|
46
46
|
| R11d 요약면 | 현황 줄 ≤ **80자** 한두 문장 · 수치 칸 라벨 ≤ **16자**(이름표) · 요약 문단 본문 ≤ 160자 세 문장 | gazette.py `text_scan`(자수) · 문장 수는 사람 눈 | ⚠ |
|
|
47
47
|
| R11e 절 머리 | 도입문 ≤ 90자 두 문장 · 여백 주석 why 30 · key 45 · advice 40자(19자/줄 단) | gazette.py `text_scan`(자수) · 도입문 문장 수는 사람 눈 | ⚠ |
|
|
48
48
|
| R11f 판단 항목 | 본문 ≤ 220자 네 문장 — 첫 문장은 기한, 상세는 건으로 | gazette.py `text_scan`(자수 · 문장 수는 R12b) | ⚠ |
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
|
|
68
68
|
- 색이 **뜻**에 맞는가 — 초록=정상·노랑=주의·빨강=위험처럼 독자가 이미 아는 뜻을 거스르지 않는가.
|
|
69
69
|
- 색이 **선택지의 옳고 그름**을 판정하지 않는가 — 한 길만 빨강, 다른 길만 초록이면 그림이 답을 먼저 말한다. 빨강·초록은 이용자 상태를 나르는 그림에만.
|
|
70
|
-
- 캡션이
|
|
70
|
+
- 캡션이 독자가 볼 자리를 가리키는가 — 그림이 이미 보여 주는 것을 되풀이하면 장식이다. 「…을 보라」 같은 해라체 명령은 렌더러가 센다.
|
|
71
71
|
- 라벨이 **자기 표식**을 가리키는가 — 기계는 거리만 잰다.
|
|
72
72
|
- 한 그림에 **한 생각**인가 — 패널 둘까지.
|
|
73
73
|
- 선택지 도판들이 **같은 잣대**인가 — 한 선택지만 합성·실사·크게·먼저가 아닌가, 칩 이름이 캡션·표의 이름과 같은가(R13d 의 뜻 쪽).
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
<!doctype html><meta charset="utf-8">
|
|
2
|
-
<!-- pair.mjs 가 쓴 두 칸 비교 도판 — 손으로 고치지 말고 인자를 바꿔 다시 쓴다.
|
|
3
|
-
layout column · 칸 350 · 사이 20 · 좌우 20 · 배율 1.34(원본 520px ÷ dpr 2) · 틀 높이 489
|
|
4
|
-
left plate-pair-before.png 520×730 · right plate-pair-after.png 520×470 · crop-top 0 · mark 12,54,236,120 · seam 54 -->
|
|
5
|
-
<style>
|
|
6
|
-
body{margin:0;background:#fff;font-family:Pretendard,"Apple SD Gothic Neo","Noto Sans KR",sans-serif;color:#111}
|
|
7
|
-
.wrap{width:760px;padding:16px 20px 20px;box-sizing:border-box;background:#fff;display:flex;gap:20px;align-items:flex-start}
|
|
8
|
-
.col{width:350px;flex:none}
|
|
9
|
-
.lab{font-size:17px;font-weight:700;height:28px;line-height:28px;margin:0 0 8px;display:flex;align-items:center;gap:10px;white-space:nowrap}
|
|
10
|
-
.tag{display:inline-block;background:#e5e7eb;color:#111;font-size:15px;font-weight:700;line-height:24px;padding:0 10px;border-radius:2px;flex:none}
|
|
11
|
-
.shot{position:relative;width:350px;height:491px;border:1px solid #bbb;border-radius:4px;overflow:hidden;box-sizing:border-box;background:#fff}
|
|
12
|
-
.shot img{position:absolute;left:0;display:block;width:348px}
|
|
13
|
-
.mark{position:absolute;border:3px solid #111;border-radius:6px;box-sizing:border-box}
|
|
14
|
-
.seam{position:absolute;left:10px;right:10px;border-top:2px dashed #595959}
|
|
15
|
-
</style>
|
|
16
|
-
<div class="wrap">
|
|
17
|
-
<div class="col">
|
|
18
|
-
<p class="lab"><span class="tag">그대로 두면</span>카드가 보임</p>
|
|
19
|
-
<div class="shot"><img src="plate-pair-before.png" alt="" data-dpr="2" data-min-text="13" style="top:0px"><div class="mark" style="left:16.06px;top:72.28px;width:315.88px;height:160.62px"></div></div>
|
|
20
|
-
</div>
|
|
21
|
-
<div class="col">
|
|
22
|
-
<p class="lab"><span class="tag">바꾸면</span>카드가 빠지고 지표가 올라옴</p>
|
|
23
|
-
<div class="shot"><img src="plate-pair-after.png" alt="" data-dpr="2" data-min-text="13" style="top:0px"><div class="seam" style="top:71.28px"></div></div>
|
|
24
|
-
</div>
|
|
25
|
-
</div>
|