genetics-mcp 2.20.0 → 2.22.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 +5 -4
- package/bin/x86_64-apple-darwin/gcell +0 -0
- package/bin/x86_64-pc-windows-gnu/gcell.exe +0 -0
- package/bin/x86_64-unknown-linux-musl/gcell +0 -0
- package/genetics.mjs +258 -30
- package/package.json +2 -2
- package/skills/genetics-report/SKILL.md +17 -1
- package/skills/genetics-report/references/lim-portal-format.md +16 -4
|
Binary file
|
|
Binary file
|
package/bin/manifest.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"aarch64-apple-darwin": "
|
|
3
|
-
"aarch64-unknown-linux-musl": "
|
|
4
|
-
"x86_64-apple-darwin": "
|
|
5
|
-
"x86_64-
|
|
2
|
+
"aarch64-apple-darwin": "cb352eda0bbcf7419a7ca7243be783dfd3fece7a7e5fc3b5574474c92ac349f3",
|
|
3
|
+
"aarch64-unknown-linux-musl": "43a93d3366f4700a74564adca0531312d4ef2079eb3d38ae5617e9c2199806de",
|
|
4
|
+
"x86_64-apple-darwin": "c21172e1519287d0990149b668fc595be8f656d39fa30f5b811963d492c9e8d4",
|
|
5
|
+
"x86_64-pc-windows-gnu": "10cdaa9e0f06b35843fe65006dbaed70e382ee61a1a86f0627d9e9df87557df3",
|
|
6
|
+
"x86_64-unknown-linux-musl": "72e65bdd62e2afc7de01be2311ecfe55bc3b656764e09df4e382782ba66981c4"
|
|
6
7
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -49,6 +49,22 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
|
|
|
49
49
|
process.exit(0);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/*
|
|
53
|
+
* **낡은 Node 는 여기서 말하고 멈춘다.** 서버가 쓰는 전역 `fetch` 는 Node 18 부터다.
|
|
54
|
+
* 그 아래에서는 첫 도구 호출에서 `fetch is not defined` 가 나는데, 그 글자는 클라이언트의
|
|
55
|
+
* 로그 구석에만 남아 **「도구가 그냥 안 된다」로 보인다.** 뜰 때 한 줄로 말하는 편이 낫다.
|
|
56
|
+
* Windows 에서 특히 중요하다 — 옛 판(7·8)에는 Node 18 이 아예 없다(공식 지원표: Node 14·16 은
|
|
57
|
+
* Windows 8.1 부터, 지금 판은 Windows 10 부터).
|
|
58
|
+
*/
|
|
59
|
+
if (typeof fetch !== 'function') {
|
|
60
|
+
process.stderr.write(
|
|
61
|
+
`genetics-mcp ${VERSION}: 이 Node(${process.version})에는 전역 fetch 가 없다 — Node 18 이상이 필요하다.\n`
|
|
62
|
+
+ (process.platform === 'win32'
|
|
63
|
+
? ' Windows 는 Node 18+ 가 Windows 10 이상에서만 돈다(공식 지원표). 7·8 에서는 포털의 브라우저 커넥터로 연결할 것.\n'
|
|
64
|
+
: ' nodejs.org 의 LTS 를 깔고 다시 열 것.\n'));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
52
68
|
/*
|
|
53
69
|
* 짝인 스킬을 편다.
|
|
54
70
|
*
|
|
@@ -706,13 +722,33 @@ async function fetchImage(url) {
|
|
|
706
722
|
* 방어를 늘려야 하기 때문이다. 그래서 굳히는 것은 여기서 한다: 계약 §7.1 ④ 의
|
|
707
723
|
* 「발행 도구의 프로세스가 원본을 받아 포맷 규정대로 굳힌다」가 정확히 이 자리다.
|
|
708
724
|
*/
|
|
725
|
+
/*
|
|
726
|
+
* 「이것을 까세요」는 **그 사람의 기계에서 되는 말**이어야 한다. 예전에는 brew·apt 만
|
|
727
|
+
* 적었는데, 그 두 줄은 Windows 에서 칠 수가 없다 — 안내가 아니라 막다른 길이 된다.
|
|
728
|
+
*/
|
|
729
|
+
function installHint(kind) {
|
|
730
|
+
const P = {
|
|
731
|
+
webp: { win: 'winget install Google.LibWebp', mac: 'brew install webp', apt: 'apt install webp' },
|
|
732
|
+
librsvg:{ win: 'winget install --id=GNOME.librsvg -e 또는 scoop install librsvg',
|
|
733
|
+
mac: 'brew install librsvg', apt: 'apt install librsvg2-bin' },
|
|
734
|
+
}[kind];
|
|
735
|
+
return process.platform === 'win32' ? P.win : `${P.mac} · ${P.apt}`;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** npx 가 옛 판을 물고 있을 때 비우는 한 줄 — 셸마다 다르다. */
|
|
739
|
+
function npxCacheResetHint(platform = process.platform) {
|
|
740
|
+
return platform === 'win32'
|
|
741
|
+
? 'rmdir /s /q "%LOCALAPPDATA%\\npm-cache\\_npx" && npx -y genetics-mcp@latest'
|
|
742
|
+
: 'rm -rf ~/.npm/_npx && npx -y genetics-mcp@latest';
|
|
743
|
+
}
|
|
744
|
+
|
|
709
745
|
function rasterizeSvg(buf) {
|
|
710
746
|
const r = spawnSync('rsvg-convert',
|
|
711
747
|
['-w', String(ASSET_LONG_EDGE), '-b', 'white', '-f', 'png'],
|
|
712
748
|
{ input: buf, maxBuffer: IMAGE_MAX_DOWNLOAD });
|
|
713
749
|
if (r.error && r.error.code === 'ENOENT') {
|
|
714
750
|
throw new Error('rsvg-convert 가 없어 벡터 원본을 래스터로 굳힐 수 없습니다.\n'
|
|
715
|
-
+
|
|
751
|
+
+ `librsvg 를 설치하세요(${installHint('librsvg')}). `
|
|
716
752
|
+ 'SVG 를 그대로 싣지 않습니다 — 계약이 발행 데이터로 받는 형식을 두 래스터로 못박았습니다.');
|
|
717
753
|
}
|
|
718
754
|
if (r.status !== 0) throw new Error(`벡터 래스터화 실패: ${String(r.stderr).slice(0, 200)}`);
|
|
@@ -765,7 +801,7 @@ function encodeShot(buf) {
|
|
|
765
801
|
|
|
766
802
|
function cwebpMissing() {
|
|
767
803
|
return new Error('cwebp 가 없어 자산을 계약 규격(WebP)으로 굳힐 수 없습니다.\n'
|
|
768
|
-
+
|
|
804
|
+
+ `libwebp 를 설치하세요(${installHint('webp')}). `
|
|
769
805
|
+ '다른 포맷으로 우회하지 않습니다 — 계약이 포맷을 규정으로 못박았습니다.');
|
|
770
806
|
}
|
|
771
807
|
|
|
@@ -799,7 +835,7 @@ function encodeAsset(buf, role, provider) {
|
|
|
799
835
|
const r = spawnSync('cwebp', args, { input: buf, maxBuffer: IMAGE_MAX_DOWNLOAD });
|
|
800
836
|
if (r.error && r.error.code === 'ENOENT') {
|
|
801
837
|
throw new Error('cwebp 가 없어 자산을 계약 규격(WebP q78 · 긴 변 1400px)으로 굳힐 수 없습니다.\n'
|
|
802
|
-
+
|
|
838
|
+
+ `libwebp 를 설치하세요(${installHint('webp')}). `
|
|
803
839
|
+ '다른 포맷으로 우회하지 않습니다 — 계약이 포맷을 규정으로 못박았습니다.');
|
|
804
840
|
}
|
|
805
841
|
if (r.status !== 0) throw new Error(`자산 인코딩 실패: ${String(r.stderr).slice(0, 200)}`);
|
|
@@ -1674,11 +1710,46 @@ function scrubTranscript(raw) {
|
|
|
1674
1710
|
return { jsonl: out.join('\n') + '\n', lines: out.length, dropped, redacted, cwd: cwdMode(cwds), cwd_raw: cwdMode(cwdsRaw), cwds_raw: rawAll };
|
|
1675
1711
|
}
|
|
1676
1712
|
|
|
1677
|
-
|
|
1713
|
+
/*
|
|
1714
|
+
* cwd → 트랜스크립트 디렉터리. **해석된 경로**로 슬러그를 만든다(심볼릭 링크 함정).
|
|
1715
|
+
*
|
|
1716
|
+
* ★ **이 규칙은 우리 것이 아니라 Claude Code 의 것이다.** 우리가 정하는 자리가 아니라
|
|
1717
|
+
* **맞춰야 하는 자리**다 — 어긋나면 우리가 놓은 파일을 `claude --resume` 이 못 찾는다.
|
|
1718
|
+
* 그래서 기억으로 쓰지 않고 **바이너리에서 떠 왔다**(2.1.274, 2026-09-18):
|
|
1719
|
+
*
|
|
1720
|
+
* var jY = 200;
|
|
1721
|
+
* function JY(e){ let r=0; for(let n=0;n<e.length;n++) r=(r<<5)-r+e.charCodeAt(n)|0; return r }
|
|
1722
|
+
* function Le(e){ return Math.abs(JY(e)).toString(36) }
|
|
1723
|
+
* function k(e){ return e.replace(/[^a-zA-Z0-9]/g, "-") }
|
|
1724
|
+
* function eR(e){ let n=k(e); if(n.length<=jY) return n; return `${n.slice(0,jY)}-${Le(e)}` }
|
|
1725
|
+
*
|
|
1726
|
+
* 옛 코드는 `/` 와 `_` 만 바꿨다. 그러면 **점 하나만 들어가도 어긋난다** — 이 기계 실측:
|
|
1727
|
+
* `…/spreadjs-v19-0-3_/.vendor-src` 의 실재 폴더는 `…-v19-0-3---vendor-src` 인데 옛 규칙은
|
|
1728
|
+
* `…-v19-0-3--.vendor-src` 를 냈다(실재 0건). Windows 는 더 나쁘다 — `\` 와 `:` 가 그대로
|
|
1729
|
+
* 남아 `C:\…\projects\C:\Users\…` 라는 **만들 수 없는 경로**가 나왔다(path.win32 로 실측).
|
|
1730
|
+
* 비영숫자를 전부 바꾸면 드라이브 문자도 `C-` 가 되어 Windows 에서도 유효한 이름이 된다.
|
|
1731
|
+
*/
|
|
1732
|
+
const TRANSCRIPT_SLUG_MAX = 200; // Claude Code 의 jY
|
|
1733
|
+
|
|
1734
|
+
/** Claude Code 의 `JY` — 자바 문자열 해시와 같은 셈(32비트 랩어라운드). */
|
|
1735
|
+
function transcriptSlugHash(s) {
|
|
1736
|
+
let r = 0;
|
|
1737
|
+
for (let i = 0; i < s.length; i += 1) r = (((r << 5) - r) + s.charCodeAt(i)) | 0;
|
|
1738
|
+
return Math.abs(r).toString(36);
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
/** Claude Code 의 `eR` — 경로 하나를 폴더 이름 하나로. */
|
|
1742
|
+
function transcriptSlug(real) {
|
|
1743
|
+
const n = String(real).replace(/[^a-zA-Z0-9]/g, '-');
|
|
1744
|
+
return n.length <= TRANSCRIPT_SLUG_MAX
|
|
1745
|
+
? n
|
|
1746
|
+
: `${n.slice(0, TRANSCRIPT_SLUG_MAX)}-${transcriptSlugHash(real)}`;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1678
1749
|
function transcriptDir(dir) {
|
|
1679
1750
|
let real = dir;
|
|
1680
1751
|
try { real = fs.realpathSync(dir); } catch { /* 없으면 준 값 그대로 */ }
|
|
1681
|
-
return path.join(os.homedir(), '.claude', 'projects', real
|
|
1752
|
+
return path.join(os.homedir(), '.claude', 'projects', transcriptSlug(real));
|
|
1682
1753
|
}
|
|
1683
1754
|
|
|
1684
1755
|
/** uuid 로 이 기계의 트랜스크립트를 찾는다. 지금 프로젝트를 먼저 보고, 없으면 전체를 훑는다. */
|
|
@@ -1777,8 +1848,33 @@ function cursorActiveFromLedger() {
|
|
|
1777
1848
|
* 조용히 비켜난다(beatTargets:transcriptOf null → skip). 대화 본문을 올리는 길(두뇌
|
|
1778
1849
|
* 파서까지)은 별도 단계다 — DESIGN-cursor-colony.md.
|
|
1779
1850
|
*/
|
|
1780
|
-
|
|
1781
|
-
|
|
1851
|
+
/*
|
|
1852
|
+
* Cursor 의 사용자 데이터 자리는 **플랫폼마다 다르다**(VS Code 계열 공통 규약).
|
|
1853
|
+
* 여기 macOS 자리만 박혀 있었다 — Windows·Linux 에서는 `workspaceStorage` 를 영영 못 찾고
|
|
1854
|
+
* 조용히 빈 값으로 떨어졌다(try/catch 안이라 아무도 못 봤다).
|
|
1855
|
+
* darwin ~/Library/Application Support/Cursor/User
|
|
1856
|
+
* win32 %APPDATA%\Cursor\User (APPDATA 가 없으면 ~/AppData/Roaming 로 잇는다)
|
|
1857
|
+
* linux $XDG_CONFIG_HOME/Cursor/User → 없으면 ~/.config/Cursor/User
|
|
1858
|
+
*/
|
|
1859
|
+
function cursorUserRoot(platform = process.platform, env = process.env, home = os.homedir()) {
|
|
1860
|
+
if (env.GENETICS_CURSOR_ROOT) return env.GENETICS_CURSOR_ROOT;
|
|
1861
|
+
if (platform === 'win32') {
|
|
1862
|
+
return path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Cursor', 'User');
|
|
1863
|
+
}
|
|
1864
|
+
if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Cursor', 'User');
|
|
1865
|
+
return path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'Cursor', 'User');
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const CURSOR_ROOT = cursorUserRoot();
|
|
1869
|
+
|
|
1870
|
+
/*
|
|
1871
|
+
* sqlite3 실행체. macOS 는 자리가 고정이라 **절대경로로** 부르고(PATH 오염을 안 탄다),
|
|
1872
|
+
* 나머지는 PATH 에서 찾는다 — Windows 는 sqlite3 가 기본 탑재가 아니라 없을 수 있고,
|
|
1873
|
+
* 없으면 아래 장부 갈래로 떨어진다(없음은 오류가 아니다).
|
|
1874
|
+
*/
|
|
1875
|
+
function sqliteBin(platform = process.platform) {
|
|
1876
|
+
return platform === 'darwin' ? '/usr/bin/sqlite3' : 'sqlite3';
|
|
1877
|
+
}
|
|
1782
1878
|
|
|
1783
1879
|
let cursorSeen = { at: 0, id: '', folder: '' }; // 15초 캐시 — 도구마다 sqlite 를 안 두드리게
|
|
1784
1880
|
|
|
@@ -1815,7 +1911,7 @@ function cursorActiveComposer() {
|
|
|
1815
1911
|
if (!best || mt > best.mt) best = { dir, wsPath, mt };
|
|
1816
1912
|
}
|
|
1817
1913
|
if (best) {
|
|
1818
|
-
const r = spawnSync(
|
|
1914
|
+
const r = spawnSync(sqliteBin(),
|
|
1819
1915
|
['-readonly', path.join(best.dir, 'state.vscdb'),
|
|
1820
1916
|
"SELECT value FROM ItemTable WHERE key='composer.composerData'"],
|
|
1821
1917
|
{ encoding: 'utf8', timeout: 2000 });
|
|
@@ -4012,13 +4108,17 @@ const TOOLS = [
|
|
|
4012
4108
|
+ '각 메모의 `anchor.path`(예 `case:k7m2p9/fix`)가 **발행 데이터의 어느 칸을 고치라는 것인지**를 '
|
|
4013
4109
|
+ '그대로 말한다 — 그 칸을 고쳐 update_report 로 되돌려 보내고, resolve_note 로 반영을 표시한다.\n'
|
|
4014
4110
|
+ '`anchor_state` 가 `unmoored`·`reparented` 면 문서가 바뀌어 원래 자리를 잃은 것이다 — '
|
|
4015
|
-
+ '그래도 메모는 남아 있고, `anchor.quote_exact` 가 저자가 무엇을 보고 썼는지
|
|
4111
|
+
+ '그래도 메모는 남아 있고, `anchor.quote_exact` 가 저자가 무엇을 보고 썼는지 말한다.\n'
|
|
4112
|
+
+ '같은 호출이 **검토 회신**(고객에게 보이는 글 — 판단 항목의 답이 여기로 온다)도 함께 보여 준다. '
|
|
4113
|
+
+ '문서를 고치기 **전에** 회신의 본문을 읽는다: 각 회신의 where 가 어느 물음·건·도판에 대한 말인지 말한다. '
|
|
4114
|
+
+ '회신대로 고친 뒤의 「원문이 바뀜」은 정상이다 — 핀을 살리려고 문장을 되돌리지 않는다.',
|
|
4016
4115
|
inputSchema: {
|
|
4017
4116
|
type: 'object',
|
|
4018
4117
|
properties: {
|
|
4019
4118
|
id: { type: 'integer', description: '보고서 id' },
|
|
4020
4119
|
state: { type: 'string', description: '`open`(기본, 아직 반영 안 한 것) 또는 `all`' },
|
|
4021
4120
|
uid: { type: 'string', description: '그 건·절의 uid 에 달린 것만(선택)' },
|
|
4121
|
+
channel: { type: 'string', description: '`all`(기본 — 메모와 검토 회신 둘 다) · `notes`(저자 메모만) · `replies`(검토 회신만)' },
|
|
4022
4122
|
},
|
|
4023
4123
|
required: ['id'],
|
|
4024
4124
|
},
|
|
@@ -4029,7 +4129,8 @@ const TOOLS = [
|
|
|
4029
4129
|
'메모에 **답글로** 회신한다. 이견이 있거나 물을 것이 있으면 대화창이 아니라 여기에 쓴다 — '
|
|
4030
4130
|
+ '저자는 자기가 메모를 단 그 자리에서 답을 본다.\n'
|
|
4031
4131
|
+ '고칠 수 없는 이유, 다르게 본 근거, 되묻는 것이 여기 온다. '
|
|
4032
|
-
+ '고쳤으면 답글이 아니라 resolve_note
|
|
4132
|
+
+ '고쳤으면 답글이 아니라 resolve_note 다.\n'
|
|
4133
|
+
+ '★ **검토 회신에는 이 도구로 답하지 않는다** — 그쪽은 고객에게 보이는 글이라 사람이 포털에서 쓴다. 할 말은 작성자에게 대화로 전한다.',
|
|
4033
4134
|
inputSchema: {
|
|
4034
4135
|
type: 'object',
|
|
4035
4136
|
properties: {
|
|
@@ -4043,19 +4144,22 @@ const TOOLS = [
|
|
|
4043
4144
|
{
|
|
4044
4145
|
name: 'resolve_note',
|
|
4045
4146
|
description:
|
|
4046
|
-
'그
|
|
4147
|
+
'그 메모(또는 검토 회신)를 **반영했다**고 표시한다. 반영이 실린 판번호(body_rev)가 함께 남아 '
|
|
4047
4148
|
+ '「무엇에 반영됐는지」를 나중에도 안다.\n'
|
|
4048
4149
|
+ '★ 고치기 **전에** 부르지 말 것 — 이 표시가 곧 「이 메모는 끝났다」이고, '
|
|
4049
|
-
+ '표시하는 순간 미반영 목록에서 빠져 다시 눈에 안
|
|
4150
|
+
+ '표시하는 순간 미반영 목록에서 빠져 다시 눈에 안 띈다.\n'
|
|
4151
|
+
+ '검토 회신은 `note` 대신 `reply` 로 가리킨다 — 표시해도 회신은 고객에게 그대로 보이고, 그 회신이 집은 문장이 바뀐 것을 update_report 가 더는 경고하지 않는다. '
|
|
4152
|
+
+ '문서에 반영할 것이 없는 회신(인사·이미 답한 물음)도 읽었으면 표시한다.',
|
|
4050
4153
|
inputSchema: {
|
|
4051
4154
|
type: 'object',
|
|
4052
4155
|
properties: {
|
|
4053
4156
|
id: { type: 'integer', description: '보고서 id' },
|
|
4054
|
-
note: { type: 'integer', description: '메모 id' },
|
|
4157
|
+
note: { type: 'integer', description: '메모 id(`reply` 와 둘 중 하나)' },
|
|
4158
|
+
reply: { type: 'integer', description: '검토 회신 id — list_notes 의 [회신 N]. 뿌리 회신만 표시할 수 있다' },
|
|
4055
4159
|
rev: { type: 'integer', description: '반영이 실린 body_rev. 안 주면 지금 판' },
|
|
4056
4160
|
resolved: { type: 'boolean', description: '거짓이면 미반영으로 되돌린다' },
|
|
4057
4161
|
},
|
|
4058
|
-
required: ['id'
|
|
4162
|
+
required: ['id'],
|
|
4059
4163
|
},
|
|
4060
4164
|
},
|
|
4061
4165
|
{
|
|
@@ -4627,6 +4731,14 @@ const TOOLS = [
|
|
|
4627
4731
|
* 찍는 자리가 흩어져 있으면 또 한 곳이 빠지므로 여기 하나로 모은다. */
|
|
4628
4732
|
// 자산을 굳히며 이쪽(작성자 기계)에서만 알 수 있는 것 — 서버 경고와 한 목록으로 낸다(readability.md R8).
|
|
4629
4733
|
let ASSET_HINTS = [];
|
|
4734
|
+
/* 회신 핀 예보(update_report) — 서버가 핀마다 낸 판정(`pins[]`)을 한 줄로 센다. 경고는 약해지는 핀만 말하므로,
|
|
4735
|
+
* 이 줄이 없으면 「전부 제자리」와 「핀이 없다」와 「예보가 안 돌았다(옛 서버)」를 에이전트가 못 가른다. 핀이 없으면 안 찍는다. */
|
|
4736
|
+
function pinsText(r) {
|
|
4737
|
+
const p = Array.isArray(r?.pins) ? r.pins : [];
|
|
4738
|
+
if (!p.length) return '';
|
|
4739
|
+
const n = (s) => p.filter((x) => x?.state === s).length;
|
|
4740
|
+
return `\n회신 핀 ${p.length}건 — 제자리 ${n('exact')} · 옮겨 붙음 ${n('moved')} · 원문이 바뀜 ${n('drifted')} · 상위로 ${n('reparented')} · 자리 잃음 ${n('unmoored')}`;
|
|
4741
|
+
}
|
|
4630
4742
|
function warnText(r) {
|
|
4631
4743
|
const w = [...(r?.warnings ?? []), ...ASSET_HINTS];
|
|
4632
4744
|
return w.length
|
|
@@ -4709,11 +4821,23 @@ const CELL_DIR = path.join(os.homedir(), '.config/genetics/bin');
|
|
|
4709
4821
|
const CELL_PATH = path.join(CELL_DIR, process.platform === 'win32' ? 'gcell.exe' : 'gcell');
|
|
4710
4822
|
|
|
4711
4823
|
/** 이 기계의 타깃 삼중. 모르면 `null` — 지어내지 않는다. */
|
|
4712
|
-
function cellTarget() {
|
|
4713
|
-
const a =
|
|
4824
|
+
function cellTarget(platform = process.platform, arch = process.arch) {
|
|
4825
|
+
const a = arch, p = platform;
|
|
4714
4826
|
if (p === 'darwin') return a === 'arm64' ? 'aarch64-apple-darwin' : a === 'x64' ? 'x86_64-apple-darwin' : null;
|
|
4715
4827
|
if (p === 'linux') return a === 'arm64' ? 'aarch64-unknown-linux-musl' : a === 'x64' ? 'x86_64-unknown-linux-musl' : null;
|
|
4716
|
-
|
|
4828
|
+
// Windows 는 mingw 로 굽는다(cell/.cargo/config.toml). arm64 Windows 는 아직 안 굽는다 — 없는 것은 없는 것이다.
|
|
4829
|
+
if (p === 'win32') return a === 'x64' ? 'x86_64-pc-windows-gnu' : null;
|
|
4830
|
+
return null;
|
|
4831
|
+
}
|
|
4832
|
+
|
|
4833
|
+
/**
|
|
4834
|
+
* 이 플랫폼에 **세포가 있을 수 있나**. 「아직 안 깔렸다」와 「이 플랫폼엔 애초에 없다」를 가른다.
|
|
4835
|
+
*
|
|
4836
|
+
* ⚠ 이 갈래가 없으면 안내가 **막다른 길**이 된다: 못 쓴다는 글이 `install_cell` 을 가리키고,
|
|
4837
|
+
* 그 도구는 다시 「이 판에 없다」를 낸다 — 모델이 그 둘을 오가며 설치를 되풀이한다.
|
|
4838
|
+
*/
|
|
4839
|
+
function cellPlatformSupported(platform = process.platform, arch = process.arch) {
|
|
4840
|
+
return cellTarget(platform, arch) !== null;
|
|
4717
4841
|
}
|
|
4718
4842
|
|
|
4719
4843
|
function cellManifest() {
|
|
@@ -4881,7 +5005,7 @@ function tenantNoticeText(v) {
|
|
|
4881
5005
|
}
|
|
4882
5006
|
if (v.client_old) {
|
|
4883
5007
|
return `genetics: 이 폴더는 ${T} 로 확정돼 있고 자동 올리기도 켜져 있지만, 이 판(${v.version})이 서버 최소 판(${v.min_client})보다 낡아 `
|
|
4884
|
-
+
|
|
5008
|
+
+ `**이 대화는 안 올라간다**. npx 캐시를 비우고 다시 깔면 그때부터 올라간다: ${npxCacheResetHint()}.`;
|
|
4885
5009
|
}
|
|
4886
5010
|
const who = v.confirmed_by ? `${v.confirmed_by}${josaIGa(v.confirmed_by)} ` : '';
|
|
4887
5011
|
return `genetics: 이 세션은 ${T} 소속으로 확정된 폴더(${v.path} · ${who}${String(v.confirmed_at || '').slice(0, 10)} 확정, 자동 올리기 켬)에서 `
|
|
@@ -4938,7 +5062,7 @@ function tenantNoticeText(v) {
|
|
|
4938
5062
|
+ 'login 도구로 로그인하면 그때부터 확인한다.';
|
|
4939
5063
|
case 'client_old':
|
|
4940
5064
|
return `genetics: 이 판(${v.version})은 서버 최소 판(${v.min_client})보다 낡아 폴더 소속·자동 올리기가 안 돈다. `
|
|
4941
|
-
+
|
|
5065
|
+
+ `npx 캐시를 비우고 다시 깔 것: ${npxCacheResetHint()}.`;
|
|
4942
5066
|
default:
|
|
4943
5067
|
return '';
|
|
4944
5068
|
}
|
|
@@ -5151,10 +5275,14 @@ function toolsNow() {
|
|
|
5151
5275
|
* 지시형은 프롬프트 주입으로 거부된다(2026-08-22 실측).
|
|
5152
5276
|
*/
|
|
5153
5277
|
function cellNotice(why) {
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5278
|
+
const head = `genetics 세포(gcell)를 못 쓴다 — ${why}. 그래서 이 저장소에서 남이 무엇을 바꿨는지 못 싣는다.`;
|
|
5279
|
+
// **깔 수 없는 곳에 설치를 시키지 않는다.** 이 플랫폼에 세포가 없으면 그 말이 곧 다음 할 일이다.
|
|
5280
|
+
if (!cellPlatformSupported()) {
|
|
5281
|
+
return [head,
|
|
5282
|
+
`이 플랫폼(${process.platform}/${process.arch})에는 세포가 없다 — 깔 수 있는 것이 없으니 `
|
|
5283
|
+
+ '`install_cell` 을 부르지 말 것. 나머지 도구는 그대로 쓴다.'].join('\n');
|
|
5284
|
+
}
|
|
5285
|
+
return [head, '세포는 선택이 아니라 필수다. `install_cell` 도구가 깔고, `uninstall_cell` 이 지운다.'].join('\n');
|
|
5158
5286
|
}
|
|
5159
5287
|
|
|
5160
5288
|
/* ── ★★ 메일 레인 — 두뇌·다른 세션·사람이 **나를 지목해** 보낸 것 ──────────
|
|
@@ -5456,6 +5584,55 @@ let NOTES_LAST = '';
|
|
|
5456
5584
|
let NOTES_QUIET = 0;
|
|
5457
5585
|
const NOTES_REMIND_EVERY = 8;
|
|
5458
5586
|
|
|
5587
|
+
/*
|
|
5588
|
+
* 열린 검토 회신(125) — 메모와 같은 방식으로 기억하고 꼬리에서 말한다. 회신은 **판단 항목의 답이 오는 자리**라
|
|
5589
|
+
* 문서를 고치기 전에 읽혀야 한다: 2026-09-17 에는 이 통로가 없어 답이 와 있는 항을 다섯 판 동안 다시 물었다.
|
|
5590
|
+
*/
|
|
5591
|
+
const REPLIES_OPEN = new Map(); // 보고서 id → 열린 회신 건수
|
|
5592
|
+
let REPLIES_LAST = '';
|
|
5593
|
+
let REPLIES_QUIET = 0;
|
|
5594
|
+
function repliesSeen(id, n) {
|
|
5595
|
+
if (!Number.isInteger(id) || id <= 0 || !Number.isInteger(n)) return;
|
|
5596
|
+
if (n > 0) REPLIES_OPEN.set(id, n); else REPLIES_OPEN.delete(id);
|
|
5597
|
+
}
|
|
5598
|
+
function repliesSays() {
|
|
5599
|
+
const rows = [];
|
|
5600
|
+
REPLIES_OPEN.forEach((n, id) => { if (n > 0) rows.push(`#${id} ${n}건`); });
|
|
5601
|
+
if (!rows.length) { REPLIES_LAST = ''; return ''; }
|
|
5602
|
+
const t = `검토 회신 미반영 — ${rows.join(' · ')} · list_notes(id) 가 본문과 함께 보여 준다. `
|
|
5603
|
+
+ '문서를 고치기 전에 읽고, 반영했으면 resolve_note(id, reply). 고객에게 보이는 답글은 사람이 포털에서 쓴다.';
|
|
5604
|
+
if (t === REPLIES_LAST && ++REPLIES_QUIET < NOTES_REMIND_EVERY) return '';
|
|
5605
|
+
REPLIES_LAST = t;
|
|
5606
|
+
REPLIES_QUIET = 0;
|
|
5607
|
+
return t;
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5610
|
+
/** 검토 회신 목록을 사람이 읽는 줄로. 답글은 뿌리 밑에 붙인다(메모 목록과 같은 규율). */
|
|
5611
|
+
function repliesText(id, r) {
|
|
5612
|
+
if (!r.replies || !r.replies.length) return `검토 회신 — 보고서 #${id} · ${r.note || '열린 검토 회신이 없다.'}`;
|
|
5613
|
+
const STATE = { exact: '', moved: '(옮겨 붙음)', drifted: '(원문이 바뀜)', reparented: '(문장을 잃어 상위로)', unmoored: '(자리를 잃음)' };
|
|
5614
|
+
const side = (c) => (c.side === 'client' ? '고객' : '우리');
|
|
5615
|
+
const kids = new Map();
|
|
5616
|
+
for (const c of r.replies) {
|
|
5617
|
+
if (c.parent_id !== null) {
|
|
5618
|
+
if (!kids.has(c.parent_id)) kids.set(c.parent_id, []);
|
|
5619
|
+
kids.get(c.parent_id).push(c);
|
|
5620
|
+
}
|
|
5621
|
+
}
|
|
5622
|
+
const lines = [`검토 회신(고객에게 보이는 글) · 보고서 #${id} · 본문 판 body_rev=${r.body_rev} · ${r.note}`, ''];
|
|
5623
|
+
if (r.capped) lines.splice(1, 0, '⚠ 목록이 잘렸다 — 아래가 전부가 아니다.');
|
|
5624
|
+
for (const c of r.replies) {
|
|
5625
|
+
if (c.parent_id !== null) continue;
|
|
5626
|
+
lines.push(`[회신 ${c.id}] ${c.where || '문서 전체'} ${STATE[c.state_now] || ''}${c.resolved ? ` · 반영됨(판 ${c.resolved_rev ?? '?'})` : ''}`.trimEnd());
|
|
5627
|
+
if (c.anchor?.quote_exact) lines.push(` 집은 글: 「${c.anchor.quote_exact}」`);
|
|
5628
|
+
lines.push(` ${c.author || ''}(${side(c)}) ${String(c.created_at || '').slice(0, 10)}: ${c.body}`);
|
|
5629
|
+
for (const k of (kids.get(c.id) || [])) lines.push(` ↳ ${k.author || ''}(${side(k)}): ${k.body}`);
|
|
5630
|
+
lines.push('');
|
|
5631
|
+
}
|
|
5632
|
+
lines.push('반영했으면 resolve_note(id, reply: N). 회신대로 고친 뒤의 「원문이 바뀜」은 정상이다. 고객에게 보이는 답글은 이 도구로 달지 않는다.');
|
|
5633
|
+
return lines.join('\n').trimEnd();
|
|
5634
|
+
}
|
|
5635
|
+
|
|
5459
5636
|
/** 응답에 실려 온 건수를 기억한다. 서버가 그 칸을 모르는 판이면 아무 일도 안 한다. */
|
|
5460
5637
|
function notesSeen(id, n) {
|
|
5461
5638
|
if (!Number.isInteger(id) || id <= 0 || !Number.isInteger(n)) return;
|
|
@@ -5493,6 +5670,8 @@ function worldAppend(result) {
|
|
|
5493
5670
|
*/
|
|
5494
5671
|
const nn = notesSays();
|
|
5495
5672
|
if (nn) result.content.push({ type: 'text', text: nn });
|
|
5673
|
+
const rr = repliesSays();
|
|
5674
|
+
if (rr) result.content.push({ type: 'text', text: rr });
|
|
5496
5675
|
const t = worldDelta();
|
|
5497
5676
|
if (!t) return;
|
|
5498
5677
|
/*
|
|
@@ -5752,6 +5931,11 @@ async function callTool(name, args = {}) {
|
|
|
5752
5931
|
notesSeen(Number(x.id), x.open_notes);
|
|
5753
5932
|
if (x.open_notes > 0) bits.push(`첨삭 메모 ${x.open_notes}건 미반영`);
|
|
5754
5933
|
}
|
|
5934
|
+
// 열린 검토 회신(125) — 판단 항목의 답이 와 있는 문서를 목록에서 알아보게. 옛 서버·자격 없음이면 안 찍는다.
|
|
5935
|
+
if (Number.isInteger(x.open_replies)) {
|
|
5936
|
+
repliesSeen(Number(x.id), x.open_replies);
|
|
5937
|
+
if (x.open_replies > 0) bits.push(`검토 회신 ${x.open_replies}건 미반영`);
|
|
5938
|
+
}
|
|
5755
5939
|
const tag = bits.length ? ` [${bits.join(' · ')}]` : '';
|
|
5756
5940
|
// slug 를 싣는다. 중복 slug 는 409 로 잘 막히지만 **미리 알 수가 없었다** —
|
|
5757
5941
|
// 목록에 이름표가 없어 새 이름을 고를 때 참고할 것이 없었다(2026-08-18 검토).
|
|
@@ -5803,6 +5987,7 @@ async function callTool(name, args = {}) {
|
|
|
5803
5987
|
return JSON.stringify(r.current_text, null, 2);
|
|
5804
5988
|
}
|
|
5805
5989
|
notesSeen(Number(args.id), r.open_notes);
|
|
5990
|
+
repliesSeen(Number(args.id), r.open_replies);
|
|
5806
5991
|
if (!args.outline) {
|
|
5807
5992
|
/*
|
|
5808
5993
|
* ⚠ 전에는 여기서 **`r.data` 만** 돌려줬다. 서버가 정성껏 붙인 것이 이 한 줄에서
|
|
@@ -5820,6 +6005,10 @@ async function callTool(name, args = {}) {
|
|
|
5820
6005
|
head.push(`[정본 ${owner} · 본문 판 body_rev=${r.body_rev ?? '?'}`
|
|
5821
6006
|
+ (r.sha256 ? ` · 지금 발행물 ${r.bytes}B sha256:${String(r.sha256).slice(0, 12)}…` : '')
|
|
5822
6007
|
+ `] — 고칠 때는 update_report 에 body_rev 를 함께 실으면, 그 사이 누가 고쳤을 때 409 로 멈춘다.`);
|
|
6008
|
+
// 열린 검토 회신 — 고치기 전에 읽어야 한다(판단 항목의 답이 거기 있다). 첫 줄 가까이에 둔다.
|
|
6009
|
+
if (Number.isInteger(r.open_replies) && r.open_replies > 0) {
|
|
6010
|
+
head.push(`검토 회신 ${r.open_replies}건이 열려 있다 — 고치기 전에 list_notes(id: ${Number(args.id)}) 로 본문을 읽는다.`);
|
|
6011
|
+
}
|
|
5823
6012
|
// 그림은 서버가 보관한다(바이트는 에이전트를 안 지난다) — 되돌려 보낼 때의 계약을 여기서 말한다(#34).
|
|
5824
6013
|
const kept = Array.isArray(r.data?.assets)
|
|
5825
6014
|
? r.data.assets.filter((a) => typeof a?.data === 'string' && a.data.startsWith('(생략')).length : 0;
|
|
@@ -5837,13 +6026,29 @@ async function callTool(name, args = {}) {
|
|
|
5837
6026
|
}
|
|
5838
6027
|
case 'list_notes': {
|
|
5839
6028
|
const id = Number(args.id);
|
|
6029
|
+
const channel = args.channel === undefined ? 'all' : String(args.channel);
|
|
6030
|
+
if (!['all', 'notes', 'replies'].includes(channel)) throw new Error('channel 은 all · notes · replies 중 하나입니다.');
|
|
5840
6031
|
const qs = new URLSearchParams();
|
|
5841
6032
|
if (args.state) qs.set('state', String(args.state));
|
|
5842
6033
|
if (args.uid) qs.set('uid', String(args.uid));
|
|
5843
|
-
const
|
|
6034
|
+
const q = qs.toString() ? '?' + qs : '';
|
|
6035
|
+
// 검토 회신(125) — 메모 뒤에 붙인다. 옛 서버(창구 없음)는 메모만 낸다; 회신만 달라고 했으면 그 실패를 그대로 말한다.
|
|
6036
|
+
let repliesPart = '';
|
|
6037
|
+
if (channel !== 'notes') {
|
|
6038
|
+
try {
|
|
6039
|
+
const rr = await api('GET', `/api/reports/${id}/replies${q}`);
|
|
6040
|
+
repliesSeen(id, rr.open);
|
|
6041
|
+
repliesPart = repliesText(id, rr);
|
|
6042
|
+
} catch (e) {
|
|
6043
|
+
if (channel === 'replies' || Number(e?.status) !== 404) throw e;
|
|
6044
|
+
}
|
|
6045
|
+
}
|
|
6046
|
+
if (channel === 'replies') return repliesPart;
|
|
6047
|
+
const tail = repliesPart ? `\n\n${repliesPart}` : '';
|
|
6048
|
+
const r = await api('GET', `/api/reports/${id}/notes${q}`);
|
|
5844
6049
|
notesSeen(id, r.open);
|
|
5845
6050
|
if (!r.notes || !r.notes.length) {
|
|
5846
|
-
return `보고서 #${id} — ${r.note || '메모가 없다.'}`;
|
|
6051
|
+
return `보고서 #${id} — ${r.note || '메모가 없다.'}${tail}`;
|
|
5847
6052
|
}
|
|
5848
6053
|
/*
|
|
5849
6054
|
* 답글을 부모 밑에 붙여 낸다. 평평하게 내면 에이전트가 **자기가 이미 한 말**을
|
|
@@ -5873,7 +6078,7 @@ async function callTool(name, args = {}) {
|
|
|
5873
6078
|
}
|
|
5874
6079
|
lines.push('');
|
|
5875
6080
|
}
|
|
5876
|
-
return lines.join('\n').trimEnd();
|
|
6081
|
+
return lines.join('\n').trimEnd() + tail;
|
|
5877
6082
|
}
|
|
5878
6083
|
case 'reply_note': {
|
|
5879
6084
|
const id = Number(args.id);
|
|
@@ -5883,6 +6088,15 @@ async function callTool(name, args = {}) {
|
|
|
5883
6088
|
}
|
|
5884
6089
|
case 'resolve_note': {
|
|
5885
6090
|
const id = Number(args.id);
|
|
6091
|
+
if ((args.note === undefined) === (args.reply === undefined)) throw new Error('note(메모 id) 와 reply(검토 회신 id) 중 **하나만** 주세요.');
|
|
6092
|
+
if (args.reply !== undefined) {
|
|
6093
|
+
const rb = { reply: Number(args.reply) };
|
|
6094
|
+
if (args.rev !== undefined) rb.rev = Number(args.rev);
|
|
6095
|
+
if (args.resolved !== undefined) rb.resolved = !!args.resolved;
|
|
6096
|
+
const rr = await api('PATCH', `/api/reports/${id}/replies`, rb);
|
|
6097
|
+
if (Number.isInteger(rr.open)) repliesSeen(id, rr.open);
|
|
6098
|
+
return rr.says;
|
|
6099
|
+
}
|
|
5886
6100
|
const body = { note: Number(args.note) };
|
|
5887
6101
|
if (args.rev !== undefined) body.rev = Number(args.rev);
|
|
5888
6102
|
if (args.resolved !== undefined) body.resolved = !!args.resolved;
|
|
@@ -5967,16 +6181,16 @@ async function callTool(name, args = {}) {
|
|
|
5967
6181
|
throw e;
|
|
5968
6182
|
}
|
|
5969
6183
|
// 고친 직후가 「무엇을 아직 안 고쳤나」를 볼 가장 좋은 순간이다.
|
|
5970
|
-
if (!r.dry_run) notesSeen(Number(args.id), r.open_notes);
|
|
6184
|
+
if (!r.dry_run) { notesSeen(Number(args.id), r.open_notes); repliesSeen(Number(args.id), r.open_replies); }
|
|
5971
6185
|
if (r.dry_run) {
|
|
5972
|
-
return `시험 실행 — 아무것도 바꾸지 않았습니다.\n${r.tenant} · 건 ${r.cases}` + warnText(r);
|
|
6186
|
+
return `시험 실행 — 아무것도 바꾸지 않았습니다.\n${r.tenant} · 건 ${r.cases}` + pinsText(r) + warnText(r);
|
|
5973
6187
|
}
|
|
5974
6188
|
// 상태를 함께 알린다 — 이 문서를 다음에도 API 로 고칠 수 있는지가 여기서 갈린다.
|
|
5975
6189
|
const st = r.state ? ` [${r.state}]` : '';
|
|
5976
6190
|
// 서버가 초안이라고 답하면 이 도구의 동사로 다음 할 일을 잇는다(서버 note 는 HTTP 경로로 말한다).
|
|
5977
6191
|
const open = r.published === false ? ` 이 도구에서는 release_report(id: ${Number(args.id)}) 가 그 일입니다.` : '';
|
|
5978
6192
|
return `교체 완료 — ${r.tenant} · 건 ${r.cases}${st}\n열람 주소: ${r.url}\n${r.note || ''}${open}`
|
|
5979
|
-
+ pendingText(r) + warnText(r);
|
|
6193
|
+
+ pendingText(r) + pinsText(r) + warnText(r);
|
|
5980
6194
|
}
|
|
5981
6195
|
case 'world': {
|
|
5982
6196
|
const facts = worldFacts(8);
|
|
@@ -6742,7 +6956,15 @@ const IS_MAIN = (() => {
|
|
|
6742
6956
|
* 자리 고르기는 **순수 함수**로 뺀다(smoke 가 떼어내 잰다): PATH 에 있고 · 실재하고 · 쓸 수 있는
|
|
6743
6957
|
* 첫 후보. 없는 폴더를 만들거나 rc 파일을 고치지 않는다 — 남의 셸 설정에 손대는 설치는 못 믿는다.
|
|
6744
6958
|
*/
|
|
6745
|
-
|
|
6959
|
+
/*
|
|
6960
|
+
* ⚠ **Windows 에는 이 자리가 없다.** 후보 셋은 전부 유닉스의 PATH 자리이고, 심링크 자체도
|
|
6961
|
+
* Windows 에서는 개발자 모드나 관리자 권한이 있어야 만들어진다(EPERM). 그래서 그 판에서는
|
|
6962
|
+
* **걸지 않고**, 대신 「이 폴더를 PATH 에 넣으라」고 그 판의 말로 말한다 — 없는 자리를
|
|
6963
|
+
* 나열하며 못 걸었다고 하는 것은 안내가 아니다.
|
|
6964
|
+
*/
|
|
6965
|
+
const CELL_LINK_CANDIDATES = process.platform === 'win32'
|
|
6966
|
+
? []
|
|
6967
|
+
: ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local/bin')];
|
|
6746
6968
|
function cellLinkDir(pathEnv, candidates, exists, writable) {
|
|
6747
6969
|
const onPath = new Set(String(pathEnv || '').split(path.delimiter).filter(Boolean));
|
|
6748
6970
|
for (const d of candidates) {
|
|
@@ -6755,6 +6977,10 @@ function cellLinkOnPath() {
|
|
|
6755
6977
|
(d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } },
|
|
6756
6978
|
(d) => { try { fs.accessSync(d, fs.constants.W_OK); return true; } catch { return false; } });
|
|
6757
6979
|
if (!dir) {
|
|
6980
|
+
if (process.platform === 'win32') {
|
|
6981
|
+
return `PATH 에 안 걸었다(Windows 는 심링크에 권한이 필요하다) — 전체 경로 ${CELL_PATH} 로 부르거나 `
|
|
6982
|
+
+ `이 폴더를 PATH 에 넣을 것: setx PATH "%PATH%;${CELL_DIR}"`;
|
|
6983
|
+
}
|
|
6758
6984
|
return `PATH 에 걸 자리를 못 찾았다(${CELL_LINK_CANDIDATES.join(' · ')} 중 PATH 에 있고 쓸 수 있는 곳이 없다) — 전체 경로 ${CELL_PATH} 로 부를 것.`;
|
|
6759
6985
|
}
|
|
6760
6986
|
const link = path.join(dir, 'gcell');
|
|
@@ -7230,4 +7456,6 @@ export { encodeAsset, imageDims, imageHostAllowed, materializeAssets, warnText,
|
|
|
7230
7456
|
autoBlockSetVerdict, autoBlockPut, cellPinSource,
|
|
7231
7457
|
// 2026-09-15 — 「서버가 아는 행인가」는 소속 칸이 아니라 서버를 거친 표식으로 가른다(첫 업로드가 영영 안 서던 자리)
|
|
7232
7458
|
autoRowOnServer,
|
|
7459
|
+
// 2026-09-18 Windows — 남의 규칙(Claude Code 슬러그)·플랫폼 갈래는 시험이 표로 걷는다
|
|
7460
|
+
transcriptSlug, transcriptDir, cursorUserRoot, sqliteBin, npxCacheResetHint, cellPlatformSupported,
|
|
7233
7461
|
HOLDS_FILE, UPDATE_BUDGET_MS };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genetics-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.22.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 && node schedule.test.mjs && bash ../cell/tests/cellcycle.sh",
|
|
21
|
+
"test": "node smoke.mjs && node notify.test.mjs && node schedule.test.mjs && node windows.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"
|
|
@@ -43,7 +43,7 @@ version: "4.0.0"
|
|
|
43
43
|
| 4~6 원고 | `report.json` · 코드 작업을 녹이면 원고 밖 상태 표(반드시 지키는 것 3) | — | 요약 근거 · 요지 칸 수(4~6단계) · 절마다 `marg` | 서버 요약 근거 게이트(거절) · 사람 | 공통 원인이 실재하는가 |
|
|
44
44
|
| 6 도판 굽기 | 도판 HTML → PNG · `plates.json` | `node assets/figure.mjs --batch plates.json --report report.json` · 두 칸 비교는 `shot.mjs --after` → `pair.mjs` | exit 0(✗0 · 합계 2,400,000B 안) | `figure.mjs` R0~R10·R13 · `pair.mjs` · 칩 문구의 뜻은 사람 눈 | — |
|
|
45
45
|
| 7 로컬 렌더 | 단일 HTML | `python3 assets/render.py report.json` | ⚠0 · `forbid` 적중 0 | `gazette.py` `text_scan()`·`brief_checks()`·`tally_scan()` · `build.py guard()` | — |
|
|
46
|
-
| 7 dry_run | 경고 목록 | `publish_report`/`update_report` 에 `dry_run: true` | 작성자가 고칠 수 있는 ⚠0 — 못 없애는 경고는 까닭을 핸드오프에 | `report_publish.php report_build_checked()`(렌더러·입고·그림·문체) · 경고 기준선 대조는 사람 | — |
|
|
46
|
+
| 7 dry_run | 경고 목록 | `publish_report`/`update_report` 에 `dry_run: true` | 작성자가 고칠 수 있는 ⚠0 — 못 없애는 경고는 까닭을 핸드오프에 | `report_publish.php report_build_checked()`(렌더러·입고·그림·문체) · `update_report` 는 `comments.php comment_anchor_forecast()`(회신 핀 자리) · 경고 기준선 대조는 사람 | — |
|
|
47
47
|
| 7 독자 검토 | 워크플로 결과(`merged`·`questions`·`dropped`) | `references/reader-review.workflow.js`(0.5단계 7행) · 구조를 안 바꾼 낱말 손질 판은 `references/edition-review.workflow.js`(결과 `kept`·`weak`) | 사실 틀림 반영 · 「⚠ 도판 N장 미열람」 없음 · 반영 뒤 로컬 렌더·dry_run 다시 | 워크플로 관점 + JS 거름 · `questions`·`dropped` 는 사람 | `questions[]` |
|
|
48
48
|
| 7 조판 검사 | 검사기 출력 | `gazette-report` 검사기 8종(단일 HTML 전달일 때) | 각 0건 · **표본 수를 함께 본다**(0표본 통과는 통과가 아니다) | 검사기 — 실행법은 `gazette-report` 스킬 | — |
|
|
49
49
|
| 7 사람 눈 | — | 인쇄 미리보기까지 연다 | `references/process.md` 「7단계 — 검수」 사람의 눈 표 | 사람 | — |
|
|
@@ -51,6 +51,7 @@ version: "4.0.0"
|
|
|
51
51
|
| 9 발행 뒤 대조 | 블록 목록 | `get_report(id, outline: true)` | `aside.marg` 수 = 절 수 · 옛 서식 블록 0 · 빈 블록 0 | 사람(블록 목록을 읽음) — 대조 기계 없음 | — |
|
|
52
52
|
| 9 열람 확인 | 캡처 | 열람 주소를 연다 | 칩·굵게·링크가 원문 글자가 아니라 서식으로 섬 · 도판 쌍·판단 절이 섬 | 사람 눈 — 못 열면 까닭을 핸드오프에 | — |
|
|
53
53
|
| 9a 첨삭 메모 반영 | 반영된 판 · 닫힌 메모 · 까닭을 단 답글 | `list_reports` → `list_notes` → `update_report` → `resolve_note`·`reply_note`(9a단계) | 미반영 메모 0 — 남은 것은 전부 답글로 까닭이 달림 | 사람이 메모를 닮 · 서버는 미반영 수만 셈(`open_notes`, 우리 팀에게만) | 사람이 「메모 해놨다」고 할 때만 돈다 — 무엇을 고칠지 되묻지 않음 |
|
|
54
|
+
| 9b 검토 회신 반영 | 회신마다 「어느 물음의 답인가」 대조 · 반영된 판 · 반영 표시 | **개정 전에** `list_notes(id)`(검토 회신이 본문과 함께 실린다) → 원고 반영 → `update_report` → `resolve_note(id, reply, rev)`(9b단계) | 열린 회신 0 — 남은 것은 사람에게 넘긴 까닭이 있음 · 원고에 「회신 없음」이라고 쓴 자리가 회신과 어긋나지 않음 | 서버는 열린 회신 수(`open_replies`)를 목록·되읽기·교체 응답에 싣고, 열린 회신의 문장이 바뀌면 교체 경고로 말함 · 본문 읽기와 대조는 사람·에이전트 | 고객에게 보이는 답글 문안(에이전트는 달지 않는다) |
|
|
54
55
|
| 10 공개 직전 날짜 | `report.json` | `update_report` 한 번(날짜만 고치는 이 판은 독자 검토 없이 로컬 렌더·dry_run 만) | 작성일 세 자리가 한 값이고 공개하는 날(3c) | `gazette.py` `date_scan()` ⚠(세 자리가 다르면) · 공개하는 날인지는 사람 | 공개일 |
|
|
55
56
|
| 10 공개 | 공개된 링크 | `release_report(id)` | 사용자 승인 | 사람(승인 행위) | **반드시** 공개 승인 |
|
|
56
57
|
|
|
@@ -351,6 +352,21 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
|
|
|
351
352
|
|
|
352
353
|
`anchor_state` 가 `unmoored`·`reparented` 면 문서가 바뀌어 자리를 잃은 메모다 — `quote_exact` 로 본문에서 찾아 고치고, 못 찾으면 답글로 묻는다. 끝나면 사람에게 **한 줄로** 알린다: 반영 N건 · 답글로 되물은 N건 · 문서 링크.
|
|
353
354
|
|
|
355
|
+
### 9b단계 — 검토 회신 반영: 개정하기 전에 회신의 **본문**부터
|
|
356
|
+
|
|
357
|
+
검토 회신(문서의 한 자리에 핀으로 붙는 글)은 **판단 항목의 답이 오는 자리**다 — 고객도 쓰고 우리 담당자도 쓴다. 이미 나간 문서를 고칠 때는 고치기 **전에** 회신을 읽는다. `list_reports` 줄의 `검토 회신 N건 미반영`, `get_report` 머리의 「검토 회신 N건이 열려 있다」가 읽을 것이 있다는 신호다.
|
|
358
|
+
|
|
359
|
+
| 순서 | 도구 | 할 일 |
|
|
360
|
+
|---|---|---|
|
|
361
|
+
| 1 | `list_notes(id)` | 검토 회신이 **본문과 함께** 실린다. `[회신 N]` 줄의 자리(「판단 항 「…」」·건·절·도판)가 **어느 물음에 대한 말인지**, 그 아래가 누가(고객/우리) 무슨 말을 했는지다. 답글은 뿌리 밑에 붙는다 |
|
|
362
|
+
| 2 | (대조) | 회신마다 원고와 맞춰 본다 — 이미 답이 온 판단 항을 **열린 항으로 다시 묻고 있지 않은가**, 원고가 「회신에 언급이 없어 미결」이라고 쓴 자리가 회신과 어긋나지 않는가, 회신의 낱말과 정본(고객 문서)의 낱말이 갈리면 정본 낱말로 맞췄는가 |
|
|
363
|
+
| 3 | `update_report(dry_run: true)` → `update_report` | 반영한다. 답이 온 판단 항은 닫힌 항 표로 옮기고 출처에 회신 날짜를 적는다. 교체 경고의 「열린 검토 회신 …」 줄은 회신의 본문과 함께 온다 — 그 회신을 이 판이 반영한 것이면 그대로 발행한다 |
|
|
364
|
+
| 4 | `resolve_note(id, reply, rev)` | **반영한 뒤에만.** 표시해도 회신은 고객에게 그대로 보인다. 문서에 반영할 것이 없는 회신(인사·이미 답한 물음)도 읽었으면 표시한다 |
|
|
365
|
+
|
|
366
|
+
- **회신대로 고치면 그 회신이 집은 문장은 바뀐다. 그것이 정상이다**(열람 화면의 「원문이 바뀜」). 핀을 「제자리」로 돌리려고 발행 문장을 옛 표기로 되돌리지 않는다 — 실제로 그렇게 해서 렌더러 경고만 얹은 판이 있었다.
|
|
367
|
+
- **고객에게 보이는 답글은 에이전트가 달지 않는다.** 창구에 쓰는 길이 없다. 고객에게 할 말(「N차에 반영했습니다」)은 문안을 사람에게 넘기고, 사람이 포털에서 쓴다. 우리 팀끼리의 고칠 지시는 검토 회신이 아니라 첨삭 메모(9a)다.
|
|
368
|
+
- 닫힌 판단 항은 원고에서 뺄 수밖에 없다(요청은 그려지는 한 열려 있다). 그 항에 달린 회신은 상위 절로 올라가 붙으니, 빼는 판에서 그 회신을 반영 표시한다.
|
|
369
|
+
|
|
354
370
|
### 8단계′ — 전달: 단일 HTML (Lim 밖 고객·메일 첨부)
|
|
355
371
|
|
|
356
372
|
```bash
|
|
@@ -197,7 +197,7 @@ python3 assets/render.py <report.json>
|
|
|
197
197
|
`cases[].remaining` 이 있으면 그 건은 대조표에서 **미결**로 선다.
|
|
198
198
|
그 행이 독자에게 넘어가는 몫이라 다른 행과 시각적으로 갈린다.
|
|
199
199
|
|
|
200
|
-
### `cases[].uid` · `sections[].uid` —
|
|
200
|
+
### `cases[].uid` · `sections[].uid` · `open_items[].uid` — 건·절·판단 항목의 **불변 이름**
|
|
201
201
|
|
|
202
202
|
**적지 않아도 된다. 다만 받은 것은 돌려보내라.**
|
|
203
203
|
|
|
@@ -215,9 +215,20 @@ python3 assets/render.py <report.json>
|
|
|
215
215
|
| `uid` 를 빼고 제목도 바꿨다 | **새 이름이 발급된다** |
|
|
216
216
|
| 처음 발행(`publish_report`) | 전부 새로 발급된다 — 적을 필요 없다 |
|
|
217
217
|
|
|
218
|
-
**왜 지켜야 하나**: 저자가 그 건에 달아 둔 메모(`list_notes`)
|
|
219
|
-
이름이 바뀌면
|
|
220
|
-
저자가 다시 붙여야 한다.
|
|
218
|
+
**왜 지켜야 하나**: 저자가 그 건에 달아 둔 메모(`list_notes`)와 **고객이 문장에 달아 둔 검토 회신 핀**이
|
|
219
|
+
이 이름을 문다. 이름이 바뀌면 사라지지는 않지만(인용문으로 다시 찾는다) 「자리를 잃음」으로 떨어져
|
|
220
|
+
저자가 다시 붙여야 한다. 판단 항목(`open_items[]`)도 같은 이름을 들고(2026-09-17), 렌더러가 그 항의
|
|
221
|
+
`<li>` 에 `data-uid` 로 내보낸다 — 항을 고치거나 순서를 바꿔도 핀이 그 항에 남는다.
|
|
222
|
+
|
|
223
|
+
**검토 회신은 자리 표식이 아니라 판단 항목의 답이다 — 개정 전에 본문부터 읽는다**(2026-09-17~18 사고). 한 보고서를
|
|
224
|
+
다시 쓰며 핀의 자리(앵커)만 재고 본문을 안 읽었더니, 이미 답이 달린 판단 항을 다섯 판 동안 열린 항으로 다시 물었고
|
|
225
|
+
「회신에 언급이 없어 미결」이라고 사실과 다르게 적었고, 핀을 「제자리」로 돌리려고 발행 문장을 옛 표기로 되돌려 렌더러 경고만
|
|
226
|
+
얹었다. 그래서 서버는 셋을 한다 — ① `list_reports`·`get_report`·`update_report` 응답이 **열린 검토 회신 수**(`open_replies`)를
|
|
227
|
+
싣고 ② `list_notes(id)` 가 회신을 **본문·자리(어느 판단 항·건·도판인가)·지금 발행물에서의 자리 판정**과 함께 보여 주고
|
|
228
|
+
③ `update_report` 는 `dry_run`·실전 둘 다에서 **열린 회신**의 문장이 이 원문에 없으면 경고한다(회신 본문과 함께).
|
|
229
|
+
회신대로 고쳐 문장이 바뀌는 것은 정상이다 — 반영한 뒤 `resolve_note(id, reply, rev)` 로 표시하면 그 회신은 더는 경고하지 않는다.
|
|
230
|
+
대조는 열람 셸과 같은 엄격 일치라 띄어쓰기 하나만 고쳐도 「원문이 바뀜」이다. 판정 전체는 응답 `pins[]`(도구 출력의
|
|
231
|
+
「회신 핀 N건 — …」 줄). 절차는 SKILL 9b단계.
|
|
221
232
|
|
|
222
233
|
★ **같은 제목을 두 건에 쓰지 마라.** 위 2단이 어느 쪽인지 못 갈라 둘 다 새 이름을 받는다.
|
|
223
234
|
|
|
@@ -400,6 +411,7 @@ python3 assets/render.py <report.json>
|
|
|
400
411
|
| 그룹에 배정되지 않은 건 N개 | `cases[].group` 이 어긋났다(그룹이 아예 없을 때) |
|
|
401
412
|
| 그림 N장은 놓을 자리가 없어 | `logo`·`figure` 밖의 `role`(`hero`·`chapter`·`eof`) |
|
|
402
413
|
| 도판 N장이 어느 절에도 걸리지 않아 | `sections[].plates` 나 `cases[].plates` 에 그 id 를 안 적었다 — 11-1절·11-10절 |
|
|
414
|
+
| 열린 검토 회신 #N(이름 · 날짜) 「회신 본문」 — 집은 문장 「…」이 이 원문에 없습니다 — 원문이 바뀜 / 문장을 잃어 상위로 / 자리를 잃음 | `update_report` 만. **아직 반영 표시가 안 된** 회신이 집은 문장이 새 원문에 없다(3.5절 uid 항) — 이 판이 그 회신을 반영한 것이면 발행 뒤 `resolve_note(id, reply, rev)`, 아니면 회신 본문부터 읽는다(SKILL 9b). 핀을 살리려고 문장을 되돌리지 않는다 |
|
|
403
415
|
| 절 …의 도판 「…」를 assets 에서 못 찾았습니다 | `plates` 가 없는 `assets[].id` 를 가리킨다(건 `plates` 면 「건 NN-n 의 도판」) |
|
|
404
416
|
| 건 NN-n 에 묶인 도판 「…」는 전폭(wide)인데 건 안이라 본문 단 폭으로 섭니다 | `cases[].plates` 가 `wide:true` 자산을 가리킨다 — 건 안은 두 단 그리드라 전폭이 못 선다. 절 `plates` 로 옮기거나 `wide` 를 끈다(11-10절) |
|
|
405
417
|
| 도판 「…」에 그림 바이트가 없습니다 | `file`·`source` 없이 이름만 왔다. 로컬 `render.py` 는 `file` 을 바이트로 굳히지 않아 `file` 이 있어도 뜬다 — 로컬에서는 이 줄을 빼고 나머지 ⚠ 를 읽는다 |
|