agentic-flow-kit 1.7.16 → 1.8.1

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 (161) hide show
  1. package/README.md +11 -7
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +9 -8
  5. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  6. package/modules/hosts/claude/commands/wf-review.md +2 -2
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +1 -1
  9. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  10. package/modules/hosts/codex/commands/wf-new-task.md +9 -8
  11. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  12. package/modules/hosts/codex/commands/wf-review.md +2 -2
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  15. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  16. package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
  17. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  18. package/modules/hosts/cursor/commands/wf-review.md +2 -2
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  21. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  22. package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
  23. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  24. package/modules/hosts/opencode/commands/wf-review.md +2 -2
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +1 -1
  27. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  28. package/modules/hosts/trae/commands/wf-new-task.md +9 -8
  29. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  30. package/modules/hosts/trae/commands/wf-review.md +2 -2
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.mjs +6 -2
  34. package/src/doctor.mjs +11 -2
  35. package/src/executor.mjs +25 -8
  36. package/src/executor.test.mjs +4 -1
  37. package/src/fresh-init.test.mjs +1 -1
  38. package/src/init.mjs +20 -3
  39. package/src/profiles.mjs +7 -3
  40. package/src/rule-budget.test.mjs +2 -2
  41. package/src/stack-profile.test.mjs +6 -0
  42. package/src/validate.mjs +6 -1
  43. package/src/validate.test.mjs +1 -1
  44. package/templates/.gitattributes +4 -1
  45. package/templates/AGENTS.md +2 -2
  46. package/templates/_agents/commands/build.md +1 -1
  47. package/templates/_agents/commands/maintain.md +1 -1
  48. package/templates/_agents/commands/new-task.md +9 -8
  49. package/templates/_agents/commands/plan.md +1 -1
  50. package/templates/_agents/commands/review.md +2 -2
  51. package/templates/_agents/commands/test.md +1 -1
  52. package/templates/_agents/engine-lock.json +130 -42
  53. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  54. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  55. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  56. package/templates/_agents/lane-surfaces.txt +4 -1
  57. package/templates/_agents/level-hints.txt +1 -1
  58. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  59. package/templates/_agents/mechanism-registry.md +27 -0
  60. package/templates/_agents/rule-budgets.txt +2 -2
  61. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  62. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  63. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  64. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  65. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  66. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  67. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  68. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  69. package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
  70. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  71. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  72. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  73. package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
  74. package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
  75. package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
  76. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
  77. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
  78. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  79. package/templates/_agents/scripts/check-loop.mjs +215 -34
  80. package/templates/_agents/scripts/check-loop.test.mjs +582 -49
  81. package/templates/_agents/scripts/check-prejudgment.mjs +64 -0
  82. package/templates/_agents/scripts/check-registry.mjs +45 -0
  83. package/templates/_agents/scripts/check-scope.mjs +96 -0
  84. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  85. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  86. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  87. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  88. package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
  89. package/templates/_agents/scripts/confirm-doc.mjs +114 -38
  90. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  91. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  92. package/templates/_agents/scripts/fill-intent.mjs +5 -3
  93. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  94. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  95. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  96. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  97. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  98. package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
  99. package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
  100. package/templates/_agents/scripts/gate-checklist.mjs +6 -0
  101. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  102. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  103. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  104. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  105. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  106. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
  107. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  108. package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
  109. package/templates/_agents/scripts/git-core.mjs +45 -0
  110. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  111. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  112. package/templates/_agents/scripts/kb-search.mjs +19 -4
  113. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  114. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  115. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  116. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  117. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  118. package/templates/_agents/scripts/level-criteria.mjs +9 -10
  119. package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
  120. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  121. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  122. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  123. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  124. package/templates/_agents/scripts/pipeline-run.mjs +6 -22
  125. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  126. package/templates/_agents/scripts/policy.mjs +28 -0
  127. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  128. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  129. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  130. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  131. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  132. package/templates/_agents/scripts/run-tests.mjs +99 -0
  133. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  134. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  135. package/templates/_agents/scripts/stage-gates.mjs +34 -12
  136. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  137. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  138. package/templates/_agents/scripts/verify.mjs +135 -58
  139. package/templates/_agents/scripts/verify.test.mjs +156 -47
  140. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  141. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  142. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  143. package/templates/_agents/scripts-test-exempt.txt +1 -0
  144. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  145. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  146. package/templates/_agents/skills/review-method/SKILL.md +55 -0
  147. package/templates/_agents/workflow-enums.txt +8 -1
  148. package/templates/_githooks/pre-commit +110 -90
  149. package/templates/_githooks/pre-merge-commit +27 -23
  150. package/templates/_githooks/pre-push +4 -3
  151. package/templates/_github/workflows/kit-ci.yml +4 -3
  152. package/templates/workflow/README.md +14 -7
  153. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  154. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  155. package/templates/workflow/papercuts.md +2 -0
  156. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  157. package/templates/workflow/regression-checklist.md +1 -0
  158. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  159. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  160. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  161. package/templates/_agents/scripts/rule-budget.sh +0 -74
@@ -0,0 +1,27 @@
1
+ # 机制使用量台账 (Mechanism Usage Registry)
2
+
3
+ > 门禁棘轮的出口:每个门禁机制的「触发统计口径 + 退役阈值 + 状态」单源登记于此。
4
+ > **台账的目的不是记录而是强制处置**:新增门禁机制须登记一行(一进一出,并点名它替代/合并的
5
+ > 既有机制,无替代则说明增量理由);机制退役时状态改「已退役」并在最近触发列留档;
6
+ > 低频机制按退役阈值降级。
7
+ > 列序固定:机制 | 门禁位置 | 触发统计口径 | 退役阈值 | 最近触发 | 状态 | 处置期限。
8
+ > 本表归装户自持运营(sync 永不覆盖,同 metric-claims.txt 口径);kit 引擎做 WARN 级格式校验
9
+ > (check-loop 检查 22:列数 / 状态枚举 / 待退役处置期限——advisory 不阻断,永不 hard),
10
+ > 对账纪律靠下方「维护纪律」人工执行;判定数据面由 check-loop 每次真实运行自动落账到
11
+ > `.agents/judgments.jsonl`(judgment-ledger,本地 gitignore),月度聚合经 agg-judgments.mjs 查账。
12
+
13
+ | 机制 | 门禁位置 | 触发统计口径 | 退役阈值 | 最近触发 | 状态 | 处置期限 |
14
+ |---|---|---|---|---|---|---|
15
+ | 确认指纹对账(示例行) | check-loop 检查 15 | hard「指纹不配对/确认内容漂移」计数(月,经 agg-judgments 查判定流水) | 不退役(防伪造确认主干) | 示例:建门留档见包源仓 git 历史与 CHANGELOG | 现役 | — |
16
+ | 触达面判低门(示例行) | check-lane-surface(pre-commit/CI 泳道完整性) | hard「触达面判低/产品契约面」计数(月) | 不退役(级别闭环主干) | 示例:在用 | 现役 | — |
17
+
18
+ > 以上两行为**示例行**,只示范登记口径,不参与装户对账;装户按自有机制增删行,
19
+ > 首次对账时可将示例行替换为真实运营数据。
20
+
21
+ ## 维护纪律
22
+
23
+ - **一进一出**:新增门禁机制必须同时在本表登记,并点名它替代/合并的既有机制(无替代则说明增量理由)。
24
+ - **月度对账**:每月初按「触发统计口径」列刷新计数(半自动:`node .agents/scripts/agg-judgments.mjs`
25
+ 查判定流水 `.agents/judgments.jsonl`——check-loop 每次真实运行自动落账,替代人工 grep 输出;
26
+ git log 统计口径不变);达到阈值的行改「待退役」并填处置期限。
27
+ - **退役动作**:状态改「已退役」当日删除机制代码 + 其测试 + 本表更新,同一提交完成(不留死代码)。
@@ -1,7 +1,7 @@
1
- # 常驻面体积预算(单源:check-loop.sh 检查 13(--all,WARN)与 .githooks/pre-commit(--staged,硬拦)共读)
1
+ # 常驻面体积预算(单源:check-hygiene 检查 13(--all,WARN)与 .githooks/pre-commit(--staged,硬拦)共读)
2
2
  # 格式:<路径或 glob> <上限字节>;路径以 / 结尾表示目录合计。上限 = 当前实测 + 余量。
3
3
  # 超限含义:常驻面只增不减——须先删除或下沉被取代条目再增(一进一出)。
4
- # 本文件会被 core.autocrlf 检出为 CRLF,读取方(rule-budget.sh)已显式去 \r,禁止依赖行尾格式。
4
+ # 本文件会被 core.autocrlf 检出为 CRLF,读取方(rule-budget.mjs)按 /\r?\n/ 切分天然容 \r,禁止依赖行尾格式。
5
5
  # 口径:预算比较用「索引 / HEAD 字节」(git cat-file,LF 归一);禁用工作区 wc -c 核对(CRLF 膨胀)。
6
6
  # 2026-10-06 backflow-loop-audit-remediation:6 份命令文档顶部加「装户边界」标注(build.md 实测 8386B
7
7
  # 超 8192 被拦)——单篇上限 8192→8704(与装户 Shipyard.Material 同值,512B 步进);目录合计未超不动。
@@ -2,6 +2,7 @@
2
2
  /* agg-delegations: 量化证据聚合
3
3
  * 读 workflow/delegations.md 两张结果表(按表头签名识别:委派表含「被委派方」列、自做表=日期+任务一句话;不依赖 ## 节标题)+ 扫 workflow/incidents/ 按月计数,
4
4
  * 输出各月指标(一次通过率 / 平均返工次数 / 主兜底占比 / **产出接受率**)、并发扩容门判定、可粘贴的月度快照行。
5
+ * 分片感知(2026-10-13 ledger-shard-ledgers 回灌):读侧文本 = 主台账 ∪ delegations.d/*.md,单源 = ledger-shards.mjs。
5
6
  * 结果取值:一次通过 | 返工×N | 返工×N(门禁噪声)| 主兜底 | 返工待修(未闭环,不计入率)。
6
7
  * 门禁噪声档(2026-10-08 selfmeasure-and-modularize):剥离「返工由门禁面而非设计面触发」的形态——
7
8
  * 扩容门第 3 项只对设计返工判 =0,门禁噪声返工单列观测。存量行零回填(旧四档解析逐条不变)。
@@ -95,15 +96,28 @@ function splitTables(text) {
95
96
  return tables;
96
97
  }
97
98
 
99
+ // 分片读侧文本(2026-10-13 ledger-shard-ledgers 回灌):单源 = ledger-shards.readLedgerText
100
+ // (主台账 ∪ delegations.d/*.md,月序即时间序)。.cjs 不能静态 import .mjs、require(esm) 需
101
+ // Node≥20.19(engines ≥18 不假设)——CLI 入口以 import() 装载后注入此变量再进 main;
102
+ // 库调用方(gen-workflow-dashboard)经 opts.text 显式注入合并文本。两者都缺位 → 回落主台账直读
103
+ // (与旧签名行为一致,不静默造数)。
104
+ let shardReadText = null;
98
105
  // readLedger:root 显式参数 + soft 模式(2026-10-08 workflow-dashboard 使能导出,判据单源复用——
99
106
  // 仪表盘 require 本函数取行,不重写表解析;缺省 root/无 soft 行为与原签名完全一致,main() 零变化)
100
107
  function readLedger(root = ROOT, opts = {}) {
101
108
  const ledger = path.join(root, 'workflow', 'delegations.md');
102
- if (!fs.existsSync(ledger)) {
109
+ let text;
110
+ if (typeof opts.text === 'string') {
111
+ text = opts.text; // 库调用方注入的合并文本(读侧单源 = ledger-shards.readLedgerText)
112
+ } else if (!fs.existsSync(ledger)) {
103
113
  if (opts.soft) return [];
104
114
  fail(`找不到 ${ledger}`);
115
+ } else if (shardReadText) {
116
+ try { text = shardReadText(root, 'delegations'); } // 分片感知读(fail-loud 包装:单分片读失败不静默吞)
117
+ catch (err) { if (opts.soft) return []; fail(`台账读取失败: ${ledger} (${err.message})`); }
118
+ } else {
119
+ text = fs.readFileSync(ledger, 'utf8');
105
120
  }
106
- const text = fs.readFileSync(ledger, 'utf8');
107
121
  const tables = splitTables(text);
108
122
  const delegated = parseTableRows(tables.delegated.join('\n')).map((c) => ({
109
123
  scope: '委派',
@@ -278,7 +292,11 @@ function main() {
278
292
 
279
293
  // isMain 守卫 + 纯函数导出(board-kb-p1:gateMonth / expansionVerdict 供测试 import,import 不执行 main)
280
294
  if (require.main === module) {
281
- main();
295
+ // 分片读侧装载(2026-10-13 ledger-shard-ledgers):动态 import 后进 main;装载失败 fail-loud
296
+ // (指标缺数会喂错扩容门,不静默退化主台账直读)。
297
+ import('./ledger-shards.mjs')
298
+ .then((m) => { shardReadText = m.readLedgerText; main(); })
299
+ .catch((err) => fail(`ledger-shards.mjs 装载失败: ${err.message}`));
282
300
  } else {
283
301
  module.exports = { parseResult, parseVerdict, metrics, gateMonth, expansionVerdict, fmtAccept, GATE, ACCEPT_MIN_SAMPLE, readLedger };
284
302
  }
@@ -8,7 +8,7 @@
8
8
  // 门禁数与质量指标负相关,但此前无任何数据能回答「该加门还是该拆门」(metrics/DASHBOARD 全部指标
9
9
  // 100% 指向产出物、0 条指向门禁自身)。加门是焦虑驱动的唯一理性反应,因为拆门没有依据。
10
10
  //
11
- // 耗时口径**含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 sh rule-budget.sh)——
11
+ // 耗时口径**含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 rule-budget.mjs)——
12
12
  // 门禁真实成本本就要含子进程,不做剔除;输出中显式标注防误读为「纯计算耗时」。
13
13
  // 坏行容忍跳过(与 confirmations.jsonl / delegations 台账同容错口径)。
14
14
  // 用法:node .agents/scripts/agg-gate-stats.mjs [--days 30]
@@ -84,13 +84,13 @@ function main() {
84
84
  const { runs, list, noiseTop, dead, costTotal } = aggregate(rows, Date.now(), Number.isFinite(days) ? days : null);
85
85
 
86
86
  if (!runs || !list.length) {
87
- console.log('门禁 ROI:未采集——跑 `node .agents/scripts/check-loop.mjs --gate-stats` 后重试');
87
+ console.log('门禁 ROI:未采集——跑 `node .agents/scripts/check-loop.mjs --all --gate-stats` 后重试(--all 全段成本面;2026-10-13 分道后裸跑只记硬门段)');
88
88
  console.log('(数据落 .agents/cache/gate-stats.jsonl,该目录已 gitignore;首次采集后本表才有内容)');
89
89
  return;
90
90
  }
91
91
 
92
92
  console.log(`# 门禁 ROI 聚合(窗口 ${days} 天 / 有效样本 ${runs} 次运行)\n`);
93
- console.log('耗时口径**含子进程时间**(检查 11 调生成器 --check、检查 13 调 sh rule-budget.sh)——门禁真实成本,不做剔除。\n');
93
+ console.log('耗时口径**含子进程时间**(检查 11 调生成器 --check、检查 13 调 rule-budget.mjs)——门禁真实成本,不做剔除。\n');
94
94
  console.log('| 检查 | 名称 | 命中/运行 | 拦截 | 警告 | 累计耗时 | 均耗时 | 每次命中警告数 |');
95
95
  console.log('|------|------|-----------|------|------|----------|--------|----------------|');
96
96
  for (const a of list) {
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ // agg-judgments.mjs — check-loop 判定流水月度聚合(2026-10-11 judgment-ledger)
3
+ // 用法:node .agents/scripts/agg-judgments.mjs [--month=YYYY-MM]
4
+ // 源:.agents/judgments.jsonl(check-loop.mjs 每次真实运行追加一行,本地 gitignore;
5
+ // 缺文件 = 尚无运行记录,提示后 exit 0——与 delegations 台账「必在」不同,这是渐积数据)。
6
+ // 消费:mechanism-registry 月度对账(.agents/mechanism-registry.md「触发统计口径」列的 WARN/BLOCK 计数)
7
+ // 与 advisory→hard 升格 / 退役判定的直接查账面——替代原「人工 grep check-loop 输出」。
8
+ // 测试:node .agents/scripts/agg-judgments.test.mjs(纯函数 aggFromRows 进程内断言)
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
14
+ const ROOT = path.resolve(SCRIPT_DIR, '..', '..');
15
+ const LEDGER = path.join(ROOT, '.agents', 'judgments.jsonl');
16
+
17
+ const argMonth = (process.argv.find((a) => a.startsWith('--month=')) || '').split('=')[1] || null;
18
+
19
+ // aggFromRows(rows, month):按月×规则×级别聚合。rows = 已解析的台账行数组;
20
+ // month = 'YYYY-MM' 过滤(null = 全部)。输出 { byMonth: { 'YYYY-MM': { runs, rules: { rule: { WARN, BLOCK } } } } }。
21
+ export function aggFromRows(rows, month = null) {
22
+ const byMonth = {};
23
+ for (const row of rows || []) {
24
+ if (typeof row.ts !== 'string' || !/^\d{4}-\d{2}/.test(row.ts)) continue;
25
+ const ym = row.ts.slice(0, 7);
26
+ if (month && ym !== month) continue;
27
+ const b = (byMonth[ym] ||= { runs: 0, rules: {} });
28
+ b.runs++;
29
+ for (const r of Array.isArray(row.rules) ? row.rules : []) {
30
+ if (!r || typeof r.rule !== 'string') continue;
31
+ const cell = (b.rules[r.rule] ||= { WARN: 0, BLOCK: 0 });
32
+ if (r.level === 'WARN') cell.WARN += r.count || 0;
33
+ else if (r.level === 'BLOCK') cell.BLOCK += r.count || 0;
34
+ }
35
+ }
36
+ return { byMonth };
37
+ }
38
+
39
+ function readRows() {
40
+ if (!fs.existsSync(LEDGER)) return null;
41
+ const rows = [];
42
+ let bad = 0;
43
+ for (const line of fs.readFileSync(LEDGER, 'utf8').split(/\r?\n/)) {
44
+ if (!line.trim()) continue;
45
+ try {
46
+ rows.push(JSON.parse(line));
47
+ } catch {
48
+ bad++;
49
+ }
50
+ }
51
+ return { rows, bad };
52
+ }
53
+
54
+ function main() {
55
+ const read = readRows();
56
+ if (!read) {
57
+ console.error(`agg-judgments: ${path.relative(ROOT, LEDGER)} 不存在——尚无运行记录(check-loop 真实运行一次即有一行)`);
58
+ process.exit(0);
59
+ }
60
+ const { byMonth } = aggFromRows(read.rows, argMonth);
61
+ const months = Object.keys(byMonth).sort();
62
+ if (!months.length) {
63
+ console.log(`agg-judgments: ${argMonth || '全部月份'} 无记录(台账共 ${read.rows.length} 行)`);
64
+ return;
65
+ }
66
+ for (const ym of months) {
67
+ const b = byMonth[ym];
68
+ console.log(`📅 ${ym}|运行 ${b.runs} 次`);
69
+ const rules = Object.keys(b.rules).sort();
70
+ if (!rules.length) { console.log(' (无规则判定)'); continue; }
71
+ for (const rule of rules) {
72
+ const { WARN, BLOCK } = b.rules[rule];
73
+ console.log(` ${rule.padEnd(24)} WARN×${WARN} BLOCK×${BLOCK}`);
74
+ }
75
+ }
76
+ if (read.bad) console.error(`agg-judgments: 跳过坏行 ${read.bad}(JSON 解析失败,不中断聚合)`);
77
+ }
78
+
79
+ const isMain = process.argv[1] && process.argv[1].endsWith('agg-judgments.mjs');
80
+ if (isMain) main();
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ // agg-judgments.test.mjs — 判定流水聚合纯函数套件(2026-10-11 judgment-ledger)
3
+ // aggFromRows 进程内断言:按月分桶 / --month 过滤 / WARN·BLOCK 计数 / 坏 rules 字段容忍。
4
+ import { aggFromRows } from './agg-judgments.mjs';
5
+
6
+ let pass = 0;
7
+ let fail = 0;
8
+ const check = (desc, cond, detail = '') => {
9
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
10
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
11
+ };
12
+
13
+ const ROWS = [
14
+ { ts: '2026-10-01T01:00:00Z', mode: 'hard', rules: [{ rule: '三件套不全', level: 'WARN', count: 2 }] },
15
+ { ts: '2026-10-02T01:00:00Z', mode: 'hygiene', rules: [{ rule: '三件套不全', level: 'WARN', count: 1 }, { rule: '配对断裂', level: 'BLOCK', count: 1 }] },
16
+ { ts: '2026-09-30T01:00:00Z', mode: 'hard', rules: [{ rule: '归因缺字段', level: 'WARN', count: 3 }] },
17
+ { ts: '2026-10-03T01:00:00Z', mode: 'hard', rules: [] },
18
+ { ts: 'bad-ts', mode: 'hard', rules: [] },
19
+ { ts: '2026-10-04T01:00:00Z', mode: 'hard', rules: 'not-array' },
20
+ ];
21
+
22
+ // ---- S1 按月分桶 + 计数 ----
23
+ {
24
+ const { byMonth } = aggFromRows(ROWS);
25
+ check('S1 两个月桶(坏 ts 行不计)', Object.keys(byMonth).sort().join(',') === '2026-09,2026-10', JSON.stringify(Object.keys(byMonth)));
26
+ check('S1 2026-10 运行 4 次', byMonth['2026-10'].runs === 4, String(byMonth['2026-10'].runs));
27
+ check('S1 跨行同规则 WARN 累加(2+1=3)', byMonth['2026-10'].rules['三件套不全'].WARN === 3, JSON.stringify(byMonth['2026-10'].rules));
28
+ check('S1 BLOCK 计数独立', byMonth['2026-10'].rules['配对断裂'].BLOCK === 1);
29
+ check('S1 2026-09 归因缺字段 WARN×3', byMonth['2026-09'].rules['归因缺字段'].WARN === 3);
30
+ check('S1 空 rules 行只计 runs', byMonth['2026-10'].runs === 4 && byMonth['2026-10'].rules['配对断裂'].WARN === 0);
31
+ }
32
+
33
+ // ---- S2 month 过滤 ----
34
+ {
35
+ const { byMonth } = aggFromRows(ROWS, '2026-09');
36
+ check('S2 过滤单月只留 2026-09', Object.keys(byMonth).join(',') === '2026-09' && byMonth['2026-09'].runs === 1);
37
+ check('S2 过滤无命中 → 空桶', aggFromRows(ROWS, '2025-01').byMonth['2025-01'] === undefined);
38
+ }
39
+
40
+ // ---- S3 空入参 ----
41
+ check('S3 空行数组 → 空 byMonth', Object.keys(aggFromRows([]).byMonth).length === 0);
42
+ check('S3 null 入参不 throw', Object.keys(aggFromRows(null).byMonth).length === 0);
43
+
44
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
45
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ // archive-sweep — workflow 终态件滚动归档(2026-10-09;2026-10-13 修锚;B9 自 Shipyard.Material
3
+ // 回灌——判定面逐字对齐,出处沿革留痕在包源仓 git 历史与 CHANGELOG,装户径不引实例路径)
4
+ // **归档单位 = 同名家族**(intent/spec/plan/incident 同名文件同进同退):check-loop 检查 1 的结构配对
5
+ // 是「家族双向在场」审计——intent 缺 spec/plan、spec/plan 缺 intent、incident 缺 plan/spec 均为
6
+ // hard-block(源仓首批按单篇准入产生数十处配对断裂的返工实证,留痕在包源仓 git 历史与 CHANGELOG)。
7
+ // 家族任一成员不满足条件(非终态/未满窗/被活跃引用/无日期),整族留守。
8
+ // 终态时间三级锚(2026-10-13 修锚):confirmations 终态行 ts(精确)→ **内容 last-touch**(回退)→ 文件名日期
9
+ // 回退级原用 git last-touch,会被批量提交系统性污染:实例仓曾发生大规模 frontmatter 迁移与两步式
10
+ // 留痕补录(数百文件量级),把存量老文档的 last-touch 集体刷成当日 → 21 天窗口恒不满足、活跃目录
11
+ // 只进不出。修法:回退级改「最后一次**正文**变更」——frontmatter-only 提交(状态/确认指纹/备注行增删改)
12
+ // 不计入,因其不改变文档的实质内容与归档安全性(归档只依赖终态 + 无活跃引用,二者均由当前 frontmatter
13
+ // 与引用扫描独立判定)。污染事件与修锚实测数据留痕在包源仓 git 历史与 CHANGELOG。
14
+ // 判定:逐提交比对 blob,剥 frontmatter 区后正文逐字节相同 → 视为 frontmatter-only,跳过。
15
+ // 归档形态:git mv → workflow/archive/<家族最晚终态月>/{类型}/,整族同批 = 整体退出活跃面——
16
+ // check-loop 配对门 / gen-workflow-index / kb-search 活跃语料均只扫四目录直下,归档不产生配对断裂
17
+ // (check-ledger-invariant 已归档感知:台账行原路径未命中再探 workflow/archive/*/)。
18
+ // 差异收敛(B10a 随动):kb-search 已同源仓扩 archive/<批次>/<type> 检索覆盖——归档件退出活跃目录
19
+ // 但不退出 kb 检索面,防复发不丢历史。
20
+ // 用法:
21
+ // node .agents/scripts/archive-sweep.mjs # dry-run(缺省),--window 21
22
+ // node .agents/scripts/archive-sweep.mjs --window 21 --execute # git mv 执行
23
+ // 验证:node .agents/scripts/archive-sweep.test.mjs
24
+ import { execFileSync } from 'node:child_process';
25
+ import fs from 'node:fs';
26
+ import path from 'node:path';
27
+ import { readLedgerRows } from './ledger-loader.mjs';
28
+
29
+ const DIRS = ['intents', 'specs', 'plans', 'incidents'];
30
+ export const TERMINAL = new Set(['done', 'superseded', 'cancelled', 'closed']);
31
+ export const ACTIVE = new Set(['draft', 'approved', 'open', 'fixed']);
32
+
33
+ export function parseStatus(text) {
34
+ const lines = text.split(/\r?\n/);
35
+ const end = lines.indexOf('---', 1);
36
+ for (let i = 1; i < (end === -1 ? Math.min(lines.length, 15) : end); i++) {
37
+ const s = /^状态:\s*(\S+)/.exec(lines[i]);
38
+ if (s) return s[1];
39
+ }
40
+ return '';
41
+ }
42
+
43
+ // bodyOf(text):剥 frontmatter 区(首 --- 至闭合 ---)后的正文,CRLF 归一。
44
+ // 用途:判定一次提交是否只动了 frontmatter(状态/确认指纹/备注)——此类提交不改变文档实质内容,
45
+ // 不应刷新「内容 last-touch」(2026-10-13 修锚,见文件头注释)。无 frontmatter → 全文即正文。
46
+ export function bodyOf(text) {
47
+ const norm = String(text).replace(/\r\n/g, '\n');
48
+ const lines = norm.split('\n');
49
+ if (!/^---\s*$/.test(lines[0] || '')) return norm;
50
+ for (let i = 1; i < lines.length; i++) {
51
+ if (/^---\s*$/.test(lines[i])) return lines.slice(i + 1).join('\n');
52
+ }
53
+ return norm; // 未闭合 → 保守视全文为正文(宁可刷新,不误判为 frontmatter-only)
54
+ }
55
+
56
+ // 纯函数:候选分析(fixture 测试直接驱动;docs 项 = { rel, file, dir, text })
57
+ // 归档单位 = 同名家族:家族候选 ⇔ 全成员终态 + 全成员有终态时间且超窗 + 无成员被活跃引用
58
+ export function analyze({ docs, ledgerTs = {}, lastTouch = {}, activeTexts = [], windowDays = 21, today = Date.now() }) {
59
+ const referenced = new Set();
60
+ const fams = new Map();
61
+ for (const d of docs) {
62
+ if (d.file.startsWith('_TEMPLATE')) continue;
63
+ if (!fams.has(d.file)) fams.set(d.file, []);
64
+ fams.get(d.file).push(d);
65
+ }
66
+ for (const text of activeTexts) for (const file of fams.keys()) if (text.includes(file)) referenced.add(file);
67
+
68
+ const active = [];
69
+ const terminal = [];
70
+ const candidates = [];
71
+ const tooRecent = [];
72
+ const noDate = [];
73
+ for (const [, members] of fams) {
74
+ const enriched = members.map((d) => {
75
+ const st = parseStatus(d.text);
76
+ const tTime = ledgerTs[d.rel] || lastTouch[d.rel] || /^(\d{4}-\d{2}-\d{2})/.exec(d.file)?.[1] || '';
77
+ const src = ledgerTs[d.rel] ? 'ledger' : lastTouch[d.rel] ? 'git' : 'name';
78
+ return { ...d, st, tTime, src };
79
+ });
80
+ const allTerminal = enriched.every((x) => TERMINAL.has(x.st));
81
+ const refd = enriched.some((x) => referenced.has(x.file));
82
+ const undated = enriched.filter((x) => !x.tTime);
83
+ const recent = enriched.filter((x) => x.tTime && Math.floor((today - new Date(x.tTime).getTime()) / 864e5) <= windowDays);
84
+ const archivable = allTerminal && !refd && !undated.length && !recent.length;
85
+ for (const x of enriched) {
86
+ if (TERMINAL.has(x.st)) terminal.push(x);
87
+ if (ACTIVE.has(x.st)) active.push(x);
88
+ }
89
+ if (!archivable) {
90
+ if (undated.length) noDate.push(...undated);
91
+ if (recent.length) tooRecent.push(...recent);
92
+ continue;
93
+ }
94
+ const month = enriched.map((x) => x.tTime).sort().pop().slice(0, 7); // 家族最晚终态月,整族同批
95
+ for (const x of enriched) candidates.push({ ...x, month });
96
+ }
97
+ return { active, terminal, candidates, referenced, tooRecent, noDate };
98
+ }
99
+
100
+ export function buildDocList(root, dirs = DIRS) {
101
+ const docs = [];
102
+ for (const d of dirs) {
103
+ const dir = path.join(root, 'workflow', d);
104
+ if (!fs.existsSync(dir)) continue; // 目录缺失(fixture / 部分类型未建)→ 跳过,不抛
105
+ for (const f of fs.readdirSync(dir)) {
106
+ if (!f.endsWith('.md')) continue;
107
+ const rel = `workflow/${d}/${f}`;
108
+ docs.push({ rel, file: f, dir: d, text: fs.readFileSync(path.join(dir, f), 'utf8') });
109
+ }
110
+ }
111
+ return docs;
112
+ }
113
+
114
+ function loadLedgerTs(root) {
115
+ const out = {};
116
+ // 台账统一视图(主台账+分片透明合并,坏行跳过)。源仓走 ledger-loader.loadAllConfirmations
117
+ // (同 doc|stage|fingerprint 键去重取末次);kit 读侧单源 = readLedgerRows(不去重)。对本判据
118
+ // 无语义差:loadLedgerTs 取「每 doc 终态行最大日期」,同键末次行的日期 ≥ 该键其余行,两侧 max
119
+ // 恒等——不为锚查询给 loader 加第二个出口(读侧出口越少越不易漂移)。
120
+ for (const e of readLedgerRows(root)) {
121
+ if (TERMINAL.has(e.stage) && e.doc) {
122
+ const d = (e.ts || '').slice(0, 10);
123
+ if (d && (!out[e.doc] || d > out[e.doc])) out[e.doc] = d;
124
+ }
125
+ }
126
+ return out;
127
+ }
128
+
129
+ // loadLastTouch(root):每篇 workflow 文档的「最后一次正文变更」日期(YYYY-MM-DD)。
130
+ // 逐提交(新→旧)取该文件的 blob 与上一版比对:正文(bodyOf)逐字节相同 → frontmatter-only 提交,
131
+ // 跳过不记;正文有变 → 记该提交日期并停止追踪该文件(新→旧遍历,首遇即最近一次正文变更)。
132
+ // 批量迁移/补录提交(只改状态与确认指纹)因此不再刷新锚——2026-10-13 修锚的核心。
133
+ // 实现:单次 `git log --raw` 拿「提交 × 文件 × 前后 blob sha」,再按需 `git cat-file --batch` 取内容
134
+ // (同 blob 内容天然去重,批量提交里数十文件多为同一份 frontmatter 改写,实际读取量远小于文件数)。
135
+ export function loadLastTouch(root) {
136
+ const out = {};
137
+ const raw = execFileSync('git', ['log', '--raw', '--no-renames', '--format=%x01%cI', '--', 'workflow'], {
138
+ cwd: root, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024,
139
+ });
140
+ // 收集待比对项:按 path 分组,保留**全部**历史条目(新→旧),逐条比对直到找到正文变更
141
+ const byPath = new Map();
142
+ for (const entry of raw.split('\u0001')) {
143
+ const lines = entry.split('\n');
144
+ const date = (lines[0] || '').slice(0, 10);
145
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
146
+ for (const line of lines.slice(1)) {
147
+ // `:<oldmode> <newmode> <oldsha> <newsha> <status>\t<path>`
148
+ const m = /^:\d+ \d+ ([0-9a-f]+) ([0-9a-f]+) [A-Z]\d*\t(.+)$/.exec(line);
149
+ if (!m) continue;
150
+ const [, oldSha, newSha, p] = m;
151
+ if (!byPath.has(p)) byPath.set(p, []);
152
+ byPath.get(p).push({ date, oldSha, newSha });
153
+ }
154
+ }
155
+ // 批量取 blob 内容(--batch 一次喂全部 sha,输出 `<full-sha> blob <size>\n<content>\n`)
156
+ // 注意:--batch 回显**完整** sha,而输入是 core.abbrev 缩写——按输入顺序回填,键用请求时的缩写,
157
+ // 否则 blobs.get(缩写) 恒 miss → 全部走保守回退(2026-10-13 实测踩中)。
158
+ const shas = [...new Set([...byPath.values()].flat().flatMap((x) => [x.oldSha, x.newSha]).filter((s) => s && !/^0+$/.test(s)))];
159
+ const blobs = new Map();
160
+ if (shas.length) {
161
+ const outBuf = execFileSync('git', ['cat-file', '--batch'], {
162
+ cwd: root, input: shas.join('\n') + '\n', maxBuffer: 512 * 1024 * 1024,
163
+ });
164
+ // 按 header 行切分(内容可能含任意字节,故按 size 精确切片);第 i 个响应 ↔ shas[i]
165
+ let pos = 0;
166
+ let idx = 0;
167
+ while (pos < outBuf.length && idx < shas.length) {
168
+ const nl = outBuf.indexOf(0x0a, pos);
169
+ if (nl === -1) break;
170
+ const header = outBuf.toString('utf8', pos, nl);
171
+ const hm = /^([0-9a-f]+) blob (\d+)$/.exec(header);
172
+ if (!hm) { pos = nl + 1; continue; }
173
+ const size = Number(hm[2]);
174
+ const start = nl + 1;
175
+ blobs.set(shas[idx], outBuf.toString('utf8', start, start + size));
176
+ pos = start + size + 1; // 跳过内容后的换行
177
+ idx++;
178
+ }
179
+ }
180
+ // 逐 path 新→旧扫描:首个「正文有变」的提交日期即内容 last-touch;全为 frontmatter-only → 不记
181
+ // (调用方回退到文件名日期)。新增文件(oldSha 全 0)→ 该次提交即首次入库,记为正文变更。
182
+ for (const [p, entries] of byPath) {
183
+ for (const { date, oldSha, newSha } of entries) {
184
+ // 全 0 sha(新增文件,git 按 core.abbrev 输出 7 位 `0000000`)→ 首次入库即正文变更
185
+ if (/^0+$/.test(oldSha)) { out[p] = date; break; }
186
+ const before = blobs.get(oldSha);
187
+ const after = blobs.get(newSha);
188
+ // 取不到内容(异常态)→ 保守记为正文变更(宁可刷新,不误跳过)
189
+ if (before === undefined || after === undefined) { out[p] = date; break; }
190
+ if (bodyOf(before) !== bodyOf(after)) { out[p] = date; break; }
191
+ }
192
+ }
193
+ return out;
194
+ }
195
+
196
+ function main() {
197
+ const argv = process.argv.slice(2);
198
+ const execute = argv.includes('--execute');
199
+ const wArg = argv.find((a) => a.startsWith('--window='));
200
+ const windowDays = wArg ? Number(wArg.split('=')[1]) : 21;
201
+ if (!Number.isFinite(windowDays) || windowDays < 0) {
202
+ console.error('archive-sweep: --window 须为非负数字');
203
+ process.exit(2);
204
+ }
205
+ const root = process.cwd();
206
+ const docs = buildDocList(root);
207
+ const r = analyze({
208
+ docs,
209
+ ledgerTs: loadLedgerTs(root),
210
+ lastTouch: loadLastTouch(root),
211
+ activeTexts: docs.filter((d) => ACTIVE.has(parseStatus(d.text))).map((d) => d.text),
212
+ windowDays,
213
+ });
214
+ const byDir = {};
215
+ for (const d of DIRS) byDir[d] = r.candidates.filter((x) => x.dir === d).length;
216
+ const fams = new Set(r.candidates.map((x) => x.file));
217
+ console.log(`窗口 ${windowDays} 天|候选 ${r.candidates.length} 篇 / ${fams.size} 族(${JSON.stringify(byDir)})|活跃 ${r.active.length}|未满窗 ${r.tooRecent.length}|被活跃引用排除 ${r.referenced.size}|无日期 ${r.noDate.length}`);
218
+ if (!execute) {
219
+ for (const x of r.candidates) console.log(` [dry] ${x.rel} → workflow/archive/${x.month}/${x.dir}/${x.file}`);
220
+ console.log('dry-run 结束(加 --execute 执行 git mv)');
221
+ return;
222
+ }
223
+ let moved = 0;
224
+ for (const x of r.candidates) {
225
+ const destDir = path.join(root, 'workflow', 'archive', x.month, x.dir);
226
+ fs.mkdirSync(destDir, { recursive: true });
227
+ execFileSync('git', ['mv', x.rel, path.join('workflow', 'archive', x.month, x.dir, x.file)], { cwd: root });
228
+ moved++;
229
+ }
230
+ console.log(`execute 完成:git mv ${moved} 篇 → workflow/archive/<家族最晚终态月>/`);
231
+ }
232
+
233
+ if (process.argv[1] && process.argv[1].endsWith('archive-sweep.mjs')) main();
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ // archive-sweep.test.mjs — analyze 纯函数 fixture(2026-10-09;2026-10-13 加 bodyOf 与内容锚回归;B9 自 Shipyard.Material 回灌)
3
+ import { analyze, parseStatus, bodyOf, loadLastTouch } from './archive-sweep.mjs';
4
+
5
+ let pass = 0;
6
+ let fail = 0;
7
+ const check = (desc, cond, detail = '') => {
8
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
9
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
10
+ };
11
+ const doc = (dir, file, st) => ({ rel: `workflow/${dir}/${file}`, file, dir, text: `---\n状态: ${st}\n---\n# t\n` });
12
+ const daysAgo = (n) => new Date(Date.now() - n * 864e5).toISOString().slice(0, 10);
13
+
14
+ // 场景 1:状态解析(CRLF 容忍)
15
+ check('parseStatus 基础+CRLF', parseStatus('---\r\n状态: done\r\n---\nx') === 'done' && parseStatus('---\n状态: open\n') === 'open');
16
+
17
+ // 场景 1b:bodyOf——剥 frontmatter(2026-10-13 修锚的判定基元)
18
+ {
19
+ const a = '---\n状态: approved\n级别: L1\n---\n\n# T\n\n正文 A\n';
20
+ const b = '---\n状态: done\n确认指纹: abc\n级别: L1\n---\n\n# T\n\n正文 A\n';
21
+ check('bodyOf:frontmatter-only 变更 → 正文相等', bodyOf(a) === bodyOf(b));
22
+ const c = '---\n状态: done\n级别: L1\n---\n\n# T\n\n正文 B\n';
23
+ check('bodyOf:正文变更 → 正文不等', bodyOf(b) !== bodyOf(c));
24
+ check('bodyOf:无 frontmatter → 全文即正文', bodyOf('# T\n\n正文\n') === '# T\n\n正文\n');
25
+ check('bodyOf:未闭合 frontmatter → 保守返回全文', bodyOf('---\n状态: done\n正文\n') === '---\n状态: done\n正文\n');
26
+ check('bodyOf:CRLF 归一', bodyOf('---\r\n状态: done\r\n---\r\n正文\r\n') === bodyOf('---\n状态: done\n---\n正文\n'));
27
+ }
28
+
29
+ // 场景 2:终态/活跃分类 + 模板排除
30
+ {
31
+ const docs = [doc('intents', 'a.md', 'done'), doc('plans', 'b.md', 'draft'), doc('intents', '_TEMPLATE.md', 'draft'), doc('incidents', 'c.md', 'closed')];
32
+ const r = analyze({ docs, windowDays: 21 });
33
+ check('分类:终态 2 / 活跃 1 / 模板排除', r.terminal.length === 2 && r.active.length === 1 && !JSON.stringify(r).includes('_TEMPLATE'));
34
+ }
35
+
36
+ // 场景 3:窗口过滤(台账 ts 优先于 git 回退)
37
+ {
38
+ const docs = [doc('specs', 'old.md', 'done'), doc('specs', 'new.md', 'done')];
39
+ const r = analyze({
40
+ docs, windowDays: 21,
41
+ ledgerTs: { 'workflow/specs/old.md': daysAgo(30) },
42
+ lastTouch: { 'workflow/specs/old.md': daysAgo(2), 'workflow/specs/new.md': daysAgo(2) },
43
+ });
44
+ check('窗口:台账 30 天入候选、git 2 天未满窗', r.candidates.length === 1 && r.candidates[0].file === 'old.md' && r.candidates[0].src === 'ledger' && r.tooRecent.length === 1);
45
+ }
46
+
47
+ // 场景 4:活跃引用排除(git 回退源)+ 无时间源兜底文件名日期
48
+ {
49
+ const docs = [doc('intents', '2026-09-01-refd.md', 'done'), doc('intents', '2026-09-01-fallback.md', 'done')];
50
+ const r = analyze({
51
+ docs, windowDays: 21,
52
+ activeTexts: ['参见 ../intents/2026-09-01-refd.md 的结论'],
53
+ lastTouch: { 'workflow/intents/2026-09-01-fallback.md': daysAgo(30) },
54
+ });
55
+ check('引用排除 + git 回退源入候选', r.candidates.length === 1 && r.candidates[0].file === '2026-09-01-fallback.md' && r.candidates[0].src === 'git' && r.referenced.has('2026-09-01-refd.md'));
56
+ }
57
+
58
+ // 场景 4b:台账/git 均无时间源 → 文件名日期兜底
59
+ {
60
+ const r = analyze({ docs: [doc('specs', '2026-09-01-nodate.md', 'done')], windowDays: 21 });
61
+ check('文件名日期兜底', r.candidates.length === 1 && r.candidates[0].src === 'name' && r.candidates[0].month === '2026-09');
62
+ }
63
+
64
+ // 场景 5:归档月份取终态时间当月
65
+ {
66
+ const r = analyze({ docs: [doc('plans', '2026-09-03-x.md', 'done')], windowDays: 21, ledgerTs: { 'workflow/plans/2026-09-03-x.md': '2026-09-05' } });
67
+ check('归档月 = 终态时间当月', r.candidates[0] !== undefined && r.candidates[0].month === '2026-09');
68
+ }
69
+
70
+ // 场景 6:家族原子——intent 未终态锚住同名家族整族留守;全终态家族整族入候选
71
+ {
72
+ const docs = [
73
+ doc('intents', '2026-09-05-live.md', 'approved'),
74
+ doc('plans', '2026-09-05-live.md', 'done'),
75
+ doc('intents', '2026-09-04-family.md', 'done'),
76
+ doc('specs', '2026-09-04-family.md', 'done'),
77
+ doc('plans', '2026-09-04-family.md', 'done'),
78
+ ];
79
+ const r = analyze({
80
+ docs, windowDays: 21,
81
+ ledgerTs: { 'workflow/plans/2026-09-05-live.md': daysAgo(30), 'workflow/intents/2026-09-04-family.md': daysAgo(30), 'workflow/specs/2026-09-04-family.md': daysAgo(30), 'workflow/plans/2026-09-04-family.md': daysAgo(30) },
82
+ });
83
+ check('家族原子:非终态 intent 整族留守', r.candidates.length === 3 && !r.candidates.some((x) => x.file === '2026-09-05-live.md'));
84
+ check('家族原子:全终态家族三成员同批', r.candidates.filter((x) => x.file === '2026-09-04-family.md').length === 3);
85
+ }
86
+
87
+ // 场景 7:loadLastTouch 集成——真实 git 仓库,钉住「frontmatter-only 提交不刷新内容锚」
88
+ // (2026-10-13 修锚回归:原实现用 git last-touch,被批量迁移/补录提交污染 → 21 天窗口恒不满足。
89
+ // 本场景同时覆盖 cat-file --batch 回显完整 sha 而输入为缩写这一踩坑点——键错则全部走保守回退。)
90
+ {
91
+ const { execFileSync } = await import('node:child_process');
92
+ const os = await import('node:os');
93
+ const path = await import('node:path');
94
+ const fs = await import('node:fs');
95
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'archive-sweep-git-'));
96
+ const git = (...a) => execFileSync('git', a, { cwd: root, encoding: 'utf8' });
97
+ git('init', '-q');
98
+ git('config', 'user.email', 't@t');
99
+ git('config', 'user.name', 't');
100
+ const dir = path.join(root, 'workflow', 'plans');
101
+ fs.mkdirSync(dir, { recursive: true });
102
+ const p = path.join(dir, '2026-09-01-x.md');
103
+ const commit = (msg, date) => {
104
+ git('add', '-A');
105
+ execFileSync('git', ['commit', '-q', '-m', msg], { cwd: root, env: { ...process.env, GIT_AUTHOR_DATE: date, GIT_COMMITTER_DATE: date } });
106
+ };
107
+ // ① 首次入库(正文 v1)
108
+ fs.writeFileSync(p, '---\n状态: draft\n级别: L1\n---\n\n# T\n\n正文 v1\n', 'utf8');
109
+ commit('c1', '2026-09-01T10:00:00+08:00');
110
+ // ② frontmatter-only(状态 draft→approved)
111
+ fs.writeFileSync(p, '---\n状态: approved\n级别: L1\n---\n\n# T\n\n正文 v1\n', 'utf8');
112
+ commit('c2', '2026-09-20T10:00:00+08:00');
113
+ // ③ frontmatter-only(approved→done + 确认指纹)
114
+ fs.writeFileSync(p, '---\n状态: done\n级别: L1\n确认指纹: abc\n---\n\n# T\n\n正文 v1\n', 'utf8');
115
+ commit('c3', '2026-09-28T10:00:00+08:00');
116
+ const rel = 'workflow/plans/2026-09-01-x.md';
117
+ const t1 = loadLastTouch(root);
118
+ check('loadLastTouch:frontmatter-only 提交不刷新锚(锚停在正文变更日 2026-09-01)',
119
+ t1[rel] === '2026-09-01', `got ${t1[rel]}`);
120
+ // ④ 正文变更 → 锚前移到该日
121
+ fs.writeFileSync(p, '---\n状态: done\n级别: L1\n确认指纹: abc\n---\n\n# T\n\n正文 v2\n', 'utf8');
122
+ commit('c4', '2026-10-05T10:00:00+08:00');
123
+ const t2 = loadLastTouch(root);
124
+ check('loadLastTouch:正文变更刷新锚(2026-10-05)', t2[rel] === '2026-10-05', `got ${t2[rel]}`);
125
+ fs.rmSync(root, { recursive: true, force: true });
126
+ }
127
+
128
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
129
+ process.exit(fail ? 1 : 0);
@@ -0,0 +1,37 @@
1
+ // check-attribution.mjs — 检查 24:incident 归因字段核查 [warning](2026-10-13 B6b-2 自 Shipyard.Material
2
+ // check-loop-sections/s24-incident-attribution.mjs 回灌;段体判据逐字对齐,出处沿革留痕在包源仓 git
3
+ // 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 背景(incident-attribution):incident 无「来源」标准字段,「来源×环境」分布只能逐件人工判读,
5
+ // 两轮口径相抵不可机器复算。本段把归因变成机器校验收敛面:模板新增「来源:」/「影响环境:」两枚枚举键
6
+ // (单源 .agents/workflow-enums.txt incident.source.all / incident.env.all),
7
+ // 锚起的件「发现」日 ≥ attributionSince(policy v9;v1–v8 缺键 → 整段跳过)时必填且值须在枚举内 → 否则 WARN。
8
+ // 只查字段存在性与枚举合法性,不判读内容(判读留给 gen-workflow-metrics 的机器复算表)。
9
+ // 存量豁免:发现日 < 锚 → 整体跳过;`流程: legacy` 回填件 → 跳过(同检查 3 口径)。
10
+ // kit 适配(源仓→kit 逐条):告警文案里的枚举单源路径平铺 .agents/config/workflow-enums.txt → .agents/workflow-enums.txt。
11
+ import path from 'node:path';
12
+
13
+ export function run(ctx) {
14
+ const { ENUMS, inSet, docFiles, fmGet, kitPolicy, warnings } = ctx;
15
+ // --- 24. incident 归因字段核查 [warning](锚 attributionSince)---
16
+ {
17
+ const anchor = kitPolicy.attributionSince;
18
+ if (anchor) {
19
+ const KEY_SOURCE = '来源';
20
+ const KEY_ENV = '影响环境';
21
+ for (const inc of docFiles('incidents')) {
22
+ const name = path.basename(inc);
23
+ if (fmGet(inc, '流程') === 'legacy') continue; // 回填件豁免(同检查 3)
24
+ // 生效日锚:发现日优先、缺则取文件名前缀(与检查 12 moduleSince 同款退化)
25
+ let d = fmGet(inc, '发现') || fmGet(inc, '日期');
26
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) d = /^\d{4}-\d{2}-\d{2}/.test(name) ? name.slice(0, 10) : '';
27
+ if (!d || d < anchor) continue; // 锚前存量不回填
28
+ const src = fmGet(inc, KEY_SOURCE);
29
+ const env = fmGet(inc, KEY_ENV);
30
+ if (!src) warnings.push(`- [WARN 归因缺字段] ${name} 缺「${KEY_SOURCE}:」字段(${anchor} 起新建 incident 必填,取值见 .agents/workflow-enums.txt incident.source.all)`);
31
+ else if (!inSet(src, ENUMS['incident.source.all'])) warnings.push(`- [WARN 归因非法值] ${name}「${KEY_SOURCE}: ${src}」不在枚举(${ENUMS['incident.source.all'].join('/')})`);
32
+ if (!env) warnings.push(`- [WARN 归因缺字段] ${name} 缺「${KEY_ENV}:」字段(${anchor} 起新建 incident 必填,取值见 .agents/workflow-enums.txt incident.env.all)`);
33
+ else if (!inSet(env, ENUMS['incident.env.all'])) warnings.push(`- [WARN 归因非法值] ${name}「${KEY_ENV}: ${env}」不在枚举(${ENUMS['incident.env.all'].join('/')})`);
34
+ }
35
+ }
36
+ }
37
+ }