genetics-mcp 2.8.1 → 2.9.1

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.
Binary file
package/bin/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "aarch64-apple-darwin": "c0f7ca318d5e4e040b6118e835cbad3e467597d61e94d150c555ed050b5d1f57",
3
- "aarch64-unknown-linux-musl": "5588db11a4aad41a65609d4cf14f389d106fb8b74023aa745ae4765738243006",
4
- "x86_64-apple-darwin": "857d738a752776813e9ee52e972e93d046502aa8364ec9fe2d1ece8a0d770914",
5
- "x86_64-unknown-linux-musl": "f01bf90ab7014764487773b9ac91ad8532ac6a727eedd6ff50151c3e6eab127e"
2
+ "aarch64-apple-darwin": "0be4865075eacbd2c83cb2fad02b92458969a33eea636b2f68d7d704f6cf60b5",
3
+ "aarch64-unknown-linux-musl": "206709a078fa1814b49aa8e59012a28542496bc1d08b4e75c9ee716c00a09a11",
4
+ "x86_64-apple-darwin": "519dfad58dc6e7dc67a820bd386e4a6d54eb4e3c5c706f5178d147d0d0ed0cae",
5
+ "x86_64-unknown-linux-musl": "56b7c532ae56e814abfbe460d7761ddec3a0631f2eea0cc68ad29d6efe2fa2cd"
6
6
  }
Binary file
Binary file
package/genetics.mjs CHANGED
@@ -635,12 +635,65 @@ const REPORT_DATA = {
635
635
  * 믿는데 실제로는 무시된다 — 거짓말하는 인자다. 고를 것이 생기면 그때 연다.
636
636
  */
637
637
  title_tag: { type: 'string', description: '브라우저 탭 제목' },
638
- title: { type: 'string', description: '표지 제목' },
638
+ title: { type: 'string', description: '표지 대제목 **첫 행** (굵게)' },
639
+ title_sub: {
640
+ type: 'string',
641
+ description: '표지 대제목 **둘째 행** — 회차·부제. "1차 보고" 처럼 **짧게**. '
642
+ + '여기는 42px 표제 자리라 문장을 넣으면 조판이 무너진다. 한 문단 요약은 subtitle 로.',
643
+ },
639
644
  kicker: { type: 'string', description: '표지 윗줄 (예: "회사명 · 프로젝트")' },
640
- subtitle: { type: 'string', description: '표지 한 문단 요약' },
645
+ subtitle: { type: 'string', description: '표지 한 문단 요약 — 대제목 **아래**에 앉는다' },
641
646
  description: { type: 'string' },
642
- meta: { type: 'array', description: '표지 메타 [[라벨, 값], ...]' },
643
- stats: { type: 'array', description: '표지 숫자 [{value, unit, label}]' },
647
+ doc_no: { type: 'string', description: '문서번호. 표지 머리 왼쪽 박스 (예: "LIM-2026-0824-01")' },
648
+ client: {
649
+ type: 'string',
650
+ description: '고객사명. 표지 머리 **오른쪽**은 고객사 마크의 자리다 — 로고 파일을 '
651
+ + '받았으면 assets 에 role:"logo" 로 넣고, 못 받았으면 이 이름을 활자로 세운다. '
652
+ + '비워 두지 마라: 빈 자리는 「받을 것을 못 받은 문서」로 읽힌다.',
653
+ },
654
+ issuer: {
655
+ type: 'array',
656
+ description: '표지 맨 아래 발행처 스트립 ["발행처", "대상", "2026. 8."] — 마지막 칸이 날짜다.',
657
+ },
658
+ meta: { type: 'array', description: '표지 메타 [[라벨, 값], ...] · 4칸 권장' },
659
+ stats: { type: 'array', description: '요약면 수치 칸 [{value, unit, label, accent?, href?}] · accent:true 는 **독자 몫**인 첫 칸' },
660
+ history: {
661
+ type: 'array',
662
+ description: '발행 이력 [{no, date, note}] — 회차 문서임을 문서가 스스로 밝히는 자리.',
663
+ },
664
+ guide: {
665
+ type: 'array',
666
+ description: '이 문서 읽는 법 [{q, a: [문장, ...]}] — **독자가 실제로 던질 질문**만. '
667
+ + '「시간이 없다면 어디를 봐야 하는가?」 「어디까지 확인한 것인가?」 처럼. '
668
+ + '채우려고 넣으면 안 넣느니만 못하다.',
669
+ },
670
+ parts: {
671
+ type: 'array',
672
+ description: 'PART 묶음 [{no, title, lede?, tone?, questions?}] — 절만 늘어놓으면 '
673
+ + '「먼저 읽을 것 / 경과 / 남은 것」이라는 독자의 읽기 순서가 사라진다. '
674
+ + 'tone 은 you(독자 몫)·neutral(기록)·wait(대기)·hold(유보) 중 하나. '
675
+ + 'questions 는 「이 PART 가 답하는 질문」.',
676
+ },
677
+ notice: {
678
+ type: 'object',
679
+ description: '요약면 현황 줄 {state, text} — state 는 critical|info|success. '
680
+ + '**상태에 따라 바뀌는 유일한 줄**이다. 떠 있는 알림 배너를 따로 두지 마라.',
681
+ },
682
+ bar: { type: 'array', description: '요약면 상태 막대 [{value, label, tone}] · tone 은 done|wait' },
683
+ bar_note: { type: 'string', description: '막대가 뜻하는 바의 한정 조건 (※ 각주)' },
684
+ bar_alt: { type: 'string', description: '막대의 대체 텍스트 (보조기기용)' },
685
+ appendix: {
686
+ type: 'array',
687
+ description: '부록 [{no, title, rows: [[라벨, 값], ...], note?}] — 본문에서 **반복 참조되는** '
688
+ + '기준·환경을 뒤로 뺀다(확인 환경, 판정 기준 등).',
689
+ },
690
+ colophon: {
691
+ type: 'object',
692
+ description: '판권지 {title?, sub?, rows?, mark?, badge?, note?, tail?} — 표지와 같은 문법으로 '
693
+ + '문서를 닫는다. badge 는 「내부 공유용」 같은 표시.',
694
+ },
695
+ remain: { type: 'string', description: '머리띠 우측 판독부의 수치(요청 잔여 건수). 생략하면 open_items 개수.' },
696
+ running_footer: { type: 'string', description: '인쇄 러닝 푸터의 뒷말. 생략하면 client.' },
644
697
  grades: { type: 'object', description: '확인 등급 정의 {이름: [클래스, 설명]}' },
645
698
  groups: { type: 'array', description: '건 묶음 [{key, no}]' },
646
699
  sections: {
@@ -656,7 +709,21 @@ const REPORT_DATA = {
656
709
  + '· `logic`: {kicker, title_lines[], lede, models:[{label, flow[], body}, …]} — 대조 도해. '
657
710
  + '`flow` 는 「현재 상태 → 인접 등급 → 도착점」 같은 단계이고 **마지막 칸이 강조**된다. '
658
711
  + '첫 모델이 「현재」, 그다음이 「개선」으로 그려진다(자리로 정한다).\n'
659
- + '둘 다 **데이터가 있을 때만** 그려진다 — 없는 문구를 지어내지 말 것.',
712
+ + '둘 다 **데이터가 있을 때만** 그려진다 — 없는 문구를 지어내지 말 것.\n'
713
+ + '\n'
714
+ + '관보 문법이 절마다 요구하는 칸:\n'
715
+ + '· `part`: 이 절이 속한 PART 의 no. 안 주면 첫 묶음으로 떨어진다.\n'
716
+ + '· `marg`: {why, key, advice} — **좌측 여백 주석 단. 이것이 본체다.** '
717
+ + '없으면 두 단 본문이 한 단으로 서고, 문서는 「문서처럼 꾸민 웹페이지」가 된다. '
718
+ + 'why 는 **독자가 실제로 던질 질문**(「무엇을 조치해야 하는가?」), '
719
+ + 'key 는 이 절에서 하나만 가져간다면 그것, advice 는 어디를 참조하라는 것. '
720
+ + '채우려고 넣으면 안 넣느니만 못하다.\n'
721
+ + '· `lede`: 절 간지의 도입문 — 다루는 범위와 **다루지 않는 범위**.\n'
722
+ + '· `sub`: 차례 행에 붙는 한 줄 설명. `short`: 측면 색인 레일용 짧은 이름.\n'
723
+ + '· `tone`: 장 색을 뜻으로 고른다 — you(독자 몫)·deep(요지)·neutral(허브)·'
724
+ + 'done(끝난 것)·wait(대기)·hold(유보). 생략하면 kind 가 정한다.\n'
725
+ + '· kind:status 전용 — `board_title`(대조표 제목) · `board_hint`(표 아래 안내).\n'
726
+ + '· kind:note 전용 — `paragraphs`(강조 박스의 항목) · `limit_label`(그 박스 제목).',
660
727
  },
661
728
  chapters: {
662
729
  type: 'array',
@@ -722,8 +789,11 @@ const REPORT_DATA = {
722
789
  properties: {
723
790
  id: { type: 'string', description: '영소문자·숫자·하이픈 1~40자. 문서 안 참조 이름' },
724
791
  role: {
725
- type: 'string', enum: ['hero', 'chapter', 'figure', 'eof'],
726
- description: '`eof` 는 **문서 끝 꼬리 카드**다. 안 줘도 된다 — 서버가 기본 카드를 '
792
+ type: 'string', enum: ['hero', 'chapter', 'figure', 'eof', 'logo'],
793
+ description: '`logo` 는 **표지 우상단 고객사 마크**다 — provider 는 user-provided, '
794
+ + 'origin 에 제공자를 적는다. 못 받았으면 넣지 말고 data.client 에 이름만 적어라 '
795
+ + '(렌더러가 활자로 세운다).\n'
796
+ + '`eof` 는 **문서 끝 꼬리 카드**다. 안 줘도 된다 — 서버가 기본 카드를 '
727
797
  + '끼운다(계약 9절). 이 문서만의 카드가 있을 때만 준다. 그때는 무손실(PNG 또는 '
728
798
  + '무손실 WebP)이어야 하고 줄이지 않고 원본 크기로 실린다.',
729
799
  },
@@ -1640,6 +1710,8 @@ async function beatTick() {
1640
1710
  beatLastSent = Date.now();
1641
1711
  const reply = await beatOnce(recs);
1642
1712
  await autoPushIfAhead(reply, recs);
1713
+ // 두뇌가 짚은 모순을 긷는다 — 실패해도 심박을 안 멈춘다(안 오는 것이 고장은 아니다).
1714
+ try { await brainPull(); } catch { /* 그물·자격 문제는 다음 바퀴에 다시 */ }
1643
1715
  } finally {
1644
1716
  beatRunning = false;
1645
1717
  }
@@ -2180,6 +2252,23 @@ function cellClean(s) {
2180
2252
  * `gcell delta` 의 글 출력은 2026-08-23 에 고쳤는데, MCP 는 `--json` 을 **자기가 포맷**하고
2181
2253
  * 있어서 여기만 여전히 시각을 버리고 있었다. 같은 결함이 두 자리에 있었던 것이다.
2182
2254
  */
2255
+ /**
2256
+ * 사건 한 줄을 **누구**로 부를까. 서버의 `world_who()`(`lib/world_delta.php`)와 **같은 규칙**이다 —
2257
+ * 두 자로 말하면 같은 사실을 화면과 도구가 다르게 말한다.
2258
+ *
2259
+ * ⚠ **`unknown` 은 이름이 아니다.** 전에는 `client` 하나만 찍어 「unknown 세션이 썼다」로
2260
+ * 나갔고, 사람이 읽으면 「unknown 이라는 세포」로 보였다(사용자 지적 2026-08-24).
2261
+ * 세포는 세션을 알고 있었다 — 아는 것을 안 쓰고 모르는 것을 이름 자리에 놓고 있었다.
2262
+ */
2263
+ function worldWho(c) {
2264
+ const client = cellClean(c.client || '').trim();
2265
+ const sid = cellClean(c.session || c.session_uuid || '').trim();
2266
+ const short = sid ? sid.slice(0, 8) : '';
2267
+ if (client && client !== 'unknown') return short ? `${client}(${short}) 세션이` : `${client} 세션이`;
2268
+ if (short) return `${short} 세션이`;
2269
+ return '누군가';
2270
+ }
2271
+
2183
2272
  function worldFacts(max = 8) {
2184
2273
  const { bin } = cellCheck();
2185
2274
  if (!bin) return null; // 세포가 없다 — 「없다」와 구별한다
@@ -2194,7 +2283,7 @@ function worldFacts(max = 8) {
2194
2283
  const t = at > 0
2195
2284
  ? new Date(at * 1000).toTimeString().slice(0, 5)
2196
2285
  : '시각 미상';
2197
- return `${cellClean(c.path)} — ${cellClean(c.client)} 세션이 ${t}에 썼다`;
2286
+ return `${cellClean(c.path)} — ${worldWho(c)} ${t}에 썼다`;
2198
2287
  });
2199
2288
  if (ch.length > max) out.push(`그 밖 ${ch.length - max}건`);
2200
2289
  return out;
@@ -2268,7 +2357,122 @@ function cellNotice(why) {
2268
2357
  ].join('\n');
2269
2358
  }
2270
2359
 
2360
+ /* ── ★★ 백신 받기 — 두뇌가 짚은 **모순**을 이 세션 눈앞에 놓는다 ──────────────
2361
+ *
2362
+ * 사용자의 진단(2026-08-24): 여러 세포가 한 군체를 다룰 때 **정상적으로 일하는 사람이
2363
+ * 두뇌에겐 바이러스가 된다.** 그러면 두뇌가 그 세션에게 **되돌려 줄 줄 알아야** 한다.
2364
+ *
2365
+ * ── 왜 세포(gcell)가 아니라 여기서 긷나 ────────────────────────────────────
2366
+ * 세포도 같은 장부를 긷지만 `kind != "write"` 를 **버린다**(`cell/src/server.rs:254`).
2367
+ * 통과시켜도 `root.join(ref)` 로 ref 를 경로 취급해 **없는 파일**을 「최근에 바뀐 것」으로
2368
+ * 만든다 — 그 파일의 주석이 바로 그 해악을 경고한다. 세포를 다시 굽는 것은 별건이라,
2369
+ * 검토는 이 자리에서 **직접** 긷는다(사람 자격으로 열리는 창구다).
2370
+ *
2371
+ * ── 처음 한 바퀴에 옛날 것을 읊지 않는다 ────────────────────────────────────
2372
+ * 커서가 없으면 지난 것을 전부 「지금 일」로 읽는다. 그래서 첫 바퀴는 **커서만 맞추고**
2373
+ * 최근 10분 것만 말한다. 커서는 색인에 적어 두므로 이 프로세스가 죽었다 살아나도 안 잃는다.
2374
+ */
2375
+ const BRAIN_POLL_MS = 120_000; // 굽기는 분 단위다 — 20초 심박마다 두드릴 이유가 없다
2376
+ const BRAIN_FRESH_MS = 600_000; // 첫 바퀴에 「방금 것」으로 볼 나이
2377
+ const BRAIN_PAGE = 200; // 서버 상한(WORLD_PAGE_MAX)과 같다
2378
+ let brainAt = 0;
2379
+ let brainCursor = null; // null = 아직 색인에서 안 읽었다
2380
+ const brainQueue = []; // 아직 안 말한 모순
2381
+
2382
+ async function brainPull() {
2383
+ const mine = process.env.CLAUDE_CODE_SESSION_ID || '';
2384
+ if (!mine) return;
2385
+ const e = indexRead()[mine];
2386
+ if (!e?.tenant || !e?.tenant_id) return; // 아직 어느 소속처인지 모른다
2387
+ const cred = credential();
2388
+ // **키로는 세계 장부를 못 연다** — 서버가 사람 토큰만 받는다(`api.php` 의 world 라우트).
2389
+ if (!cred || cred.kind !== 'user') return;
2390
+ if (Date.now() - brainAt < BRAIN_POLL_MS) return;
2391
+ brainAt = Date.now();
2392
+
2393
+ const first = brainCursor === null;
2394
+ if (first) brainCursor = Number(e.brain_cursor || 0);
2395
+ const cwd = relHome(process.cwd());
2396
+ const fresh = [];
2397
+ for (let page = 0; page < 10; page++) {
2398
+ const r = await api('GET', '/api/world?tenant=' + encodeURIComponent(e.tenant)
2399
+ + '&since=' + brainCursor + '&cwd=' + encodeURIComponent(cwd) + '&limit=' + BRAIN_PAGE);
2400
+ const evs = Array.isArray(r?.events) ? r.events : [];
2401
+ for (const ev of evs) if (ev?.kind === 'review' && ev?.ref) fresh.push(ev);
2402
+ if (typeof r?.cursor === 'number' && r.cursor > brainCursor) brainCursor = r.cursor;
2403
+ if (!r?.truncated) break;
2404
+ }
2405
+ indexNote(mine, { tenant: e.tenant, tenant_id: e.tenant_id, brain_cursor: brainCursor });
2406
+
2407
+ const now = Date.now();
2408
+ for (const ev of fresh) {
2409
+ if (!brainFresh(ev, first, now)) continue;
2410
+ brainQueue.push(String(ev.ref));
2411
+ }
2412
+ }
2413
+
2414
+ /**
2415
+ * 이 사건을 **지금 일로 말해도 되나.**
2416
+ *
2417
+ * 커서가 있는 바퀴(=이 프로세스가 이미 따라잡은 뒤)라면 내려온 것은 정의상 새것이라 전부 참.
2418
+ * **첫 바퀴만** 나이를 본다 — 커서가 0이면 지난 것이 통째로 내려오는데, 그것을 읊으면
2419
+ * 이미 끝난 이야기를 **지금 하는 일로** 읽는다. 그게 이 물건이 막으려던 바로 그 병이다.
2420
+ *
2421
+ * ⚠ 시각을 못 읽으면 **안 말한다.** 모르는 것을 「방금」으로 치는 쪽이 더 위험하다.
2422
+ */
2423
+ function brainFresh(ev, first, now) {
2424
+ if (!first) return true;
2425
+ const at = brainAtMs(ev);
2426
+ return at > 0 && now - at <= BRAIN_FRESH_MS;
2427
+ }
2428
+
2429
+ /**
2430
+ * 서버가 준 `at` 을 밀리초로. **서버는 시간대 표식 없는 UTC 로 준다.**
2431
+ *
2432
+ * ⚠⚠ 맨몸 `Date.parse("2026-08-24 01:50:30")` 은 그 문자열을 **로컬 시각**으로 읽는다.
2433
+ * 서버는 `world_at()` 에서 `gmdate('Y-m-d H:i:s')` 로 쓰므로 실제로는 UTC 다
2434
+ * (서버 스스로도 그것을 안다 — `world_delta.php` 가 화면에 쓸 때 `…substr(at,11,5) . 'Z'`).
2435
+ * 그래서 **KST(+9) 기계에서는 방금 난 모순이 9시간 낡은 것**으로 보인다.
2436
+ * 첫 바퀴는 나이를 보고 버리므로 백신이 통째로 사라지고, **커서는 그대로 전진해**
2437
+ * 그 사건들은 이 세션에서 영영 안 온다. 조용한 손실이다.
2438
+ *
2439
+ * 2026-08-24 실측: 같은 하네스에 서버 꼴을 넣으면 백신 0건, `Z` 를 붙이면 1건.
2440
+ * 내 첫 PoC 가 이걸 놓친 이유는 픽스처가 `toISOString()` 이라 **실제로 깨지는 그 꼴을
2441
+ * 비켜 갔기** 때문이다 — 픽스처는 서버가 진짜 주는 글자여야 한다.
2442
+ *
2443
+ * ⚠ 시각을 못 읽으면 **0**. 모르는 것을 「방금」으로 치는 쪽이 더 위험하다.
2444
+ */
2445
+ const BRAIN_AT_NAIVE = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})$/;
2446
+ function brainAtMs(ev) {
2447
+ const s = String(ev?.at || '').trim();
2448
+ const m = BRAIN_AT_NAIVE.exec(s);
2449
+ return (m ? Date.parse(`${m[1]}T${m[2]}Z`) : Date.parse(s)) || 0;
2450
+ }
2451
+
2452
+ /**
2453
+ * 큐에 쌓인 모순을 **한 번씩만** 말한다.
2454
+ *
2455
+ * ⚠ **지시를 안 붙인다.** 실측 2026-08-22: 「…라고 말하라」를 실었더니 모델이 *「가짜 시스템」*
2456
+ * 이라며 거부했다 — 옳은 거부다. 서버도 `world_event_clean()` 으로 지시형을 막는다.
2457
+ * 여기서는 **어디를 보면 되는지**만 덧붙인다(사실).
2458
+ */
2459
+ function brainSays() {
2460
+ if (!brainQueue.length) return '';
2461
+ const n = brainQueue.length;
2462
+ const lines = brainQueue.splice(0, n).map((t) => ` · ${t}`);
2463
+ return '두뇌가 이 폴더의 지식에서 모순을 짚었다:\n' + lines.join('\n')
2464
+ + '\n전문은 포털 지식 화면의 검토 목록에 있다. search_cases 로 그 주제를 찾으면'
2465
+ + ' 「⚠ 상태:」가 붙은 장이 어느 것인지 보인다.';
2466
+ }
2467
+
2271
2468
  function worldAppend(result) {
2469
+ /*
2470
+ * ★ 백신이 **먼저**다. 세계 델타는 「누가 무엇을 썼다」는 배경이고, 모순은 지금 하는 일을
2471
+ * 바꿀 수 있는 사실이다. 그리고 델타가 비어도(조용한 구간) 백신은 나가야 한다 —
2472
+ * 아래 `if (!t) return;` 뒤에 두면 **조용한 날엔 영영 안 간다.**
2473
+ */
2474
+ const v = brainSays();
2475
+ if (v) result.content.push({ type: 'text', text: v });
2272
2476
  const t = worldDelta();
2273
2477
  if (!t) return;
2274
2478
  /*
@@ -2612,7 +2816,14 @@ async function callTool(name, args = {}) {
2612
2816
  + '이번에 고치면 remember_case 로 남길 것.';
2613
2817
  }
2614
2818
  return `«${args.q}» — ${r.hits.length}건(${r.scanned}장 중${r.expanded ? ', 의미 확장으로 다시 찾음' : ''})\n`
2615
- + r.hits.map((h) => `· ${h.path} [${h.score}점]\n ${h.excerpt}`).join('\n')
2819
+ /*
2820
+ * ★ **뒤집힌 결정을 살아 있는 것으로 읽지 않게 한다.** 점수로는 구별이 안 되고,
2821
+ * 찾는 쪽은 위에 뜬 것을 지금 참인 것으로 읽는다 — 그렇게 낡은 결정이 다음
2822
+ * 세포에게 전달된다. 「유효」는 **서버가 걸러서** 안 온다 — 그 규칙을 여기 두면
2823
+ * 화면과 갈린다(`wiki_page_status()` 머리말).
2824
+ */
2825
+ + r.hits.map((h) => `· ${h.path} [${h.score}점]${h.status ? `\n ⚠ 상태: ${h.status}` : ''}`
2826
+ + `\n ${h.excerpt}`).join('\n')
2616
2827
  + '\n전문은 get 이 아니라 포털 지식 화면에서 본다(경로 그대로).';
2617
2828
  }
2618
2829
  case 'list_sessions': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.8.1",
3
+ "version": "2.9.1",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",