agentic-flow-kit 1.8.1 → 1.8.7

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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-design.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +1 -0
  5. package/modules/hosts/claude/commands/wf-review.md +1 -1
  6. package/modules/hosts/codex/commands/wf-build.md +1 -1
  7. package/modules/hosts/codex/commands/wf-design.md +1 -1
  8. package/modules/hosts/codex/commands/wf-new-task.md +1 -0
  9. package/modules/hosts/codex/commands/wf-review.md +1 -1
  10. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  11. package/modules/hosts/cursor/commands/wf-design.md +1 -1
  12. package/modules/hosts/cursor/commands/wf-new-task.md +1 -0
  13. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  14. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  15. package/modules/hosts/opencode/commands/wf-design.md +1 -1
  16. package/modules/hosts/opencode/commands/wf-new-task.md +1 -0
  17. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  18. package/modules/hosts/trae/commands/wf-build.md +1 -1
  19. package/modules/hosts/trae/commands/wf-design.md +1 -1
  20. package/modules/hosts/trae/commands/wf-new-task.md +1 -0
  21. package/modules/hosts/trae/commands/wf-review.md +1 -1
  22. package/package.json +1 -1
  23. package/src/jscpd-drift.test.mjs +3 -2
  24. package/templates/AGENTS.md +1 -0
  25. package/templates/_agents/commands/build.md +1 -1
  26. package/templates/_agents/commands/design.md +1 -1
  27. package/templates/_agents/commands/new-task.md +1 -0
  28. package/templates/_agents/commands/review.md +1 -1
  29. package/templates/_agents/engine-lock.json +32 -24
  30. package/templates/_agents/hooks/commit-check.cjs +57 -30
  31. package/templates/_agents/mechanism-registry.md +8 -0
  32. package/templates/_agents/notes/engine-internals.md +17 -0
  33. package/templates/_agents/rule-budgets.txt +10 -2
  34. package/templates/_agents/scripts/agg-judgments.mjs +77 -1
  35. package/templates/_agents/scripts/agg-judgments.test.mjs +49 -1
  36. package/templates/_agents/scripts/archive-sweep.mjs +3 -1
  37. package/templates/_agents/scripts/check-convergence.mjs +8 -3
  38. package/templates/_agents/scripts/check-lane-surface.mjs +9 -2
  39. package/templates/_agents/scripts/check-lane-surface.test.mjs +17 -3
  40. package/templates/_agents/scripts/check-ledger-invariant.mjs +109 -24
  41. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +29 -1
  42. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -14
  43. package/templates/_agents/scripts/check-loop.mjs +41 -3
  44. package/templates/_agents/scripts/check-loop.test.mjs +37 -0
  45. package/templates/_agents/scripts/check-scope.test.mjs +4 -1
  46. package/templates/_agents/scripts/check-scripts-syntax.mjs +33 -10
  47. package/templates/_agents/scripts/closeout-l1.mjs +217 -0
  48. package/templates/_agents/scripts/closeout-l1.test.mjs +105 -0
  49. package/templates/_agents/scripts/commit-check-trigger.test.mjs +49 -0
  50. package/templates/_agents/scripts/confirm-doc.mjs +27 -8
  51. package/templates/_agents/scripts/confirm-doc.test.mjs +68 -9
  52. package/templates/_agents/scripts/gen-workflow-metrics.mjs +158 -9
  53. package/templates/_agents/scripts/gen-workflow-metrics.test.mjs +119 -0
  54. package/templates/_agents/scripts/lane-surface-audit.test.mjs +1 -1
  55. package/templates/_agents/scripts/ledger-shards.mjs +42 -5
  56. package/templates/_agents/scripts/ledger-shards.test.mjs +17 -0
  57. package/templates/_agents/scripts/mechanism-retire-watch.mjs +183 -0
  58. package/templates/_agents/scripts/mechanism-retire-watch.test.mjs +114 -0
  59. package/templates/_agents/scripts/pre-push.test.mjs +27 -11
  60. package/templates/_agents/scripts/rule-budget.mjs +14 -1
  61. package/templates/_agents/scripts/rule-budget.test.mjs +118 -0
  62. package/templates/_agents/scripts/solidify-task.mjs +3 -1
  63. package/templates/_agents/scripts/solidify-task.test.mjs +10 -10
  64. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  65. package/templates/_agents/scripts/verify.mjs +237 -78
  66. package/templates/_agents/scripts/verify.test.mjs +44 -0
  67. package/templates/_agents/scripts-test-exempt.txt +2 -1
  68. package/templates/_agents/skills/review-method/SKILL.md +3 -0
  69. package/templates/_githooks/pre-commit +9 -4
  70. package/templates/_githooks/pre-merge-commit +4 -3
  71. package/templates/_githooks/pre-push +129 -75
  72. package/templates/workflow/README.md +2 -1
  73. package/templates/workflow/intents/_TEMPLATE.md +5 -0
  74. package/templates/workflow/specs/_TEMPLATE.md +1 -1
@@ -14,10 +14,12 @@ import path from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { loadEnums } from './workflow-enums.mjs';
16
16
  import { readLedgerRows as loadLedgerRows } from './ledger-loader.mjs';
17
+ import { analyze, buildDocList, loadLastTouch, loadLedgerTs, parseStatus, ACTIVE } from './archive-sweep.mjs';
17
18
 
18
19
  const USAGE = `用法:node .agents/scripts/gen-workflow-metrics.mjs [--month YYYY-MM] [--dry-run] [--help]
19
20
 
20
21
  事实源:workflow/{intents,specs,plans,incidents}/*.md(排除 _TEMPLATE.md)+ workflow/INDEX.md + .agents/rule-budgets.txt
22
+ + workflow/reviews.d/*.md(独立复核效果台账)+ workflow/specs/*.md 与 workflow/archive/*/specs/*.md(spec 体积观察)
21
23
  产物:workflow/metrics.md —— 每月一行(同月重跑即更新该行,不重复追加);明细打印在 stdout,不落表。
22
24
  选项:
23
25
  --month YYYY-MM 指定月份键(默认取当前月;用于补记历史)
@@ -201,12 +203,134 @@ const sortedDays = [...funnel.days].sort((a, b) => a - b);
201
203
  const medianDays = sortedDays.length
202
204
  ? (sortedDays.length % 2 ? sortedDays[(sortedDays.length - 1) / 2] : (sortedDays[sortedDays.length / 2 - 1] + sortedDays[sortedDays.length / 2]) / 2)
203
205
  : null;
204
- const funnelRow = `| ${month} | ${funnel.closed} | ${funnel.full} | ${funnel.legacy} | ${funnel.once} | ${funnel.rework} | ${medianDays === null ? '—' : medianDays.toFixed(1)} | ${funnel.ledgerRows} |`;
206
+ // 中位周期渲染:**真实的极小值不得被印成 `0.0`**。建件与确认常在同一会话内完成时,真实周期可 < 0.05 天,
207
+ // `toFixed(1)` 会把它印成 `0.0`——读者无法区分「真实的亚小时周期」与「口径失效」。
208
+ // 修法(最小):仅当四舍五入会得到 `0.0` 且值 > 0 时改写 `<0.1`;null 写 `—`;恰好为 0 仍写 `0.0`。
209
+ function fmtMedianDays(v) {
210
+ if (v == null) return '—';
211
+ const s = v.toFixed(1);
212
+ return s === '0.0' && v > 0 ? '<0.1' : s;
213
+ }
214
+ const funnelRow = `| ${month} | ${funnel.closed} | ${funnel.full} | ${funnel.legacy} | ${funnel.once} | ${funnel.rework} | ${fmtMedianDays(medianDays)} | ${funnel.ledgerRows} |`;
205
215
 
206
216
  const docs = scanDocs();
207
217
  const surf = scanSurface();
208
218
  const row = `| ${month} | ${docs.files}(活跃 ${docs.active} / 终态 ${docs.terminal}) | ${kb(docs.bytes)} | ${docs.withModule}/${docs.files} | ${kb(surf.total)}(峰值 ${surf.peak ? `${surf.peak.pct.toFixed(1)}% ${surf.peak.label}` : '—'}) | ${kb(idxBytes)}(${Number.isNaN(idxActive) ? '—' : idxActive} 行) |`;
209
219
 
220
+ // ---- 复核效果(独立复核效果台账):workflow/reviews.d/ 意见级处置分片按月聚合 ----
221
+ // 口径:一行 = 一次独立复核事件;自洽不变式 = 采纳+拒绝+误报 = 意见数。台账是「门禁制造返工 vs 拦下问题」
222
+ // 的意见级信号源,脏行比缺行更有害——字段非法 / 自洽破坏一律 throw(文案点名分片与行),不做静默跳过
223
+ // (同本脚本 fail-loud 纪律)。目录不存在 = 合法零数据(机制先于数据建账,出占位行)。
224
+ function scanReviews() {
225
+ const dir = path.join(WORKFLOW, 'reviews.d');
226
+ if (!fs.existsSync(dir)) return {};
227
+ const byMonth = {};
228
+ for (const f of fs.readdirSync(dir)) {
229
+ if (!f.endsWith('.md')) continue;
230
+ const p = path.join(dir, f);
231
+ let text;
232
+ try {
233
+ text = fs.readFileSync(p, 'utf8');
234
+ } catch (e) {
235
+ throw new Error(`复核效果台账分片不可读:${p}(${e.message})——坏分片不静默跳过`);
236
+ }
237
+ for (const line of text.split(/\r?\n/)) {
238
+ if (!/^\|\s*\d{4}-\d{2}-\d{2}\s*\|/.test(line)) continue; // 表头/分隔线/注记行跳过
239
+ const cells = line.split('|').slice(1, -1).map((c) => c.trim());
240
+ const bad = cells.length < 8 || [3, 4, 5, 6].some((i) => !/^\d+$/.test(cells[i]));
241
+ if (bad) throw new Error(`复核效果台账行字段非法(8 列起,意见数/采纳/拒绝/误报须为非负整数):${p}\n 行:${line.trim()}`);
242
+ const [total, adopted, rejected, misreport] = [3, 4, 5, 6].map((i) => Number(cells[i]));
243
+ if (adopted + rejected + misreport !== total) {
244
+ throw new Error(`复核效果台账自洽破坏(采纳 ${adopted} + 拒绝 ${rejected} + 误报 ${misreport} = ${adopted + rejected + misreport} ≠ 意见数 ${total}):${p}\n 行:${line.trim()}`);
245
+ }
246
+ const b = (byMonth[cells[0].slice(0, 7)] ||= { events: 0, total: 0, adopted: 0, rejected: 0, misreport: 0 });
247
+ b.events++;
248
+ b.total += total;
249
+ b.adopted += adopted;
250
+ b.rejected += rejected;
251
+ b.misreport += misreport;
252
+ }
253
+ }
254
+ return byMonth;
255
+ }
256
+ const reviews = scanReviews();
257
+ // 复核效果表头(第三表)。源 = workflow/reviews.d/ 意见级处置分片(一行 = 一次独立复核事件,
258
+ // 采纳+拒绝+误报 = 意见数),整表确定性再生、历史行不落盘保留;区分「门禁制造返工 vs 拦下问题」。
259
+ // 声明须在 DRY 出口之前(--dry-run 也打印该表)。
260
+ const REVIEW_HEAD = [
261
+ '## 复核效果(源:workflow/reviews.d/ 意见级处置台账;存量未建账期无数据)',
262
+ '',
263
+ '| 月份 | 复核事件 | 意见数 | 采纳 | 拒绝 | 误报 | 采纳率 | 误报率 |',
264
+ '|---|---|---|---|---|---|---|---|',
265
+ ];
266
+ // 整表由分片确定性再生(历史行不落盘保留,分片即真源);率列 = 采纳(误报)÷ 意见数,意见数为 0 写 —。
267
+ const reviewRows = Object.keys(reviews).sort().map((ym) => {
268
+ const b = reviews[ym];
269
+ const rate = (n) => (b.total ? `${((n / b.total) * 100).toFixed(0)}%` : '—');
270
+ return `| ${ym} | ${b.events} | ${b.total} | ${b.adopted} | ${b.rejected} | ${b.misreport} | ${rate(b.adopted)} | ${rate(b.misreport)} |`;
271
+ });
272
+ if (!reviewRows.length) reviewRows.push('| (无记录) | 0 | 0 | 0 | 0 | — | — |'); // 零数据占位——表始终输出,机制可见
273
+
274
+ // ---- spec 体积观察(spec 生命周期口径):活跃 specs + 归档 specs 分开统计,先观察后设限 ----
275
+ // advisory 上限显式延后(先让体积可见):本表只出数、不判限。
276
+ // 口径:份数 = .md 数(排除 _TEMPLATE.md,与 scanDocs 一致);行数 = 换行数(尾换行不生幻影行,对齐 wc -l);
277
+ // 验收条数 = `- [ ]`/`- [x]` checkbox 行总数(不区分勾选);中位 = 升序取中,偶数取中间两数均值(可为 x.5);
278
+ // 归档 = workflow/archive/<月>/specs/ 各月合并。目录不存在 = 合法(该行出全 0,不报错)。
279
+ const medianOf = (arr) => {
280
+ if (!arr.length) return 0;
281
+ const s = [...arr].sort((x, y) => x - y);
282
+ return s.length % 2 ? s[(s.length - 1) / 2] : (s[s.length / 2 - 1] + s[s.length / 2]) / 2;
283
+ };
284
+ function collectSpecMetrics(dir) {
285
+ if (!fs.existsSync(dir)) return null;
286
+ const lines = [];
287
+ const checks = [];
288
+ for (const f of fs.readdirSync(dir)) {
289
+ if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
290
+ const text = fs.readFileSync(path.join(dir, f), 'utf8');
291
+ const ls = text.split(/\r?\n/);
292
+ if (ls[ls.length - 1] === '') ls.pop();
293
+ lines.push(ls.length);
294
+ checks.push((text.match(/^\s*- \[( |x)\]/gm) || []).length);
295
+ }
296
+ return { lines, checks };
297
+ }
298
+ const specStats = (m) => (m ? {
299
+ n: m.lines.length,
300
+ medLines: medianOf(m.lines),
301
+ maxLines: m.lines.length ? Math.max(...m.lines) : 0,
302
+ medChecks: medianOf(m.checks),
303
+ maxChecks: m.checks.length ? Math.max(...m.checks) : 0,
304
+ } : null);
305
+ const specActiveStats = specStats(collectSpecMetrics(path.join(WORKFLOW, 'specs')));
306
+ const specArchiveMetrics = { lines: [], checks: [] };
307
+ const archiveRoot = path.join(WORKFLOW, 'archive');
308
+ if (fs.existsSync(archiveRoot)) {
309
+ for (const ent of fs.readdirSync(archiveRoot, { withFileTypes: true })) {
310
+ if (!ent.isDirectory()) continue;
311
+ const sub = collectSpecMetrics(path.join(archiveRoot, ent.name, 'specs'));
312
+ if (sub) {
313
+ specArchiveMetrics.lines.push(...sub.lines);
314
+ specArchiveMetrics.checks.push(...sub.checks);
315
+ }
316
+ }
317
+ }
318
+ const specArchiveStats = specStats(specArchiveMetrics);
319
+ const fmtSpecNum = (v) => (Number.isInteger(v) ? String(v) : v.toFixed(1));
320
+ const specRow = (label, st) => st
321
+ ? `| ${label} | ${st.n} | ${fmtSpecNum(st.medLines)} | ${st.maxLines} | ${fmtSpecNum(st.medChecks)} | ${st.maxChecks} |`
322
+ : `| ${label} | 0 | 0 | 0 | 0 | 0 |`;
323
+ // spec 体积表头(第四表。先观察后设限:advisory 上限延后、不设门——口径单源 = workflow/README.md
324
+ // 「spec 生命周期口径」条目)。整表每次确定性再生。
325
+ const SPEC_HEAD = [
326
+ '## spec 体积观察(源:specs 活跃+归档全量扫描;advisory 上限延后——口径见 workflow/README.md「spec 生命周期口径」)',
327
+ '',
328
+ '| 范围 | 份数 | 行数中位 | 行数最大 | 验收条数中位 | 验收条数最大 |',
329
+ '|---|---|---|---|---|---|',
330
+ ];
331
+ const specRows = [specRow('活跃', specActiveStats), specRow('归档', specArchiveStats)];
332
+
333
+
210
334
  // ---- 明细(stdout,不落表):类型分布 + 逐条预算占用 ----
211
335
  console.log(`📅 ${month} 快照`);
212
336
  console.log(` 文档:${docs.files} 篇 / ${kb(docs.bytes)}(活跃 ${docs.active} / 终态 ${docs.terminal}${docs.invalid ? ` / 状态缺失或非法 ${docs.invalid}` : ''});模块已填 ${docs.withModule}/${docs.files}`);
@@ -214,12 +338,33 @@ for (const [t, v] of Object.entries(docs.perType)) console.log(` ${t.padEnd(1
214
338
  console.log(` 索引:workflow/INDEX.md ${kb(idxBytes)}(活跃 ${Number.isNaN(idxActive) ? '—' : idxActive} 行)`);
215
339
  for (const e of surf.entries) console.log(` ${e.label.padEnd(44)} ${String(e.max).padStart(6)} / ${String(e.lim).padStart(6)} B(${e.pct.toFixed(1)}%)`);
216
340
  console.log(` 常驻面合计:${kb(surf.total)}`);
217
- console.log(` 闭环漏斗:收口 ${funnel.closed}(完整链 ${funnel.full} = 一次通过 ${funnel.once} + 返工 ${funnel.rework};协议前 ${funnel.legacy})|中位周期 ${medianDays === null ? '—' : `${medianDays.toFixed(1)} 天`}|台账行 ${funnel.ledgerRows}`);
341
+ console.log(` 闭环漏斗:收口 ${funnel.closed}(完整链 ${funnel.full} = 一次通过 ${funnel.once} + 返工 ${funnel.rework};协议前 ${funnel.legacy})|中位周期 ${fmtMedianDays(medianDays)} 天|台账行 ${funnel.ledgerRows}`);
218
342
  for (const it of funnel.items) console.log(` ${String(it.doc).padEnd(58)} ${it.kind.padEnd(7)} ${it.days} 天`);
219
343
  if (funnel.items.length) console.log(' ℹ️ 机器口径可与 workflow/delegations.md 自做表的人工自报交叉对照(差异即信号,不对账)');
344
+ // 归档积压提醒(2026-10-15 减重手段⑥ 自源仓回灌,回路别等人喊):滚动归档已随确认门自动跑
345
+ // (confirm-doc archive-on-close),历史积压走 archive-sweep 手工批次——月度 stdout 报候选数使积压可见。
346
+ // 判定单源 = archive-sweep.analyze(21 天窗,禁第二套口径);analyze/loadLastTouch 抛错(如
347
+ // 浅克隆无 git 历史)fail-open 静默——观测提醒不挡快照。
348
+ try {
349
+ const allDocs = buildDocList(process.cwd());
350
+ const arch = analyze({
351
+ docs: allDocs,
352
+ ledgerTs: loadLedgerTs(process.cwd()),
353
+ lastTouch: loadLastTouch(process.cwd()),
354
+ activeTexts: allDocs.filter((d) => ACTIVE.has(parseStatus(d.text))).map((d) => d.text),
355
+ windowDays: 21,
356
+ });
357
+ const fams = new Set(arch.candidates.map((x) => x.file));
358
+ console.log(` 归档积压:候选 ${arch.candidates.length} 篇 / ${fams.size} 族(21 天窗)${arch.candidates.length ? '——node .agents/scripts/archive-sweep.mjs 查看,--execute 执行' : ''}`);
359
+ } catch { /* fail-open */ }
220
360
  console.log(` ${DRY ? '(--dry-run,未写盘)' : ''}`);
221
361
 
222
- if (DRY) { console.log(`\n将写入行:\n${row}\n${funnelRow}`); process.exit(0); }
362
+ if (DRY) {
363
+ console.log(`\n将写入行:\n${row}\n${funnelRow}`);
364
+ console.log(`\n复核效果表(整表再生):\n${[...REVIEW_HEAD, ...reviewRows].join('\n')}`);
365
+ console.log(`\nspec 体积观察表(整表再生):\n${[...SPEC_HEAD, ...specRows].join('\n')}`);
366
+ process.exit(0);
367
+ }
223
368
 
224
369
  // ---- 组装:GENERATED 区内的月表按月份键 upsert(同月重跑覆盖,行序按月升序)----
225
370
  const HEADER = `# workflow 规模与规则面月度快照
@@ -231,9 +376,10 @@ const TABLE_HEAD = [
231
376
  '| 月份 | 文档(活跃/终态) | 文档字节 | 模块已填/总 | 常驻面字节(预算峰值) | INDEX(活跃行) |',
232
377
  '|---|---|---|---|---|---|',
233
378
  ];
234
- // 漏斗表头(第二表)
379
+ // 漏斗表头(第二表)。中位渲染契约:取不到写 —;恰好 0 写 0.0;四舍五入会得 0.0 的真实极小值写 <0.1
380
+ //(建件与确认常在同会话内完成时该列常态即亚小时级,勿读作口径失效)。
235
381
  const FUNNEL_HEAD = [
236
- '## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl)',
382
+ '## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl。中位周期 = 该文件首次进入 git 到终态确认;取不到写 —,不写 0.0;真实极小值写 <0.1)',
237
383
  '',
238
384
  '| 月份 | 收口 | 完整链 | 协议前 | 一次通过 | 返工件 | 中位周期(天) | 台账行数 |',
239
385
  '|---|---|---|---|---|---|---|---|',
@@ -243,9 +389,12 @@ let rows = [];
243
389
  let funnelRows = [];
244
390
  if (existing.includes(BEGIN) && existing.includes(END)) {
245
391
  const section = existing.slice(existing.indexOf(BEGIN), existing.indexOf(END));
246
- const marker = FUNNEL_HEAD[0];
247
- const bodyPart = section.includes(marker) ? section.slice(0, section.indexOf(marker)) : section;
248
- const funnelPart = section.includes(marker) ? section.slice(section.indexOf(marker)) : '';
392
+ // 分段标记用短语而非表头全文——表头措辞会随口径演进,回吸判据须与之解耦(旧文件的旧表头也能正确分段)
393
+ const funnelMarker = '## 闭环漏斗';
394
+ const reviewMarker = '## 复核效果'; // 复核行同为 `| YYYY-MM |` 形态,不切出会被误吸进漏斗行
395
+ const bodyPart = section.includes(funnelMarker) ? section.slice(0, section.indexOf(funnelMarker)) : section;
396
+ const restPart = section.includes(funnelMarker) ? section.slice(section.indexOf(funnelMarker)) : '';
397
+ const funnelPart = restPart.includes(reviewMarker) ? restPart.slice(0, restPart.indexOf(reviewMarker)) : restPart;
249
398
  rows = bodyPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
250
399
  funnelRows = funnelPart.split(/\r?\n/).filter((l) => /^\|\s*\d{4}-\d{2}\s*\|/.test(l));
251
400
  }
@@ -255,7 +404,7 @@ rows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
255
404
  funnelRows = funnelRows.filter((l) => !l.startsWith(`| ${month} `));
256
405
  funnelRows.push(funnelRow);
257
406
  funnelRows.sort((a, b) => a.slice(2, 9).localeCompare(b.slice(2, 9)));
258
- const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${[...FUNNEL_HEAD, ...funnelRows].join('\n')}\n\n${END}`;
407
+ const inner = `${BEGIN}\n\n${[...TABLE_HEAD, ...rows].join('\n')}\n\n${[...FUNNEL_HEAD, ...funnelRows].join('\n')}\n\n${[...REVIEW_HEAD, ...reviewRows].join('\n')}\n\n${[...SPEC_HEAD, ...specRows].join('\n')}\n\n${END}`;
259
408
  let out;
260
409
  if (existing.includes(BEGIN) && existing.includes(END)) {
261
410
  out = existing.slice(0, existing.indexOf(BEGIN)) + inner + existing.slice(existing.indexOf(END) + END.length);
@@ -216,6 +216,125 @@ const mkfixLedger = (lines) => {
216
216
  fs.rmSync(root, { recursive: true, force: true });
217
217
  }
218
218
 
219
+ {
220
+ // L8:亚小时真实周期(同会话建件+确认)→ 渲染 <0.1,不印 0.0(「取不到」与「极快」不得同形)
221
+ const root = mkfixLedger([
222
+ LROW({ ts: '2026-10-01T00:30:00.000Z' }),
223
+ LROW({ ts: '2026-10-01T01:00:00.000Z', stage: 'done', prev: 'approved' }),
224
+ ]);
225
+ const r = run(root, ['--month', '2026-10']);
226
+ const md = readMetrics(root);
227
+ check('漏斗 L8 亚小时真实周期渲染 <0.1(表行与 stdout 同步,不印 0.0)',
228
+ r.status === 0 && /\| 2026-10 \| 1 \| 1 \| 0 \| 1 \| 0 \| <0\.1 \| 2 \|/.test(md) && /中位周期 <0\.1 天/.test(r.stdout),
229
+ `${r.stdout}${md}`);
230
+ fs.rmSync(root, { recursive: true, force: true });
231
+ }
232
+
233
+ // ---- 复核效果台账:workflow/reviews.d/ 意见级处置分片 → 第三表 ----
234
+ // mkfixReviews:在 mkfix 基础上写复核效果台账分片(shards: { 分片文件名: [数据行...] },含表头)
235
+ const mkfixReviews = (shards) => {
236
+ const root = mkfix();
237
+ fs.mkdirSync(path.join(root, 'workflow', 'reviews.d'), { recursive: true });
238
+ for (const [name, rows] of Object.entries(shards)) {
239
+ fs.writeFileSync(path.join(root, 'workflow', 'reviews.d', name), [
240
+ `# 独立复核效果台账 — ${name.replace(/\.md$/, '')}`,
241
+ '',
242
+ '| 日期 | 对象 | 复核员(宿主) | 意见数 | 采纳 | 拒绝 | 误报 | 备注 |',
243
+ '|------|------|--------------|--------|------|------|------|------|',
244
+ ...rows,
245
+ '',
246
+ ].join('\n'), 'utf8');
247
+ }
248
+ return root;
249
+ };
250
+ {
251
+ // R1:同月两行聚合 + 跨月分片各一行、按月升序(率列 = 采纳/误报 ÷ 意见数,toFixed(0)%)
252
+ const root = mkfixReviews({
253
+ '2026-10.md': [
254
+ '| 2026-10-14 | workflow/specs/2026-10-14-x.md | independent-reviewer(zcode) | 5 | 3 | 1 | 1 | P1 修 1;P2 采纳 3 拒 1 误 1 |',
255
+ '| 2026-10-16 | diff 318922a9..7c8d9eb7 | independent-reviewer(新会话) | 4 | 2 | 2 | 0 | P2×4 全采纳 |',
256
+ ],
257
+ '2026-09.md': [
258
+ '| 2026-09-30 | workflow/plans/2026-09-30-y.md | independent-reviewer(zcode) | 2 | 1 | 0 | 1 | 误报=指向已被覆盖的代码 |',
259
+ ],
260
+ });
261
+ const r = run(root, ['--month', '2026-10']);
262
+ const md = readMetrics(root);
263
+ check('复核 R1:同月两行聚合(事件 2 / 意见 9 / 采纳 5 / 拒绝 3 / 误报 1 / 采纳率 56% / 误报率 11%)',
264
+ r.status === 0 && /\| 2026-10 \| 2 \| 9 \| 5 \| 3 \| 1 \| 56% \| 11% \|/.test(md), `exit=${r.status}\n${r.stdout}${r.stderr}\n${md}`);
265
+ check('复核 R1:跨月分片各一行且按月升序(9 月 1/2/1/0/0 → 50%/50% 紧邻 10 月行)',
266
+ /## 复核效果[\s\S]*?\| 2026-09 \| 1 \| 2 \| 1 \| 0 \| 1 \| 50% \| 50% \|\n\| 2026-10 \|/.test(md), md);
267
+ check('复核 R1:复核行不被误吸进漏斗表(漏斗段零 2026-09 行——分段回吸切开了同形日期行)',
268
+ (() => { const seg = md.slice(md.indexOf('## 闭环漏斗'), md.indexOf('## 复核效果')); return !/\| 2026-09 \|/.test(seg); })(), md);
269
+ fs.rmSync(root, { recursive: true, force: true });
270
+ }
271
+ {
272
+ // R2:自洽破坏(采纳+拒绝+误报 ≠ 意见数)→ fail-loud(exit 1,报文点名分片与不变式)
273
+ const root = mkfixReviews({
274
+ '2026-10.md': [
275
+ '| 2026-10-14 | workflow/specs/2026-10-14-x.md | independent-reviewer(zcode) | 5 | 2 | 1 | 1 | 2+1+1=4 ≠ 5,应被拦 |',
276
+ ],
277
+ });
278
+ const r = run(root, ['--month', '2026-10']);
279
+ check('复核 R2:自洽破坏(5 ≠ 2+1+1)→ exit 1 且报文含「自洽破坏」并点名分片 2026-10.md',
280
+ r.status === 1 && /自洽破坏/.test(r.stderr) && /2026-10\.md/.test(r.stderr), `exit=${r.status}\n${r.stderr}`);
281
+ fs.rmSync(root, { recursive: true, force: true });
282
+ }
283
+ {
284
+ // R3:目录不存在 / 分片存在但空表 → 合法零数据,占位行不崩
285
+ const rootNone = mkfix(); // 不建 reviews.d
286
+ const rNone = run(rootNone, ['--month', '2026-10']);
287
+ const mdNone = readMetrics(rootNone);
288
+ check('复核 R3:目录不存在 → exit 0 且出「(无记录)」占位行',
289
+ rNone.status === 0 && /\| (无记录) \| 0 \| 0 \| 0 \| 0 \| — \| — \|/.test(mdNone), `exit=${rNone.status}\n${rNone.stderr}\n${mdNone}`);
290
+ fs.rmSync(rootNone, { recursive: true, force: true });
291
+ const rootEmpty = mkfixReviews({ '2026-10.md': [] }); // 分片存在、表为空
292
+ const rEmpty = run(rootEmpty, ['--month', '2026-10']);
293
+ const mdEmpty = readMetrics(rootEmpty);
294
+ check('复核 R3b:分片空表 → exit 0 且出「(无记录)」占位行',
295
+ rEmpty.status === 0 && /\| (无记录) \| 0 \| 0 \| 0 \| 0 \| — \| — \|/.test(mdEmpty), `exit=${rEmpty.status}\n${rEmpty.stderr}\n${mdEmpty}`);
296
+ fs.rmSync(rootEmpty, { recursive: true, force: true });
297
+ }
298
+
299
+ // ---- spec 体积观察(spec 生命周期口径):活跃 specs + 归档 specs 分开统计 ----
300
+ // mkSpec:生成恰好 lines 行(尾换行对齐 wc -l)、其中 checks 行为 checkbox 的 spec 文本
301
+ const mkSpec = (lines, checks) => {
302
+ const rows = [`# spec 夹具 ${lines}行${checks}验收`];
303
+ for (let i = 0; i < checks; i++) rows.push(`- [${i % 2 ? 'x' : ' '}] 验收 ${i + 1}`);
304
+ while (rows.length < lines) rows.push('填充');
305
+ return rows.join('\n') + '\n';
306
+ };
307
+ {
308
+ // S1:活跃 2 份(10 行/3 验收、12 行/5 验收 → 偶数中位取均值)+ 归档 3 份跨单月目录(6/8/10 行 → 奇数中位取中)
309
+ const root = mkfix();
310
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-10-01-a1.md'), mkSpec(10, 3), 'utf8');
311
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '2026-10-02-a2.md'), mkSpec(12, 5), 'utf8');
312
+ fs.writeFileSync(path.join(root, 'workflow', 'specs', '_TEMPLATE.md'), mkSpec(999, 9), 'utf8'); // 模板不计入
313
+ fs.mkdirSync(path.join(root, 'workflow', 'archive', '2026-09', 'specs'), { recursive: true });
314
+ fs.writeFileSync(path.join(root, 'workflow', 'archive', '2026-09', 'specs', '2026-09-01-b1.md'), mkSpec(6, 2), 'utf8');
315
+ fs.writeFileSync(path.join(root, 'workflow', 'archive', '2026-09', 'specs', '2026-09-02-b2.md'), mkSpec(8, 4), 'utf8');
316
+ fs.writeFileSync(path.join(root, 'workflow', 'archive', '2026-09', 'specs', '2026-09-03-b3.md'), mkSpec(10, 1), 'utf8');
317
+ const r = run(root, ['--month', '2026-10']);
318
+ const md = readMetrics(root);
319
+ check('spec S1:活跃行(2 份 / 行数中位 11 = (10+12)/2 / 最大 12 / 验收中位 4 / 最大 5;_TEMPLATE 不计)',
320
+ r.status === 0 && /\| 活跃 \| 2 \| 11 \| 12 \| 4 \| 5 \|/.test(md), `exit=${r.status}\n${r.stderr}\n${md}`);
321
+ check('spec S1:归档行(3 份 / 行数中位 8 取中 / 最大 10 / 验收中位 2 / 最大 4)',
322
+ /\| 归档 \| 3 \| 8 \| 10 \| 2 \| 4 \|/.test(md), md);
323
+ check('spec S1:表序正确(复核效果表之后、GENERATED:END 之前)',
324
+ /## 复核效果[\s\S]*## spec 体积观察[\s\S]*<!-- GENERATED:END -->/.test(md), md);
325
+ fs.rmSync(root, { recursive: true, force: true });
326
+ }
327
+ {
328
+ // S2:specs 目录空 + 无 archive 目录 → 两行全 0 不崩(合法零数据)
329
+ const root = mkfix();
330
+ const r = run(root, ['--month', '2026-10']);
331
+ const md = readMetrics(root);
332
+ check('spec S2:specs 空目录 + 无 archive → 活跃/归档两行全 0,exit 0',
333
+ r.status === 0 && /\| 活跃 \| 0 \| 0 \| 0 \| 0 \| 0 \|/.test(md) && /\| 归档 \| 0 \| 0 \| 0 \| 0 \| 0 \|/.test(md),
334
+ `exit=${r.status}\n${r.stderr}\n${md}`);
335
+ fs.rmSync(root, { recursive: true, force: true });
336
+ }
337
+
219
338
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
220
339
  if (fail) {
221
340
  console.log(`\n${USAGE}`);
@@ -89,5 +89,5 @@ const run = (root, ...args) => {
89
89
  check('场景 4:incident(发现键)×3 → db/procs 候选', r.code === 0 && r.out.includes('db/procs'), r.out);
90
90
  }
91
91
 
92
- console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
92
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
93
93
  process.exit(fail ? 1 : 0);
@@ -226,8 +226,30 @@ export function renderShard(name, rows, eol = '\n') {
226
226
  return out.length ? `${out.join(eol)}${eol}` : '';
227
227
  }
228
228
 
229
+ // insertIntoSection(text, section, lines):把行插进已有分片的对应 ## 节(下一节标题之前)。
230
+ // 节不存在则整节追加到文末。无 section 的行不走这里(保持追加到文末 = 末表)。
231
+ function insertIntoSection(text, section, lines) {
232
+ const eol = text.includes('\r\n') ? '\r\n' : '\n';
233
+ const parts = text.split(/\r?\n/);
234
+ if (parts.length && parts[parts.length - 1] === '') parts.pop();
235
+ const start = parts.findIndex((l) => l.startsWith('## ') && l.includes(section));
236
+ if (start < 0) {
237
+ return `${parts.join(eol)}${eol}${eol}## ${section}${eol}${eol}${lines.join(eol)}${eol}`;
238
+ }
239
+ let end = parts.length;
240
+ for (let i = start + 1; i < parts.length; i++) {
241
+ if (parts[i].startsWith('## ')) { end = i; break; }
242
+ }
243
+ let at = end;
244
+ while (at > start && parts[at - 1].trim() === '') at--;
245
+ parts.splice(at, 0, ...lines);
246
+ return `${parts.join(eol)}${eol}`;
247
+ }
248
+
229
249
  // appendRows(root, name, rows, { month, section }):把数据行追加进指定月分片(缺省取行内日期月)。
230
250
  // rows 可为字符串数组或 { line, section } 数组(多表台账须带 section 才能归对表)。
251
+ // 分片已存在时:带 section 的行插入该节(不落到文末自做表);不带 section 的行仍追加到文末
252
+ // (2026-10-16 自源仓 13f67ef7 回灌——修「合并丢共有行」时新增行误落末表)。
231
253
  // 返回实际写入的分片路径列表。写失败 fail-loud(不静默吞)。
232
254
  export function appendRows(root, name, rows, { month, section = '' } = {}) {
233
255
  const written = [];
@@ -247,9 +269,21 @@ export function appendRows(root, name, rows, { month, section = '' } = {}) {
247
269
  const existingRows = new Set(existing.split(/\r?\n/).filter((l) => l.trim()));
248
270
  const fresh = rs.filter((r) => !existingRows.has(r.line));
249
271
  if (!fresh.length) { written.push(p); continue; } // 已在分片 → 幂等跳过
250
- const next = existing
251
- ? `${existing.replace(/\s*$/, '')}\n${fresh.map((r) => r.line).join('\n')}\n`
252
- : renderShard(name, fresh, '\n');
272
+ let next;
273
+ if (!existing) next = renderShard(name, fresh, '\n');
274
+ else {
275
+ const placed = fresh.filter((r) => r.section);
276
+ const tail = fresh.filter((r) => !r.section);
277
+ let text = existing;
278
+ const bySec = new Map();
279
+ for (const r of placed) {
280
+ if (!bySec.has(r.section)) bySec.set(r.section, []);
281
+ bySec.get(r.section).push(r.line);
282
+ }
283
+ for (const [sec, lines] of bySec) text = insertIntoSection(text, sec, lines);
284
+ if (tail.length) text = `${text.replace(/\s*$/, '')}\n${tail.map((r) => r.line).join('\n')}\n`;
285
+ next = text.endsWith('\n') ? text : `${text}\n`;
286
+ }
253
287
  fs.writeFileSync(p, next, 'utf8');
254
288
  written.push(p);
255
289
  }
@@ -296,11 +330,14 @@ function main() {
296
330
  if (!row) { console.error('--append 须带 --row "<数据行>"'); process.exit(2); }
297
331
  const mIdx = argv.indexOf('--month');
298
332
  const month = mIdx >= 0 ? argv[mIdx + 1] : undefined;
299
- const written = appendRows(root, name, [row], { month });
333
+ const sIdx = argv.indexOf('--section');
334
+ const section = sIdx >= 0 ? argv[sIdx + 1] : '';
335
+ if (sIdx >= 0 && !section) { console.error('--section 须带节标题(如 委派结果)'); process.exit(2); }
336
+ const written = appendRows(root, name, [row], { month, section });
300
337
  console.log(`✓ 追加 1 行 → ${written.map((p) => path.relative(root, p).split(path.sep).join('/')).join(', ')}`);
301
338
  return;
302
339
  }
303
- console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM]');
340
+ console.error('用法:node .agents/scripts/ledger-shards.mjs --check | --split <ledger> [--dry-run] | --append <ledger> --row "<行>" [--month YYYY-MM] [--section <节标题>]');
304
341
  process.exit(2);
305
342
  }
306
343
 
@@ -247,6 +247,23 @@ const wf = (root, ...p) => path.join(root, 'workflow', ...p);
247
247
  fs.rmSync(root, { recursive: true, force: true });
248
248
  }
249
249
 
250
+ // ---- S20 appendRows --section(2026-10-16 自源仓 13f67ef7 回灌):已有双表时委派行插入委派节,
251
+ // 不带 section 仍落文末(修「合并丢共有行」时新增行误落末表)----
252
+ {
253
+ const root = mkroot();
254
+ appendRows(root, 'delegations', ['| 2026-10-15 | model | 委派甲 | 一次通过 | 备注 | |'], { section: '委派结果' });
255
+ appendRows(root, 'delegations', ['| 2026-10-15 | 自做甲 | 一次通过 | 备注 | |'], { section: '自做任务结果' });
256
+ appendRows(root, 'delegations', ['| 2026-10-15 | model | 委派乙 | 一次通过 | 备注 | |'], { section: '委派结果' });
257
+ appendRows(root, 'delegations', ['| 2026-10-15 | 自做乙 | 一次通过 | 备注 | |']);
258
+ const shard = fs.readFileSync(shardPathFor(root, 'delegations', '2026-10'), 'utf8');
259
+ const iSelf = shard.indexOf('## 自做任务结果');
260
+ const iDelegRow = shard.indexOf('委派乙');
261
+ const iSelfRow = shard.indexOf('自做乙');
262
+ check('S20 后追加的委派行仍在自做节之前', iDelegRow >= 0 && iDelegRow < iSelf, shard);
263
+ check('S20b 不带 section 的行落在文末', iSelf >= 0 && iSelf < iSelfRow, shard);
264
+ fs.rmSync(root, { recursive: true, force: true });
265
+ }
266
+
250
267
  // ---- S18 appendRows 幂等:同行走两次只落一次 ----
251
268
  {
252
269
  const root = mkroot();
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ // mechanism-retire-watch.mjs — 机制退役催办自动化(2026-10-14 源仓 L5 自举接线 1b/1c;2026-10-16 回灌 kit)
3
+ // 用途:读机制台账「退役阈值/最近触发」+ judgments 聚合,阈值触达自动产动作项——让机制的「死」与「生」同样强制。
4
+ // 自动化的是**感知/排序**(谁触达了阈值),处置仍人放行(改状态/删代码走人 + 一进一出)。
5
+ // 口径单源:.agents/mechanism-registry.md 列序(机制|门禁位置|触发统计口径|退役阈值|最近触发|状态|处置期限)
6
+ // 与 .agents/judgments.jsonl(check-loop 每次真实运行落账,per-rule WARN/BLOCK 计数)。
7
+ // 判定(确定性,无 LLM):
8
+ // ① 现役行 + 阈值形态「连续 N 月 = 0」+ 口径列含「规则名」→ 以有数据的末月为终点取 N 个日历月,缺月不算零命中,全 0 即触达 → 动作项。
9
+ // ② 待退役行 + 处置期限为日期且已过 → 动作项(逾期)。
10
+ // ③ 其余阈值形态(不退役主干 / 使用率型 / 数据源型)→ 非自动可判,仅计数,交人工月判。
11
+ // 缺月不算零命中(2026-10-15 源仓修正):漏跑数月后陈旧月份不得凑成「连续」——日历窗口内有缺月即
12
+ // 交人工判,不产触达动作(与 agg-judgments --retire-candidates 的「数据月」口径互补:彼按有数据月,
13
+ // 此按日历连续)。
14
+ // 退出码恒 0(催办报告不该阻断调度器;测量失败打 [ERROR] 行)。
15
+ // 用法:node .agents/scripts/mechanism-retire-watch.mjs [--json] [--root <仓库根>] [--now <ISO 日期>]
16
+ // 测试:node .agents/scripts/mechanism-retire-watch.test.mjs(纯函数 evaluate 进程内断言)
17
+ // kit 适配(源仓→kit):台账路径 `.agents/config/mechanism-registry.md` → kit 平铺
18
+ // `.agents/mechanism-registry.md`(同 check-registry.mjs 检查 22 口径)。
19
+ import fs from 'node:fs';
20
+ import path from 'node:path';
21
+ import { fileURLToPath, pathToFileURL } from 'node:url';
22
+ import { aggFromRows } from './agg-judgments.mjs';
23
+
24
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
25
+ const argv = process.argv.slice(2);
26
+ const argOf = (k) => { const i = argv.indexOf(k); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : ''; };
27
+ const asJson = argv.includes('--json');
28
+ const ROOT = path.resolve(argOf('--root') || path.join(SCRIPT_DIR, '..', '..'));
29
+ const NOW = argOf('--now') ? new Date(argOf('--now')) : new Date();
30
+
31
+ const REGISTRY_REL = path.join('.agents', 'mechanism-registry.md');
32
+ const LEDGER_REL = path.join('.agents', 'judgments.jsonl');
33
+
34
+ // parseRegistry(text):7 列管道表 → 行对象数组。列序固定(check-loop §22 按此解析)。
35
+ export function parseRegistry(text) {
36
+ const rows = [];
37
+ for (const l of String(text).split(/\r?\n/)) {
38
+ if (!/^\|/.test(l)) continue;
39
+ if (/^\|[\s:|-]+\|?\s*$/.test(l)) continue; // 分隔行
40
+ const cells = l.split('|').slice(1, -1).map((c) => c.trim());
41
+ if (cells.length !== 7) continue;
42
+ if (cells[0] === '机制') continue; // 表头
43
+ const [name, gate, criterion, threshold, lastTrigger, status, deadline] = cells;
44
+ rows.push({ name, gate, criterion, threshold, lastTrigger, status, deadline });
45
+ }
46
+ return rows;
47
+ }
48
+
49
+ // ruleNamesOf(criterion):从「触发统计口径」列抽「」内规则名(/ 分隔)。无「」= 无法自动映射。
50
+ export function ruleNamesOf(criterion) {
51
+ const names = [];
52
+ for (const m of String(criterion).matchAll(/「([^」]+)」/g)) {
53
+ for (const n of m[1].split('/')) {
54
+ const t = n.trim();
55
+ if (t) names.push(t);
56
+ }
57
+ }
58
+ return names;
59
+ }
60
+
61
+ // zeroMonthsOf(threshold):阈值形态「连续 N 月 = 0」→ N;否则 null(非自动可判)。
62
+ export function zeroMonthsOf(threshold) {
63
+ const m = String(threshold).match(/连续\s*(\d+)\s*月\s*=\s*0/);
64
+ return m ? Number(m[1]) : null;
65
+ }
66
+
67
+ // localDay(d):本地日历日 YYYY-MM-DD。期限比较不用 UTC 零点,避免北京时 08:00 后把当天判逾期。
68
+ export function localDay(d) {
69
+ const dt = d instanceof Date ? d : new Date(d);
70
+ const m = String(dt.getMonth() + 1).padStart(2, '0');
71
+ const day = String(dt.getDate()).padStart(2, '0');
72
+ return `${dt.getFullYear()}-${m}-${day}`;
73
+ }
74
+
75
+ // calendarWindow(endYm, n):以有数据的末月为终点,向前取 n 个日历月(含终点)。
76
+ export function calendarWindow(endYm, n) {
77
+ const [y, m] = String(endYm).split('-').map(Number);
78
+ const out = [];
79
+ for (let i = n - 1; i >= 0; i--) {
80
+ const d = new Date(Date.UTC(y, m - 1 - i, 1));
81
+ out.push(`${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`);
82
+ }
83
+ return out;
84
+ }
85
+
86
+ // evaluate(registryRows, judgmentRows, now):纯函数——产动作项。测试进程内断言。
87
+ export function evaluate(registryRows, judgmentRows, now = new Date()) {
88
+ const { byMonth } = aggFromRows(judgmentRows || [], null);
89
+ const months = Object.keys(byMonth).sort(); // 升序
90
+ const actions = [];
91
+ const notAuto = [];
92
+
93
+ for (const r of registryRows) {
94
+ if (r.status === '已退役') continue;
95
+
96
+ if (r.status === '待退役') {
97
+ const d = String(r.deadline).match(/\d{4}-\d{2}-\d{2}/);
98
+ if (d && localDay(now) > d[0]) {
99
+ actions.push({ kind: 'overdue', name: r.name, detail: `待退役处置期限 ${d[0]} 已过(今 ${localDay(now)})——按台账维护纪律「退役动作」当日删代码+测试+更新本表` });
100
+ } else if (!d) {
101
+ actions.push({ kind: 'no-deadline', name: r.name, detail: `待退役但处置期限非日期(${r.deadline || '空'})——补时间表或改回现役` });
102
+ }
103
+ continue;
104
+ }
105
+
106
+ // 现役
107
+ const n = zeroMonthsOf(r.threshold);
108
+ const rules = ruleNamesOf(r.criterion);
109
+ if (n === null) { notAuto.push({ name: r.name, reason: '阈值形态非「连续 N 月 = 0」' }); continue; }
110
+ if (!rules.length) { notAuto.push({ name: r.name, reason: '口径列未列规则名(无法映射 judgments)' }); continue; }
111
+ if (months.length < n) { notAuto.push({ name: r.name, reason: `judgments 仅 ${months.length} 月 < 阈值 ${n} 月(数据不足)` }); continue; }
112
+
113
+ // 日历连续:缺月不算零命中(漏跑数月后陈旧月份不得凑成「连续」)。
114
+ const window = calendarWindow(months[months.length - 1], n);
115
+ const missing = window.filter((ym) => !byMonth[ym]);
116
+ if (missing.length) {
117
+ notAuto.push({ name: r.name, reason: `日历窗口 ${window.join(' / ')} 缺月 ${missing.join(' / ')}(断档不算零命中)` });
118
+ continue;
119
+ }
120
+ const hits = window.map((ym) => {
121
+ const cell = byMonth[ym].rules;
122
+ let c = 0;
123
+ for (const rule of rules) {
124
+ const e = cell[rule];
125
+ if (e) c += (e.WARN || 0) + (e.BLOCK || 0);
126
+ }
127
+ return { ym, count: c };
128
+ });
129
+ const total = hits.reduce((s, h) => s + h.count, 0);
130
+ if (total === 0) {
131
+ actions.push({ kind: 'threshold', name: r.name, detail: `连续 ${n} 月零触发(${window.join(' / ')};规则 ${rules.join('、')})——阈值触达,评估退役(改状态「待退役」并填处置期限)` });
132
+ }
133
+ }
134
+ return { actions, notAuto, months, monthCount: months.length };
135
+ }
136
+
137
+ function readJudgments(root) {
138
+ const p = path.join(root, LEDGER_REL);
139
+ if (!fs.existsSync(p)) return { rows: [], missing: true };
140
+ const rows = [];
141
+ for (const line of fs.readFileSync(p, 'utf8').split(/\r?\n/)) {
142
+ if (!line.trim()) continue;
143
+ try { rows.push(JSON.parse(line)); } catch { /* 坏行跳过,不中断 */ }
144
+ }
145
+ return { rows, missing: false };
146
+ }
147
+
148
+ function main() {
149
+ const regPath = path.join(ROOT, REGISTRY_REL);
150
+ if (!fs.existsSync(regPath)) {
151
+ console.error(`[ERROR] mechanism-retire-watch: 台账不存在 ${REGISTRY_REL}`);
152
+ if (asJson) console.log(JSON.stringify({ error: 'registry-missing', actions: [] }, null, 2));
153
+ process.exit(0);
154
+ }
155
+ const registryRows = parseRegistry(fs.readFileSync(regPath, 'utf8'));
156
+ const { rows: judgmentRows, missing } = readJudgments(ROOT);
157
+ const { actions, notAuto, monthCount } = evaluate(registryRows, judgmentRows, NOW);
158
+
159
+ if (asJson) {
160
+ console.log(JSON.stringify({ registryRows: registryRows.length, judgmentMonths: monthCount, judgmentsMissing: missing, actions, notAuto }, null, 2));
161
+ return;
162
+ }
163
+
164
+ console.log(`# 机制退役催办(${NOW.toISOString()})`);
165
+ console.log('');
166
+ console.log(`台账 ${REGISTRY_REL}|机制行 ${registryRows.length}|judgments 月数 ${monthCount}${missing ? '(台账缺失——尚无运行记录)' : ''}`);
167
+ console.log('');
168
+ if (!actions.length) {
169
+ console.log('✅ 无阈值触达 / 无逾期——本轮无动作项。');
170
+ } else {
171
+ console.log(`## 动作项 ${actions.length} 条`);
172
+ for (const a of actions) console.log(`- [${a.kind}] ${a.name}:${a.detail}`);
173
+ }
174
+ if (notAuto.length) {
175
+ console.log('');
176
+ console.log(`## 非自动可判(${notAuto.length} 条,交人工月判)`);
177
+ for (const x of notAuto) console.log(`- ${x.name}:${x.reason}`);
178
+ }
179
+ console.log('');
180
+ console.log('> 处置仍人放行:改状态/删代码走人 + 一进一出(台账维护纪律)。本报告不自动改文件。');
181
+ }
182
+
183
+ if (import.meta.url === pathToFileURL(process.argv[1] || '').href) main();