genetics-mcp 2.20.0 → 2.21.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 +114 -12
- package/package.json +1 -1
- 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,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": "70aa0772b147db518b10f22ab6a027a3b7b6a98bbc6d477505bf69cb9d9b447c",
|
|
3
|
+
"aarch64-unknown-linux-musl": "87fecda2d2348a0403957b3ab7b7ed55dff72560d6768ad241d93861ab5c56d0",
|
|
4
|
+
"x86_64-apple-darwin": "4877439d3b788c303fced3dd458331f21d3661f4eb463572c260e2c82888c165",
|
|
5
|
+
"x86_64-unknown-linux-musl": "58e7821dd31b81232175023a86db8659cb2f5c5d47620e7b939df4e155f93b98"
|
|
6
6
|
}
|
|
Binary file
|
|
Binary file
|
package/genetics.mjs
CHANGED
|
@@ -4012,13 +4012,17 @@ const TOOLS = [
|
|
|
4012
4012
|
+ '각 메모의 `anchor.path`(예 `case:k7m2p9/fix`)가 **발행 데이터의 어느 칸을 고치라는 것인지**를 '
|
|
4013
4013
|
+ '그대로 말한다 — 그 칸을 고쳐 update_report 로 되돌려 보내고, resolve_note 로 반영을 표시한다.\n'
|
|
4014
4014
|
+ '`anchor_state` 가 `unmoored`·`reparented` 면 문서가 바뀌어 원래 자리를 잃은 것이다 — '
|
|
4015
|
-
+ '그래도 메모는 남아 있고, `anchor.quote_exact` 가 저자가 무엇을 보고 썼는지
|
|
4015
|
+
+ '그래도 메모는 남아 있고, `anchor.quote_exact` 가 저자가 무엇을 보고 썼는지 말한다.\n'
|
|
4016
|
+
+ '같은 호출이 **검토 회신**(고객에게 보이는 글 — 판단 항목의 답이 여기로 온다)도 함께 보여 준다. '
|
|
4017
|
+
+ '문서를 고치기 **전에** 회신의 본문을 읽는다: 각 회신의 where 가 어느 물음·건·도판에 대한 말인지 말한다. '
|
|
4018
|
+
+ '회신대로 고친 뒤의 「원문이 바뀜」은 정상이다 — 핀을 살리려고 문장을 되돌리지 않는다.',
|
|
4016
4019
|
inputSchema: {
|
|
4017
4020
|
type: 'object',
|
|
4018
4021
|
properties: {
|
|
4019
4022
|
id: { type: 'integer', description: '보고서 id' },
|
|
4020
4023
|
state: { type: 'string', description: '`open`(기본, 아직 반영 안 한 것) 또는 `all`' },
|
|
4021
4024
|
uid: { type: 'string', description: '그 건·절의 uid 에 달린 것만(선택)' },
|
|
4025
|
+
channel: { type: 'string', description: '`all`(기본 — 메모와 검토 회신 둘 다) · `notes`(저자 메모만) · `replies`(검토 회신만)' },
|
|
4022
4026
|
},
|
|
4023
4027
|
required: ['id'],
|
|
4024
4028
|
},
|
|
@@ -4029,7 +4033,8 @@ const TOOLS = [
|
|
|
4029
4033
|
'메모에 **답글로** 회신한다. 이견이 있거나 물을 것이 있으면 대화창이 아니라 여기에 쓴다 — '
|
|
4030
4034
|
+ '저자는 자기가 메모를 단 그 자리에서 답을 본다.\n'
|
|
4031
4035
|
+ '고칠 수 없는 이유, 다르게 본 근거, 되묻는 것이 여기 온다. '
|
|
4032
|
-
+ '고쳤으면 답글이 아니라 resolve_note
|
|
4036
|
+
+ '고쳤으면 답글이 아니라 resolve_note 다.\n'
|
|
4037
|
+
+ '★ **검토 회신에는 이 도구로 답하지 않는다** — 그쪽은 고객에게 보이는 글이라 사람이 포털에서 쓴다. 할 말은 작성자에게 대화로 전한다.',
|
|
4033
4038
|
inputSchema: {
|
|
4034
4039
|
type: 'object',
|
|
4035
4040
|
properties: {
|
|
@@ -4043,19 +4048,22 @@ const TOOLS = [
|
|
|
4043
4048
|
{
|
|
4044
4049
|
name: 'resolve_note',
|
|
4045
4050
|
description:
|
|
4046
|
-
'그
|
|
4051
|
+
'그 메모(또는 검토 회신)를 **반영했다**고 표시한다. 반영이 실린 판번호(body_rev)가 함께 남아 '
|
|
4047
4052
|
+ '「무엇에 반영됐는지」를 나중에도 안다.\n'
|
|
4048
4053
|
+ '★ 고치기 **전에** 부르지 말 것 — 이 표시가 곧 「이 메모는 끝났다」이고, '
|
|
4049
|
-
+ '표시하는 순간 미반영 목록에서 빠져 다시 눈에 안
|
|
4054
|
+
+ '표시하는 순간 미반영 목록에서 빠져 다시 눈에 안 띈다.\n'
|
|
4055
|
+
+ '검토 회신은 `note` 대신 `reply` 로 가리킨다 — 표시해도 회신은 고객에게 그대로 보이고, 그 회신이 집은 문장이 바뀐 것을 update_report 가 더는 경고하지 않는다. '
|
|
4056
|
+
+ '문서에 반영할 것이 없는 회신(인사·이미 답한 물음)도 읽었으면 표시한다.',
|
|
4050
4057
|
inputSchema: {
|
|
4051
4058
|
type: 'object',
|
|
4052
4059
|
properties: {
|
|
4053
4060
|
id: { type: 'integer', description: '보고서 id' },
|
|
4054
|
-
note: { type: 'integer', description: '메모 id' },
|
|
4061
|
+
note: { type: 'integer', description: '메모 id(`reply` 와 둘 중 하나)' },
|
|
4062
|
+
reply: { type: 'integer', description: '검토 회신 id — list_notes 의 [회신 N]. 뿌리 회신만 표시할 수 있다' },
|
|
4055
4063
|
rev: { type: 'integer', description: '반영이 실린 body_rev. 안 주면 지금 판' },
|
|
4056
4064
|
resolved: { type: 'boolean', description: '거짓이면 미반영으로 되돌린다' },
|
|
4057
4065
|
},
|
|
4058
|
-
required: ['id'
|
|
4066
|
+
required: ['id'],
|
|
4059
4067
|
},
|
|
4060
4068
|
},
|
|
4061
4069
|
{
|
|
@@ -4627,6 +4635,14 @@ const TOOLS = [
|
|
|
4627
4635
|
* 찍는 자리가 흩어져 있으면 또 한 곳이 빠지므로 여기 하나로 모은다. */
|
|
4628
4636
|
// 자산을 굳히며 이쪽(작성자 기계)에서만 알 수 있는 것 — 서버 경고와 한 목록으로 낸다(readability.md R8).
|
|
4629
4637
|
let ASSET_HINTS = [];
|
|
4638
|
+
/* 회신 핀 예보(update_report) — 서버가 핀마다 낸 판정(`pins[]`)을 한 줄로 센다. 경고는 약해지는 핀만 말하므로,
|
|
4639
|
+
* 이 줄이 없으면 「전부 제자리」와 「핀이 없다」와 「예보가 안 돌았다(옛 서버)」를 에이전트가 못 가른다. 핀이 없으면 안 찍는다. */
|
|
4640
|
+
function pinsText(r) {
|
|
4641
|
+
const p = Array.isArray(r?.pins) ? r.pins : [];
|
|
4642
|
+
if (!p.length) return '';
|
|
4643
|
+
const n = (s) => p.filter((x) => x?.state === s).length;
|
|
4644
|
+
return `\n회신 핀 ${p.length}건 — 제자리 ${n('exact')} · 옮겨 붙음 ${n('moved')} · 원문이 바뀜 ${n('drifted')} · 상위로 ${n('reparented')} · 자리 잃음 ${n('unmoored')}`;
|
|
4645
|
+
}
|
|
4630
4646
|
function warnText(r) {
|
|
4631
4647
|
const w = [...(r?.warnings ?? []), ...ASSET_HINTS];
|
|
4632
4648
|
return w.length
|
|
@@ -5456,6 +5472,55 @@ let NOTES_LAST = '';
|
|
|
5456
5472
|
let NOTES_QUIET = 0;
|
|
5457
5473
|
const NOTES_REMIND_EVERY = 8;
|
|
5458
5474
|
|
|
5475
|
+
/*
|
|
5476
|
+
* 열린 검토 회신(125) — 메모와 같은 방식으로 기억하고 꼬리에서 말한다. 회신은 **판단 항목의 답이 오는 자리**라
|
|
5477
|
+
* 문서를 고치기 전에 읽혀야 한다: 2026-09-17 에는 이 통로가 없어 답이 와 있는 항을 다섯 판 동안 다시 물었다.
|
|
5478
|
+
*/
|
|
5479
|
+
const REPLIES_OPEN = new Map(); // 보고서 id → 열린 회신 건수
|
|
5480
|
+
let REPLIES_LAST = '';
|
|
5481
|
+
let REPLIES_QUIET = 0;
|
|
5482
|
+
function repliesSeen(id, n) {
|
|
5483
|
+
if (!Number.isInteger(id) || id <= 0 || !Number.isInteger(n)) return;
|
|
5484
|
+
if (n > 0) REPLIES_OPEN.set(id, n); else REPLIES_OPEN.delete(id);
|
|
5485
|
+
}
|
|
5486
|
+
function repliesSays() {
|
|
5487
|
+
const rows = [];
|
|
5488
|
+
REPLIES_OPEN.forEach((n, id) => { if (n > 0) rows.push(`#${id} ${n}건`); });
|
|
5489
|
+
if (!rows.length) { REPLIES_LAST = ''; return ''; }
|
|
5490
|
+
const t = `검토 회신 미반영 — ${rows.join(' · ')} · list_notes(id) 가 본문과 함께 보여 준다. `
|
|
5491
|
+
+ '문서를 고치기 전에 읽고, 반영했으면 resolve_note(id, reply). 고객에게 보이는 답글은 사람이 포털에서 쓴다.';
|
|
5492
|
+
if (t === REPLIES_LAST && ++REPLIES_QUIET < NOTES_REMIND_EVERY) return '';
|
|
5493
|
+
REPLIES_LAST = t;
|
|
5494
|
+
REPLIES_QUIET = 0;
|
|
5495
|
+
return t;
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
/** 검토 회신 목록을 사람이 읽는 줄로. 답글은 뿌리 밑에 붙인다(메모 목록과 같은 규율). */
|
|
5499
|
+
function repliesText(id, r) {
|
|
5500
|
+
if (!r.replies || !r.replies.length) return `검토 회신 — 보고서 #${id} · ${r.note || '열린 검토 회신이 없다.'}`;
|
|
5501
|
+
const STATE = { exact: '', moved: '(옮겨 붙음)', drifted: '(원문이 바뀜)', reparented: '(문장을 잃어 상위로)', unmoored: '(자리를 잃음)' };
|
|
5502
|
+
const side = (c) => (c.side === 'client' ? '고객' : '우리');
|
|
5503
|
+
const kids = new Map();
|
|
5504
|
+
for (const c of r.replies) {
|
|
5505
|
+
if (c.parent_id !== null) {
|
|
5506
|
+
if (!kids.has(c.parent_id)) kids.set(c.parent_id, []);
|
|
5507
|
+
kids.get(c.parent_id).push(c);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
const lines = [`검토 회신(고객에게 보이는 글) · 보고서 #${id} · 본문 판 body_rev=${r.body_rev} · ${r.note}`, ''];
|
|
5511
|
+
if (r.capped) lines.splice(1, 0, '⚠ 목록이 잘렸다 — 아래가 전부가 아니다.');
|
|
5512
|
+
for (const c of r.replies) {
|
|
5513
|
+
if (c.parent_id !== null) continue;
|
|
5514
|
+
lines.push(`[회신 ${c.id}] ${c.where || '문서 전체'} ${STATE[c.state_now] || ''}${c.resolved ? ` · 반영됨(판 ${c.resolved_rev ?? '?'})` : ''}`.trimEnd());
|
|
5515
|
+
if (c.anchor?.quote_exact) lines.push(` 집은 글: 「${c.anchor.quote_exact}」`);
|
|
5516
|
+
lines.push(` ${c.author || ''}(${side(c)}) ${String(c.created_at || '').slice(0, 10)}: ${c.body}`);
|
|
5517
|
+
for (const k of (kids.get(c.id) || [])) lines.push(` ↳ ${k.author || ''}(${side(k)}): ${k.body}`);
|
|
5518
|
+
lines.push('');
|
|
5519
|
+
}
|
|
5520
|
+
lines.push('반영했으면 resolve_note(id, reply: N). 회신대로 고친 뒤의 「원문이 바뀜」은 정상이다. 고객에게 보이는 답글은 이 도구로 달지 않는다.');
|
|
5521
|
+
return lines.join('\n').trimEnd();
|
|
5522
|
+
}
|
|
5523
|
+
|
|
5459
5524
|
/** 응답에 실려 온 건수를 기억한다. 서버가 그 칸을 모르는 판이면 아무 일도 안 한다. */
|
|
5460
5525
|
function notesSeen(id, n) {
|
|
5461
5526
|
if (!Number.isInteger(id) || id <= 0 || !Number.isInteger(n)) return;
|
|
@@ -5493,6 +5558,8 @@ function worldAppend(result) {
|
|
|
5493
5558
|
*/
|
|
5494
5559
|
const nn = notesSays();
|
|
5495
5560
|
if (nn) result.content.push({ type: 'text', text: nn });
|
|
5561
|
+
const rr = repliesSays();
|
|
5562
|
+
if (rr) result.content.push({ type: 'text', text: rr });
|
|
5496
5563
|
const t = worldDelta();
|
|
5497
5564
|
if (!t) return;
|
|
5498
5565
|
/*
|
|
@@ -5752,6 +5819,11 @@ async function callTool(name, args = {}) {
|
|
|
5752
5819
|
notesSeen(Number(x.id), x.open_notes);
|
|
5753
5820
|
if (x.open_notes > 0) bits.push(`첨삭 메모 ${x.open_notes}건 미반영`);
|
|
5754
5821
|
}
|
|
5822
|
+
// 열린 검토 회신(125) — 판단 항목의 답이 와 있는 문서를 목록에서 알아보게. 옛 서버·자격 없음이면 안 찍는다.
|
|
5823
|
+
if (Number.isInteger(x.open_replies)) {
|
|
5824
|
+
repliesSeen(Number(x.id), x.open_replies);
|
|
5825
|
+
if (x.open_replies > 0) bits.push(`검토 회신 ${x.open_replies}건 미반영`);
|
|
5826
|
+
}
|
|
5755
5827
|
const tag = bits.length ? ` [${bits.join(' · ')}]` : '';
|
|
5756
5828
|
// slug 를 싣는다. 중복 slug 는 409 로 잘 막히지만 **미리 알 수가 없었다** —
|
|
5757
5829
|
// 목록에 이름표가 없어 새 이름을 고를 때 참고할 것이 없었다(2026-08-18 검토).
|
|
@@ -5803,6 +5875,7 @@ async function callTool(name, args = {}) {
|
|
|
5803
5875
|
return JSON.stringify(r.current_text, null, 2);
|
|
5804
5876
|
}
|
|
5805
5877
|
notesSeen(Number(args.id), r.open_notes);
|
|
5878
|
+
repliesSeen(Number(args.id), r.open_replies);
|
|
5806
5879
|
if (!args.outline) {
|
|
5807
5880
|
/*
|
|
5808
5881
|
* ⚠ 전에는 여기서 **`r.data` 만** 돌려줬다. 서버가 정성껏 붙인 것이 이 한 줄에서
|
|
@@ -5820,6 +5893,10 @@ async function callTool(name, args = {}) {
|
|
|
5820
5893
|
head.push(`[정본 ${owner} · 본문 판 body_rev=${r.body_rev ?? '?'}`
|
|
5821
5894
|
+ (r.sha256 ? ` · 지금 발행물 ${r.bytes}B sha256:${String(r.sha256).slice(0, 12)}…` : '')
|
|
5822
5895
|
+ `] — 고칠 때는 update_report 에 body_rev 를 함께 실으면, 그 사이 누가 고쳤을 때 409 로 멈춘다.`);
|
|
5896
|
+
// 열린 검토 회신 — 고치기 전에 읽어야 한다(판단 항목의 답이 거기 있다). 첫 줄 가까이에 둔다.
|
|
5897
|
+
if (Number.isInteger(r.open_replies) && r.open_replies > 0) {
|
|
5898
|
+
head.push(`검토 회신 ${r.open_replies}건이 열려 있다 — 고치기 전에 list_notes(id: ${Number(args.id)}) 로 본문을 읽는다.`);
|
|
5899
|
+
}
|
|
5823
5900
|
// 그림은 서버가 보관한다(바이트는 에이전트를 안 지난다) — 되돌려 보낼 때의 계약을 여기서 말한다(#34).
|
|
5824
5901
|
const kept = Array.isArray(r.data?.assets)
|
|
5825
5902
|
? r.data.assets.filter((a) => typeof a?.data === 'string' && a.data.startsWith('(생략')).length : 0;
|
|
@@ -5837,13 +5914,29 @@ async function callTool(name, args = {}) {
|
|
|
5837
5914
|
}
|
|
5838
5915
|
case 'list_notes': {
|
|
5839
5916
|
const id = Number(args.id);
|
|
5917
|
+
const channel = args.channel === undefined ? 'all' : String(args.channel);
|
|
5918
|
+
if (!['all', 'notes', 'replies'].includes(channel)) throw new Error('channel 은 all · notes · replies 중 하나입니다.');
|
|
5840
5919
|
const qs = new URLSearchParams();
|
|
5841
5920
|
if (args.state) qs.set('state', String(args.state));
|
|
5842
5921
|
if (args.uid) qs.set('uid', String(args.uid));
|
|
5843
|
-
const
|
|
5922
|
+
const q = qs.toString() ? '?' + qs : '';
|
|
5923
|
+
// 검토 회신(125) — 메모 뒤에 붙인다. 옛 서버(창구 없음)는 메모만 낸다; 회신만 달라고 했으면 그 실패를 그대로 말한다.
|
|
5924
|
+
let repliesPart = '';
|
|
5925
|
+
if (channel !== 'notes') {
|
|
5926
|
+
try {
|
|
5927
|
+
const rr = await api('GET', `/api/reports/${id}/replies${q}`);
|
|
5928
|
+
repliesSeen(id, rr.open);
|
|
5929
|
+
repliesPart = repliesText(id, rr);
|
|
5930
|
+
} catch (e) {
|
|
5931
|
+
if (channel === 'replies' || Number(e?.status) !== 404) throw e;
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
if (channel === 'replies') return repliesPart;
|
|
5935
|
+
const tail = repliesPart ? `\n\n${repliesPart}` : '';
|
|
5936
|
+
const r = await api('GET', `/api/reports/${id}/notes${q}`);
|
|
5844
5937
|
notesSeen(id, r.open);
|
|
5845
5938
|
if (!r.notes || !r.notes.length) {
|
|
5846
|
-
return `보고서 #${id} — ${r.note || '메모가 없다.'}`;
|
|
5939
|
+
return `보고서 #${id} — ${r.note || '메모가 없다.'}${tail}`;
|
|
5847
5940
|
}
|
|
5848
5941
|
/*
|
|
5849
5942
|
* 답글을 부모 밑에 붙여 낸다. 평평하게 내면 에이전트가 **자기가 이미 한 말**을
|
|
@@ -5873,7 +5966,7 @@ async function callTool(name, args = {}) {
|
|
|
5873
5966
|
}
|
|
5874
5967
|
lines.push('');
|
|
5875
5968
|
}
|
|
5876
|
-
return lines.join('\n').trimEnd();
|
|
5969
|
+
return lines.join('\n').trimEnd() + tail;
|
|
5877
5970
|
}
|
|
5878
5971
|
case 'reply_note': {
|
|
5879
5972
|
const id = Number(args.id);
|
|
@@ -5883,6 +5976,15 @@ async function callTool(name, args = {}) {
|
|
|
5883
5976
|
}
|
|
5884
5977
|
case 'resolve_note': {
|
|
5885
5978
|
const id = Number(args.id);
|
|
5979
|
+
if ((args.note === undefined) === (args.reply === undefined)) throw new Error('note(메모 id) 와 reply(검토 회신 id) 중 **하나만** 주세요.');
|
|
5980
|
+
if (args.reply !== undefined) {
|
|
5981
|
+
const rb = { reply: Number(args.reply) };
|
|
5982
|
+
if (args.rev !== undefined) rb.rev = Number(args.rev);
|
|
5983
|
+
if (args.resolved !== undefined) rb.resolved = !!args.resolved;
|
|
5984
|
+
const rr = await api('PATCH', `/api/reports/${id}/replies`, rb);
|
|
5985
|
+
if (Number.isInteger(rr.open)) repliesSeen(id, rr.open);
|
|
5986
|
+
return rr.says;
|
|
5987
|
+
}
|
|
5886
5988
|
const body = { note: Number(args.note) };
|
|
5887
5989
|
if (args.rev !== undefined) body.rev = Number(args.rev);
|
|
5888
5990
|
if (args.resolved !== undefined) body.resolved = !!args.resolved;
|
|
@@ -5967,16 +6069,16 @@ async function callTool(name, args = {}) {
|
|
|
5967
6069
|
throw e;
|
|
5968
6070
|
}
|
|
5969
6071
|
// 고친 직후가 「무엇을 아직 안 고쳤나」를 볼 가장 좋은 순간이다.
|
|
5970
|
-
if (!r.dry_run) notesSeen(Number(args.id), r.open_notes);
|
|
6072
|
+
if (!r.dry_run) { notesSeen(Number(args.id), r.open_notes); repliesSeen(Number(args.id), r.open_replies); }
|
|
5971
6073
|
if (r.dry_run) {
|
|
5972
|
-
return `시험 실행 — 아무것도 바꾸지 않았습니다.\n${r.tenant} · 건 ${r.cases}` + warnText(r);
|
|
6074
|
+
return `시험 실행 — 아무것도 바꾸지 않았습니다.\n${r.tenant} · 건 ${r.cases}` + pinsText(r) + warnText(r);
|
|
5973
6075
|
}
|
|
5974
6076
|
// 상태를 함께 알린다 — 이 문서를 다음에도 API 로 고칠 수 있는지가 여기서 갈린다.
|
|
5975
6077
|
const st = r.state ? ` [${r.state}]` : '';
|
|
5976
6078
|
// 서버가 초안이라고 답하면 이 도구의 동사로 다음 할 일을 잇는다(서버 note 는 HTTP 경로로 말한다).
|
|
5977
6079
|
const open = r.published === false ? ` 이 도구에서는 release_report(id: ${Number(args.id)}) 가 그 일입니다.` : '';
|
|
5978
6080
|
return `교체 완료 — ${r.tenant} · 건 ${r.cases}${st}\n열람 주소: ${r.url}\n${r.note || ''}${open}`
|
|
5979
|
-
+ pendingText(r) + warnText(r);
|
|
6081
|
+
+ pendingText(r) + pinsText(r) + warnText(r);
|
|
5980
6082
|
}
|
|
5981
6083
|
case 'world': {
|
|
5982
6084
|
const facts = worldFacts(8);
|
package/package.json
CHANGED
|
@@ -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` 이 있어도 뜬다 — 로컬에서는 이 줄을 빼고 나머지 ⚠ 를 읽는다 |
|