genetics-mcp 2.17.0 → 2.18.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/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/gazette-report/SKILL.md +14 -3
- package/skills/gazette-report/scripts/boundary.py +31 -2
- package/skills/gazette-report/scripts/tone.mjs +18 -2
- package/skills/gazette-report/scripts/tone.test.mjs +13 -0
- package/skills/genetics-report/SKILL.md +126 -53
- package/skills/genetics-report/assets/example/cases-briefing.json +5 -5
- package/skills/genetics-report/assets/example/report-briefing.json +583 -199
- package/skills/genetics-report/assets/example/report.json +15 -15
- package/skills/genetics-report/assets/figure.mjs +288 -0
- package/skills/genetics-report/assets/mockshot.mjs +44 -0
- package/skills/genetics-report/assets/shot.mjs +66 -0
- package/skills/genetics-report/references/content-pipeline.md +14 -5
- package/skills/genetics-report/references/lim-portal-format.md +209 -47
- package/skills/genetics-report/references/process.md +40 -11
- package/skills/genetics-report/references/readability.md +77 -0
- package/skills/genetics-report/references/reader-review.workflow.js +49 -0
- package/skills/genetics-report/references/visual-first.md +132 -0
- package/skills/gazette-report/scripts/package-lock.json +0 -24
- package/skills/genetics-report/assets/example//354/230/210/354/213/234-/353/263/264/352/263/240/354/204/234.html +0 -2258
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
2
|
+
"aarch64-apple-darwin": "1e93db10afd8d6125508d20af650c290c6967f19ac4e24543dae2a42ced1fb7f",
|
|
3
3
|
"aarch64-unknown-linux-musl": "aa25f20daefda931d8497d346bd47fb9b8a2acc3136ec2b9eeff18773a789e8c",
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
4
|
+
"x86_64-apple-darwin": "84b539085af2ad89018296d54fe0a89ec932cae1ee0b67a445684ddbb8dfb8fb",
|
|
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',
|
package/package.json
CHANGED
|
@@ -144,7 +144,8 @@ report-v2 144종을 이렇게 훑어 4건이 나왔다 — CSS 만 봐서는 하
|
|
|
144
144
|
| 층 | 무엇 | 빠뜨리면 |
|
|
145
145
|
|---|---|---|
|
|
146
146
|
| **가구** | 표지 · 차례 · 간지 · 측면 색인 · 표 캡션 · 판권지 | 웹페이지로 읽힘 |
|
|
147
|
-
| **페이지 문법** | **두 단 본문 + 여백 주석 단** · 위치 브레드크럼 머리띠 · PART 층위
|
|
147
|
+
| **페이지 문법** | **두 단 본문 + 여백 주석 단** · 위치 브레드크럼 머리띠 · PART 층위 | 「문서처럼 꾸민 웹페이지」에서 멈춤 |
|
|
148
|
+
| **자리별 톤** | 기록 자리는 개조식, **요청 자리는 하십시오체**(톤 절) | 기록이 겉돌거나, 요청이 하대로 읽힘 |
|
|
148
149
|
|
|
149
150
|
본문 쪽의 실제 골격:
|
|
150
151
|
|
|
@@ -291,7 +292,17 @@ PDF의 파스텔을 그대로 옮기면 「로봇처럼 베낀 것」이 된다.
|
|
|
291
292
|
|
|
292
293
|
질문형(Why 블록, 활용법 라벨)은 `~하는가?` / `~인가?`. `~합니까?` 아님.
|
|
293
294
|
|
|
294
|
-
검사:
|
|
295
|
+
검사: **기록 자리**에서 `습니다|십시오|합니다|입니다|됩니다|주세요` 정규식이 **0건**이어야 한다(스크립트 태그 제외).
|
|
296
|
+
|
|
297
|
+
⚠ **요청 자리는 반대다 — 하십시오체로 맺는다.** 상대에게 직접 말을 거는 자리
|
|
298
|
+
(요청 절의 강조문·판단 항목, PART I 간지, 판권지 회신 안내)는 종결까지 존댓말이어야 한다 —
|
|
299
|
+
「확인 **부탁드립니다**」·「회신 주시면 **반영하겠습니다**」·「열어 **주시기 바랍니다**」.
|
|
300
|
+
명사형 종결 `-ㅁ/-음` 은 한국어 상대높임 등급 **밖**이라 청자 높임이 0 이고, 사람에게 직접 쓰면
|
|
301
|
+
하대로 읽힌다 — 겸양 어간이 든 「부탁드림」도 존대가 아니다. 제목은 명사형 「~ 요청」(검수·보고) 또는 물음(결정 문서의 판단 항목)으로 둔다.
|
|
302
|
+
`tone.mjs` 는 판단 항목 본문(`p.ask__body`)과 「」 인용만 빼고 센다(2026-09-12, 그 파일 ① 경어체 절) — **그 밖의 요청 자리
|
|
303
|
+
(강조문·간지·판권지 회신 안내)에서 난 S1 경어체 지적은 사람이 걸러 읽고 그대로 둔다.** 이것을 몰라 말투 반려가 네 번 났다
|
|
304
|
+
(2026-09-02 · 09-03 ×2 · 09-11). 정본은 genetics(hnl)
|
|
305
|
+
`decisions/고객-문서-요청-자리는-하십시오체로-맺는다-명사형-종결은-하대로-읽힌다.md`.
|
|
295
306
|
|
|
296
307
|
### 한 문장 한 줄 — 한글은 마침표에서 줄을 바꾼다
|
|
297
308
|
|
|
@@ -437,7 +448,7 @@ flush」로 두 방식이 섞인다 — 실측: 여백 주석 단에서 Key Poin
|
|
|
437
448
|
| `read.mjs` | 읽는 글 전수의 **글자/줄 · 행간 · 대비** | 43자 이하 · 행간 1.6+ · 지적 0 |
|
|
438
449
|
| `flexcheck.mjs` | flex/grid 안에서 **문장이 토막 나는 곳** | 0건 |
|
|
439
450
|
| `fontchk.mjs` | **렌더된** 활자 크기·굵기 분포를 4개 폭(1440·1100·900·390)에서. CSS 토큰화로는 안 잡히는 것 | 스케일 밖 **0** · 굵기 **3종** · 굵게 **≤55%** |
|
|
440
|
-
| `tone.mjs` | ① 본문 **경어체 잔존**(S1) ② **늘어짐 지표 9종**(S2, 천 자당 밀도) — 문두 접속사 · 추측 종결 · 이중 완곡 · 「이는 ~」 · 형식명사 결합 · 괄호 부연 · 낫표 · 대시 · 인라인 강조 ③ **한 문장 한 줄**(S1) — 마침표로 이어 붙인 블록 | 경어체
|
|
451
|
+
| `tone.mjs` | ① 본문 **경어체 잔존**(S1) ② **늘어짐 지표 9종**(S2, 천 자당 밀도) — 문두 접속사 · 추측 종결 · 이중 완곡 · 「이는 ~」 · 형식명사 결합 · 괄호 부연 · 낫표 · 대시 · 인라인 강조 ③ **한 문장 한 줄**(S1) — 마침표로 이어 붙인 블록 | 경어체 **기록 자리 0건**(요청 자리 지적은 의도 — 위 톤 절) · 이어 붙인 블록 **0개** · 밀도 초과 **0항** |
|
|
441
452
|
| `sentbreak.py` | 검사가 아니라 **적용** — 문장마다 `<span class="sn">` 을 새긴다. 멱등 | 돌린 뒤 `contentdiff.sh` 통과 |
|
|
442
453
|
| `contentdiff.sh` | **내용 불변 게이트** — 조판이 본문 낱말 · `id` · `data-*` · 앵커를 바꿨는지 전후 대조 | 전 항목 차이 **0** |
|
|
443
454
|
| `pdfcheck.sh` | 실제 PDF 생성 → 쪽수 · 절별 새 쪽 시작 · 쪽번호 · 러닝 푸터 | 표지 단독 · 절마다 새 쪽 · 푸터 = 쪽수-1 |
|
|
@@ -6,8 +6,23 @@
|
|
|
6
6
|
"""
|
|
7
7
|
import re
|
|
8
8
|
|
|
9
|
-
# ① 날짜 꼴을 먼저 가린다: 2026. 8. 21. / 2026. 8.
|
|
10
|
-
|
|
9
|
+
# ① 날짜 꼴을 먼저 가린다: 2026. 8. 21. / 2026. 8. / 9. 8.
|
|
10
|
+
#
|
|
11
|
+
# ⚠ 연도 없는 「달. 일.」을 오래 안 가려서 사고가 났다(2026-09-11, FISOFT #176 표 6-1).
|
|
12
|
+
# 관보 표는 연도를 열 머리나 기준일로 빼고 칸에는 `9. 8.` 만 적는 것이 표준인데,
|
|
13
|
+
# 그 꼴이 마스크에 없어 `9. 8. 출시됨` 이 `9.` / `8.` / `출시됨` **세 줄로 쪼개졌다.**
|
|
14
|
+
# 같은 칸의 아이폰 쪽은 마침표가 없어 한 줄로 멀쩡히 서는 바람에, 안드로이드만
|
|
15
|
+
# 파편으로 보여 「아직 심사 안 된 것」으로 읽혔다.
|
|
16
|
+
#
|
|
17
|
+
# 좁게 잡는다 — 두 수가 **마침표와 공백만 사이에 두고 맞붙은** 꼴만 가린다.
|
|
18
|
+
# · `부록 2. 자세한 …` 은 뒤가 한글이라 안 걸린다.
|
|
19
|
+
# · `전환 완료. 4건은 …` 은 `4` 뒤가 마침표가 아니라 안 걸린다.
|
|
20
|
+
# · `1. 첫째. 2. 둘째.` 처럼 사이에 글이 낀 열거도 안 걸린다.
|
|
21
|
+
# · 앞이 숫자·마침표면 제외해 온전한 연도 꼴의 뒷부분을 물지 않는다(연도 꼴이 먼저 걸리므로 이중 방어).
|
|
22
|
+
DATE = re.compile(
|
|
23
|
+
r'\d{4}\.\s*\d{1,2}\.(?:\s*\d{1,2}\.)?' # 2026. 8. 21. / 2026. 8.
|
|
24
|
+
r'|(?<![\d.])\d{1,2}\.\s*\d{1,2}\.(?!\s*\d)' # 9. 8. (연도 없는 달·일)
|
|
25
|
+
)
|
|
11
26
|
TAG = re.compile(r'<[^>]*>')
|
|
12
27
|
|
|
13
28
|
# ② 문장 끝
|
|
@@ -94,6 +109,20 @@ if __name__ == '__main__':
|
|
|
94
109
|
# (① 앞의 경계가 안 잡혀 두 문장이 붙었다).
|
|
95
110
|
("세 가지를 확인함. ① 정상 성립함. ② 본문이 열림.", 2),
|
|
96
111
|
("확인함.\n① 성립함.\n② 열림.\n③ 일치함.", 3),
|
|
112
|
+
# 연도 없는 날짜 — 2026-09-11 실측. 고치기 전엔 여기서 0 대신 2 가 나왔다
|
|
113
|
+
# (FISOFT #176 표 6-1 의 안드로이드 칸이 세 줄로 쪼개진 자국).
|
|
114
|
+
("9. 8. 출시됨", 0),
|
|
115
|
+
("9. 10. 출시됨", 0),
|
|
116
|
+
("9. 8.", 0),
|
|
117
|
+
("전체 출시가 9. 10. 에 끝났음. 아이폰은 대기임.", 1),
|
|
118
|
+
# 그 마스크가 삼키면 안 되는 것들 — 위 넷을 고치면서 깨지기 쉬운 자리.
|
|
119
|
+
# 둘 다 옛 정규식과 **경계 수가 같음**을 대조로 확인함(2026-09-11, 8표본 중
|
|
120
|
+
# 달라진 것은 연도 없는 날짜 하나뿐).
|
|
121
|
+
# ⚠ 아래 3 은 숫자 열거 머리(`1.` `2.`)를 문장 끝으로 세는 **원래 동작**이다.
|
|
122
|
+
# 머리글자 배제(INITIAL)가 라틴 한 글자만 보기 때문인데, 관보 본문 열거는
|
|
123
|
+
# 원문자(①②)를 쓰므로 여태 문제가 안 됐다. 이번 수정과는 무관하다.
|
|
124
|
+
("1. 첫째 항목임. 2. 둘째 항목임.", 3),
|
|
125
|
+
("확인한 절은 6. 자세한 것은 부록 2.", 1),
|
|
97
126
|
]
|
|
98
127
|
bad = 0
|
|
99
128
|
for s, want in T:
|
|
@@ -48,7 +48,18 @@ export function analyze(raw0) {
|
|
|
48
48
|
|
|
49
49
|
// ── ① 경어체 (S1) ────────────────────────────────────────────
|
|
50
50
|
const POLITE = /[^<>]{4,}?(?:습니다|십시오|합니다|입니다|됩니다|드립니다|주세요|하세요|해요)[.。]?/g;
|
|
51
|
-
|
|
51
|
+
// 2026-09-12 — 두 자리는 세지 않는다. ⑴ 판단 항목 본문(`p.ask__body`)은 상대에게 말을 거는 **요청 자리**라
|
|
52
|
+
// 종결까지 하십시오체가 규칙이다(genetics-report SKILL 5·5a). 여기서 세면 규칙을 지킨 문서가 떨어지고,
|
|
53
|
+
// 떨어지지 않으려고 「회신 주시면 됨」 같은 하대로 돌아간다 — 2026-09-03 에 그렇게 반려됐다.
|
|
54
|
+
// ⑵ 「」 안은 원문 인용이다(SKILL 11 — 인용은 원문 문구 그대로). 앱 문구 「다시 입력해 주세요」를 인용한
|
|
55
|
+
// 표가 경어체로 잡히면 인용을 비틀게 된다. 기록 자리(문단·표·요약)의 우리 문장은 그대로 센다.
|
|
56
|
+
const politeTxt = raw
|
|
57
|
+
.replace(/<p class="ask__body"[^>]*>[\s\S]*?<\/p>/g, ' ')
|
|
58
|
+
.replace(/<[^>]+>/g, ' ')
|
|
59
|
+
.replace(/'/g, "'").replace(/&/g, '&').replace(/ /g, ' ')
|
|
60
|
+
.replace(/「[^「」]{0,200}」/g, ' ')
|
|
61
|
+
.replace(/\s+/g, ' ').trim();
|
|
62
|
+
const polite = politeTxt.match(POLITE) || [];
|
|
52
63
|
lines.push(`[S1] 경어체 잔존: ${polite.length}건 (0 이어야 함 — 개조식은 ~함 / ~했음 / ~필요)`);
|
|
53
64
|
polite.slice(0, 12).forEach(h => lines.push(' ✗ ' + h.trim().slice(0, 80)));
|
|
54
65
|
|
|
@@ -73,7 +84,12 @@ export function analyze(raw0) {
|
|
|
73
84
|
lines.push(`\n[S1] 상대에게 내리는 명령: ${order.length + duty.length}건 (0 이어야 함 — 상대가 할 일에는 높임 어간을 살린다)`);
|
|
74
85
|
[...duty, ...order].slice(0, 12).forEach(h => lines.push(' ✗ ' + h.trim()));
|
|
75
86
|
if (order.length + duty.length) {
|
|
76
|
-
|
|
87
|
+
// ⚠ 2026-09-11 교체. 옛 처방은 「회신 주시면 됨」이었는데 그 표현 자체가 2026-09-03 에
|
|
88
|
+
// 반려됐다(「이런 말투 존나 싸가지 없지 않아?」). 명사형 종결 `-ㅁ/-음` 은 한국어
|
|
89
|
+
// 상대높임 등급 밖이라 청자 높임이 0 이고, 사람에게 직접 쓰면 하대로 읽힌다 —
|
|
90
|
+
// 겸양 어간이 든 「부탁드림」도 마찬가지다. 요청 자리는 종결까지 존댓말로 맺는다.
|
|
91
|
+
lines.push(' → 「회신 필요」 → 「회신 부탁드립니다」 · 「~해 주셔야 함」 → 「~해 주시기 바랍니다」');
|
|
92
|
+
lines.push(' → 제목은 명사형 「~ 요청」 그대로. 「부탁드림·바람·하시면 됨」은 존대가 아니다.');
|
|
77
93
|
}
|
|
78
94
|
|
|
79
95
|
// ── ②b-1 문체 유형 (S1 — 한 번만 나와도 고친다) ─────────────
|
|
@@ -137,6 +137,19 @@ test('상대에게 내리는 명령 — 개조식의 반대쪽 벽 (2026-09-02)'
|
|
|
137
137
|
assert.match(analyze(wrap('검수 결과는 별도 회신 필요.')).lines.at(-1), /상대에게 내리는 명령 1건/);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
test('경어체 — 판단 항목 본문(요청 자리)과 「」 인용은 세지 않는다, 기록 자리는 센다(2026-09-12)', () => {
|
|
141
|
+
const ask = '<ol class="asks"><li class="ask" id="case-01"><div class="ask__head"><h4 class="ask__title">방식 A 를 들일 것인가</h4></div>'
|
|
142
|
+
+ '<p class="ask__body"><span class="sn">2026년 9월 30일까지 회신해 주시기 바랍니다.</span> <span class="sn">고른 길은 다음 작업에 넣겠습니다.</span></p></li></ol>';
|
|
143
|
+
const r = analyze(ask + wrap('앱 안내문의 맺음은 「주소를 다시 입력해 주세요」임.'));
|
|
144
|
+
assert.equal(r.polite.length, 0, r.polite.join(' | '));
|
|
145
|
+
// 대조군 — 같은 문장이 기록 자리(문단)에 서면 센다. 뮤테이션: 요청 자리 제외를 문단까지 넓히면 여기서 죽는다.
|
|
146
|
+
const c = analyze(wrap('고른 길은 다음 작업에 넣겠습니다.'));
|
|
147
|
+
assert.equal(pos(c.polite.length), 1);
|
|
148
|
+
// 대조군 — 인용 괘 밖의 경어체는 센다(괘 안만 뺀다).
|
|
149
|
+
const q = analyze(wrap('앱 문구는 「다시 입력」이며 이대로 두면 됩니다.'));
|
|
150
|
+
assert.equal(pos(q.polite.length) >= 1, true);
|
|
151
|
+
});
|
|
152
|
+
|
|
140
153
|
test('양성 표본 수 — 0표본 통과 방지', () => {
|
|
141
154
|
console.log(` 양성 표본 ${positives}건 (0 이면 이 시험은 아무것도 안 잰 것)`);
|
|
142
155
|
assert.equal(positives >= 28, true, String(positives));
|