genetics-mcp 2.19.3 → 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 +23 -3
- package/skills/genetics-report/references/history.md +13 -0
- package/skills/genetics-report/references/lim-portal-format.md +41 -7
|
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
|
|
@@ -515,13 +531,17 @@ render·css·폰트를 함께 얹는다. 직접 부르면 옛 서식 시트가
|
|
|
515
531
|
**공급사 용어도 같다** — AWS 의 Extended Support 를 「연장 지원료」로 직역하면 「지원해 주는 돈」으로 읽힌다(같은 날 반려). 요금·기능은 그것이 무엇인지로 부르고(「구버전 유지 요금」),
|
|
516
532
|
공급사 이름은 첫 등장 자리나 용어표에 한 번만 붙인다. **이 스킬의 규칙 문장도 원고 말투가 아니다** — 캡션 규칙 「무엇을 보라는 것인지」를 베껴 캡션이 「…을 보라.」가 됐다.
|
|
517
533
|
독자에게 말 거는 자리는 하십시오체, 기록 자리는 개조식이고, 해라체 명령은 어디에도 없다.
|
|
518
|
-
집행자: `gazette.py` `text_scan()` ⚠ 「선택지를 「길」로 부른 곳」·「해라체 명령이 N곳」(두 경로 —
|
|
534
|
+
집행자: `gazette.py` `text_scan()` ⚠ 「선택지를 「길」로 부른 곳」·「해라체 명령이 N곳」(두 경로 — 운영 반영 확인 2026-09-17) · 비유·조어·직역 일반은 **없음 — 사람의 눈**(경위: `references/history.md` §13).
|
|
519
535
|
13. **우리 쪽 보안·운영 실수와 자격증명 취급 사실을 문서에 적지 않는다 — 구두로 전한다.**
|
|
520
536
|
「비밀번호가 메신저로 오갔음」「키를 메일로 보냈음」은 교체 권고의 근거로 옳아도 고객 문서의 자리가 아니다 —
|
|
521
537
|
박제되고 책임 소재를 글로 남긴다. 권고는 사용자에게 채팅으로만, 문서에는 그 사건의 원인·조치·확인만(`references/content-pipeline.md` 유형 L).
|
|
522
538
|
경계: 그 사건 범위 안의 조언(심사에 넘긴 시험 계정을 「외부에 알려진 것으로 봄」)은 남긴다.
|
|
523
539
|
집행자: `gazette.py` `text_scan()` ⚠ 「자격증명 노출」 — 자격증명 명사와 노출 경로·동사(메신저·메일·평문·오갔·보냈…)가 한 문장 30자 안에 같이 선 곳만. 명사 하나(「비밀번호 재설정」)는 안 셈 ·
|
|
524
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).
|
|
525
545
|
|
|
526
546
|
## 흔한 실패 모드
|
|
527
547
|
|
|
@@ -564,8 +584,8 @@ doc-pptx 의 슬롭 3종 중 **대시 대비 헤드라인**("복붙에서 위임
|
|
|
564
584
|
| 권고 낱말(제목·표 열·절 이름·여백 주석·요약면)(10번) | `gazette.py` ⚠ **W1** — 이름에 「의견」이 든 절은 안 셈 | **두 경로 다** |
|
|
565
585
|
| 고객·제3자 자료에서 낸 도판(`user-provided`)의 출처 줄에 원본 위치(쪽·행·주소·건 §) 누락(11번) | `gazette.py` ⚠ **W2** | **두 경로 다** |
|
|
566
586
|
| 도판 출처 줄의 제작 설명·그림 번호 · 출처 줄 길이(0.5단계 5a행) | `gazette.py` `text_scan()` ⚠ — 캡션은 안 봄 | **두 경로 다** |
|
|
567
|
-
| 선택지를 「길」로 부른 곳(스킬 장치 이름의 유출, 12번) | `gazette.py` `text_scan()` ⚠ — 「막대 길이」「길게」는 안 세고 「어느 길이」·열 머리 「길」은 셈. 「사진」「단추」 같은 비유는 **없음 — 사람의 눈** | **두 경로 다**
|
|
568
|
-
| 해라체 명령(「…을 보라」「…하라」, 12번) | `gazette.py` `text_scan()` ⚠ — 「보라색」「정할 것」은 안 셈. 공급사 용어 직역(「지원료」)은 **없음 — 사람의 눈** | **두 경로 다**
|
|
587
|
+
| 선택지를 「길」로 부른 곳(스킬 장치 이름의 유출, 12번) | `gazette.py` `text_scan()` ⚠ — 「막대 길이」「길게」는 안 세고 「어느 길이」·열 머리 「길」은 셈. 「사진」「단추」 같은 비유는 **없음 — 사람의 눈** | **두 경로 다** |
|
|
588
|
+
| 해라체 명령(「…을 보라」「…하라」, 12번) | `gazette.py` `text_scan()` ⚠ — 「보라색」「정할 것」은 안 셈. 공급사 용어 직역(「지원료」)은 **없음 — 사람의 눈** | **두 경로 다** |
|
|
569
589
|
| 건 행 이름(`labels`) 낱말이 다른 절 부제·표 제목·열 머리에 다시 섬 · 선택지 기호 한 글자에 이름 둘(0.5단계 3행) | `gazette.py` `text_scan()` ⚠ · 뜻이 같은지는 사람 눈 | **두 경로 다** |
|
|
570
590
|
| 그림·표 번호 가리킴 — 없는 번호 · 조사 · 서지 못한/서지 않는 칸의 토큰 · `plates` 순서(0.5단계 6행) | `gazette.py` `text_scan()`·`build()` ⚠ — 번호 셈은 `fig_order()` 한 곳 | **두 경로 다** |
|
|
571
591
|
| 괄호 안 부연 · 흐름도 칸 `note` · 도판 캡션 문장 수(readability R11) | `gazette.py` `text_scan()` ⚠ — 한 줄 45자·요약·도입문 문장 수는 사람 눈 | **두 경로 다** |
|
|
@@ -150,3 +150,16 @@
|
|
|
150
150
|
실측(문장 11개): 노출 문장 4/4 잡음 · 정상 문장 6/7 통과 · 오탐 1(「메일 발송 기능이 계정 정보와 무관」 — 경고라 사람이 넘긴다).
|
|
151
151
|
「우리 실수」 일반(「늦게 알았음」 류)은 기계가 못 재 사람 눈과 독자 검토 decider 관점에 둔다.
|
|
152
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) |
|
|
@@ -556,7 +560,7 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
|
|
|
556
560
|
| 그림 바이트 | `file`(**로컬 절대경로**) 또는 `source`(허용 호스트 https). 둘 다 주면 `file` 이 이긴다 |
|
|
557
561
|
| 캡션 | `assets[].caption` — 번호 뒤에 붙는다. **번호는 적지 않는다**(자리가 매긴다). **한 문장으로** — 마침표가 둘이면 문장 분리가 캡션을 줄로 가르고 렌더러가 ⚠ 로 센다(부연은 괄호·가운뎃점으로). 정직성은 끝의 한 낱말(「— 도안」·「— 재현」·「— 합성」)이면 된다. 판정 칩은 렌더러가 문장 블록 밖에 붙인다(2026-09-09) |
|
|
558
562
|
| 출처 | `assets[].source_text` — 독자가 원본에 닿는 **위치 한 구절**(쪽·행·주소·건 § 가운데 하나, readability R12e). 제작 메타는 `origin` 에만, 바깥 원본이 없는 우리 도안·흐름은 비운다 |
|
|
559
|
-
| 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠). 본문 단 도판을 먼저, 전폭을 뒤에 적는다(순서가 번호와 갈리면 ⚠) |
|
|
563
|
+
| 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠). 본문 단 도판을 먼저, 전폭을 뒤에 적는다(순서가 번호와 갈리면 ⚠). 한 건의 근거인 화면은 절이 아니라 **그 건**에 적는다 — `cases[].plates`(11-10절). 건 안은 늘 본문 단 폭이라 전폭 자산은 절에 둔다(건에 두면 ⚠) |
|
|
560
564
|
| 전폭 | `assets[].wide: true` — 두 단 밖, 절 끝에 문서 폭(readability §1 기준 창의 전폭)으로 선다. 그 절의 그림 번호는 본문 단 도판 뒤다 |
|
|
561
565
|
|
|
562
566
|
**전폭(`wide: true`)은 렌더러 `7f1cc25` 부터 그리고, 2026-09-12 운영 배포(`3a9fe3f`)에 들어 있다.** 두 단 밖에 전폭 폭으로 서고
|
|
@@ -566,7 +570,7 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
|
|
|
566
570
|
→ 그런 화면을 근거로 실어야 하면 **글자를 도판에만 맡기지 말고 부록에 전사(轉寫)하라.**
|
|
567
571
|
도판은 「그 화면이 실재한다」를 맡고, 읽히는 것은 부록이 맡는다.
|
|
568
572
|
|
|
569
|
-
**그림 번호의 순서**는 절마다 차트 → 흐름도 → 본문 단 도판(`plates` 순) → 전폭 도판이다(`gazette.py Doc.fig_order()` 한 벌 —
|
|
573
|
+
**그림 번호의 순서**는 절마다 건에 묶인 도판(건 순 · `cases[].plates` 순) → 차트 → 흐름도 → 본문 단 도해 → 본문 단 도판(`plates` 순) → 전폭 도해 → 전폭 도판이다(`gazette.py Doc.fig_order()` 한 벌 —
|
|
570
574
|
그리는 쪽·가리킴 토큰·번호 대조 ⚠ 가 같은 셈을 읽는다). 표는 따로 센다. 셈은 서버 기준이고 **바이트와 무관**하다 —
|
|
571
575
|
그림 바이트가 없어 로컬에서 안 그려지는 도판도 자리를 차지하므로 로컬 번호가 운영 번호와 같다. `assets` 에 없는 id 는 세지 않는다.
|
|
572
576
|
본문에서 그림을 가리킬 때는 번호를 손으로 적지 않고 `{{그림:<id>}}` 로 쓴다(§11-6 — ⚠ **운영 반영 확인 전**이라 배포 뒤 열람 DOM 확인 전에는 쓰지 않는다).
|
|
@@ -878,7 +882,8 @@ AWS 요금 조사(2026-09)에서 트래픽·로그·DB 용량이 8월 1일 앞
|
|
|
878
882
|
| `translation` | 우리말 풀이 한 문장(선택). 우리 글이므로 표기를 푼다 |
|
|
879
883
|
|
|
880
884
|
인용은 절 본문의 **맨 앞**에 선다(원문이 가장 강한 근거이고 표·도해는 그것을 줄인 것). `kind` 와 무관하다.
|
|
881
|
-
번호는 없다 — 그림·표가 아니다.
|
|
885
|
+
번호는 없다 — 그림·표가 아니다. (2026-09-17 까지 렌더러는 주석과 달리 kind 블록 **뒤**에 붙였다 — 같은 날 코드를 이 문장에 맞췄다.)
|
|
886
|
+
한 건에 딸린 인용(그 건의 거절문·회신)은 `cases[].quotes` 로 **그 건 안**에 둔다(11-10절) — 절에 두면 첫 건의 것처럼 읽힌다.
|
|
882
887
|
|
|
883
888
|
## 11-9. 도해 — Archify 를 흡수한다 (2026-09-16)
|
|
884
889
|
|
|
@@ -919,6 +924,35 @@ AWS 요금 조사(2026-09)에서 트래픽·로그·DB 용량이 8월 1일 앞
|
|
|
919
924
|
관문(⚠, §10-1): 바이트 250,000 초과 · viewBox 없음 · **가장 작은 글자 × (표시 폭 ÷ viewBox 폭) < 15px**(readability R1, `svg` 길에만).
|
|
920
925
|
`diagram.mjs` 가 같은 잣대로 먼저 재므로 렌더러 ⚠ 는 마지막 그물이다. 그림 번호는 차트 → 흐름도 → 도해 → 도판 순.
|
|
921
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
|
+
|
|
922
956
|
## 12. 판정 밖의 두 모드 — 가르치는 문서 · R&D 브리핑
|
|
923
957
|
|
|
924
958
|
독자가 **판정**하는 문서(검수 보고)와 **배우는** 문서, **결정하는** 문서는 뼈대가 다르다.
|