genetics-mcp 2.14.0 → 2.16.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 +232 -10
- package/package.json +1 -1
- package/skills/gazette-report/README.md +71 -0
- package/skills/gazette-report/SKILL.md +479 -0
- package/skills/gazette-report/scripts/boundary.py +97 -0
- package/skills/gazette-report/scripts/contentdiff.sh +36 -0
- package/skills/gazette-report/scripts/cssaudit.sh +78 -0
- package/skills/gazette-report/scripts/extract.sh +84 -0
- package/skills/gazette-report/scripts/flexcheck.mjs +28 -0
- package/skills/gazette-report/scripts/fontchk.mjs +62 -0
- package/skills/gazette-report/scripts/kcheck.mjs +146 -0
- package/skills/gazette-report/scripts/package-lock.json +24 -0
- package/skills/gazette-report/scripts/package.json +5 -0
- package/skills/gazette-report/scripts/pdfcheck.sh +34 -0
- package/skills/gazette-report/scripts/read.mjs +85 -0
- package/skills/gazette-report/scripts/sentbreak.py +142 -0
- package/skills/gazette-report/scripts/tone.mjs +172 -0
- package/skills/gazette-report/scripts/tone.test.mjs +118 -0
- package/skills/gazette-report/scripts/typesurvey.mjs +48 -0
- package/skills/gazette-report/template/README.md +89 -0
- package/skills/gazette-report/template/gazette.css +2120 -0
- package/skills/gazette-report/template/skeleton.html +477 -0
- package/skills/genetics-cell-id/SKILL.md +9 -7
- package/skills/genetics-report/SKILL.md +53 -6
- package/skills/genetics-report/assets/example/cases-briefing.json +32 -0
- package/skills/genetics-report/assets/example/report-briefing.json +271 -0
- package/skills/genetics-report/assets/example/report.json +1 -1
- package/skills/genetics-report/references/content-pipeline.md +20 -0
- package/skills/genetics-report/references/lim-portal-format.md +100 -7
- package/skills/genetics-report/references/process.md +11 -0
|
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": "b41c63ec51d07fa2d11fe88283fefa6783b6447ec9a9637779d04fb1f528db92",
|
|
3
|
+
"aarch64-unknown-linux-musl": "34610013e6f6358954e1d66bde546a2757cb47d163e5c7251989d28de8ee6d19",
|
|
4
|
+
"x86_64-apple-darwin": "e97149a290f2079c39cfd19ac9d034b6e0990afc5bb67a352052a48193d43a89",
|
|
5
|
+
"x86_64-unknown-linux-musl": "85b9f4eccd2c4b4556bceafc7d6b1a88484847256d95fef71cd033e7670f27c4"
|
|
6
6
|
}
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -524,7 +524,21 @@ async function materializeAssets(data) {
|
|
|
524
524
|
const { data: dropped, file: fpath, ...meta } = a ?? {};
|
|
525
525
|
if (!a?.source && !fpath) {
|
|
526
526
|
// 둘 다 없으면 예전 경로 그대로 — data 를 그대로 통과시킨다(판 A 하위호환).
|
|
527
|
-
|
|
527
|
+
// get_report 가 준 표식('(생략 — NB)')과 bytes 칸의 처리(2026-08-27, 운영 #34):
|
|
528
|
+
// brief 없는 자산 → data·bytes 를 빼고 **id 만** 보낸다. 서버가 같은 id 의 저장 바이트를 잇는다
|
|
529
|
+
// (report_carry_assets — 「빈 data」는 id 잇기 갈래다).
|
|
530
|
+
// brief 있는 자산 → **표식을 그대로** 싣는다. data 를 비우면 서버가 「brief 만 왔다 =
|
|
531
|
+
// 다시 그려라」로 읽어(carry 의 brief 갈래 → asset_jobs_plan) 저장 그림을 버리고 크레딧을
|
|
532
|
+
// 태운다 — 표식은 빈 값이 아니라서 carry 가 저장 바이트로 갈아끼우고 brief 는 남는다.
|
|
533
|
+
const { bytes: _b, ...rest } = meta;
|
|
534
|
+
const placeholder = typeof dropped === 'string' && dropped.startsWith('(생략');
|
|
535
|
+
if (placeholder) {
|
|
536
|
+
const briefed = typeof rest.brief === 'string' && rest.brief.trim() !== '';
|
|
537
|
+
if (briefed) out.push({ ...rest, data: dropped });
|
|
538
|
+
else { const { data: _d, ...idOnly } = rest; out.push(idOnly); }
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
out.push(dropped === undefined ? rest : { ...rest, data: dropped });
|
|
528
542
|
continue;
|
|
529
543
|
}
|
|
530
544
|
// 원격보다 **로컬이 이긴다** — 손에 든 실물이 주소보다 확실하다.
|
|
@@ -953,7 +967,8 @@ const REPORT_DATA = {
|
|
|
953
967
|
+ '로 두면 화질을 계단으로 내려 예산 안에 앉힌다. 그 도판은 `sections[].plates` 에 id 를 적어야 자리가 생긴다. '
|
|
954
968
|
+ '크기와 형식은 인코딩 결과에서 읽으므로 선언하지 않는다. 전부 data: URI 로 문서에 박히며 '
|
|
955
969
|
+ '(열람 CSP 가 외부 이미지를 막는다), 서버가 출처·매직바이트·예산(장당 80,000B·합계 400,000B)을 '
|
|
956
|
-
+ '다시 검증한다.'
|
|
970
|
+
+ '다시 검증한다. **갱신(update_report)에서 이 칸을 빼면 저장된 그림이 유지되고, `[]` 면 지워진다** — '
|
|
971
|
+
+ 'plates 가 가리키는 그림이 없으면 422.',
|
|
957
972
|
items: {
|
|
958
973
|
type: 'object',
|
|
959
974
|
properties: {
|
|
@@ -1266,6 +1281,25 @@ function findTranscript(uuid) {
|
|
|
1266
1281
|
const f = path.join(root, d.name, `${uuid}.jsonl`);
|
|
1267
1282
|
if (fs.existsSync(f)) return f;
|
|
1268
1283
|
}
|
|
1284
|
+
return cursorTranscriptOf(uuid);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
/*
|
|
1288
|
+
* Cursor 대화의 전사(조각 3, 2026-08-28) — `~/.cursor/projects/<슬러그>/agent-transcripts/<uuid>/<uuid>.jsonl`.
|
|
1289
|
+
* 설계 문서 §1.2 의 sqlite 전제는 낡았다 — 훅 payload 의 `transcript_path` 가 가리키는 jsonl 이
|
|
1290
|
+
* 실재한다(이 기계 157개 실측). 슬러그는 워크스페이스 기준(앞 대시 없음)이라 cwd 로 못 짚고,
|
|
1291
|
+
* 자리가 몇 안 되니 **전부 센다**(세포 world.rs 의 cursor_transcript_dirs 와 같은 결정).
|
|
1292
|
+
* ⚠ 30일 지난 것은 Cursor 가 스스로 지운다(.agent-data-cleanup-*) — 없음은 오류가 아니다.
|
|
1293
|
+
*/
|
|
1294
|
+
function cursorTranscriptOf(uuid) {
|
|
1295
|
+
const base = path.join(os.homedir(), '.cursor', 'projects');
|
|
1296
|
+
let dirs;
|
|
1297
|
+
try { dirs = fs.readdirSync(base, { withFileTypes: true }); } catch { return null; }
|
|
1298
|
+
for (const d of dirs) {
|
|
1299
|
+
if (!d.isDirectory()) continue;
|
|
1300
|
+
const f = path.join(base, d.name, 'agent-transcripts', uuid, `${uuid}.jsonl`);
|
|
1301
|
+
if (fs.existsSync(f)) return f;
|
|
1302
|
+
}
|
|
1269
1303
|
return null;
|
|
1270
1304
|
}
|
|
1271
1305
|
|
|
@@ -1434,9 +1468,16 @@ async function pushSession(tenant, uuid, title, dryRun) {
|
|
|
1434
1468
|
* 진짜 출처는 이미 손에 있었다: **본문 각 줄의 `cwd`**(scrubTranscript 가 센다).
|
|
1435
1469
|
* 없을 때만 옛 방식으로 물러선다 — 그래야 퇴보가 없다.
|
|
1436
1470
|
*/
|
|
1471
|
+
/*
|
|
1472
|
+
* ⚠ Cursor 전사(조각 3)에는 줄 cwd 가 아예 없다 — 그때 process.cwd() 로 물러서면
|
|
1473
|
+
* **올리는 프로세스의 자리**가 그 대화의 자리로 적힌다(위 scrubcheck 사고와 같은 병).
|
|
1474
|
+
* 같은 창의 Cursor MCP 면 워크스페이스 폴더가 진짜 자리다(cursorWorkspaceFolder) —
|
|
1475
|
+
* 그것도 모르면 **빈 값**으로 둔다. 모르는 것은 모른다고 적는다(자리 판정은 빈 자리를 안다).
|
|
1476
|
+
*/
|
|
1477
|
+
const isCursorBody = f.includes(`${path.sep}.cursor${path.sep}projects${path.sep}`);
|
|
1437
1478
|
const body = {
|
|
1438
1479
|
tenant, uuid,
|
|
1439
|
-
origin_cwd: sc.cwd || relHome(process.cwd()),
|
|
1480
|
+
origin_cwd: sc.cwd || (isCursorBody ? relHome(cursorWorkspaceFolder() || '') : relHome(process.cwd())),
|
|
1440
1481
|
origin_git: gitHere(), tools: toolsStamp(),
|
|
1441
1482
|
jsonl: sc.jsonl,
|
|
1442
1483
|
};
|
|
@@ -2157,7 +2198,9 @@ const TOOLS = [
|
|
|
2157
2198
|
name: 'list_my_tenants',
|
|
2158
2199
|
description:
|
|
2159
2200
|
'**내가 속한 곳 목록.** 다른 도구가 요구하는 `tenant` 값(slug, 이름도 받는다)을 여기서 얻는다.\n'
|
|
2160
|
-
+ '값을
|
|
2201
|
+
+ '값을 **아직 모를 때만** 이것부터 부른다. 사용자 글에 `tenant:` 줄, `/t/<slug>/`, 또는 세션 uuid 가 있으면 '
|
|
2202
|
+
+ '그것을 쓰고 **되묻지 마라** — 고르지 말라는 것은 없는 값을 추측하지 말라는 뜻이다.\n'
|
|
2203
|
+
+ '여럿인데 사용자 글에 값이 없으면 사용자에게 고르게 한다.\n'
|
|
2161
2204
|
+ '지금 폴더에 대해 서버가 **본 것**을 함께 준다 — 「이 자리는 지금까지 어디에서 올라왔나」. '
|
|
2162
2205
|
+ '그것은 **기록이지 근거가 아니다**: 서버가 한 곳으로 좁히지 못하거나 자리를 못 읽으면 '
|
|
2163
2206
|
+ '그렇게 말한다. 좁혀지지 않으면 **사용자에게 물어라.**',
|
|
@@ -2286,8 +2329,10 @@ const TOOLS = [
|
|
|
2286
2329
|
name: 'update_report',
|
|
2287
2330
|
description:
|
|
2288
2331
|
'기존 보고서의 내용을 교체한다. 보고서 id·주소가 그대로라 **이미 고객에게 나간 열람 링크가 새 내용을 가리킨다**. ' +
|
|
2289
|
-
'사람이
|
|
2290
|
-
'
|
|
2332
|
+
'사람이 편집기로 고친 문서도 교체할 수 있다(2026-08-25) — 대신 무엇을 덮게 되는지 목록이 먼저 말하고, 덮은 뒤 되돌릴 판이 응답에 온다. ' +
|
|
2333
|
+
'**그림 계약**: `assets` 를 **빼고** 보내면 서버가 저장된 그림을 그대로 유지한다. 전부 지우려면 `assets: []` 를 명시하고 ' +
|
|
2334
|
+
'`sections[].plates`·`chapters[].asset` 에서도 그 id 를 빼야 한다 — 가리키는 곳이 없는 id 는 422 로 거절된다. ' +
|
|
2335
|
+
'한 장만 바꾸려면 그 id 에 새 `source`/`file` 을 주고 나머지는 get_report 가 준 표식 그대로 두면 된다.',
|
|
2291
2336
|
inputSchema: {
|
|
2292
2337
|
type: 'object',
|
|
2293
2338
|
properties: {
|
|
@@ -2341,6 +2386,71 @@ const TOOLS = [
|
|
|
2341
2386
|
required: ['tenant', 'title', 'symptom', 'fix'],
|
|
2342
2387
|
},
|
|
2343
2388
|
},
|
|
2389
|
+
{
|
|
2390
|
+
name: 'remember_decision',
|
|
2391
|
+
description:
|
|
2392
|
+
'**정한 것을 팀의 기억에 적는다** — 무엇을·누가·언제·왜·무슨 조건이면 다시 여나. '
|
|
2393
|
+
+ '그 소속처 지식의 `decisions/` 에 적힌다.\n'
|
|
2394
|
+
+ '언제 부르는가 — 사람이 이런 뜻으로 말할 때:\n'
|
|
2395
|
+
+ ' "이렇게 가기로 했다" · "이 안으로 정했다" · "그건 안 하기로 했다" · "반려했다" · '
|
|
2396
|
+
+ '"이 방향으로 합의했다" · "결정 남겨줘" · "왜 이렇게 정했는지 적어둬"\n'
|
|
2397
|
+
+ '★ **`remember_case` 와 무엇이 다른가**: 사례는 **겪은 문제**(증상이 있다), 결정은 **정한 것**'
|
|
2398
|
+
+ '(증상이 없다). 기획 결정·디자인 근거·반려 이유처럼 **고장이 아닌 일**은 전부 이쪽이다. '
|
|
2399
|
+
+ '증상을 지어내 사례로 적지 말 것 — 그러면 다음 사람이 증상으로 찾다가 못 찾는다.\n'
|
|
2400
|
+
+ '무엇을 적는가: **왜**가 이 장의 값이다(그때의 근거·수치·관찰). 그리고 **무슨 조건이면 다시 여나** — '
|
|
2401
|
+
+ '이것을 비우면 그 결정은 「아직 열린 것」으로 읽힌다(닫혔는지 아무도 모르므로). '
|
|
2402
|
+
+ '끝난 결정이면 "조건 없음(종결)"이라고 적을 것.\n'
|
|
2403
|
+
+ '모르는 칸은 **비운다** — 지어내면 「적히지 않음」보다 나쁘다.',
|
|
2404
|
+
inputSchema: {
|
|
2405
|
+
type: 'object',
|
|
2406
|
+
properties: {
|
|
2407
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2408
|
+
title: { type: 'string', description: '한 줄 제목. 파일 이름이 된다. ⚠ 같은 제목이면 덮어쓴다 — **다른 결정이면 제목을 달리할 것**(서로 다른 결정은 합치지 않는다).' },
|
|
2409
|
+
what: { type: 'string', description: '무엇을 정했나 — 한 문장.' },
|
|
2410
|
+
who: { type: 'string', description: '정한 사람 또는 그 자리에 있던 사람. 모르면 비운다.' },
|
|
2411
|
+
when: { type: 'string', description: '정한 날짜(예: 2026-08-27). 모르면 비운다 — 오늘로 지어내지 말 것.' },
|
|
2412
|
+
why: { type: 'string', description: '그때의 근거 — 수치·관찰. 이 칸이 이 장의 값이다.' },
|
|
2413
|
+
reopen: { type: 'string', description: '무슨 조건이면 다시 여나. **비우면 「열린 결정」으로 읽힌다.** 끝났으면 "조건 없음(종결)".' },
|
|
2414
|
+
status: { type: 'string', description: '상태 — "유효"(기본) · "뒤집힘 → decisions/<그 결정>" · "조건 충족 → <무엇이> (날짜)".' },
|
|
2415
|
+
tags: { type: 'array', items: { type: 'string' }, description: '갈래(최대 8). ★ 이것이 있어야 이 결정이 「일」 후보 계산에 들어간다.' },
|
|
2416
|
+
},
|
|
2417
|
+
required: ['tenant', 'title', 'what'],
|
|
2418
|
+
},
|
|
2419
|
+
},
|
|
2420
|
+
{
|
|
2421
|
+
name: 'list_breaks',
|
|
2422
|
+
description:
|
|
2423
|
+
'**대화에서 정한 것이 문서에 닿았나** — 연결된 채널(Slack)에서 「~하기로 했다」가 선 뒤 '
|
|
2424
|
+
+ '그 소속처의 결정 문서가 움직이지 않은 것을 센다.\n'
|
|
2425
|
+
+ '언제 부르는가: 일을 시작할 때(「지금 걸려 있는 게 뭐지」) · 사람이 "요즘 대화로만 정하고 '
|
|
2426
|
+
+ '문서가 안 남는다"고 할 때 · 결정을 적기 전에(이미 신호가 서 있으면 그것부터).\n'
|
|
2427
|
+
+ '**0 이면 0 이라고 말하고, 창구가 안 붙은 것(채널 0)과 신호 없음을 갈라 말한다.** '
|
|
2428
|
+
+ '자동 처방은 없다 — 열린 신호를 닫는 길은 사람이 결정을 적는 것(remember_decision)이다.',
|
|
2429
|
+
inputSchema: {
|
|
2430
|
+
type: 'object',
|
|
2431
|
+
properties: {
|
|
2432
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2433
|
+
},
|
|
2434
|
+
required: ['tenant'],
|
|
2435
|
+
},
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
name: 'works',
|
|
2439
|
+
description:
|
|
2440
|
+
'**이 소속처에 지금 어떤 일이 있나** — 열린 일의 목록·상태(계획/진행 중/멈춤)·세션 수.\n'
|
|
2441
|
+
+ '언제 부르는가: 작업을 시작할 때(「내가 붙을 일이 이미 있나」 — 새로 벌이기 전에 먼저) · '
|
|
2442
|
+
+ '사람이 「지금 뭐가 진행 중이지」라고 물을 때 · 세션 요지에 「어느 일에도 속해 있지 않다」가 왔을 때.\n'
|
|
2443
|
+
+ '「계획」 = 사람이 착수 전에 세운 일(세션 0) — 이 목록에서 이름을 보고 그 방향의 작업이면 '
|
|
2444
|
+
+ '거기 붙는 것이 맞다(소속 확정은 사람이 화면에서 한다 — 이 도구는 읽기 전용이다).\n'
|
|
2445
|
+
+ '요지 전문은 get_case(tenant, "work/<slug>.md") 로 읽는다. **0 이면 0 이라고 말한다.**',
|
|
2446
|
+
inputSchema: {
|
|
2447
|
+
type: 'object',
|
|
2448
|
+
properties: {
|
|
2449
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2450
|
+
},
|
|
2451
|
+
required: ['tenant'],
|
|
2452
|
+
},
|
|
2453
|
+
},
|
|
2344
2454
|
{
|
|
2345
2455
|
name: 'search_cases',
|
|
2346
2456
|
description:
|
|
@@ -2487,6 +2597,7 @@ const TOOLS = [
|
|
|
2487
2597
|
name: 'resume_session',
|
|
2488
2598
|
description:
|
|
2489
2599
|
'서버에 있는 대화를 **지금 이 창이 이어받는다.** 포털의 「이어받기」 버튼이 복사해 주는 문구가 이 도구를 부른다.\n' +
|
|
2600
|
+
'붙여넣기에 tenant·uuid 가 있으면 **지금 바로 이 도구를 부른다.** list_my_tenants 로 소속처를 되묻지 마라.\n' +
|
|
2490
2601
|
'한 번에 세 가지를 한다 — ① 그 대화의 요지를 받아 **여기서 바로 읽는다**(창을 새로 열 필요가 없다), ' +
|
|
2491
2602
|
'② 원본 jsonl 을 이 기계의 제자리에 놓는다(나중에 `claude --resume <uuid>` 로 원본을 열 수 있다), ' +
|
|
2492
2603
|
'③ 이 기계가 그 대화를 쥐었다고 서버에 알린다(다른 기계가 「누가 이어가는지」를 본다).\n' +
|
|
@@ -2495,7 +2606,7 @@ const TOOLS = [
|
|
|
2495
2606
|
inputSchema: {
|
|
2496
2607
|
type: 'object',
|
|
2497
2608
|
properties: {
|
|
2498
|
-
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다
|
|
2609
|
+
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다). 사용자 글의 tenant: 줄을 그대로 쓴다. 값이 있을 때 list_my_tenants 로 되묻지 않는다.' },
|
|
2499
2610
|
uuid: { type: 'string', description: '이어받을 세션 id.' },
|
|
2500
2611
|
into: { type: 'string', description: '원본 파일을 놓을 자리(생략하면 지금 폴더). '
|
|
2501
2612
|
+ '`--resume` 은 그 폴더의 트랜스크립트를 찾으므로 이 값이 곧 원본을 열 자리다.' },
|
|
@@ -3455,6 +3566,7 @@ async function callTool(name, args = {}) {
|
|
|
3455
3566
|
return ` · ${t.name}${sl}${seen(t)}${mark(t)} · 최근 ${when}`;
|
|
3456
3567
|
});
|
|
3457
3568
|
return `속한 곳 ${list.length}곳입니다. **사용자에게 어디로 할지 물어보세요** — 고르지 마세요.\n`
|
|
3569
|
+
+ '이미 이 턴의 사용자 글에 slug 나 uuid 가 있으면 그것이 답입니다. 묻는 것은 값이 없을 때뿐입니다.\n'
|
|
3458
3570
|
+ `${lines.join('\n')}\n`
|
|
3459
3571
|
+ (says ? `\n${says}` : '');
|
|
3460
3572
|
}
|
|
@@ -3553,6 +3665,13 @@ async function callTool(name, args = {}) {
|
|
|
3553
3665
|
head.push(`[정본 ${owner} · 본문 판 body_rev=${r.body_rev ?? '?'}`
|
|
3554
3666
|
+ (r.sha256 ? ` · 지금 발행물 ${r.bytes}B sha256:${String(r.sha256).slice(0, 12)}…` : '')
|
|
3555
3667
|
+ `] — 고칠 때는 update_report 에 body_rev 를 함께 실으면, 그 사이 누가 고쳤을 때 409 로 멈춘다.`);
|
|
3668
|
+
// 그림은 서버가 보관한다(바이트는 에이전트를 안 지난다) — 되돌려 보낼 때의 계약을 여기서 말한다(#34).
|
|
3669
|
+
const kept = Array.isArray(r.data?.assets)
|
|
3670
|
+
? r.data.assets.filter((a) => typeof a?.data === 'string' && a.data.startsWith('(생략')).length : 0;
|
|
3671
|
+
if (kept > 0) {
|
|
3672
|
+
head.push(`그림 ${kept}장은 서버가 보관 중 — update_report 에 assets 를 **빼고** 보내면 그대로 유지, `
|
|
3673
|
+
+ '전부 지우려면 `assets: []`(sections[].plates·chapters[].asset 에서도 빼야 한다), 바꿀 장만 새 source/file 로.');
|
|
3674
|
+
}
|
|
3556
3675
|
// 데이터가 없으면 서버가 outline 을 함께 준다 — 그것이 지금 고객이 보는 것의 유일한 요약이다.
|
|
3557
3676
|
if (r.data === null && r.outline) head.push(outlineText(r.outline));
|
|
3558
3677
|
return `${head.join('\n')}\n${JSON.stringify(r.data, null, 2)}`;
|
|
@@ -3626,6 +3745,51 @@ async function callTool(name, args = {}) {
|
|
|
3626
3745
|
if (!facts.length) return '이 저장소에서 최근에 남이 바꾼 것은 없다.';
|
|
3627
3746
|
return `이 저장소에서 최근에 바뀐 것:\n${facts.map((f) => ` · ${f}`).join('\n')}`;
|
|
3628
3747
|
}
|
|
3748
|
+
case 'works': {
|
|
3749
|
+
const r = await api('GET', '/api/work/list?tenant=' + encodeURIComponent(args.tenant));
|
|
3750
|
+
const rows = r.works || [];
|
|
3751
|
+
const lines = [`${r.tenant} — 열린 일 ${r.open ?? rows.length}개` + ((r.closed ?? 0) > 0 ? ` (지난 일 ${r.closed}개)` : '')];
|
|
3752
|
+
for (const w of rows) {
|
|
3753
|
+
lines.push(` · «${w.name || w.slug}» (${w.slug}) — ${w.word} · 세션 ${w.sessions}`
|
|
3754
|
+
+ (w.doc ? ` · 요지: get_case(tenant, "${w.doc}")` : ' · 요지 문서 아직 없음'));
|
|
3755
|
+
}
|
|
3756
|
+
if (!rows.length) lines.push('열린 일이 없다 — 0건은 0건이다. 착수 전에 세우려면 사람이 군체 화면의 「일 열기」로 연다.');
|
|
3757
|
+
else lines.push('소속 확정은 사람이 화면에서 한다 — 이 도구는 목록을 읽을 뿐이다.');
|
|
3758
|
+
return lines.join('\n');
|
|
3759
|
+
}
|
|
3760
|
+
case 'list_breaks': {
|
|
3761
|
+
const r = await api('GET', '/api/breaks?tenant=' + encodeURIComponent(args.tenant));
|
|
3762
|
+
const c = r.counts || {};
|
|
3763
|
+
const lines = [`${r.tenant} — 대화에서 정한 것이 문서에 닿았나`];
|
|
3764
|
+
if (!r.wired) {
|
|
3765
|
+
// 창구가 안 붙은 것과 신호 0 은 다른 말이다 — 0 을 「깨끗함」으로 읽게 두지 않는다.
|
|
3766
|
+
lines.push('아직 연결된 채널이 없다(신호 0 과 다른 상태) — 관리 화면에서 채널을 소속처에 연결해야 세기 시작한다.');
|
|
3767
|
+
return lines.join('\n');
|
|
3768
|
+
}
|
|
3769
|
+
lines.push(`열림 ${c.open ?? 0} · 문서에 닿음 ${c.answered ?? 0} · 사람 판정 맞다 ${c.confirmed ?? 0}/아니다 ${c.dismissed ?? 0} (채널 ${r.channels}곳)`);
|
|
3770
|
+
for (const b of r.open || []) {
|
|
3771
|
+
lines.push(` · [${String(b.signal_at).slice(0, 10)} ${b.channel}] ${b.excerpt} (문서 마감 ${String(b.deadline_at).slice(0, 10)})`);
|
|
3772
|
+
}
|
|
3773
|
+
if (!(r.open || []).length) lines.push('지금 열린 신호는 없다 — 0건은 0건이다.');
|
|
3774
|
+
else lines.push('여는 길: 그 결정을 remember_decision 으로 적으면 다음 읽기에서 닫힌다.');
|
|
3775
|
+
return lines.join('\n');
|
|
3776
|
+
}
|
|
3777
|
+
case 'remember_decision': {
|
|
3778
|
+
const r = await api('POST', '/api/wiki/decision?tenant=' + encodeURIComponent(args.tenant), {
|
|
3779
|
+
title: args.title, what: args.what,
|
|
3780
|
+
who: args.who || '', when: args.when || '', why: args.why || '',
|
|
3781
|
+
reopen: args.reopen || '', status: args.status || '', tags: args.tags || [],
|
|
3782
|
+
});
|
|
3783
|
+
const lines = [`${r.note} — ${r.tenant} · ${r.path}`];
|
|
3784
|
+
// 안 적은 것을 **그렇게 말한다** — 조용히 열린 결정을 만들거나 계산 밖에 두지 않는다.
|
|
3785
|
+
if (!args.reopen) {
|
|
3786
|
+
lines.push('재개 조건을 안 적어 이 결정은 「아직 열린 것」으로 읽힌다. 끝난 결정이면 reopen 에 "조건 없음(종결)" 을 적을 것.');
|
|
3787
|
+
}
|
|
3788
|
+
if (!args.tags || !args.tags.length) {
|
|
3789
|
+
lines.push('갈래(tags)가 없어 이 결정은 「일」 후보 계산에 안 들어간다.');
|
|
3790
|
+
}
|
|
3791
|
+
return lines.join('\n');
|
|
3792
|
+
}
|
|
3629
3793
|
case 'remember_case': {
|
|
3630
3794
|
const r = await api('POST', '/api/wiki/case?tenant=' + encodeURIComponent(args.tenant), {
|
|
3631
3795
|
title: args.title, symptom: args.symptom, cause: args.cause || '', fix: args.fix,
|
|
@@ -3698,7 +3862,7 @@ async function callTool(name, args = {}) {
|
|
|
3698
3862
|
...myCitation(), // 세션 uuid 는 우리가 채운다 — 없으면 서버가 안 적는다
|
|
3699
3863
|
});
|
|
3700
3864
|
return `${r.says || '판정을 적었다.'}\n`
|
|
3701
|
-
+ '(세션이 중계한 판정이다 — 사람이 지식 화면에서 누른 판정만 도구 직전
|
|
3865
|
+
+ '(세션이 중계한 판정이다 — 사람이 지식 화면에서 누른 판정만 도구 직전 알림(트리거 제외)에 반영된다.)';
|
|
3702
3866
|
}
|
|
3703
3867
|
case 'review_verdict': {
|
|
3704
3868
|
const r = await api('POST', '/api/wiki/review_verdict?tenant=' + encodeURIComponent(args.tenant), {
|
|
@@ -4081,14 +4245,15 @@ async function handle(line) {
|
|
|
4081
4245
|
'',
|
|
4082
4246
|
'「남겨줘 / 기억해줘 / 업데이트 / 갱신 / 동기화 / 올려둬」는 **무엇을 남기느냐로 갈린다**:',
|
|
4083
4247
|
'· 알아낸 것 하나(증상과 처방) → remember_case (같은 제목이면 덮어쓰므로 갱신도 이것)',
|
|
4248
|
+
'· 정한 것(증상이 없다 — 기획·디자인의 판단, 반려, "이렇게 가기로 했다") → remember_decision',
|
|
4084
4249
|
'· 지금 이 대화 전체 → push_session (다른 기계가 이어받게)',
|
|
4085
4250
|
'· 고객에게 나갈 문서 → publish_report / update_report',
|
|
4086
4251
|
'무엇인지 모호하면 묻기 전에 search_cases 로 먼저 뒤져 본다 — 이미 있으면 갱신이 답이다.',
|
|
4087
4252
|
'',
|
|
4088
4253
|
'자격은 **`login` 으로 받는 SSO 하나면 된다** — 그것으로 내가 속한 소속처 전부에 닿는다.',
|
|
4089
4254
|
'키(rpk_)도 쓸 수 있지만 **한 소속처에만** 매이고, 발행할 때만 그쪽이 먼저 쓰인다.',
|
|
4090
|
-
'어디로 갈지는 tenant 에 소속처 **slug** 로 적는다(이름도 받는다 —
|
|
4091
|
-
|
|
4255
|
+
'어디로 갈지는 tenant 에 소속처 **slug** 로 적는다(이름도 받는다 — 모를 때만 list_my_tenants).',
|
|
4256
|
+
'사용자 글에 tenant·uuid 가 있으면 그대로 쓰고 되묻지 않는다. 값을 모르면 지어내지 말고 사용자에게 묻는다 —',
|
|
4092
4257
|
'내 소속이 아닌 곳은 존재도 안 보이므로, 404 는 "없다"가 아니라 "내 것이 아니다"일 수 있다.',
|
|
4093
4258
|
].join('\n'),
|
|
4094
4259
|
};
|
|
@@ -4154,6 +4319,58 @@ const IS_MAIN = (() => {
|
|
|
4154
4319
|
* 자리는 판이 바뀌어도 안 움직이는 `~/.config/genetics/bin/gcell` 이다. 훅이 그 경로를
|
|
4155
4320
|
* 가리키므로, 갈아 끼워도 훅을 다시 걸 필요가 없다.
|
|
4156
4321
|
*/
|
|
4322
|
+
/*
|
|
4323
|
+
* ── `gcell` 을 PATH 에 건다 (078 문턱 ④) ──────────────────────────────────
|
|
4324
|
+
* 바이너리는 `~/.config/genetics/bin` 에 깔리는데 그 폴더는 어느 셸의 PATH 에도 없다. 그래서
|
|
4325
|
+
* 사람에게 안내하는 실행법이 「api.env 를 grep 해 cut 한 경로」 두 줄이었다 — 파이프를 읽을 줄
|
|
4326
|
+
* 아는 사람만 세포에게 물을 수 있었다(SKILL.md 실측 2026-08-27). 심링크 하나면 `gcell` 로 끝난다.
|
|
4327
|
+
*
|
|
4328
|
+
* 자리 고르기는 **순수 함수**로 뺀다(smoke 가 떼어내 잰다): PATH 에 있고 · 실재하고 · 쓸 수 있는
|
|
4329
|
+
* 첫 후보. 없는 폴더를 만들거나 rc 파일을 고치지 않는다 — 남의 셸 설정에 손대는 설치는 못 믿는다.
|
|
4330
|
+
*/
|
|
4331
|
+
const CELL_LINK_CANDIDATES = ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local/bin')];
|
|
4332
|
+
function cellLinkDir(pathEnv, candidates, exists, writable) {
|
|
4333
|
+
const onPath = new Set(String(pathEnv || '').split(path.delimiter).filter(Boolean));
|
|
4334
|
+
for (const d of candidates) {
|
|
4335
|
+
if (onPath.has(d) && exists(d) && writable(d)) return d;
|
|
4336
|
+
}
|
|
4337
|
+
return null;
|
|
4338
|
+
}
|
|
4339
|
+
function cellLinkOnPath() {
|
|
4340
|
+
const dir = cellLinkDir(process.env.PATH, CELL_LINK_CANDIDATES,
|
|
4341
|
+
(d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } },
|
|
4342
|
+
(d) => { try { fs.accessSync(d, fs.constants.W_OK); return true; } catch { return false; } });
|
|
4343
|
+
if (!dir) {
|
|
4344
|
+
return `PATH 에 걸 자리를 못 찾았다(${CELL_LINK_CANDIDATES.join(' · ')} 중 PATH 에 있고 쓸 수 있는 곳이 없다) — 전체 경로 ${CELL_PATH} 로 부를 것.`;
|
|
4345
|
+
}
|
|
4346
|
+
const link = path.join(dir, 'gcell');
|
|
4347
|
+
try {
|
|
4348
|
+
const cur = fs.lstatSync(link);
|
|
4349
|
+
if (cur.isSymbolicLink() && fs.readlinkSync(link) === CELL_PATH) return ''; // 이미 걸려 있다 — 매 부팅 말하면 소음이다
|
|
4350
|
+
if (!cur.isSymbolicLink()) return `${link} 에 다른 것이 있어 안 건드렸다 — 전체 경로 ${CELL_PATH} 로 부를 것.`;
|
|
4351
|
+
fs.unlinkSync(link); // 우리 것이 아닌 심링크가 아니라 **옛 자리를 가리키는 우리 심링크**다 — 갈아 끼운다
|
|
4352
|
+
} catch { /* 없다 — 새로 건다 */ }
|
|
4353
|
+
try {
|
|
4354
|
+
fs.symlinkSync(CELL_PATH, link);
|
|
4355
|
+
return `\`gcell\` 을 PATH 에 걸었다(${link}).`;
|
|
4356
|
+
} catch (e) {
|
|
4357
|
+
return `\`gcell\` 을 PATH 에 못 걸었다(${link}): ${e.message} — 전체 경로 ${CELL_PATH} 로 부를 것.`;
|
|
4358
|
+
}
|
|
4359
|
+
}
|
|
4360
|
+
function cellUnlinkOnPath() {
|
|
4361
|
+
const out = [];
|
|
4362
|
+
for (const d of CELL_LINK_CANDIDATES) {
|
|
4363
|
+
const link = path.join(d, 'gcell');
|
|
4364
|
+
try {
|
|
4365
|
+
if (fs.lstatSync(link).isSymbolicLink() && fs.readlinkSync(link) === CELL_PATH) {
|
|
4366
|
+
fs.unlinkSync(link);
|
|
4367
|
+
out.push(`PATH 의 심링크를 걷었다: ${link}`);
|
|
4368
|
+
}
|
|
4369
|
+
} catch { /* 없다 */ }
|
|
4370
|
+
}
|
|
4371
|
+
return out;
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4157
4374
|
function cellInstall({ hooks = true, ifRepo = false } = {}) {
|
|
4158
4375
|
const out = [];
|
|
4159
4376
|
const t = cellTarget();
|
|
@@ -4178,6 +4395,9 @@ function cellInstall({ hooks = true, ifRepo = false } = {}) {
|
|
|
4178
4395
|
if (got !== want) return { ok: false, out: ['세포를 깔았는데 지문이 안 맞는다 — 쓰지 않는다.'] };
|
|
4179
4396
|
out.push(`세포를 ${CELL_PATH} 에 깔았다(${VERSION} · ${want.slice(0, 12)}…).`);
|
|
4180
4397
|
}
|
|
4398
|
+
// 판이 같아도 링크는 맞춘다 — 옛 판이 링크 없이 깔린 기계가 있다(이 기계가 그랬다).
|
|
4399
|
+
const linked = cellLinkOnPath();
|
|
4400
|
+
if (linked) out.push(linked);
|
|
4181
4401
|
if (hooks) {
|
|
4182
4402
|
// `ifRepo` 는 **자동으로 부를 때만** 준다 — 사람이 `install_cell` 을 부른 자리는 자기가
|
|
4183
4403
|
// 정한 곳이지만, 부팅마다 도는 걸음은 git 저장소가 아닌 폴더에 설정을 만들면 안 된다.
|
|
@@ -4210,6 +4430,8 @@ function cellUninstall() {
|
|
|
4210
4430
|
} else {
|
|
4211
4431
|
out.push('깔린 세포가 없다.');
|
|
4212
4432
|
}
|
|
4433
|
+
// PATH 에 건 심링크도 걷는다 — 남겨 두면 `gcell` 이 없는 파일을 가리킨다(dangling).
|
|
4434
|
+
out.push(...cellUnlinkOnPath());
|
|
4213
4435
|
// 자격 파일 옆의 경로 줄도 걷는다 — 남겨 두면 다음 판이 없는 것을 가리킨다.
|
|
4214
4436
|
try {
|
|
4215
4437
|
const env = path.join(os.homedir(), '.config/genetics/api.env');
|
package/package.json
CHANGED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# gazette-report
|
|
2
|
+
|
|
3
|
+
정적 HTML 보고서를 **한국 정부간행물(관보) 문서 문법**으로 조판하는 스킬.
|
|
4
|
+
|
|
5
|
+
`SKILL.md` 가 방법론, `scripts/` 가 검증 도구다.
|
|
6
|
+
|
|
7
|
+
## scripts
|
|
8
|
+
|
|
9
|
+
| 파일 | 무엇 | 실행 |
|
|
10
|
+
|---|---|---|
|
|
11
|
+
| `kcheck.mjs` | 대비(알파 합성)·제목 레벨·표 접근명·앵커·넘침(7폭)·인쇄 숨김·reduced-motion | `node kcheck.mjs <URL>` |
|
|
12
|
+
| `read.mjs` | 가독성 — 한글 자폭을 실측해 글자/줄·행간·대비 | `node read.mjs <URL> [폭]` |
|
|
13
|
+
| `flexcheck.mjs` | flex/grid 안에서 문장이 토막 나는 곳 | `node flexcheck.mjs <URL>` |
|
|
14
|
+
| `tone.mjs` | 개조식 톤 — 경어체 잔존 · 늘어짐 지표 9종 · **한 문장 한 줄** — **의존 0**(`fs` 뿐, playwright 불필요) | `node tone.mjs <파일.html>` |
|
|
15
|
+
| `pdfcheck.sh` | 실제 PDF 생성 → 쪽 구조·쪽번호·러닝 푸터 | `./pdfcheck.sh <URL> [out.pdf]` |
|
|
16
|
+
| `cssaudit.sh` | 활자 스케일·간격 격자·라운드·상자·표 마감·미봉책 + **route 판정** | `./cssaudit.sh <file.css>` |
|
|
17
|
+
| `fontchk.mjs` | **렌더된** 활자 크기·굵기 분포 (4개 폭) | `node fontchk.mjs <URL> 12,14,17,…` |
|
|
18
|
+
| `contentdiff.sh` | **내용 불변 게이트** — 조판이 본문·id·data·앵커를 바꿨는지 | `./contentdiff.sh <before.html> <after.html>` |
|
|
19
|
+
| `typesurvey.mjs` | **어느 것이** 어느 크기인가 — 서명별 크기·굵기 **분포** + 평균 자수 | `node typesurvey.mjs <URL> [폭]` |
|
|
20
|
+
| `extract.sh` | 검사가 아님 — 레퍼런스 CSS 에서 `template/gazette.css` 재추출 | `./extract.sh <원본.css> <나갈.css>` |
|
|
21
|
+
| `sentbreak.py` | 검사가 아님 — **한 문장 한 줄** 조판을 마크업에 새긴다(멱등, 낱말 불변) | `python3 sentbreak.py <파일.html> --apply` |
|
|
22
|
+
| `boundary.py` | 문장 끝 마침표 판별 (날짜·버전·경로·머리글자 제외). 단독 실행 시 자체 시험 | `python3 boundary.py` |
|
|
23
|
+
|
|
24
|
+
**선택자를 하드코딩하지 않는다.** `kcheck.mjs`·`read.mjs`·`typesurvey.mjs` 는 DOM 에서 글자를
|
|
25
|
+
가진 요소를 클래스 서명별로 자동 수집한다 — 어느 문서에나 그대로 쓸 수 있고, 새로 만든
|
|
26
|
+
컴포넌트도 검사 목록에 손대지 않아도 자동으로 잡힌다.
|
|
27
|
+
|
|
28
|
+
**대표값 하나로 서명을 요약하지 않는다.** 크기·굵기는 분포로 모은다. 첫 표본을 대표로 찍으면
|
|
29
|
+
같은 클래스가 변형이나 상속으로 갈리는 경우를 통째로 놓친다.
|
|
30
|
+
|
|
31
|
+
## template — 뼈대
|
|
32
|
+
|
|
33
|
+
새 문서는 `template/` 에서 시작한다. `gazette.css`(문서 무관 조판) + `skeleton.html`(가구 9요소 골격).
|
|
34
|
+
**뼈대 자체가 8종 검사를 통과한 상태**로 배포된다 — 자세한 것은 `template/README.md`.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
cp -r template 새문서 && cd 새문서 && python3 -m http.server 8781
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**뼈대를 손으로 고치지 마라.** `gazette.css` 는 레퍼런스 구현에서 뽑은 **추출물**이다.
|
|
41
|
+
조판 규칙이 바뀌면 레퍼런스를 고치고 `scripts/extract.sh` 를 다시 돌린다 —
|
|
42
|
+
손으로 따라 고쳤다가 레퍼런스에서 없앤 12px 등급이 뼈대에 47곳 남은 적이 있다.
|
|
43
|
+
|
|
44
|
+
## 준비물
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
cd scripts && npm i playwright-core # 브라우저 자동화 — **scripts/ 안에** 설치해야 한다.
|
|
48
|
+
# ESM 은 가져오는 모듈 위치에서 위로 훑는다. cwd 가 아니다.
|
|
49
|
+
brew install poppler # pdfinfo · pdftotext · pdftoppm (pdfcheck.sh)
|
|
50
|
+
export CHROME="/path/to/Google Chrome for Testing" # 없으면 ms-playwright 기본 경로
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 전형적 사용
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
python3 -m http.server 8780 & # 문서를 띄우고
|
|
57
|
+
node kcheck.mjs http://localhost:8780/index.html
|
|
58
|
+
node read.mjs http://localhost:8780/index.html 1440
|
|
59
|
+
node read.mjs http://localhost:8780/index.html 390
|
|
60
|
+
node flexcheck.mjs http://localhost:8780/index.html
|
|
61
|
+
node tone.mjs index.html
|
|
62
|
+
./pdfcheck.sh http://localhost:8780/index.html
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
전부 지적 0 이 될 때까지 고친다. **0표본 통과는 통과가 아니다** — 표본 수를 함께 본다.
|
|
66
|
+
|
|
67
|
+
## 출처
|
|
68
|
+
|
|
69
|
+
문체 검사(`tone.mjs`)의 늘어짐 지표 분류·처방, 심각도 3단계(S1/S2/S3), route 3경로는
|
|
70
|
+
[epoko77-ai/im-not-ai](https://github.com/epoko77-ai/im-not-ai) (MIT) 의 「AI 한글 티 분류 체계」에서
|
|
71
|
+
관보 보고서에도 해당하는 항목만 골라 가져왔다. 번역투·영어 인용·리듬 균일성은 가져오지 않았다.
|