genetics-mcp 2.17.1 → 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/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
- return r?.warnings?.length
2952
- ? `\n\n경고 ${r.warnings.length}건:\n· ${r.warnings.join('\n· ')}`
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.17.1",
3
+ "version": "2.18.0",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -109,6 +109,11 @@ version: "4.0.0"
109
109
  | **배운다** (유지보수·인수·온보딩·사후분석) | 원리 / 붕괴와 처방 / 적용과 남은 것 | 같은 다섯 kind 를 그 순서로 쓴다 |
110
110
  | **결정한다** (도입 여부·옵션 선택·예산·일정) | 결정 / 근거 / 이행 | `asks` → `summary` → `note`(선택지 전치 행렬) → `cases` → `note`(FCG 의견 표 한 장) → `note`(바꾸는 길의 위험) → `note`(회신 뒤 일정) |
111
111
 
112
+ **회신·결정이 없는 정황 공유 보고는 판정한다 뼈대에서 요청 PART 만 뺀다.** PART 는 「경과 / 남은 것」이고,
113
+ 요지·대조표·건은 경과 PART 하나에 함께 선다(`references/process.md` 「PART 를 먼저 가른다」).
114
+ 절의 성격(시각표·원인·앞날)으로 PART 를 새로 짓지 않는다 — 「경과 / 원인」으로 가르면 같은 건이 두 PART 에 문장만 바꿔 선다(실사례 2026-09-12).
115
+ 독자가 결정권자면 `status` 대조표를 빼고 요약면 상태 막대만 남긴다(`references/process.md` 「이 답에서 곧바로 정해지는 것」).
116
+
112
117
  결정한다 모드에 「하지 않기로 한 것」 절은 **따로 두지 않는다** — 선택지 표의 「정할 자리」·「다시 볼 때」 행이
113
118
  그 몫을 한다(반드시 지키는 것 10). FCG 의견은 절 하나, 표 한 장이 전부다 — 다른 절·열·제목에 배지 않는다.
114
119
 
@@ -172,7 +177,7 @@ Lim 보고서, Lim 에 올려줘, 고객 보고서, 수정 내역, 검수 요청
172
177
  > 반려 원문 셋(앳플리 체중 조절 권장량 4차, 같은 날): 「글만 적어서는 이해가 어려움 — 그림 자료」 →
173
178
  > 「신입은 읽다가 자겠다」 → 「색상 다변화, 핵심은 굵게, 배지·그래프 보강」. 셋 다 원고를 쓴
174
179
  > 에이전트가 「글이면 충분하다」고 판단한 자리였다. **판단을 에이전트에게 남겨 두지 않는다** —
175
- > 렌더러가 센다(`lim-portal-format.md` §10-1 — 로컬 `render.py`, 운영 dry_run 은 렌더러 배포 뒤 · 같은 문서 §11-6 머리). 여기는 그 앞의 집필 공식이다.
180
+ > 렌더러가 센다(dry_run ⚠, `lim-portal-format.md` §10-1). 여기는 그 앞의 집필 공식이다.
176
181
 
177
182
  원고를 쓰기 전에, 절마다 이 순서로 정한다. 글은 마지막이다.
178
183
 
@@ -181,11 +186,11 @@ Lim 보고서, Lim 에 올려줘, 고객 보고서, 수정 내역, 검수 요청
181
186
  | 1 | **한 장 이야기** | 문제 → 원인 → 해법 → 지금 상태 → 정할 것. 요약면 다섯 칸이 이 순서이고, 다루는 일의 **사건 경과** 표 한 장을 1절에 둔다(이 문서의 판 이력은 `history` — 3b) |
182
187
  | 2 | **예시 한 명** | 실존 이용자 하나의 실제 숫자로 문서 전체를 관통시킨다(같은 사람이 그림·표·건에 반복) |
183
188
  | 3 | **용어 잠금 표** | 같은 낱말이 두 값을 가리키면 그 자리에서 문서가 죽는다. 뜻이 갈리는 낱말은 표 한 장으로 값·정의·보이는 자리를 박는다 |
184
- | 4 | **절마다 시각 장치 먼저** | 개념은 그림(HTML 개념도 → `assets/figure.mjs`), 실제 화면은 번호 표식을 얹은 캡처(`assets/shot.mjs`), 견주기는 표, 「A 면 B」는 흐름도, 분포는 막대 차트. 글은 그 옆 3줄. 도판은 `references/readability.md` 의 규칙표(R1~R10)를 `figure.mjs` 로 통과시킨다 — 글자 17/15px·행간 1.4·뜻 색 3·그림 안 문장 0·무손실. **글 층도 같은 문서의 R11** — 한 문장 60자(한 줄은 45자)·표 칸 20자(5열 이상 12자)·캡션 60자·괄호 문장당 1·숫자 나열은 표 행. 로컬 `render.py` 가 ⚠ 로 센다 — 운영 dry_run 은 렌더러 배포 뒤부터(운영 반영 전 — `lim-portal-format.md` §11-6 머리). 9/10 「도판은 좋아졌는데 가독성은 아직」 반려 |
189
+ | 4 | **절마다 시각 장치 먼저** | 개념은 그림(HTML 개념도 → `assets/figure.mjs`), 실제 화면은 번호 표식을 얹은 캡처(`assets/shot.mjs`), 견주기는 표, 「A 면 B」는 흐름도, 분포는 막대 차트. 글은 그 옆 3줄. 도판은 `references/readability.md` 의 규칙표(R1~R10)를 `figure.mjs` 로 통과시킨다 — 글자 17/15px·행간 1.4·뜻 색 3·그림 안 문장 0·무손실. **글 층도 같은 문서의 R11** — 한 문장 60자(한 줄은 45자)·표 칸 20자(5열 이상 12자)·캡션 60자·괄호 문장당 1·숫자 나열은 표 행. 렌더러 dry_run 과 로컬 `render.py` 가 ⚠ 로 센다. 9/10 「도판은 좋아졌는데 가독성은 아직」 반려 |
185
190
  | 5 | **판단 요청** | 요청은 **고객이 고를 물음**이다 — 제목은 물음, 본문 첫 문장은 기한과 두 길의 이름. 결과는 한눈표 **한 곳에만**, 열은 「항 · 지금 · 그대로 두면 · 바꾸면」으로 **대칭**. 6항 이상이면 관문 흐름도(정하면 어느 길로)를 곁에 둔다. 권고 열·시한 칩·한 길의 결과만 있는 열은 쓰지 않고 `priority:"must"` 도 쓰지 않는다. **화면 변화**(팝업·안내·칸의 유무)가 따르면 글과 표만으로 묻지 않고 **고객 디자인 시스템 규격의 도안**을 붙여 결재를 청한다 — `references/visual-first.md` 「화면 도안」 절. 집행자: 기한은 `gazette.py` ⚠(`doc_type` 에 「브리핑」), 권고 낱말은 `gazette.py` ⚠ W1(카피 게이트 표), 도안 유무는 없음 — 사람의 눈(`references/process.md` 7단계 표) |
186
- | 5a | **원본 확인** | 주장·그림마다 **원본 이름 · 쪽(행) · 찾을 낱말**과 「건 §n.n」을 적는다. 「흐리다」「못 연다」고 쓰기 전에 **잰다** — PDF 는 `pdfimages -list` 전체, 웹은 실제로 열어 찾는다. 고객이 운영에서 **실제로 누를 수 있는 링크**만 근거로 친다 — 지금은 건 `source_url` 뿐이고, 렌더러 배포 뒤에는 글자 안의 맨 주소·`[글자](주소)`(도판 출처 줄 `source_text` 포함)도 링크가 된다(운영 반영 전 — `lim-portal-format.md` §11-6 머리). 집행자: `references/reader-review.workflow.js` 「원본 대조」 관점(7행) · 도판 출처 줄은 `gazette.py` ⚠ W2 — 그 밖은 없음, 사람의 눈 |
187
- | 6 | **표기** | 절·PART 의 `tone`, 차트 행 `tone`, 건의 `source_url`, 요약면 `notice.state`. 색은 뜻으로(`lim-portal-format.md` §11-6). ⛔ 인라인 표기(`**굵게**`·`==강조==`·`{{ok|칩}}`·`[글자](주소)`·**맨 주소** 링크)는 **운영 반영 전 — `lim-portal-format.md` §11-6 머리** — 운영에 오르기 전에 쓰면 열람 화면에 원문이 글자로 찍힌다 |
188
- | 7 | **독자 검토** | 발행 전 `references/reader-review.workflow.js` 로 네 관점(처음 읽는 사람·결정권자·정보 디자이너·원본 대조) 검토를 돌리고 상위 발견을 반영한다 |
191
+ | 5a | **원본 확인** | 주장·그림마다 **원본 이름 · 쪽(행) · 찾을 낱말**과 「건 §n.n」을 적는다. 「흐리다」「못 연다」고 쓰기 전에 **잰다** — PDF 는 `pdfimages -list` 전체, 웹은 실제로 열어 찾는다. 고객이 운영에서 **실제로 누를 수 있는 링크**만 근거로 친다 — 건 `source_url`, 그리고 글자 안의 맨 주소·`[글자](주소)`(도판 출처 줄 `source_text` 포함). 집행자: `references/reader-review.workflow.js` 「원본 대조」 관점(7행) · 도판 출처 줄은 `gazette.py` ⚠ W2 — 그 밖은 없음, 사람의 눈 |
192
+ | 6 | **표기** | 인라인 표기(`**굵게**`·`==강조==`·`{{ok|칩}}`·`[글자](주소)`·**맨 주소**)와 절·PART 의 `tone`, 차트 행 `tone`, 건의 `source_url`, 요약면 `notice.state`. 색은 뜻으로(`lim-portal-format.md` §11-6) |
193
+ | 7 | **독자 검토** | 발행 전 `references/reader-review.workflow.js` 로 다섯 관점(처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조) 검토를 돌리고 상위 발견을 반영한다. **구조 발견(PART·절 합치기·빼기)을 먼저** 반영한다 — 문장·표를 더하는 처방이 절 구성보다 앞서면 겹침이 커진다(2026-09-12 실측: 처방 반영 뒤 PART 사이에 다시 선 시각표 행 8→12) |
189
194
  | 8 | **실물 확인** | 발행 뒤 열람 주소를 열어 그림·표·칩이 섰는지 캡처로 본다. dry_run 경고 0 이 목표 |
190
195
 
191
196
  상세와 도판 예산(장당 80KB · 합계 400KB), 그림 만드는 법, 반려 사례는 `references/visual-first.md`.
@@ -285,6 +290,8 @@ report.json + cases.json 을 하나로 합쳐 `publish_report` 에 준다
285
290
  서버가 렌더하므로 **HTML 을 만들지 않는다.**
286
291
 
287
292
  **`dry_run: true` 로 먼저 태우고 `warnings` 를 읽는다.** 그것이 실질적 유일한 품질 관문이다.
293
+ 단 **구조는 세지 않는다** — PART·절 사이에서 같은 사실을 다시 푸는 되풀이를 재는 경고는 렌더러에도 서버에도 없다.
294
+ 경고 0 은 구조 통과가 아니다. 그 몫은 독자 검토 「구조」 관점과 사람의 눈(`references/process.md` 7단계 「PART 간지」 행)이다.
288
295
  서버는 「빈 칸」을 안 막는다 — 막는 것은 **조용히 사라지는 것**뿐이고, 관보의 본체가
289
296
  빠지면 ⚠ 로 소리 내어 센다. `marg` 가 비었다는 경고를 못 보고 넘기면 두 단이 한 단으로 선다.
290
297
 
@@ -535,13 +542,14 @@ doc-pptx 의 슬롭 3종 중 **대시 대비 헤드라인**("복붙에서 위임
535
542
  | 경어체 잔존(기록 자리) · 한 문장 한 줄 | `tone.mjs` S1 — 판단 항목 본문(`p.ask__body`)과 「」 인용은 세지 않는다(5a · 11) | 로컬 렌더 산출물 |
536
543
  | 학생 보고서투 · 완충어 · 근거 없는 총평 · 단독 부정 종결 | `tone.mjs` S1/S2(2026-08-27 추가) | 로컬 렌더 산출물 |
537
544
  | 금칙 값(고객사·지역·제품명·티켓) | `build.py` `forbid` | **로컬 렌더만** — 포털 발행은 안 돈다(`report_publish.php:76`) |
538
- | 판단 항목에 기한 없음 · `asks` 절 없음 · 선택지 표에 근거 등급 열이나 행 없음(브리핑) | `gazette.py` `brief_checks()` ⚠ | **두 경로 다**(dry_run·로컬) — 「판단 항목」 문구와 행 인정은 렌더러 배포 뒤 |
545
+ | 판단 항목에 기한 없음 · `asks` 절 없음 · 선택지 표에 근거 등급 열이나 행 없음(브리핑) | `gazette.py` `brief_checks()` ⚠ | **두 경로 다**(dry_run·로컬) |
539
546
  | 본문 절의 회차 서술(「이번 회차…」) | `gazette.py` `round_talk_scan()` ⚠ | **두 경로 다**(dry_run·로컬) |
540
- | 본문의 진행 상태 어휘(정해짐·회신 대기·N판) · 개수로 상태를 세는 표 제목(3b) | `gazette.py` `text_scan()` 과정 어휘 ⚠(2026-09-11) | 로컬 렌더 · 운영 dry_run 은 렌더러 배포 뒤(운영 반영 전 — `lim-portal-format.md` §11-6 머리) |
541
- | 권고 낱말(제목·표 열·절 이름·여백 주석·요약면) — 결정권이 고객에게 있는 문서(10번) | `gazette.py` ⚠ **W1** | 로컬 렌더 · 운영 dry_run 은 렌더러 배포 뒤 |
542
- | 고객·제3자 자료에서 낸 도판(`user-provided`)의 출처 줄에 원본 위치(쪽·행·주소·건 §) 누락(11번) | `gazette.py` ⚠ **W2** | 로컬 렌더 · 운영 dry_run 은 렌더러 배포 뒤 |
547
+ | 본문의 진행 상태 어휘(정해짐·회신 대기·N판) · 개수로 상태를 세는 표 제목(3b) | `gazette.py` `text_scan()` 과정 어휘 ⚠(2026-09-11) | **두 경로 다**(dry_run·로컬) |
548
+ | 권고 낱말(제목·표 열·절 이름·여백 주석·요약면) — 결정권이 고객에게 있는 문서(10번) | `gazette.py` ⚠ **W1** | **두 경로 다**(dry_run·로컬) |
549
+ | 고객·제3자 자료에서 낸 도판(`user-provided`)의 출처 줄에 원본 위치(쪽·행·주소·건 §) 누락(11번) | `gazette.py` ⚠ **W2** | **두 경로 다**(dry_run·로컬) |
543
550
  | 확인 포기 문구(「흐리다」「못 연다」)를 원본을 재지 않고 쓴 것(11번) | **없음** — 사람의 눈(`process.md` 7단계 표 · 독자 검토 「원본 대조」 관점) | — |
544
551
  | 부정 서술의 주어가 사람·조직·국가 | **없음** — 사람의 눈 검수 | — |
552
+ | 같은 건 묶음(요지·대조표·건)이 요청 없이 두 PART 로 갈림 · 앞 PART 가 뒤 PART 물음에 먼저 답함 · 두 절 `marg.key` 가 같은 명제(readability R12g) | 독자 검토 「구조」 관점(`references/reader-review.workflow.js`) · 사람의 눈(`references/process.md` 7단계 「PART 간지」 행) — **기계 없음** | 발행 전 |
545
553
 
546
554
  다만 **조사·체언 종결 조각문** 규칙은 보고서 문맥에서 완화한다. 목차 항목과 건 제목은
547
555
  "…던 문제", "…내역 정리"처럼 명사구로 끝나는 것이 한국어 보고서의 관례다. 슬라이드 헤드라인과
@@ -104,7 +104,7 @@
104
104
  "note": "「확인 방법」의 판정 기준은 부록 1에 있음.",
105
105
  "marg": {
106
106
  "why": "어디까지 확인한 것인가?",
107
- "key": "두 건 모두 운영 반영까지 확인함.",
107
+ "key": "「실데이터 대조」는 화면 재현까지는 안 했다는 뜻임.",
108
108
  "advice": "판정 기준은 부록 1."
109
109
  } },
110
110
 
@@ -425,8 +425,6 @@ python3 assets/render.py <report.json>
425
425
 
426
426
  ### 10-1. 시각 우선 관문 — 글로만 선 원고를 렌더러가 센다 (2026-09-08)
427
427
 
428
- > ⛔ 이 표 **전체**가 운영 반영 전이다 — 지금은 로컬 `render.py` 만 센다(§11-6 머리).
429
-
430
428
  > 「읽기가 아직도 어려움 — 색상 다변화, 핵심은 굵게, 배지·그래프」 반려(앳플리 4차)의 처방.
431
429
  > 문턱은 `gazette.py` 상수(`PROSE_MAX` 등)와 **같은 값**이다 — 이 표가 계약이다.
432
430
 
@@ -436,7 +434,7 @@ python3 assets/render.py <report.json>
436
434
  | 절 N 「이름」의 k번째 문단이 M자입니다 | 문단 한 개 220자 초과(세 줄) | 두 문장으로 끊거나 수치를 표로 내린다 |
437
435
  | 절 N 「이름」 산문에 참조 번호가 M개입니다 | 산문(문단·도입문·여백 주석)에 6.6·2.12a 같은 번호 4개 이상 | 번호는 표·건·판단 항목에만, 산문은 뜻으로 잇는다 |
438
436
  | 판단 요청 N항이 산문으로만 섭니다 | `open_items` 6항 이상인데 asks 절에 표·흐름도 0 | 한눈표(항 · 지금 · 그대로 두면 · 바꾸면 — 두 길 대칭, 권고 열·시한 칩 없음) + 관문 흐름도(§11-2). |
439
- | 핵심 굵게 표기가 한 곳도 없습니다 | 산문 3,000자 이상, `**…**` 도 편집기 굵게 구간도 0 | 절마다 핵심 한 구절을 `**…**` 로 굵게(§11-6). 문단 절반이 굵으면 굵기가 위계를 못 나른다. ⛔ 운영 반영 전에는 따르지 않는다 — 넣으면 열람 화면에 별표가 찍힌다(§11-6 머리) |
437
+ | 핵심 굵게 표기가 한 곳도 없습니다 | 산문 3,000자 이상, `**…**` 도 편집기 굵게 구간도 0 | 절마다 핵심 한 구절을 `**…**` 로 굵게(§11-6). 문단 절반이 굵으면 굵기가 위계를 못 나른다. |
440
438
  | 과정 어휘가 %d곳에 있습니다(%s 「%s」 …) — 독자는 앞 회차·회신·판을 모릅니다. 정해진 것은 사실로 배경에, 결정 요청에는 열린 것만 | 원고의 글자 자리 어디든(`history`·`labels`·`forbid`·`origin`·`file`·`source_url`·`href`·`data`·`out`·`css`·`render` 제외)에 정규식 `정해짐 \| 회신 대기 \| 남은 물음 \| 정해진 것 \| 이미 정해진 \| 남은 것 (하나·둘·셋·넷) \| (N·첫·두·세)판` 이 1곳 이상. `%s` 둘은 자리 경로와 걸린 낱말 | 회차 낱말(「이번 회차」)만이 아니라 **진행 상태 어휘와 구조**가 금지다 — 상태 열(「상태: 정해짐/회신 대기」)·「~ 그대로(변화 없음)」도 같다. 정해진 것은 **사실**로 요지(배경)에, 결정 요청 절에는 **지금 열린 것만**. 회차의 자리는 `history` 하나. 원고는 직전 판의 차분이 아니라 **독자의 0 상태에서 다시 쓴다** — 판마다 「처음 읽는 사람이 이 절에서 무엇을 알아야 하나」를 다시 묻는다(SKILL 3b · readability R11h) |
441
439
  | 절 %s 표 「%s」 제목이 개수로 진행 상태를 세고 있습니다 — 제목은 내용을 말해야 합니다 | `tables[].caption` 이 「…(하나·둘·셋·넷)과 … (하나·둘·셋·넷)」으로 끝난다 | 표 제목은 내용을 말한다 — 「정해진 것 셋과 남은 것 하나」처럼 상태를 세지 않는다(SKILL 3b) |
442
440
  | 권고 낱말이 %d곳에 있습니다(%s) — 결정권이 고객에게 있는 문서는 판단 항목을 물음으로 세우고, FCG 의견은 한 절에만 둡니다 | 정규식 `권고(?![량치]) \| 기각 \| 하지 않기로`(권고량·권고치 같은 값 이름은 빼고) 가 다음 자리에 1곳 이상 — `sections[].name·short·sub·lede·marg(why/key/advice)` · `sections[].tables[].caption·cols` · `summary[].label·lead` · `open_items[].title` · `notice.text` · `meta[][1]` · `parts[].title·lede` · `stats[].label`. 이름에 「의견」이 든 절(FCG 의견 절)과 `cases[]`·`appendix`·`history`·`labels`·`colophon`·`origin` 은 제외. `%s` 는 자리 이름 최대 3개 | 판단 항목은 **물음**으로, 결과는 두 길 대칭 한눈표 한 곳에, FCG 의견은 이름에 「의견」이 든 절 **하나**에만(§12.3b · §12.5b) |
@@ -512,7 +510,18 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
512
510
  | 캡션 | `assets[].caption` — 번호 뒤에 붙는다. **번호는 적지 않는다**(자리가 매긴다). **한 문장으로** — 마침표가 둘이면 문장 분리가 캡션을 줄로 가른다(부연은 괄호·가운뎃점으로). 판정 칩은 렌더러가 문장 블록 밖에 붙인다(2026-09-09) |
513
511
  | 출처 | `assets[].source_text` — 언제·어디서 얻은 화면인가 |
514
512
  | 놓을 절 | `sections[].plates: ["그-id", ...]` — **여기 안 적으면 자리가 없어 안 실린다**(⚠) |
515
- | 전폭 | `assets[].wide: true` — 두 단 밖, 절 끝에 문서 폭(약 1030px)으로 선다. 그 절의 그림 번호는 마지막이 된다 |
513
+ | 전폭 | `assets[].wide: true` — 두 단 밖, 절 끝에 문서 폭(약 1030px)으로 선다. 그 절의 그림 번호는 마지막이 된다. ⚠ **Lim 서버에는 안 깔려 있다 — 아래 참조** |
514
+
515
+ ⚠ **`wide: true` 는 계약에만 있고 Lim 열람 렌더러에는 없다 (실측 2026-09-12, FISOFT #176).**
516
+ `--wide` 는 `1080px`, 본문 단은 `760px` 인데 **도판 7장 전부가 760px 로 섰다.**
517
+ `wide: true` 를 준 것과 안 준 것의 폭이 **같았고**, `figure` 에 전폭 클래스가 붙은 것은 **0개**였다(`plate`/`plates` 뿐).
518
+ `.chip` 인라인 표기와 같은 경우다 — 계약 문서에 있다고 깔려 있는 것이 아니다.
519
+
520
+ 그래서 **도판의 표시 글자 크기는 언제나 `760 ÷ 원본 긴 변` 으로 계산하라.** `wide` 로 구제되지 않는다.
521
+ 데스크톱 웹페이지 통짜 화면은 이 계산에서 구조적으로 14px 를 못 넘긴다
522
+ (#176 그림 7-1: 원본 1389px → 배율 0.547 → 14px 글자가 **7.7px**).
523
+ → 통짜 화면을 근거로 실어야 하면 **글자를 도판에만 맡기지 말고 부록에 전사(轉寫)하라.**
524
+ 도판은 「그 화면이 실재한다」를 맡고, 읽히는 것은 부록이 맡는다. #176 은 부록 4 가 그 자리다.
516
525
 
517
526
  ### 도판의 가독성은 열람 화면에서 재라 (2026-09-08 · 2026-09-10)
518
527
 
@@ -720,15 +729,8 @@ CSS 로는 못 한다. 우측 판독부에 남는 것은 문서 전체의 사실
720
729
 
721
730
  ## 11-6. 인라인 표기 — 굵게 · 강조 · 칩 · 막대 톤 (2026-09-08)
722
731
 
723
- > ⛔ **운영 반영 전 — 2026-09-12 기준 운영 렌더러는 2026-09-02 판(`ea6981e`)이다.** 이 절의 인라인 표기(칩·굵게·강조·
724
- > `[글자](주소)`·맨 주소 링크)와, §10 의 도판 크기 관문(R8·R9 「세로가 길어」·「늘려 그려집니다」)·§10-1 표 **전체**(시각 우선 · 글 층 R11 ·
725
- > 과정 어휘 · W1~W4)는 그 뒤 커밋이라, **운영 열람 화면에서는 원문이 글자로 찍히고 운영 dry_run 은 그 경고를 내지 않는다.**
726
- > 운영에도 있는 것은 §10 의 브리핑 셋(asks 절 · 기한 · 근거 등급 — 문구는 옛 「권고」·「열」)과 회차 서술 · 그림 바이트 경고다. 로컬 `render.py` 만 새 판을 부른다. 배포가 늦어진 까닭은 둘이다 —
727
- > 관문 1(배포원 불변식)이 같은 작업 트리의 남의 미추적 파일에 걸렸고, HEAD 를 올리면 운영 DB 마이그레이션이 함께
728
- > 적용돼 사용자 승인이 필요하다. 오르기 전에는 **쓰지 않는다**. 오른 뒤 확인법: 열람 화면 DOM 에서 `.chip` 수 > 0,
729
- > 그리고 과정 어휘 한 낱말을 넣은 원고를 서버 `dry_run` 에 태워 경고가 뜨는가. 오른 날 이 인용 블록과, 이것을 가리키는
730
- > 표시를 함께 지운다 — `grep -rn "운영 반영 전\|렌더러 배포 뒤" mcp/skills/genetics-report` 가 그 자리를 전부 낸다
731
- > (SKILL.md 0.5단계·카피 게이트 표, 이 문서 §10-1 머리·굵게 행·§12.6b, visual-first §0·§4·§5·§6·§8, readability R9, process 7단계, reader-review).
732
+ > 운영 반영: 2026-09-12 배포(커밋 `3a9fe3f`·`9d29c67`)로 인라인 표기와 §10-1 관문 전부가 운영에 올랐다 —
733
+ > 서버 `dry_run` 에 과정 어휘·권고 낱말·주석 3문장 탐침을 태워 세 경고가 모두 뜨는 것으로 확인했다.
732
734
  >
733
735
  > 실사례(2026-09-10): 이 변환 코드가 작업 트리에만 있고 **어느 커밋에도 없어** 발행하면 원문이 글자로 찍혔다
734
736
  > (열람 화면 DOM 에서 선 칩 **0**). 표기를 더하거나 고치면 커밋·배포 뒤 열람 화면 DOM 에서 `.chip`·`strong`·
@@ -943,7 +945,7 @@ Submissions·WHO/SUPPORT 정책 브리프(1:3:25)가 **같은 뼈대**를 요구
943
945
  - [ ] **한 길의 결과만** 있는가 — 「바꾸면」만 있고 「그대로 두면」이 없는 표 · 항 · 도판 (없음 — 사람의 눈)
944
946
  - [ ] 그림 출처 줄에 **원본 위치**(쪽 · 행 · 주소 · 건 §)가 없거나, 「흐림」·「확인 불가」처럼 독자가 확인을 **포기하게** 하는가
945
947
  (원본 위치는 렌더러 ⚠ §10-1 「도판 「…」 출처 줄에 원본 위치가 없습니다」 · 포기 문구는 없음 — 사람의 눈)
946
- - [ ] 고객이 **못 여는 링크**가 있는가 — 운영 렌더러가 인라인 링크를 싣기 전(§11-6 머리)에는 눌리는 외부 링크가 건의 `source_url` 뿐이다 (없음 — 사람의 눈)
948
+ - [ ] 고객이 **못 여는 링크**가 있는가 — 눌리는 외부 링크는 건의 `source_url` · 글자 안의 맨 주소 · `[글자](주소)` 셋이다(§11-6) (없음 — 사람의 눈)
947
949
  - [ ] 같은 항의 이름 · 기한이 **네 번 이상** 서는가 (없음 — 사람의 눈, readability R12f)
948
950
  - [ ] 사실 · 인용 · 우리 판단이 한 문장에 섞여 있는가(등급이 다른 것을 한 문장에 두면 강한 쪽으로 읽힌다) (없음 — 사람의 눈)
949
951
  - [ ] 「앞선 기술」이라고 적었는데 성숙도(표에 어느 단계인지)가 없는가 (없음 — 사람의 눈)
@@ -263,6 +263,8 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
263
263
 
264
264
  건별 `remaining` 을 **성격별로 묶는다.**
265
265
  건별로 다시 나열하지 않는다.
266
+ 요청 절이 없는 문서가 확인 못 한 것을 `note` 절에 모을 때도 같다 — 건의 `remaining` 을 문단마다 옮겨 적지 않고
267
+ 성격(작업자 미상 · 기록 없음 · 현장에서만 가려짐 같은)으로 묶고, 건은 번호로 가리킨다.
266
268
 
267
269
  > 이건 고객 숙제 목록이 아니라 **우리가 확정하지 못한 것을 밝히는** 절이다.
268
270
  > 주어를 우리로 써라.
@@ -316,11 +318,12 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
316
318
  셋 중 채운 것만 그려진다.
317
319
  하나도 없으면 `<aside class="marg">` 자체가 안 나오고 본문이 한 단이 된다.
318
320
 
319
- ### 나쁜 `marg` 의 모양 넷
321
+ ### 나쁜 `marg` 의 모양 다섯
320
322
 
321
323
  | 모양 | 왜 나쁜가 |
322
324
  |---|---|
323
325
  | 본문 첫 문장을 그대로 옮긴 `key` | 같은 것이 두 번 있게 된다. 여백 단은 요약란이 아니라 **다른 층위**다 |
326
+ | 다른 절 `key` 와 같은 명제인 `key` | 두 절에서 가져갈 것이 같다는 뜻이다. 한쪽이 자기 kind 의 몫(아래 표)을 안 적었거나 두 절이 사실은 한 절이다. 낱말이 달라도 명제가 같으면 같은 key 다 — 글자 비교로는 안 잡힌다(2026-09-12 실측: 같은 명제 두 key 의 글자 2-gram 유사도 0.09) |
324
327
  | 「이 절에서는 …을 설명합니다」류의 `why` | 독자의 질문이 아니라 저자의 목차다. 독자는 그 질문을 던지지 않는다 |
325
328
  | 어느 절에나 붙는 `advice` (「자세한 내용은 본문 참조」) | 가리키는 곳이 없으면 안 쓰느니만 못하다. **실제 절 번호**를 대라 |
326
329
  | 채우려고 넣은 세 줄 | 세 칸을 다 채우는 것이 목표가 아니다. 할 말이 둘이면 둘만 쓴다 |
@@ -372,6 +375,15 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
372
375
  | 2 경과 | 읽고 확인할 것 | `summary` · `status` · `cases` |
373
376
  | 3 남은 것 | 알아 둘 것 | `note` |
374
377
 
378
+ **`summary` · `status` · `cases` 는 한 묶음이다.** 셋은 같은 건을 요지 · 대조표 · 건 카드 세 배율로 보이는 것이라
379
+ 서로 다른 PART 에 두지 않는다. 한 사건의 시간(경과 표)과 까닭(건)을 「경과 / 원인」 두 PART 로 가르면
380
+ 앞 PART 의 요지·대조표·여백 주석이 뒤 PART 간지의 물음에 먼저 답하고, 같은 사실이 PART 이름만 바꿔 두 번 선다.
381
+ 요청(`asks`)이 없는 문서(회신 없는 정황 공유)는 1 요청만 빼고 「경과 / 남은 것」으로 가른다 — 원인은 경과 PART 안의 건이 맡는다.
382
+ 예외는 결정한다 모드 하나다 — 요지를 결정 PART 에, 건을 근거 PART 에 두는 것은 `asks` 가 곁에 있어 요지가 결정의 배경이 될 때뿐이다.
383
+ 집행자: 독자 검토 「구조」 관점(`references/reader-review.workflow.js`) · 사람의 눈(7단계 표 「PART 간지」 행) — 렌더러 ⚠ 없음.
384
+ 실사례(2026-09-12, 회신 없는 정황 보고): 「경과 / 원인」 두 PART 에서 PART I 사실 27개 중 21개가 PART II 에 다시 섰고
385
+ dry_run 경고는 0 이었다 — 반려 원문 「경과와 원인이 사실상 같은 맥락이고 문장만 다르게 표현했음」.
386
+
375
387
  절이 하나뿐인 묶음도 간지를 둔다 — 간지는 장식이 아니라 층위 표시다.
376
388
 
377
389
  ### `kind` 는 다섯뿐이다
@@ -501,7 +513,7 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
501
513
  | 차례 | 항목 수가 절 수와 맞는가. 죽은 링크가 없는가 |
502
514
  | 이 문서 읽는 법 | 질문형이 `~합니까?` 로 새지 않았는가 |
503
515
  | 요약면 | `notice.state` 가 실제 상태와 맞는가. 상태 막대 라벨이 무엇을 세는지 `bar_note` 가 밝히는가 |
504
- | PART 간지 | 인쇄에서 각각 1쪽 단독으로 서는가 |
516
+ | PART 간지 | 인쇄에서 각각 1쪽 단독으로 서는가. **간지 물음(`questions`)만 이어 읽어 PART 마다 다른 물음인가**, 앞 PART 의 요지 lead·여백 주석 key·표 행이 뒤 PART 물음에 먼저 답하지 않는가. 같은 시각·금액·건 제목이 두 PART 에 서면 한쪽은 건 번호로 가리키기만 한다(readability R12g) — 기계는 세지 않는다 |
505
517
  | 절 첫 화면 | 두 단으로 서는가. 한 단이면 `marg` 가 빈 것이다 |
506
518
  | 여백 주석 | 본문 첫 문장의 복사본이 아닌가. 셋을 채우려고 넣은 줄이 없는가 |
507
519
  | 건 필드표 | 증상→원인→조치→확인 방법 순서가 전 건 같은가 |
@@ -520,7 +532,7 @@ RANK_TAG = ['live', 'data', 'done', 'soon'] # 배지 종류
520
532
  | 판권지 | 문서가 여기서 닫히는가. 꼬리 카드가 붙어 있으면 구버전이다 |
521
533
  | 인쇄 미리보기 | 절마다 새 쪽에서 시작하는가. 표가 쪽 경계에서 잘리지 않는가 |
522
534
 
523
- 까닭(권고 낱말 · 원본 위치 · 도안 · 진행 상태 어휘 네 행): 실사례(2026-09-11, 고객사 결정 브리핑) 반려에서 왔다 — 그날엔 넷 다 기계 집행자가 없었다. 지금 기계는 낱말(과정 어휘 · W1)과 원본 위치 누락(W2)까지 잡는다(운영 dry_run 은 렌더러 배포 뒤 — lim-portal-format §11-6 머리). 틀 · 의견 자리 · 도안 유무 · 원본을 쟀는지는 여전히 이 표가 그물이다.
535
+ 까닭(권고 낱말 · 원본 위치 · 도안 · 진행 상태 어휘 네 행): 실사례(2026-09-11, 고객사 결정 브리핑) 반려에서 왔다 — 그날엔 넷 다 기계 집행자가 없었다. 지금 기계는 낱말(과정 어휘 · W1)과 원본 위치 누락(W2)까지 잡는다. 틀 · 의견 자리 · 도안 유무 · 원본을 쟀는지는 여전히 이 표가 그물이다.
524
536
 
525
537
  ---
526
538
 
@@ -29,7 +29,7 @@
29
29
  | R6 대비 | 글자/배경 ≥ **4.5:1**(24px 이상 굵은 글자 3:1). 표시 19px 미만 회색 글자는 7:1 미만이면 ⚠. **뜻을 나르는 도형**(띠·막대·핀)/배경 ≥ **3:1**. 배경은 포털 도판 바탕 `#f5f6f7` 까지 합성 | figure.mjs | ✗ |
30
30
  | R7 색 수 | 채도 있는 색(뜻 색) ≤ **3** + 검정·회색·흰. HSL 색상 30° 버킷으로 센다. 같은 뜻엔 같은 색 | figure.mjs | ✗ |
31
31
  | R8 선명도 | 출력 픽셀 폭 ≥ 표시 폭 × **1.8**(서버 긴 변 상한 1400 에서 최대 1.84). 평면 그림(HTML 개념도·도표)은 `provider: deterministic`(무손실). 화면 캡처만 `user-provided`(손실 사다리) | figure.mjs · genetics.mjs 힌트 | ⚠ (늘려 그려지면 ✗) |
32
- | R9 세로 | 표시 높이 ≤ **702px**(0.78 × 900) — 포털은 `max-height: 78vh` 로 세로 긴 그림의 **폭을 깎는다**. 넘으면 위아래 두 장으로 | figure.mjs · gazette.py(로컬 render.py — 운영 dry_run 은 렌더러 배포 뒤) | ⚠ |
32
+ | R9 세로 | 표시 높이 ≤ **702px**(0.78 × 900) — 포털은 `max-height: 78vh` 로 세로 긴 그림의 **폭을 깎는다**. 넘으면 위아래 두 장으로 | figure.mjs · gazette.py dry_run | ⚠ |
33
33
  | R10 중복·용어 | 제목·캡션·본문이 같은 말을 세 번 하지 않음. 통계 기호(SD·σ·p)는 부록에만 — `--forbid` 정규식 | figure.mjs 옵션 · 사람 눈 | ✗(forbid) |
34
34
  | R11 본문·표 | 문단 사이 ≥ 1.5em · 한 줄 한글 14~45자 · 헤더 ≥ 본문 ×1.5 굵게 · 문단 ≤ 220자 · 산문 참조 번호 ≤ 3 | gazette.css 토큰 · gazette-report `read.mjs` · §10-1 | ⚠ |
35
35
  | R11a 문장 | **한 문장 ≤ 60자**(공백 제외). 관보는 한 문장을 한 줄로 세우므로 문장 길이가 곧 줄 길이 — 본문 단은 한 줄에 45자, 60자면 1.4줄. 판단 항목·건처럼 가장 많이 읽는 자리는 **45자 안**으로 써 한 줄에 세운다. 접히는 문장은 마침표에서 둘로 끊는다(실측: #173 4판에서 45~57자 문장 20개가 한 번씩 접혔음) | gazette.py `text_scan` | ⚠ |
@@ -40,13 +40,14 @@
40
40
  | R11f 판단 항목 | 본문 ≤ 220자 네 문장 — 첫 문장은 기한, 상세는 건으로 | gazette.py `text_scan` | ⚠ |
41
41
  | R11g 부연 | 한 문장에 괄호 부연 ≤ 1, 괄호 안 ≤ 20자. 숫자 나열은 문장이 아니라 표 | gazette.py `text_scan` | ⚠ |
42
42
  | R11h 과정 어휘 | 본문에 **진행 상태 어휘 0** — 「정해짐 · 회신 대기 · 남은 물음 · 정해진 것 · N판」, 개수로 상태를 세는 표 제목(「정해진 것 셋과 남은 것 하나」). 독자는 앞 회차·회신·판을 모른다. 정해진 것은 **사실**로 배경(요지)에, 결정 요청에는 **지금 열린 것만**, 회차는 `history` 에만. 원고는 직전 판의 차분이 아니라 독자의 0 상태에서 다시 쓴다(2026-09-11 「뒷 내용을 나는 알고 너는 알아도 독자는 모름」) | gazette.py `text_scan` | ⚠ |
43
- | R12 덩어리 층 | 글 층(R11)이 문장·칸 하나의 길이를 재면 이 층은 **덩어리**(※ 주석 · 판단 항목 · 표·흐름도 · 요청 장치 · 도판 출처 · 같은 항의 되풀이)의 **문장 수와 자릿수**를 잰다. 값은 R12a~f. 전부 ⚠ 이고 거절이 아니다(실사례 2026-09-11, 고객사 결정 브리핑 — 문장은 짧아졌는데 같은 항이 여러 자리에 되풀이돼 반려) | a·b·e gazette.py `text_scan` · c·d·f 없음 — 사람 눈 | ⚠ · — |
43
+ | R12 덩어리 층 | 글 층(R11)이 문장·칸 하나의 길이를 재면 이 층은 **덩어리**(※ 주석 · 판단 항목 · 표·흐름도 · 요청 장치 · 도판 출처 · 같은 항의 되풀이)의 **문장 수와 자릿수**를 잰다. 값은 R12a~g. 전부 ⚠ 이고 거절이 아니다(실사례 2026-09-11, 고객사 결정 브리핑 — 문장은 짧아졌는데 같은 항이 여러 자리에 되풀이돼 반려 · 2026-09-12 회신 없는 정황 보고 — 같은 건이 두 PART 에 문장만 바꿔 섬) | a·b·e gazette.py `text_scan` · c·d·f·g 없음 — 사람 눈(g 는 독자 검토 「구조」 관점도) | ⚠ · — |
44
44
  | R12a 표 주석 | `tables[].note`(※ 주석) ≤ **2문장** — 문장은 렌더러가 문장 블록을 세우는 판별(`boundary.py`)로 센다 — 날짜 마침표(「2026. 9. 30.」·「9. 8.」)는 경계가 아니다. R11a 의 문장 길이도 같은 분할이다. 셋째 문장부터 ⚠ | gazette.py `text_scan` | ⚠ |
45
45
  | R12b 판단 항목 문장 | `open_items[].body` ≤ **4문장**(R11f 의 「네 문장」과 같은 값 — 자수 220 은 R11f, 문장 수는 여기서 센다). 다섯째 문장부터 ⚠ | gazette.py `text_scan` | ⚠ |
46
46
  | R12c 표 하나 | 한 절에 **같은 행 키**(같은 항·같은 선택지를 행으로 세운) 표·흐름도 ≤ **1** | 없음 — 사람 눈 | — |
47
47
  | R12d 요청 장치 | `cta` 기본 **[]** — 결론 한 줄은 `notice.text` 의 몫(lim-portal-format §12.3b) | 없음 — 사람 눈 | — |
48
48
  | R12e 도판 출처 | 출처 줄(`assets[].source_text`)에 **원본 이름 + 쪽/행 + (주소 또는 건 §)** — 보이는 출처 줄인 `source_text` 에 쪽·행·주소·건 § 가운데 하나는 있어야 한다(R11c 의 80자 안에서). `source_url` 은 도판에 그려지지 않으므로 대신하지 못한다. 기계는 고객·제3자 자료에서 낸 도판(`provider:"user-provided"`)만 센다 — 그린 도판(`deterministic`)·서버가 그린 삽화(`higgsfield`)는 세지 않는다. 파일로 싣는 합성은 `user-provided` 라 출처 줄에 바탕 도안의 자리를 적는다 | gazette.py `text_scan`(W2) | ⚠ |
49
49
  | R12f 되풀이 | **같은 항의 이름·기한**이 문서 전체에서 서는 자리 ≤ **3** — 네 자리부터는 되풀이 | 없음 — 사람 눈 | — |
50
+ | R12g PART 사이 | 같은 사실(시각·금액·건 제목·핵심 명제)이 **두 PART** 에 서면 한쪽은 건 번호로 가리키기만 한다. 요지·대조표·건(같은 건의 세 배율)은 한 PART — 요청 없는 문서에서 「경과 / 원인」으로 가르지 않는다(process.md 「PART 를 먼저 가른다」). 두 절 `marg.key` 가 낱말만 다르고 같은 명제면 되풀이다 — 글자 비교로는 안 잡힌다. 요약면 수치·요지 lead 가 문서 전체를 줄여 말하는 것은 예외(요지 문단이 건을 하나씩 옮겨 적으면 예외 아님). 실사례 2026-09-12: PART I 사실 27개 중 21개가 PART II 에 다시 섬, dry_run 경고 0 | 없음 — 독자 검토 「구조」 관점 · 사람 눈(렌더러 ⚠ 는 제안 단계) | — |
50
51
 
51
52
  ## 2. 사람 눈이 보는 것 (기계가 못 잰다)
52
53
 
@@ -1,4 +1,4 @@
1
- // 독자 검토 워크플로 — 발행 전에 원고를 네 관점으로 읽힌다(visual-first.md §7).
1
+ // 독자 검토 워크플로 — 발행 전에 원고를 다섯 관점으로 읽힌다(visual-first.md §7).
2
2
  // 실행: Workflow 도구에 이 파일을 scriptPath 로 주고 args 를 넘긴다.
3
3
  // args = { file: "<report.json 절대경로>", reader: "<독자 한 줄: 누구이며 무엇을 결정하나>",
4
4
  // example: "<예시 한 명 한 줄: 번호·측정일·실제 값>", bitmapBudget: 3 }
@@ -6,22 +6,26 @@
6
6
  // 2026-09-08 앳플리 4차에서 이 검토가 용어 충돌(한 낱말 두 값)·수치 불일치·참조 오류·반올림 설명 오류를 잡았다.
7
7
  // 2026-09-11 실사례(고객사 결정 브리핑) 반려 둘에서 더한 것 — decider 관점에 「권고가 제목·열·색·요약에 배어 동의만 받는 틀인지」,
8
8
  // 새 관점 source(원본 대조: 링크를 실제로 열어 인용·쪽·값을 대조, 「흐리다」는 원본 해상도로 재측정). 원고에 확인 없이 「원본이 흐림」이 실렸었다.
9
+ // 2026-09-12 실사례(회신 없는 정황 보고) 반려에서 더한 것 — 새 관점 structure(PART 물음과 절 내용 대조).
10
+ // 네 관점 발견 55개가 「경과 / 원인」 두 PART 의 겹침을 0건 짚었고, 더하기 처방을 반영하자 PART 사이에 다시 서는 시각표 행이 8→12 로 늘었다.
11
+ // 그래서 합치기는 구조 발견(PART·절 합치기·빼기)을 맨 앞에 두고, 부딪히면 빼기가 이긴다.
9
12
  // 집행자: 이 워크플로의 agent 읽기(스크립트). 렌더러 ⚠ 는 없음 — 링크를 못 여는 환경이면 source 관점은 「미확인」으로 적고 사람의 눈으로 넘긴다.
13
+ // PART·절 사이 되풀이도 렌더러·서버 dry_run 이 세지 않는다 — structure 관점이 유일한 기계 쪽 읽기다(readability R12g).
10
14
  export const meta = {
11
15
  name: 'report-reader-review',
12
- description: '보고서 원고(JSON)를 처음 읽는 사람·결정권자·정보 디자이너·원본 대조 네 관점으로 읽고 막히는 곳과 시각화할 곳을 합친다',
13
- phases: [{ title: 'Read', detail: '독자 4관점 병렬' }, { title: 'Merge', detail: '중복 제거·우선순위' }],
16
+ description: '보고서 원고(JSON)를 처음 읽는 사람·결정권자·정보 디자이너·원본 대조·구조 다섯 관점으로 읽고 막히는 곳·겹치는 곳·시각화할 곳을 합친다',
17
+ phases: [{ title: 'Read', detail: '독자 5관점 병렬' }, { title: 'Merge', detail: '중복 제거·구조 발견 우선' }],
14
18
  }
15
19
  const FILE = args.file
16
20
  const READER = args.reader || '고객사 기획 담당(비개발자). 판단 요청에 회신해야 한다'
17
21
  const EXAMPLE = args.example || '(예시 한 명 미지정 — 원고에서 찾아 쓰라)'
18
22
  const BUDGET = Number(args.bitmapBudget ?? 3)
19
23
  const CTX = `[읽기 전용. 파일을 수정하지 말 것. 한글로 답할 것.]
20
- 원고: ${FILE} (Lim 포털 관보 서식 보고서의 발행 JSON. sections[] 절, cases[] 건, open_items[] 판단 요청, summary, appendix, assets 도판)
24
+ 원고: ${FILE} (Lim 포털 관보 서식 보고서의 발행 JSON. parts[] PART(title·lede·questions — 그 PART 가 답하는 물음), sections[] 절(sections[].part 로 PART 에 속함), cases[] 건, open_items[] 판단 요청, summary, appendix, assets 도판)
21
25
  독자: ${READER}
22
26
  예시 한 명: ${EXAMPLE}
23
27
  도판 예산: 새 비트맵 그림은 ${BUDGET}장까지. 표·flow(4열 흐름도)·charts(막대)는 예산 밖이라 자유롭게.
24
- 작성자 기준: 「글은 직관적이고 흐름을 이해하기 쉽게, 보충 설명은 글이 아니라 그림 같은 시각 자료로. 핵심은 굵게, 상태는 칩, 색은 뜻으로」. 단 인라인 표기(굵게·칩·[글자](주소))는 운영 렌더러에 오르기 전에는 권하지 않는다(lim-portal-format §11-6 머리) — 그 전에는 tone·표·차트로.
28
+ 작성자 기준: 「글은 직관적이고 흐름을 이해하기 쉽게, 보충 설명은 글이 아니라 그림 같은 시각 자료로. 핵심은 굵게, 상태는 칩, 색은 뜻으로」.
25
29
  표기: **굵게** · ==강조== · {{ok|칩}}(톤 ok·wait·bad·info·muted) · 차트 rows[].tone.`
26
30
  const FIND = { type: 'object', properties: { findings: { type: 'array', items: { type: 'object', properties: {
27
31
  where: { type: 'string', description: '절·건·항목 번호와 자리' },
@@ -31,14 +35,15 @@ const FIND = { type: 'object', properties: { findings: { type: 'array', items: {
31
35
  visual: { type: 'string', description: '그림/표/flow/chart 로 바꾼다면 — 종류(bitmap|flow|table|chart|none) + 축·칸·숫자·라벨. 예시 한 명의 숫자로' } },
32
36
  required: ['where', 'problem', 'severity', 'rewrite', 'visual'] } }, overall: { type: 'string' } }, required: ['findings', 'overall'] }
33
37
  const LENSES = [
34
- ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳.'],
38
+ ['first-time', '처음 읽는 사람 — 배경을 모른다. 정의 없이 나오는 용어, 앞 절을 안 읽으면 못 따라가는 곳, 한 낱말이 두 값을 가리키는 곳, 문단이 길어 요지가 묻히는 곳, 산문 속 참조 번호 때문에 앞뒤로 오가야 하는 곳. 앞 절을 몰라 막히는 곳의 처방은 같은 사실을 또 적는 것이 아니라 그 자리(건 번호·표 이름)를 가리키는 것이다.'],
35
39
  ['decider', '결정권자 — 회신해야 한다. 판단 요청 각 항에서 「무엇을 고르는지(물음) / 두 길이 각각 무엇을 바꾸는지(그대로 두면 · 바꾸면) / 기한」이 3초 안에 읽히는지. 안 읽히는 항의 재구성안과, 항들을 한눈에 묶는 표·관문 흐름도. 수치·링크·건수가 절마다 어긋나는 곳(신뢰를 깨는 것). 권고가 제목·표 열·색(칩·톤)·요약면에 배어 독자가 동의만 하게 되는 틀(답정너)인지 — 배인 자리마다 짚고, 물음과 두 길 대칭으로 되돌린 재구성안.'],
36
40
  ['visual', '정보 디자이너 — 글로 설명하는데 그림이면 한 번에 보일 곳. 원리·갈림·타임라인·분포·자리별 상태. 각 그림의 축·칸·숫자·라벨을 구체로. 표의 숫자를 문장으로 되풀이하는 문단, 굵게·칩·톤을 둘 자리.'],
37
41
  ['source', '원본 대조 — 원고가 가리키는 원본을 실제로 연다: 건 cases[].source_url, 도판 출처 줄 assets[].source_text 안의 주소·쪽, 부록 출처 목록과 본문 글자 안의 맨 주소·[글자](주소). assets[].origin 은 렌더되지 않는 제공자 기록이라 대조 대상이 아니다. 인용 문구·쪽(행)·값을 원문과 대조한다. 어긋나는 인용, 원문에 없는 문장, 쪽·절 번호 오기, 고객이 눌러도 안 열리는 링크를 발견으로. 「흐리다」「못 연다」는 주장은 원본 해상도로 재측정해(PDF 는 pdfimages -list 전체, 웹은 실제로 열어) 잰 값을 problem 에 적고, 못 열었으면 「미확인」이라 적는다 — 짐작으로 판정하지 않는다. rewrite 에는 원문 문구 그대로 + 원본 이름·쪽(행)·주소로 고친 출처 줄.'],
42
+ ['structure', '구조 검토자 — parts[] 의 title·lede·questions 와 그 PART 에 속한 절(sections[].part)을 맞대어 읽는다. 짚을 곳: 앞 PART 의 요지 lead·marg.key·표 행이 뒤 PART 의 물음에 먼저 답하는 곳 / 같은 사실(시각·금액·건 제목·핵심 명제)이 두 PART 이상에 서는 곳 — 선 자리를 전부 세어 적는다 / 같은 건의 세 배율(summary·status·cases)이 asks 없이 서로 다른 PART 로 갈린 곳 / 두 절의 marg.key 가 낱말만 다르고 같은 명제인 곳 / 어느 PART 도 선언하지 않은 물음에 답하는 절 / 결정권자 독자인데 status 대조표가 건 제목을 한 번 더 세우는 곳. 요약면 수치·요지 lead 가 문서 전체를 줄여 말하는 것은 설계라 결함이 아니다 — 단 요지 문단이 건을 하나씩 옮겨 적으면 결함. rewrite 에는 합칠 절·지울 문장·대신 가리킬 건 번호를 적는다 — 더하기가 아니라 빼기로.'],
38
43
  ]
39
44
  phase('Read')
40
45
  const reads = (await parallel(LENSES.map(([k, inst]) => () => agent(`${CTX}\n[관점: ${k}] ${inst}\n원고 전체를 읽고 findings 를 8~14개, severity 로 정직하게 매겨라. overall 에 이 원고의 흐름이 어디서 끊기는지 3문장.`, { label: `read:${k}`, phase: 'Read', schema: FIND })))).filter(Boolean)
41
46
  log(`독자 관점 ${reads.length}/${LENSES.length} · 발견 ${reads.reduce((n, r) => n + r.findings.length, 0)}`)
42
47
  phase('Merge')
43
- const merged = await agent(`${CTX}\n[합치기] 아래 네 관점의 발견을 같은 자리끼리 합치고(중복 제거), severity 합산으로 정렬해 상위 10개만 남겨라. 각 항목에 가장 구체적인 rewrite 와 visual 을 고르되, 비트맵은 ${BUDGET}장까지만 'bitmap' 으로 두고 나머지는 flow/table/chart 로 바꿔 적어라. overall 에는 「작성자가 지금 바로 할 일 순서」를 5줄로 — 용어 잠금과 수치·참조·원본 대조 오류가 그림보다 먼저다.\n${JSON.stringify(reads)}`, { label: 'merge', phase: 'Merge', schema: FIND })
48
+ const merged = await agent(`${CTX}\n[합치기] 아래 다섯 관점의 발견을 같은 자리끼리 합치고(중복 제거), severity 합산으로 정렬해 상위 10개만 남겨라. 각 항목에 가장 구체적인 rewrite 와 visual 을 고르되, 비트맵은 ${BUDGET}장까지만 'bitmap' 으로 두고 나머지는 flow/table/chart 로 바꿔 적어라. 다른 관점의 더하기 처방(표·문장·행 추가)이 structure 관점의 빼기 처방과 부딪히면 빼기가 이긴다 — 같은 사실을 다른 PART·절에 또 세우는 처방은 버린다. overall 에는 「작성자가 지금 바로 할 일 순서」를 5줄로 — 구조(PART·절 합치기·빼기) 발견이 맨 먼저, 그다음 용어 잠금과 수치·참조·원본 대조, 그림은 마지막이다.\n${JSON.stringify(reads)}`, { label: 'merge', phase: 'Merge', schema: FIND })
44
49
  return { merged, raw: reads }
@@ -13,7 +13,7 @@
13
13
 
14
14
  **독자가 글을 읽어 머릿속에 그림을 그리게 하지 않는다. 그림을 먼저 주고 글은 그 옆에 3줄.**
15
15
  에이전트는 「글이면 충분하다」고 판단하는 쪽으로 기운다 — 글이 자기 산출물이기 때문이다. 그래서
16
- 판단을 에이전트에게 두지 않고 렌더러가 센다(`lim-portal-format.md` §10-1 — ⛔ 운영 dry_run 은 렌더러 배포 뒤, 지금은 로컬 `render.py`). 여기는 그 앞, 원고를
16
+ 판단을 에이전트에게 두지 않고 렌더러가 센다(`lim-portal-format.md` §10-1). 여기는 그 앞, 원고를
17
17
  쓰기 전에 정할 것의 순서다.
18
18
 
19
19
  ## 1. 한 장 이야기 — 요약면 다섯 칸의 순서
@@ -31,6 +31,8 @@
31
31
  실존 이용자 하나(번호·측정일·실제 값)를 골라 그림·표·흐름도·건·판단 항목이 **같은 숫자**를 되풀이하게
32
32
  한다. 독자는 숫자 하나를 기억하고 그 숫자가 어디로 흘러가는지 따라간다. 앳플리 4차의 1388741(여 159cm ·
33
33
  9월 4일 63.3kg · 체지방 14.4 · 기준 체중 57.3 · 적정 체중 62.0 · 조절 −1.3)이 그 예다.
34
+ 되풀이는 **자리마다 다른 물음**에 답할 때만이다 — 같은 물음(무엇이 빠졌나·얼마였나)에 그림·요지·표·건이 같은 숫자로 답하면
35
+ 되풀이다(readability R12f·R12g). 셈은 한 자리(표 또는 요약면 수치)에 두고 다른 자리는 그곳을 가리킨다.
34
36
  이름은 가린다(캡처의 닉네임 칸은 `shot.mjs --mask`).
35
37
 
36
38
  ## 3. 용어 잠금 표 — 같은 낱말이 두 값을 가리키면 거기서 죽는다
@@ -45,10 +47,10 @@
45
47
  |---|---|---|
46
48
  | 개념·원리(왜 갈리나) | 개념도(비트맵) | HTML 한 장을 **표시 폭 그대로(760px, 전폭 1030px)** 쓰고 `assets/figure.mjs` 로 굽는다(배율 auto → 1400px 출력). 라벨 17px 이상·값 23px 굵게·행간 1.5·뜻 색 3·그림 안 문장 0 — 규칙표는 `references/readability.md` |
47
49
  | 실제 화면의 확인 자리 | 번호 표식 캡처 | `assets/shot.mjs` — 페이지 안에 ①②③ 배지를 얹은 채 요소를 찍는다. 이름은 `--mask` |
48
- | 여러 항목을 축으로 견주기 | 표(`tables`) | 칸은 짧게(20자·5열 이상 12자), 수는 `align:"r"`. 상태 칩 `{{ok|보통}}`(문법 lim-portal-format §11-6 — ⛔ 운영 반영 전에는 쓰지 않는다, 같은 절 머리) |
50
+ | 여러 항목을 축으로 견주기 | 표(`tables`) | 칸은 짧게(20자·5열 이상 12자), 수는 `align:"r"`. 상태 칩 `{{ok|보통}}`(문법 lim-portal-format §11-6) |
49
51
  | 「A 가 되면 B 가 되고 C 가 된다」 | 흐름도(`flow`) | 4열 고정. 판단 절에는 「정하면 어느 길로」 관문 흐름도 |
50
52
  | 분포·비중 | 막대 차트(`charts`) | 행마다 `tone` 으로 뜻 색. 두 축 비교는 `toggle` |
51
- | 사건 경과·타임라인 | 표 | 날짜 · 일어난 일 · 상태(칩은 운영 반영 뒤 — lim-portal-format §11-6 머리) |
53
+ | 사건 경과·타임라인 | 표 | 날짜 · 일어난 일 · 상태 또는 건 번호. **일어난 일만** 적고 까닭·기제·금액 판정은 건이 가진다 — 건 절에서 같은 사건을 열만 바꿔 다시 세우지 않는다 |
52
54
  | **화면이 바뀌는 선택지**(비표시·제거·추가·이동) | 「그대로 두면 · 지금」 / 「바꾸면 · 재현」 두 컷 한 쌍 | 실제 화면 캡처에 바뀔 자리를 **검정** 테두리로, 뒤 컷은 그 자리를 실제로 빼고(아래 내용이 올라온 채) 빈 자리를 **회색 점선**으로 찍는다. 두 컷의 이름표는 **같은 모양**(앞 「그대로 두면 · 지금」, 뒤 「바꾸면 · 재현」), 문구는 「예시」라 적는다. **빨강·초록은 이용자 상태를 나르는 그림에만** — 선택지 두 컷에 칠하면 색이 두 길의 옳고 그름을 먼저 판정한다(실사례 2026-09-11, 고객사 결정 브리핑 「답정너 틀」 반려). 여러 항이 같은 자리를 가리키면 라벨을 「세 항 공통」으로 — 항 번호를 패널에 나눠 붙이면 독자는 항마다 자리가 다르다고 읽는다(2026-09-08 「비표시하면 어떤 게 비표시된다는 건지 UI 도 없다」 반려, 독자 시험 3/3 오독). 집행자: 없음 — 사람의 눈(readability.md §2) |
53
55
 
54
56
  글은 장치 옆 **3줄(220자) 안**. 표의 숫자를 문장으로 되풀이하지 않는다 — 검토가 잡은 둘째 결함이었다
@@ -100,20 +102,23 @@ genetics(lim) 사례 「결정 요청에 화면 변화가 따르면 고객 디
100
102
 
101
103
  ## 6. 표기 — 색은 뜻으로
102
104
 
103
- 차트 `rows[].tone` · 절·PART `tone` · `notice.state` · 건 `source_url`. 인라인 표기(`**굵게**` · `==강조==` · `{{톤|글자}}` · `[글자](주소)`, 문법 lim-portal-format §11-6)는 ⛔ 운영 반영 전에는 쓰지 않는다(같은 절 머리).
105
+ 차트 `rows[].tone` · 절·PART `tone` · `notice.state` · 건 `source_url` · 인라인 표기(`**굵게**` · `==강조==` · `{{톤|글자}}` · `[글자](주소)`, 문법 lim-portal-format §11-6).
104
106
  톤 다섯: ok(끝났거나 정상) · wait(사람 판단 대기) · bad(이용자에게 나쁜 상태·오류) · info(참고) · muted(범위 밖·처리 완료).
105
107
  문단 절반이 굵으면 굵기가 위계를 못 나른다 — 절마다 한둘. 문법과 렌더는 `lim-portal-format.md` §11-6.
106
108
 
107
- ## 7. 독자 검토 — 발행 전에 네 관점으로 읽힌다
109
+ ## 7. 독자 검토 — 발행 전에 다섯 관점으로 읽힌다
108
110
 
109
111
  `references/reader-review.workflow.js` 를 Workflow 도구로 돌린다(`args: {file: <report.json 절대경로>, reader: "<독자 한 줄>", example: "<예시 한 명 한 줄>"}`).
110
- 처음 읽는 사람 · 결정권자 · 정보 디자이너 · 원본 대조 네 에이전트가 원고를 읽고 「어디서 막히나 / 글로 고치면 / 그림으로 바꾸면」을
112
+ 처음 읽는 사람 · 결정권자 · 정보 디자이너 · 원본 대조 · 구조 다섯 에이전트가 원고를 읽고 「어디서 막히나 / 글로 고치면 / 그림으로 바꾸면」을
111
113
  severity 로 내고, 합치기 에이전트가 상위 10개로 줄인다. 앳플리 4차에서 이 검토가 용어 충돌·수치 불일치·
112
114
  참조 오류(6.6 링크의 이용자 번호)·반올림 설명 오류를 잡았다 — 작성자는 못 본 것들이다.
115
+ 구조 관점은 2026-09-12 에 더했다 — 네 관점 발견 55개가 「경과 / 원인」 두 PART 의 겹침을 한 건도 짚지 못했고,
116
+ 더하기 처방(표·문장 추가)을 반영하자 겹침이 오히려 커졌다. 그래서 합치기는 구조 발견(PART·절 합치기·빼기)을 맨 앞에 두고,
117
+ 구조 발견과 부딪히는 더하기 처방은 버린다.
113
118
 
114
119
  ## 8. 실물 확인 — dry_run 경고 0 이 목표, 그 다음은 눈
115
120
 
116
- 1. 로컬 `render.py` 와 `update_report(dry_run: true)` — §10-1 경고가 0 이 될 때까지 원고를 고친다(요청 자리의 경어체 경고는 의도). ⛔ 운영 dry_run 은 렌더러 배포 뒤부터 §10-1 을 센다(lim-portal-format §11-6 머리).
121
+ 1. 로컬 `render.py` 와 `update_report(dry_run: true)` — §10-1 경고가 0 이 될 때까지 원고를 고친다(요청 자리의 경어체 경고는 의도).
117
122
  2. 발행 뒤 **열람 주소를 실제로 연다**(Cursor 인앱 브라우저 `iab` 나 헤드리스 크롬). 그림 수·이미지 로드 수·표·흐름도·차트 수를 세고
118
123
  판단 절과 그림 절을 캡처로 본다. 「구조 확인 outline」은 블록이 섰는지만 말하지 보이는지는 말하지 않는다.
119
124
  3. 큰 원고(70KB 이상)는 채팅에 붙여넣지 말고 MCP 서버를 자식으로 띄워 파일 인자로 보낸다