agentic-flow-kit 1.7.15 → 1.8.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.
Files changed (206) hide show
  1. package/README.md +50 -8
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-design.md +5 -4
  4. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  5. package/modules/hosts/claude/commands/wf-new-task.md +47 -25
  6. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  7. package/modules/hosts/claude/commands/wf-review.md +2 -2
  8. package/modules/hosts/claude/commands/wf-sync-hosts.md +7 -13
  9. package/modules/hosts/claude/commands/wf-test.md +2 -2
  10. package/modules/hosts/codex/commands/wf-build.md +1 -1
  11. package/modules/hosts/codex/commands/wf-design.md +5 -4
  12. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  13. package/modules/hosts/codex/commands/wf-new-task.md +47 -25
  14. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  15. package/modules/hosts/codex/commands/wf-review.md +2 -2
  16. package/modules/hosts/codex/commands/wf-sync-hosts.md +7 -13
  17. package/modules/hosts/codex/commands/wf-test.md +2 -2
  18. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  19. package/modules/hosts/cursor/commands/wf-design.md +5 -4
  20. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  21. package/modules/hosts/cursor/commands/wf-new-task.md +47 -25
  22. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  23. package/modules/hosts/cursor/commands/wf-review.md +2 -2
  24. package/modules/hosts/cursor/commands/wf-sync-hosts.md +7 -13
  25. package/modules/hosts/cursor/commands/wf-test.md +2 -2
  26. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  27. package/modules/hosts/opencode/commands/wf-design.md +5 -4
  28. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  29. package/modules/hosts/opencode/commands/wf-new-task.md +47 -25
  30. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  31. package/modules/hosts/opencode/commands/wf-review.md +2 -2
  32. package/modules/hosts/opencode/commands/wf-sync-hosts.md +7 -13
  33. package/modules/hosts/opencode/commands/wf-test.md +2 -2
  34. package/modules/hosts/trae/commands/wf-build.md +1 -1
  35. package/modules/hosts/trae/commands/wf-design.md +5 -4
  36. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  37. package/modules/hosts/trae/commands/wf-new-task.md +47 -25
  38. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  39. package/modules/hosts/trae/commands/wf-review.md +2 -2
  40. package/modules/hosts/trae/commands/wf-sync-hosts.md +7 -13
  41. package/modules/hosts/trae/commands/wf-test.md +2 -2
  42. package/package.json +1 -1
  43. package/src/cli.mjs +47 -4
  44. package/src/doctor.mjs +48 -3
  45. package/src/executor.mjs +262 -0
  46. package/src/executor.test.mjs +295 -0
  47. package/src/fresh-init.test.mjs +1 -1
  48. package/src/init.mjs +27 -3
  49. package/src/profiles.mjs +10 -3
  50. package/src/render.mjs +3 -3
  51. package/src/rule-budget.test.mjs +2 -2
  52. package/src/stack-profile.test.mjs +6 -0
  53. package/src/sync-hosts.mjs +37 -43
  54. package/src/sync-hosts.test.mjs +67 -56
  55. package/src/validate.mjs +6 -1
  56. package/src/validate.test.mjs +1 -1
  57. package/templates/.gitattributes +12 -1
  58. package/templates/AGENTS.md +3 -3
  59. package/templates/_agents/commands/build.md +1 -1
  60. package/templates/_agents/commands/design.md +5 -4
  61. package/templates/_agents/commands/maintain.md +1 -1
  62. package/templates/_agents/commands/new-task.md +47 -25
  63. package/templates/_agents/commands/plan.md +1 -1
  64. package/templates/_agents/commands/review.md +2 -2
  65. package/templates/_agents/commands/sync-hosts.md +7 -13
  66. package/templates/_agents/commands/test.md +2 -2
  67. package/templates/_agents/control-surfaces.txt +1 -1
  68. package/templates/_agents/engine-lock.json +334 -0
  69. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  70. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  71. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  72. package/templates/_agents/lane-surfaces.txt +5 -2
  73. package/templates/_agents/level-hints.txt +30 -0
  74. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  75. package/templates/_agents/mechanism-registry.md +27 -0
  76. package/templates/_agents/rule-budgets.txt +2 -2
  77. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  78. package/templates/_agents/scripts/agg-delegations.test.mjs +5 -3
  79. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  80. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  81. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  82. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  83. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  84. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  85. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  86. package/templates/_agents/scripts/check-engine-integrity.mjs +59 -127
  87. package/templates/_agents/scripts/check-engine-integrity.test.mjs +33 -45
  88. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  89. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  90. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +74 -25
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +71 -5
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +427 -78
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +438 -4
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +13 -8
  96. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  97. package/templates/_agents/scripts/check-loop.mjs +264 -75
  98. package/templates/_agents/scripts/check-loop.test.mjs +587 -61
  99. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  100. package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
  101. package/templates/_agents/scripts/check-registry.mjs +45 -0
  102. package/templates/_agents/scripts/check-scope.mjs +96 -0
  103. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  104. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  105. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  106. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  107. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  108. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  109. package/templates/_agents/scripts/confirm-doc.mjs +136 -31
  110. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  111. package/templates/_agents/scripts/doctor.test.mjs +44 -1
  112. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  113. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  114. package/templates/_agents/scripts/fill-intent.mjs +7 -5
  115. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  116. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  117. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  118. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  119. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  120. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  121. package/templates/_agents/scripts/fixture-scripts.test.mjs +112 -0
  122. package/templates/_agents/scripts/gate-checklist.mjs +10 -9
  123. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -11
  124. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  125. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  126. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  127. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  128. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  129. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +16 -14
  130. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  131. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -36
  132. package/templates/_agents/scripts/git-core.mjs +45 -0
  133. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  134. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  135. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  136. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  137. package/templates/_agents/scripts/kb-search.mjs +19 -4
  138. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  139. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  140. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  141. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  142. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  143. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  144. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  145. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  146. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  147. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  148. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  149. package/templates/_agents/scripts/level-criteria.mjs +231 -0
  150. package/templates/_agents/scripts/level-criteria.test.mjs +281 -0
  151. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  152. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  153. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  154. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  155. package/templates/_agents/scripts/pipeline-run.mjs +10 -24
  156. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  157. package/templates/_agents/scripts/policy.mjs +75 -0
  158. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  159. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  160. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  161. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  162. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  163. package/templates/_agents/scripts/run-tests.mjs +99 -0
  164. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  165. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  166. package/templates/_agents/scripts/stage-gates.mjs +38 -21
  167. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  168. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  169. package/templates/_agents/scripts/verify.mjs +135 -58
  170. package/templates/_agents/scripts/verify.test.mjs +156 -47
  171. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  172. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  173. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  174. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  175. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  176. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  177. package/templates/_agents/scripts-test-exempt.txt +1 -0
  178. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  179. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  180. package/templates/_agents/workflow-enums.txt +8 -1
  181. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  182. package/templates/_githooks/pre-commit +109 -120
  183. package/templates/_githooks/pre-merge-commit +27 -23
  184. package/templates/_githooks/pre-push +7 -6
  185. package/templates/_github/workflows/kit-ci.yml +4 -3
  186. package/templates/workflow/README.md +23 -5
  187. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  188. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  189. package/templates/workflow/papercuts.md +2 -0
  190. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  191. package/templates/workflow/regression-checklist.md +1 -0
  192. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  193. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -89
  194. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -89
  195. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -89
  196. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -89
  197. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -90
  198. package/templates/_agents/commands/source-sync-check.md +0 -103
  199. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  200. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  201. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  202. package/templates/_agents/scripts/check-ledger.test.mjs +0 -199
  203. package/templates/_agents/scripts/rule-budget.sh +0 -74
  204. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  205. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -286
  206. package/templates/_agents/workflows/source-sync-repair.md +0 -30
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ // check-convergence.mjs — 检查 23:判据级收敛对账(2026-10-13 B6b-2 自 Shipyard.Material
2
+ // check-loop-sections/s23-convergence-audit.mjs 回灌;段体判据逐字对齐,出处沿革留痕在包源仓 git
3
+ // 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 背景(convergence-audit,参照 GitHub spec-kit converge 机理「完成声明不是证据」):
5
+ // spec 的验证承诺此前只落在叙述节(检查 21 对节内引用做存在性扫描),没有逐判据 × 逐证据的机器对账。
6
+ // 本段把「验收判据」表(workflow/specs/_TEMPLATE.md 承重节)变成机器输入:L2/L3 done spec(锚后)逐行解析
7
+ // 判据 → 证据。状态列自报「覆盖」而证据无效 → 仍判未覆盖。
8
+ // 两档(convergence-hard):
9
+ // advisory(有 convergenceSince、无 hard 键,或日期 < convergenceHardSince):证据 = 存在的路径,或命令白名单声明。WARN。
10
+ // hard(日期 ≥ convergenceHardSince):路径仍只认存在,不读正文、不判与判据是否相关(判断题留给新会话复核);
11
+ // 命令必须是全量门实际跑的那几个,且 workflow/verify-evidence.md 有不早于本 spec 日期的全绿行。
12
+ // 答不了(空、路径不存在、命令不在全量门、没有对应全绿行)= 未覆盖,HARD-BLOCK。人工走查且证据为空仍合法。
13
+ // 无「验收判据」节或表无数据行 → 两档都是 WARN「收敛无判据」。
14
+ // 存量豁免:日期 < convergenceSince → 跳过。缺键(v1–v9)→ 整段跳过。缺 hard 键(v1–v10)→ 不升 hard。
15
+ // kit 适配(源仓→kit 逐条):① 命令白名单收 kit 全量门口径——verify.mjs 全量门实际跑的就是
16
+ // `npm test` 与 `node .agents/scripts/verify.mjs` 自身(verify.mjs 编排七门、测试面走 run-tests 套件),
17
+ // 源仓的 dotnet build / npm run build / verify-ui 等实例门随 instance-ref 门修法剔除;② hard 档解锁序
18
+ // 文案对齐 kit:verify.mjs 全绿尾部自动重生成 verify-evidence.md(gen-verify-evidence 单源),
19
+ // 不再单独点名 gen-verify-evidence 步骤。
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+
23
+ // advisory 档的声明白名单(文本命中即认)。hard 档不再凭命中放行。命令面 = kit 全量门口径(见头注)。
24
+ const CMD_RE = /npm\s+(run\s+)?test|verify\.mjs/;
25
+ // 引用切分:每一处引用单独取出,防「npm test 与路径同格」被整格误判。长短形态都认。
26
+ const CITE_RE = /npm\s+(?:run\s+)?test|verify\.mjs/g;
27
+ // verify.mjs 全绿行(verifications.jsonl 绿凭证)实际覆盖的命令。不在此集的白名单命令没有对应绿记录。
28
+ const GATE_CMD = new Set(['npm test', 'npm run test', 'verify.mjs']);
29
+ const PATH_RE = /[\w.@\-\\/]+\.(mjs|js|ts|tsx|cs|md|json|html|sql)/g;
30
+ const SEC_RE = /^##\s+.*验收判据/;
31
+ // 与 gen-verify-evidence.mjs 数据行同形态(kit 六列:短 SHA | ISO 时间 | 门数 | 套件计数 | 绑定单 | runId)。
32
+ // 只取日期,不复述门数。
33
+ const EVIDENCE_ROW_RE = /^\|\s*`[0-9a-f]{7,40}`\s*\|\s*([^|]+?)\s*\|/;
34
+
35
+ export function run(ctx) {
36
+ const { ROOT, WF, docFiles, fmGet, linesOf, addedDateOf, kitPolicy, warnings, blockers } = ctx;
37
+ // --- 23. 判据级收敛对账(锚 convergenceSince / convergenceHardSince)---
38
+ function greenEvidenceDates() {
39
+ const dates = [];
40
+ let text = '';
41
+ try { text = fs.readFileSync(path.join(ROOT, WF, 'verify-evidence.md'), 'utf8'); } catch { return dates; }
42
+ for (const raw of text.split(/\r?\n/)) {
43
+ const m = raw.trim().match(EVIDENCE_ROW_RE);
44
+ if (!m) continue;
45
+ const day = m[1].trim().slice(0, 10);
46
+ if (/^\d{4}-\d{2}-\d{2}$/.test(day)) dates.push(day);
47
+ }
48
+ return dates;
49
+ }
50
+
51
+ // 命令格:每一处引用都须落在全量门里,且有不早于 spec 日期的全绿行。解析不出引用 → 不放行。
52
+ // 两侧日期同须 UTC 口径(台账渲染 UTC ts;spec「日期」fill-spec 默认 UTC,手写见 _TEMPLATE.md 验收判据注):
53
+ // 本地时区手写日期会在 UTC+8 0–8 点窗与全绿行错位一天——fail-closed,不误放。
54
+ function commandCovered(ev, specDate, greenDates) {
55
+ const cites = ev.match(CITE_RE) || [];
56
+ if (!cites.length) return false;
57
+ const fresh = greenDates.some((g) => g >= specDate);
58
+ return cites.every((c) => GATE_CMD.has(c) && fresh);
59
+ }
60
+
61
+ {
62
+ const anchor = kitPolicy.convergenceSince;
63
+ const hardAnchor = kitPolicy.convergenceHardSince;
64
+ if (anchor) {
65
+ const greenDates = greenEvidenceDates();
66
+ const isPlaceholder = (s) => !s || /^<.*>$/.test(s);
67
+ for (const spec of docFiles('specs')) {
68
+ const st = fmGet(spec, '状态');
69
+ const lvl = fmGet(spec, '级别');
70
+ if (st !== 'done' || !['L2', 'L3'].includes(lvl)) continue;
71
+ // 生效日锚:frontmatter「日期」优先(fill-spec 必写),缺则 git 首加日 / 文件名前缀(同检查 12 退化序)
72
+ let d = fmGet(spec, '日期') || addedDateOf(spec);
73
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(d)) d = /^\d{4}-\d{2}-\d{2}/.test(path.basename(spec)) ? path.basename(spec).slice(0, 10) : '';
74
+ if (!d || d < anchor) continue; // 锚前存量不回填
75
+ const hard = !!(hardAnchor && d >= hardAnchor);
76
+ const lines = linesOf(spec) || [];
77
+ const secLines = [];
78
+ let inSec = false;
79
+ for (const l of lines) {
80
+ if (SEC_RE.test(l)) { inSec = true; continue; }
81
+ if (inSec && /^##\s+/.test(l)) { inSec = false; continue; }
82
+ if (inSec) secLines.push(l);
83
+ }
84
+ // 解析表数据行(表头才是数据边界——沿用 agg-delegations splitTables 口径;分隔行/表头行/占位行跳过)
85
+ const rows = secLines
86
+ .filter((l) => /^\|.*\|\s*$/.test(l))
87
+ .filter((l) => !/^\|[\s:|-]+\|\s*$/.test(l))
88
+ .map((l) => l.split('|').slice(1, -1).map((c) => c.trim().replace(/^`|`$/g, '')))
89
+ .filter((cells) => cells.length >= 3)
90
+ .filter((cells) => !cells[1].startsWith('证据(')) // 表头行
91
+ .filter((cells) => !isPlaceholder(cells[0])); // 模板占位行
92
+ const base = path.basename(spec);
93
+ if (!rows.length) {
94
+ warnings.push(`- [WARN 收敛无判据] ${base} 无「验收判据」表或表为空——L2/L3 done spec 须逐条登记可验判据(检查 23,模板 workflow/specs/_TEMPLATE.md)`);
95
+ continue;
96
+ }
97
+ const missed = [];
98
+ for (const [crit, ev, stat] of rows.map((c) => [c[0], c[1], (c[2] || '').trim()])) {
99
+ if (stat === '人工' && isPlaceholder(ev)) continue; // 人工走查判据:空证据合法
100
+ let ok = false;
101
+ if (!isPlaceholder(ev)) {
102
+ if (hard && CMD_RE.test(ev)) ok = commandCovered(ev, d, greenDates);
103
+ else if (CMD_RE.test(ev)) ok = true; // advisory:命令白名单认声明
104
+ else {
105
+ const cands = ev.match(PATH_RE) || [];
106
+ ok = cands.some((p) => fs.existsSync(path.join(ROOT, p)));
107
+ }
108
+ }
109
+ if (!ok) missed.push(`${crit}→${ev || '(空)'}`);
110
+ }
111
+ if (missed.length) {
112
+ const detail = `${base} ${rows.length} 条判据中 ${missed.length} 条证据无效(${missed.join('; ')})`;
113
+ if (hard) blockers.push(`- [收敛未覆盖] ${detail}——状态列自报不作数;命令须是全量门(npm test / verify.mjs)且 workflow/verify-evidence.md 有不早于本 spec 日期的全绿行;路径只认存在(检查 23)。若本 spec 已标 done:先标后补会与本门互为前提(verify 门序含 check-loop)——解锁序:状态临时改回非 done → 跑 node .agents/scripts/verify.mjs 全量门(全绿尾部自动重生成 workflow/verify-evidence.md 全绿行)→ 再标 done`);
114
+ else warnings.push(`- [WARN 收敛未覆盖] ${detail}——状态列自报不作数,证据须存在或为验证命令(检查 23)`);
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }