genetics-mcp 2.17.1 → 2.18.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.
- package/bin/aarch64-apple-darwin/gcell +0 -0
- package/bin/manifest.json +2 -2
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/genetics.mjs +71 -16
- package/package.json +1 -1
- package/skills/genetics-report/SKILL.md +146 -131
- package/skills/genetics-report/assets/example/cases-briefing-designs.json +32 -0
- package/skills/genetics-report/assets/example/plate-pair-after.png +0 -0
- package/skills/genetics-report/assets/example/plate-pair-before.png +0 -0
- package/skills/genetics-report/assets/example/plate-pair.html +25 -0
- package/skills/genetics-report/assets/example/report-briefing-designs.json +736 -0
- package/skills/genetics-report/assets/example/report-briefing.json +27 -22
- package/skills/genetics-report/assets/example/report.json +5 -5
- package/skills/genetics-report/assets/figure.mjs +390 -63
- package/skills/genetics-report/assets/mockshot.mjs +3 -2
- package/skills/genetics-report/assets/pair.mjs +138 -0
- package/skills/genetics-report/assets/shot.mjs +87 -19
- package/skills/genetics-report/references/content-pipeline.md +34 -33
- package/skills/genetics-report/references/history.md +117 -0
- package/skills/genetics-report/references/illustrations.md +16 -12
- package/skills/genetics-report/references/lim-portal-format.md +175 -91
- package/skills/genetics-report/references/linear-read-test.workflow.js +47 -0
- package/skills/genetics-report/references/process.md +152 -90
- package/skills/genetics-report/references/readability.md +48 -24
- package/skills/genetics-report/references/reader-review.workflow.js +160 -27
- package/skills/genetics-report/references/visual-first.md +100 -35
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
2
|
+
"aarch64-apple-darwin": "2589e4eda175ae7f1158a3dd4b31b327fd8af85ea82bbb661fac7990567286af",
|
|
3
3
|
"aarch64-unknown-linux-musl": "aa25f20daefda931d8497d346bd47fb9b8a2acc3136ec2b9eeff18773a789e8c",
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
4
|
+
"x86_64-apple-darwin": "4de0feadcb7fe644c8004b76e0015518ffb330c6f13304684fb85aa2c99caa30",
|
|
5
5
|
"x86_64-unknown-linux-musl": "c97bcded75ec34cdb5a448159e7f057a756deb6a45fa7793b299d0040c19be93"
|
|
6
6
|
}
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -448,6 +448,15 @@ const isSvg = (b) => {
|
|
|
448
448
|
*/
|
|
449
449
|
const SHOT_QUALITY_LADDER = [82, 72, 62, 52];
|
|
450
450
|
|
|
451
|
+
/** 무손실로 굳혔을 때의 크기만 잰다(바이트는 버림). cwebp 가 없거나 실패하면 null. */
|
|
452
|
+
function losslessSize(buf) {
|
|
453
|
+
try {
|
|
454
|
+
if (isSvg(buf)) return null;
|
|
455
|
+
const r = spawnSync('cwebp', ['-quiet', '-lossless', '-exact', '-o', '-', '--', '-'], { input: buf, maxBuffer: IMAGE_MAX_DOWNLOAD });
|
|
456
|
+
return r.status === 0 ? r.stdout.length : null;
|
|
457
|
+
} catch { return null; }
|
|
458
|
+
}
|
|
459
|
+
|
|
451
460
|
function encodeShot(buf) {
|
|
452
461
|
const d = imageDims(buf);
|
|
453
462
|
const resize = (d && Math.max(d.w, d.h) > ASSET_LONG_EDGE)
|
|
@@ -526,6 +535,7 @@ function encodeAsset(buf, role, provider) {
|
|
|
526
535
|
* 그리고 있었다(2026-08-16 실측). 선언을 안 받으면 그 결함이 구조적으로 안 생긴다.
|
|
527
536
|
*/
|
|
528
537
|
async function materializeAssets(data) {
|
|
538
|
+
ASSET_HINTS = [];
|
|
529
539
|
if (!data || !Array.isArray(data.assets) || !data.assets.length) return data;
|
|
530
540
|
const out = [];
|
|
531
541
|
for (const a of data.assets) {
|
|
@@ -552,6 +562,15 @@ async function materializeAssets(data) {
|
|
|
552
562
|
// 원격보다 **로컬이 이긴다** — 손에 든 실물이 주소보다 확실하다.
|
|
553
563
|
const raw = fpath ? readLocalImage(String(fpath)) : await fetchImage(String(a.source));
|
|
554
564
|
const enc = encodeAsset(raw, String(a.role ?? ''), String(a.provider ?? ''));
|
|
565
|
+
// 평면 그림(HTML 개념도·도표)을 user-provided 로 올리면 손실 사다리가 선·글자를 뭉갠다(2026-09-09 앳플리 #173
|
|
566
|
+
// 그림 1-2 반려). 무손실이 손실보다 **작으면** 그 그림은 평면 그림이다 — 그때만 한 줄 알린다(계약은 안 바꾼다).
|
|
567
|
+
if (String(a.role) === 'figure' && String(a.provider) === 'user-provided') {
|
|
568
|
+
const ll = losslessSize(raw);
|
|
569
|
+
if (ll !== null && ll <= ASSET_MAX_ONE && ll <= enc.length) {
|
|
570
|
+
ASSET_HINTS.push(`도판 「${a.id}」: 무손실 ${ll.toLocaleString()}B 가 손실 ${enc.length.toLocaleString()}B 보다 작습니다 — `
|
|
571
|
+
+ '평면 그림이면 provider: deterministic 으로(손실 사다리는 선·글자를 뭉갭니다). 사진에 가까운 화면 캡처면 그대로.');
|
|
572
|
+
}
|
|
573
|
+
}
|
|
555
574
|
const d = imageDims(enc);
|
|
556
575
|
out.push({
|
|
557
576
|
...meta,
|
|
@@ -872,6 +891,8 @@ const REPORT_DATA = {
|
|
|
872
891
|
+ 'rows[].no 가 `4.1`·`04-1` 모양이면 그 사례로 링크가 된다. '
|
|
873
892
|
+ '그림 번호(`그림 5-1`)는 적지 않는다 — 자리가 매기고 `plates` 와 한 셈을 쓴다.\n'
|
|
874
893
|
+ '· `plates`: [assets[].id, …] — 절 본문 도판.\n'
|
|
894
|
+
+ '· 시각 우선(dry_run 이 ⚠ 로 센다 — 서식 문서 §10-1): 절마다 표·흐름도·차트·도판 중 하나, 문단 220자 안, 산문의 참조 번호 3개 이하, 판단 요청 6항 이상이면 한눈표+관문 흐름도, 3,000자 넘는 문서엔 핵심 굵게. 집필 공식(독자 한 명·예시 한 명·한 장 이야기·용어 잠금·그림 먼저)은 스킬 references/visual-first.md.\n'
|
|
895
|
+
+ '· 인라인 표기(본문 문자열 어디서나 — 문단·도입문·여백 주석·표 셀·흐름도 칸·주석·부록 행): `**굵게**` → 핵심 구절, `==강조==` → 놓치면 안 되는 한 구절(노란 밑면), `{{ok|보통}}` → 상태 칩(톤 ok·wait·bad·info·muted 다섯뿐, 글자 40자 안), `[글자](https://…)` → 새 창으로 여는 하이퍼링크, 맨 `https://…` 주소도 그대로 링크가 됨(http(s) 만, 꼬리 문장부호는 링크 밖). 표기가 없으면 한 글자도 안 바뀐다. 핵심 한두 구절에만 — 문단 절반이 굵으면 굵기가 위계를 못 나른다. 출처는 부록 글자에만 두지 말고 건의 `source_url` 에도 실어라(「출처 원문」 행이 된다).\n'
|
|
875
896
|
+ '· `tables`: [{caption, cols[], rows[[]], align?, hint?, note?}] — **관보의 자료표.** '
|
|
876
897
|
+ '여러 항목을 축으로 견주는 자리다. 불릿 목록에 수를 눌러 담고 있으면 그건 표여야 한다. '
|
|
877
898
|
+ 'kind 와 무관하게 쓴다(kind:status 의 `board_title` 대조표와는 다른 것 — 저건 건 전용이다). '
|
|
@@ -886,6 +907,7 @@ const REPORT_DATA = {
|
|
|
886
907
|
+ 'HTML 막대와 CSS 로 짓는다. 값은 **본문 글자로 남고** 막대는 그 옆에 서므로 막대가 안 보이는 '
|
|
887
908
|
+ '곳에서도 문서가 성립한다. value 는 **표기 그대로 적는다**(`"118,716"`) — 콤마는 셈에서 걷어내므로 '
|
|
888
909
|
+ '같은 문서의 표와 표기가 갈리지 않게 하려면 데이터가 표기를 정해야 한다. '
|
|
910
|
+
+ 'rows[].tone(ok·wait·bad·info·muted) 을 주면 그 행의 막대와 값에 뜻 색이 붙는다 — 예: 처리 완료 행은 muted, 위험 행은 bad. '
|
|
889
911
|
+ 'toggle{a,b} 를 주면 rows 는 value 대신 a·b 를 갖고 두 축을 `:checked` 로 갈아 낀다 '
|
|
890
912
|
+ '(체크박스는 스크립트가 아니라 상태다). 그림 번호는 `flow`·`plates` 와 **한 셈**을 쓴다.\n'
|
|
891
913
|
+ '데이터가 있을 때만 그려진다 — 없는 문구를 지어내지 말 것.\n'
|
|
@@ -1028,6 +1050,13 @@ const REPORT_DATA = {
|
|
|
1028
1050
|
+ '이 축을 포털본이 통째로 버려 2차 출처와 미확인 주장이 **사실로 단언돼 나갔다.** '
|
|
1029
1051
|
+ '증거를 실으면서 등급을 안 붙이면 문서가 실제보다 확실해 보인다.',
|
|
1030
1052
|
},
|
|
1053
|
+
wide: {
|
|
1054
|
+
type: 'boolean',
|
|
1055
|
+
description: '**전폭 도판.** 두 단 그리드 밖, 절 끝에 문서 폭(약 1030px)으로 선다. '
|
|
1056
|
+
+ '나란히 둔 화면 둘처럼 본문 단(760px)에서 절반으로 줄면 글자가 안 읽히는 그림에 켠다. '
|
|
1057
|
+
+ '켜면 그 절의 그림 번호는 마지막이 된다. 켜든 말든 모든 도판은 눌러서 크게 보기(원본 '
|
|
1058
|
+
+ '긴 변 1400px)가 열린다. 집필 때 figure.mjs --display 로 표시 글자 크기를 먼저 재라(하한 14px).',
|
|
1059
|
+
},
|
|
1031
1060
|
brief: {
|
|
1032
1061
|
type: 'string',
|
|
1033
1062
|
description: '**무엇을 그릴지 — 장면 한두 문장.** 이것만 주면 서버가 그린다(그리는 동안 '
|
|
@@ -2535,7 +2564,9 @@ const TOOLS = [
|
|
|
2535
2564
|
type: 'object',
|
|
2536
2565
|
properties: {
|
|
2537
2566
|
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2538
|
-
title: { type: 'string', description: '한 줄 제목.
|
|
2567
|
+
title: { type: 'string', description: '한 줄 제목. 같은 경로가 있으면 expected_revision 없이는 저장되지 않는다.' },
|
|
2568
|
+
expected_revision: { type: 'string', pattern: '^[a-f0-9]{64}$', description: '기존 사례 수정 시 get_case에서 읽은 revision. 신규 생성은 생략. 충돌하면 원문을 다시 읽고 변경을 재검토한다.' },
|
|
2569
|
+
commit_id: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9-]{15,79}$', description: '이 저장 작업의 고유 UUID. 첫 호출 전에 생성하고 응답 유실·재시도에는 같은 값과 같은 본문을 유지한다. 별도 수정은 새 값을 쓴다.' },
|
|
2539
2570
|
symptom: { type: 'string', description: '겪은 현상. 원인이 아니라 **눈에 보인 것**을 그대로.' },
|
|
2540
2571
|
cause: { type: 'string', description: '원인. 모르면 비운다 — 지어내지 않는다.' },
|
|
2541
2572
|
fix: { type: 'string', description: '처방. 다시 만나면 그대로 하면 되는 동작.' },
|
|
@@ -2555,7 +2586,7 @@ const TOOLS = [
|
|
|
2555
2586
|
} }, description: '사용자가 브라우저에서 **집은 요소**(iab 픽). 사용자가 "iab N" 이라 했으면 훅이 끼운 「#N <tag> · 경로:줄 · sel `…` · "글자"」 줄에서 selector·text·source 를 **그대로** 옮긴다 — 지어내지 않는다. '
|
|
2556
2587
|
+ '픽은 이 세션 밖으로 안 넘어가므로(다른 기계·다른 사람은 못 본다) 이 칸이 유일한 기록이다. 장에 「집은 자리」 절로 선다(최대 20, 칸당 300자).' },
|
|
2557
2588
|
},
|
|
2558
|
-
required: ['tenant', 'title', 'symptom', 'fix'],
|
|
2589
|
+
required: ['tenant', 'title', 'symptom', 'fix', 'commit_id'],
|
|
2559
2590
|
},
|
|
2560
2591
|
},
|
|
2561
2592
|
{
|
|
@@ -2577,7 +2608,9 @@ const TOOLS = [
|
|
|
2577
2608
|
type: 'object',
|
|
2578
2609
|
properties: {
|
|
2579
2610
|
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2580
|
-
title: { type: 'string', description: '한 줄 제목.
|
|
2611
|
+
title: { type: 'string', description: '한 줄 제목. 다른 결정은 제목을 달리한다. 기존 결정은 expected_revision을 전달해야 수정된다.' },
|
|
2612
|
+
expected_revision: { type: 'string', pattern: '^[a-f0-9]{64}$', description: '기존 결정 수정 시 get_case에서 읽은 revision. 신규 생성은 생략. 충돌하면 원문을 다시 읽고 변경을 재검토한다.' },
|
|
2613
|
+
commit_id: { type: 'string', pattern: '^[a-zA-Z0-9][a-zA-Z0-9-]{15,79}$', description: '이 저장 작업의 고유 UUID. 첫 호출 전에 생성하고 응답 유실·재시도에는 같은 값과 같은 본문을 유지한다. 별도 수정은 새 값을 쓴다.' },
|
|
2581
2614
|
what: { type: 'string', description: '무엇을 정했나 — 한 문장.' },
|
|
2582
2615
|
who: { type: 'string', description: '정한 사람 또는 그 자리에 있던 사람. 모르면 비운다.' },
|
|
2583
2616
|
when: { type: 'string', description: '정한 날짜(예: 2026-08-27). 모르면 비운다 — 오늘로 지어내지 말 것.' },
|
|
@@ -2599,7 +2632,7 @@ const TOOLS = [
|
|
|
2599
2632
|
url: { type: 'string', description: '그 화면 주소(알면)' },
|
|
2600
2633
|
} }, description: '사용자가 브라우저에서 **집은 요소**(iab 픽) — remember_case 의 같은 칸. 사용자가 "iab N" 이라 했으면 훅 줄의 #N 에서 그대로 옮긴다, 지어내지 않는다. 장에 「집은 자리」 절로 선다(최대 20).' },
|
|
2601
2634
|
},
|
|
2602
|
-
required: ['tenant', 'title', 'what'],
|
|
2635
|
+
required: ['tenant', 'title', 'what', 'commit_id'],
|
|
2603
2636
|
},
|
|
2604
2637
|
},
|
|
2605
2638
|
{
|
|
@@ -2616,8 +2649,9 @@ const TOOLS = [
|
|
|
2616
2649
|
properties: {
|
|
2617
2650
|
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2618
2651
|
path: { type: 'string', description: 'search_cases·remember_decision 이 준 path 그대로(decisions/….md). 제목을 줘도 저장과 같은 자로 찾는다.' },
|
|
2652
|
+
expected_revision: { type: 'string', pattern: '^[a-f0-9]{64}$', description: 'get_case로 읽은 삭제 대상의 revision.' },
|
|
2619
2653
|
},
|
|
2620
|
-
required: ['tenant', 'path'],
|
|
2654
|
+
required: ['tenant', 'path', 'expected_revision'],
|
|
2621
2655
|
},
|
|
2622
2656
|
},
|
|
2623
2657
|
{
|
|
@@ -2640,8 +2674,9 @@ const TOOLS = [
|
|
|
2640
2674
|
properties: {
|
|
2641
2675
|
tenant: { type: 'string', description: '대상 소속처 slug(이름도 받는다 — list_my_tenants 참조).' },
|
|
2642
2676
|
path: { type: 'string', description: 'search_cases·remember_case 가 준 path 그대로(cases/….md). 제목을 줘도 저장과 같은 자로 찾는다.' },
|
|
2677
|
+
expected_revision: { type: 'string', pattern: '^[a-f0-9]{64}$', description: 'get_case로 읽은 삭제 대상의 revision.' },
|
|
2643
2678
|
},
|
|
2644
|
-
required: ['tenant', 'path'],
|
|
2679
|
+
required: ['tenant', 'path', 'expected_revision'],
|
|
2645
2680
|
},
|
|
2646
2681
|
},
|
|
2647
2682
|
{
|
|
@@ -2947,9 +2982,12 @@ const TOOLS = [
|
|
|
2947
2982
|
* `api.php` 에 'warnings' 는 0건이었다. 그런데 도구는 이 코드를 갖고 있었으니,
|
|
2948
2983
|
* 에이전트는 「경고 없음」으로 읽었고 확인할 방법이 없었다. 전달 경로를 만든 뒤에도
|
|
2949
2984
|
* 찍는 자리가 흩어져 있으면 또 한 곳이 빠지므로 여기 하나로 모은다. */
|
|
2985
|
+
// 자산을 굳히며 이쪽(작성자 기계)에서만 알 수 있는 것 — 서버 경고와 한 목록으로 낸다(readability.md R8).
|
|
2986
|
+
let ASSET_HINTS = [];
|
|
2950
2987
|
function warnText(r) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2988
|
+
const w = [...(r?.warnings ?? []), ...ASSET_HINTS];
|
|
2989
|
+
return w.length
|
|
2990
|
+
? `\n\n경고 ${w.length}건:\n· ${w.join('\n· ')}`
|
|
2953
2991
|
: '';
|
|
2954
2992
|
}
|
|
2955
2993
|
|
|
@@ -4197,8 +4235,11 @@ async function callTool(name, args = {}) {
|
|
|
4197
4235
|
return lines.join('\n');
|
|
4198
4236
|
}
|
|
4199
4237
|
case 'remember_decision': {
|
|
4238
|
+
if (typeof args.commit_id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9-]{15,79}(?![\s\S])/.test(args.commit_id)) {
|
|
4239
|
+
throw new Error('commit_id는 영숫자로 시작하는 16~80자의 영숫자·하이픈이어야 합니다. 최초 요청의 키를 재시도에도 유지하세요.');
|
|
4240
|
+
}
|
|
4200
4241
|
const r = await api('POST', '/api/wiki/decision?tenant=' + encodeURIComponent(args.tenant), {
|
|
4201
|
-
title: args.title, what: args.what,
|
|
4242
|
+
title: args.title, what: args.what, expected_revision: args.expected_revision, commit_id: args.commit_id,
|
|
4202
4243
|
who: args.who || '', when: args.when || '', why: args.why || '',
|
|
4203
4244
|
reopen: args.reopen || '', status: args.status || '', tags: args.tags || [],
|
|
4204
4245
|
work: args.work || '', order: args.order || [], plan: args.plan || [],
|
|
@@ -4208,6 +4249,10 @@ async function callTool(name, args = {}) {
|
|
|
4208
4249
|
...myCitation(),
|
|
4209
4250
|
});
|
|
4210
4251
|
const lines = [`${r.note} — ${r.tenant} · ${r.path}`];
|
|
4252
|
+
if (r.revision) lines.push(`revision: ${r.revision}\ncommit_id: ${r.commit_id || ''}`);
|
|
4253
|
+
if (r.effects_pending) lines.push(r.effects_state === 'failed'
|
|
4254
|
+
? '원문은 저장됐지만 후속 정리가 재시도 한도에 닿았습니다. 운영 확인이 필요합니다.'
|
|
4255
|
+
: '원문은 저장됐고 후속 정리는 대기 중입니다. 같은 내용을 새로 저장하지 마세요.');
|
|
4211
4256
|
if (Number(r.picks) > 0) lines.push(`집은 자리 ${r.picks}건 실림 — 다른 기계·다른 사람도 그 요소를 본다.`);
|
|
4212
4257
|
// 안 적은 것을 **그렇게 말한다** — 조용히 열린 결정을 만들거나 계산 밖에 두지 않는다.
|
|
4213
4258
|
if (!args.reopen) {
|
|
@@ -4222,7 +4267,7 @@ async function callTool(name, args = {}) {
|
|
|
4222
4267
|
// 전체 맥락 정리 줄이 섰는지도 말한다 — 결정 저장이 종합의 박자가 됐다(2026-08-31). 옛 서버(칸 없음)는 침묵.
|
|
4223
4268
|
if (r.insights_queued === true) {
|
|
4224
4269
|
lines.push('이 결정으로 전체 맥락 정리가 다시 돕니다(몇 분 뒤 지식 화면에).');
|
|
4225
|
-
} else if (r.insights_queued === false) {
|
|
4270
|
+
} else if (r.insights_queued === false && !r.effects_pending) {
|
|
4226
4271
|
lines.push('전체 맥락 정리 줄은 안 섰다 — 재료가 아직 문턱 아래(배경 문서 3장 또는 결정·사례 5장)거나 이미 줄이 서 있다.');
|
|
4227
4272
|
}
|
|
4228
4273
|
// 표지(work:)의 앞날 — 문장의 정본은 서버(work_decl_mark_note → 응답 work_note)다.
|
|
@@ -4234,14 +4279,15 @@ async function callTool(name, args = {}) {
|
|
|
4234
4279
|
// 구서버(work_note 이전 판) — 판정값만 기계적으로 옮긴다. 안내문을 여기서 지어내면
|
|
4235
4280
|
// 새 서버의 문장과 두 벌이 된다(위와 같은 병).
|
|
4236
4281
|
lines.push(`일 표지 «${args.work}» 판정: ${r.work_mark} — 서버 판이 낡아 안내문이 없다(새 판은 문장을 함께 보낸다).`);
|
|
4237
|
-
} else if (args.work) {
|
|
4282
|
+
} else if (args.work && !r.effects_pending) {
|
|
4238
4283
|
lines.push('표지(work)를 적었지만 이 서버는 아직 그 칸을 모른다 — 판이 낡았다.');
|
|
4239
4284
|
}
|
|
4240
4285
|
return lines.join('\n');
|
|
4241
4286
|
}
|
|
4242
4287
|
case 'forget_decision': {
|
|
4243
4288
|
const r = await api('DELETE', '/api/wiki/decision?tenant=' + encodeURIComponent(args.tenant)
|
|
4244
|
-
+ '&path=' + encodeURIComponent(String(args.path || ''))
|
|
4289
|
+
+ '&path=' + encodeURIComponent(String(args.path || ''))
|
|
4290
|
+
+ '&expected_revision=' + encodeURIComponent(String(args.expected_revision || '')));
|
|
4245
4291
|
const lines = [`${r.note} — ${r.tenant} · ${r.path}`];
|
|
4246
4292
|
if (r.trashed_to) lines.push(`휴지통: ${r.trashed_to} (복구는 서버에서 사람이 파일을 되돌린다)`);
|
|
4247
4293
|
if (typeof r.index_dropped === 'number') {
|
|
@@ -4253,7 +4299,8 @@ async function callTool(name, args = {}) {
|
|
|
4253
4299
|
// forget_decision 과 같은 꼴(2026-09-03) — 서버 note·trashed_to·index_dropped 를 **그대로** 옮긴다.
|
|
4254
4300
|
// 프로즈를 여기서 다시 지으면 두 벌이 된다(remember_decision 의 work_note 교훈, 2026-09-01).
|
|
4255
4301
|
const r = await api('DELETE', '/api/wiki/case?tenant=' + encodeURIComponent(args.tenant)
|
|
4256
|
-
+ '&path=' + encodeURIComponent(String(args.path || ''))
|
|
4302
|
+
+ '&path=' + encodeURIComponent(String(args.path || ''))
|
|
4303
|
+
+ '&expected_revision=' + encodeURIComponent(String(args.expected_revision || '')));
|
|
4257
4304
|
const lines = [`${r.note} — ${r.tenant} · ${r.path}`];
|
|
4258
4305
|
if (r.trashed_to) lines.push(`휴지통: ${r.trashed_to} (복구는 서버에서 사람이 파일을 되돌린다)`);
|
|
4259
4306
|
if (typeof r.index_dropped === 'number') {
|
|
@@ -4262,8 +4309,12 @@ async function callTool(name, args = {}) {
|
|
|
4262
4309
|
return lines.join('\n');
|
|
4263
4310
|
}
|
|
4264
4311
|
case 'remember_case': {
|
|
4312
|
+
if (typeof args.commit_id !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9-]{15,79}(?![\s\S])/.test(args.commit_id)) {
|
|
4313
|
+
throw new Error('commit_id는 영숫자로 시작하는 16~80자의 영숫자·하이픈이어야 합니다. 최초 요청의 키를 재시도에도 유지하세요.');
|
|
4314
|
+
}
|
|
4265
4315
|
const r = await api('POST', '/api/wiki/case?tenant=' + encodeURIComponent(args.tenant), {
|
|
4266
4316
|
title: args.title, symptom: args.symptom, cause: args.cause || '', fix: args.fix,
|
|
4317
|
+
expected_revision: args.expected_revision, commit_id: args.commit_id,
|
|
4267
4318
|
evidence: args.evidence || '', tags: args.tags || [],
|
|
4268
4319
|
paths: args.paths || [], commands: args.commands || [], errors: args.errors || [],
|
|
4269
4320
|
picks: Array.isArray(args.picks) ? args.picks : [], // 집은 자리(2026-09-03) — 그대로 넘긴다, 모양은 서버가 검사
|
|
@@ -4273,10 +4324,14 @@ async function callTool(name, args = {}) {
|
|
|
4273
4324
|
});
|
|
4274
4325
|
// 밀기 결과도 그대로 옮긴다 — 몇에게 갔고 누가 왜 못 받았는지(구서버는 필드가 없어 침묵).
|
|
4275
4326
|
const lines = [`${r.note} — ${r.tenant} · ${r.path}`];
|
|
4327
|
+
if (r.revision) lines.push(`revision: ${r.revision}\ncommit_id: ${r.commit_id || ''}`);
|
|
4328
|
+
if (r.effects_pending) lines.push(r.effects_state === 'failed'
|
|
4329
|
+
? '원문은 저장됐지만 후속 정리가 재시도 한도에 닿았습니다. 운영 확인이 필요합니다.'
|
|
4330
|
+
: '원문은 저장됐고 후속 정리는 대기 중입니다. 같은 내용을 새로 저장하지 마세요.');
|
|
4276
4331
|
if (Number(r.picks) > 0) lines.push(`집은 자리 ${r.picks}건 실림 — 다른 기계·다른 사람도 그 요소를 본다.`);
|
|
4277
4332
|
// ★ 창구가 낸 경고를 **맨 앞에** 옮긴다 — 쓰는 그 순간이 고치기 가장 싼 자리다(2026-09-01).
|
|
4278
4333
|
if (r.warn) lines.push('⚠ ' + r.warn);
|
|
4279
|
-
if (Array.isArray(r.matched_paths)) {
|
|
4334
|
+
if (Array.isArray(r.matched_paths) && !r.effects_pending) {
|
|
4280
4335
|
if (!r.matched_paths.length) {
|
|
4281
4336
|
lines.push('경로가 없어 아무에게도 안 갔다 — 그리고 **이 장이 어느 저장소의 일인지도 안 남았다**. 근거에 저장소 상대 경로(예: www/portal/lib/x.php:12)를 적으면 그 파일을 읽은 채로 든 세션에게 가고, 소속을 잘못 적었을 때 기계가 짚을 수 있다.');
|
|
4282
4337
|
} else if (r.pushed > 0) {
|
|
@@ -4329,7 +4384,7 @@ async function callTool(name, args = {}) {
|
|
|
4329
4384
|
const mine = myCitation().session_uuid || '';
|
|
4330
4385
|
const r = await api('GET', '/api/wiki/page?tenant=' + encodeURIComponent(args.tenant)
|
|
4331
4386
|
+ '&path=' + encodeURIComponent(args.path) + (mine ? '&session=' + mine : ''));
|
|
4332
|
-
return `${r.path}${r.signals ? '\n' + r.signals : ''}\n\n${r.markdown}`;
|
|
4387
|
+
return `${r.path}\nrevision: ${r.revision}${r.signals ? '\n' + r.signals : ''}\n\n${r.markdown}`;
|
|
4333
4388
|
}
|
|
4334
4389
|
case 'case_verdict': {
|
|
4335
4390
|
const r = await api('POST', '/api/wiki/verdict?tenant=' + encodeURIComponent(args.tenant), {
|
|
@@ -4719,7 +4774,7 @@ async function handle(line) {
|
|
|
4719
4774
|
' 로컬 메모는 그 기계에서만 남는다. 다른 사람·다른 기계가 쓸 것이면 여기로 남긴다.',
|
|
4720
4775
|
'',
|
|
4721
4776
|
'「남겨줘 / 기억해줘 / 업데이트 / 갱신 / 동기화 / 올려둬」는 **무엇을 남기느냐로 갈린다**:',
|
|
4722
|
-
'· 알아낸 것 하나(증상과 처방) → remember_case (
|
|
4777
|
+
'· 알아낸 것 하나(증상과 처방) → remember_case (갱신은 get_case의 revision을 expected_revision으로 전달)',
|
|
4723
4778
|
'· 정한 것(증상이 없다 — 기획·디자인의 판단, 반려, "이렇게 가기로 했다") → remember_decision',
|
|
4724
4779
|
'· 지금 이 대화 전체 → push_session (다른 기계가 이어받게)',
|
|
4725
4780
|
'· 고객에게 나갈 문서 → publish_report / update_report',
|