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,65 @@
1
+ // judgment-ledger.mjs — check-loop 判定流水落账(2026-10-11 judgment-ledger,灰度消费端数据底座)
2
+ // 背景:机制月度对账原为「人工 grep check-loop 输出」;advisory→hard 升格与退役判定
3
+ // 均缺 per-rule 判定流水(时间戳+结论)。本模块把每次 check-loop 真实运行追加为一行 per-rule 计数
4
+ // 到 .agents/judgments.jsonl(本地,gitignore——「本机机器事实不跨机共享」口径,同 verifications.jsonl)。
5
+ // 消费:node .agents/scripts/agg-judgments.mjs(月度×规则计数);mechanism-registry 月度对账直接查账
6
+ // (机制登记表模板 .agents/mechanism-registry.md)。
7
+ // 诚实边界:非签名链(同 verify-evidence)——本地追加可被改,事后对质靠 git 历史与确认台账。
8
+ // fail-open:落账失败只打 stderr,不改变门禁裁决(观测面不得挡住裁决面)。
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { spawnSync } from 'node:child_process';
12
+
13
+ export const LEDGER_REL = path.join('.agents', 'judgments.jsonl');
14
+
15
+ // tallyJudgments(entries):'- [WARN 标签] …' → {rule,level:'WARN'};'- [标签] …' → {rule,level:'BLOCK'};
16
+ // 无括号标签行 → {rule:'未标注', level:按来源}。输出按 rule 字典序的 {rule,level,count} 数组(确定性,测试可断言)。
17
+ export function tallyJudgments(blockers = [], warnings = []) {
18
+ const tally = new Map();
19
+ const add = (raw, fallbackLevel) => {
20
+ const m = String(raw).match(/^\s*-\s*\[([^\]]+)\]/);
21
+ let rule = '未标注';
22
+ let level = fallbackLevel;
23
+ if (m) {
24
+ const tag = m[1].trim();
25
+ if (/^WARN\b/.test(tag)) { level = 'WARN'; rule = tag.replace(/^WARN\s*/, ''); }
26
+ else { rule = tag; }
27
+ }
28
+ const key = `${level}\u0000${rule}`;
29
+ const cur = tally.get(key) || { rule, level, count: 0 };
30
+ cur.count++;
31
+ tally.set(key, cur);
32
+ };
33
+ for (const b of blockers || []) add(b, 'BLOCK');
34
+ for (const w of warnings || []) add(w, 'WARN');
35
+ return [...tally.values()].sort((a, b) => (a.level + a.rule).localeCompare(b.level + b.rule));
36
+ }
37
+
38
+ const shortRev = (root) => {
39
+ try {
40
+ const r = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf8' });
41
+ return r.status === 0 ? String(r.stdout).trim() || null : null;
42
+ } catch {
43
+ return null;
44
+ }
45
+ };
46
+
47
+ // recordRun({ root, mode, blockers, warnings, now }):追加一行运行判定;守卫 fixture(CHECK_LOOP_ROOT)
48
+ // 与显式关闭(CHECK_LOOP_NO_LEDGER);任何失败 fail-open(stderr 一行,不 throw、不改 exit code)。
49
+ export function recordRun({ root, mode, blockers, warnings, now = new Date() }) {
50
+ try {
51
+ if (process.env.CHECK_LOOP_ROOT || process.env.CHECK_LOOP_NO_LEDGER) return;
52
+ const row = {
53
+ ts: now.toISOString(),
54
+ mode: mode || 'hard',
55
+ rev: shortRev(root),
56
+ blockers: (blockers || []).length,
57
+ warnings: (warnings || []).length,
58
+ rules: tallyJudgments(blockers, warnings),
59
+ };
60
+ fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
61
+ fs.appendFileSync(path.join(root, LEDGER_REL), `${JSON.stringify(row)}\n`);
62
+ } catch (err) {
63
+ console.error(`judgment-ledger: 落账失败(不阻断门禁)——${err.code || err.message}`);
64
+ }
65
+ }
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ // judgment-ledger.test.mjs — 判定流水落账套件(2026-10-11 judgment-ledger)
3
+ // 纯函数 tallyJudgments 进程内断言 + recordRun 落账行为(临时根目录 + 环境守卫)。
4
+ // 守卫口径:CHECK_LOOP_ROOT(fixture 模式)/ CHECK_LOOP_NO_LEDGER 任一在即不落账——
5
+ // 进程内切换环境变量须保存/恢复,防污染同进程后续用例。
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { tallyJudgments, recordRun, LEDGER_REL } from './judgment-ledger.mjs';
12
+
13
+ let pass = 0;
14
+ let fail = 0;
15
+ const check = (desc, cond, detail = '') => {
16
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
17
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
18
+ };
19
+
20
+ // ---- S1 tallyJudgments:标签解析 / 级别分流 / 计数 / 确定性排序 ----
21
+ {
22
+ const out = tallyJudgments(
23
+ ['- [配对断裂] incident 缺 plan:x.md', '- [配对断裂] incident 缺 plan:y.md', '- [回路断档] 选「是」未指明 intent'],
24
+ ['- [WARN 三件套不全] x.md 缺之 2', '- [WARN 归因缺字段] x.md 缺「来源:」', '- [WARN 三件套不全] y.md 缺之 3', '无标签散行'],
25
+ );
26
+ check('S1 BLOCK/WARN 分流计数', out.some((r) => r.rule === '配对断裂' && r.level === 'BLOCK' && r.count === 2)
27
+ && out.some((r) => r.rule === '回路断档' && r.level === 'BLOCK' && r.count === 1)
28
+ && out.some((r) => r.rule === '三件套不全' && r.level === 'WARN' && r.count === 2)
29
+ && out.some((r) => r.rule === '归因缺字段' && r.level === 'WARN' && r.count === 1), JSON.stringify(out));
30
+ check('S1 无标签行归「未标注」WARN', out.some((r) => r.rule === '未标注' && r.level === 'WARN' && r.count === 1), JSON.stringify(out));
31
+ const keys = out.map((r) => r.level + r.rule);
32
+ // 与实现同款 localeCompare 排序比对——被测不变量是「确定性」而非特定 collation(中文 locale 与码位排序不同)
33
+ const sorted = [...keys].sort((a, b) => a.localeCompare(b));
34
+ check('S1 输出按 level+rule 排序(确定性)', JSON.stringify(keys) === JSON.stringify(sorted), JSON.stringify(keys));
35
+ check('S1 空入参 → 空数组', tallyJudgments([], []).length === 0);
36
+ }
37
+
38
+ // ---- S2 recordRun:真落账(临时根,rev 为 null 因非 git 目录)----
39
+ {
40
+ const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
41
+ delete process.env.CHECK_LOOP_ROOT;
42
+ delete process.env.CHECK_LOOP_NO_LEDGER;
43
+ try {
44
+ const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-'));
45
+ const now = new Date('2026-10-11T01:02:03.000Z');
46
+ recordRun({ root: T, mode: 'hygiene', blockers: ['- [配对断裂] a'], warnings: ['- [WARN 归因缺字段] b'], now });
47
+ const p = path.join(T, LEDGER_REL);
48
+ const rows = fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l)) : [];
49
+ check('S2 追加一行且字段齐(ts/mode/rev/计数/rules)',
50
+ rows.length === 1 && rows[0].ts === '2026-10-11T01:02:03.000Z' && rows[0].mode === 'hygiene'
51
+ && rows[0].blockers === 1 && rows[0].warnings === 1
52
+ && Array.isArray(rows[0].rules) && rows[0].rules.length === 2, JSON.stringify(rows));
53
+ recordRun({ root: T, mode: 'hard', blockers: [], warnings: [], now });
54
+ const lines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean);
55
+ check('S2 二次调用追加不覆盖(append-only)', lines.length === 2, String(lines.length));
56
+ } finally {
57
+ if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot;
58
+ if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger;
59
+ }
60
+ }
61
+
62
+ // ---- S3 双守卫:CHECK_LOOP_ROOT / CHECK_LOOP_NO_LEDGER 任一在 → 不落账 ----
63
+ {
64
+ const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
65
+ try {
66
+ for (const guard of ['CHECK_LOOP_ROOT', 'CHECK_LOOP_NO_LEDGER']) {
67
+ delete process.env.CHECK_LOOP_ROOT;
68
+ delete process.env.CHECK_LOOP_NO_LEDGER;
69
+ process.env[guard] = '1';
70
+ const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-guard-'));
71
+ recordRun({ root: T, mode: 'hard', blockers: [], warnings: [] });
72
+ check(`S3 ${guard} 在 → 不落账`, !fs.existsSync(path.join(T, LEDGER_REL)));
73
+ }
74
+ } finally {
75
+ if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot; else delete process.env.CHECK_LOOP_ROOT;
76
+ if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger; else delete process.env.CHECK_LOOP_NO_LEDGER;
77
+ }
78
+ }
79
+
80
+ // ---- S4 落账失败 fail-open(root 指向文件而非目录 → append 必败,只 stderr 不 throw)----
81
+ {
82
+ const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
83
+ delete process.env.CHECK_LOOP_ROOT;
84
+ delete process.env.CHECK_LOOP_NO_LEDGER;
85
+ try {
86
+ const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-bad-'));
87
+ const filePath = path.join(T, '.agents'); // 占住 .agents 为普通文件 → mkdirSync 必败
88
+ fs.writeFileSync(filePath, 'not a dir');
89
+ let threw = false;
90
+ try {
91
+ recordRun({ root: T, mode: 'hard', blockers: [], warnings: [] });
92
+ } catch {
93
+ threw = true;
94
+ }
95
+ check('S4 落账失败不 throw(fail-open)', threw === false);
96
+ } finally {
97
+ if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot; else delete process.env.CHECK_LOOP_ROOT;
98
+ if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger; else delete process.env.CHECK_LOOP_NO_LEDGER;
99
+ }
100
+ }
101
+
102
+ // ---- S5 端到端:真实 check-loop.mjs fixture 运行不落账(CHECK_LOOP_ROOT 守卫在 spawn 态同样生效)----
103
+ {
104
+ const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-e2e-'));
105
+ for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(T, 'workflow', s), { recursive: true });
106
+ fs.mkdirSync(path.join(T, '.agents'), { recursive: true });
107
+ // 枚举单源随夹具内联(kit 契约:.agents/workflow-enums.txt,缺文件 fail-loud)——键集沿 verify.test.mjs mkfix 同款
108
+ fs.writeFileSync(path.join(T, '.agents', 'workflow-enums.txt'), [
109
+ 'doc.status.all=draft approved done superseded cancelled',
110
+ 'doc.status.confirmed=approved done superseded cancelled',
111
+ 'doc.status.active=draft approved',
112
+ 'doc.status.terminal=done superseded cancelled',
113
+ 'doc.status.abandoned=superseded cancelled',
114
+ 'incident.status.all=open fixed closed',
115
+ 'incident.status.active=open',
116
+ 'level.all=L1 L2 L3',
117
+ '',
118
+ ].join('\n'), 'utf8');
119
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
120
+ // 裸跑=默认硬门(2026-10-13 分道,kit 与源仓同构 hard/hygiene/all 三面)——落账守卫断言与运行面无关,裸跑即可
121
+ const r = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'check-loop.mjs')],
122
+ { env: { ...process.env, CHECK_LOOP_ROOT: T }, cwd: T, encoding: 'utf8' });
123
+ check('S5 fixture 态 spawn 运行不落账(守卫在子进程同样生效)',
124
+ !fs.existsSync(path.join(T, LEDGER_REL)), `exit=${r.status} ${r.stderr || ''}`);
125
+ fs.rmSync(T, { recursive: true, force: true });
126
+ }
127
+
128
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
129
+ process.exit(fail ? 1 : 0);
@@ -5,6 +5,7 @@
5
5
  // 语料与口径
6
6
  // workflow(流程留痕):只搜 frontmatter + H1 + 指定节(背景 / 目标 / 根因 / 复盘三件套 / 验收标准 / 影响面),
7
7
  // 默认全状态可命中、活跃(draft/approved/open)排序 ×1.3 优先;输出带「节名 + 行号」便于节级读取。
8
+ // 语料面含归档批次 workflow/archive/<批次>/<type>/(B10a 自源仓随动回灌)——归档件仍可检索(防复发不丢历史)。
8
9
  // wiki(领域知识):沿用 2026-09-21 降噪口径——默认跳过 .json/.csv、*.visual-check.* 与 >180KB bulk 文件。
9
10
  // 两侧排序均为 命中数/√文件KB(防大文件靠体量霸榜);`-n` 为每语料展示上限。
10
11
  // 结构缓存(2026-09-23 intent kb-search-structure-cache):可检索子集预提取进 .agents/cache/kb-index.json,
@@ -231,9 +232,22 @@ function saveCache() {
231
232
  function searchWorkflow(words) {
232
233
  const results = [];
233
234
  for (const type of (typeFilter || DOC_TYPES)) {
234
- const dir = path.join('workflow', type);
235
- if (!fs.existsSync(dir)) continue;
236
- for (const f of fs.readdirSync(dir)) {
235
+ // 活跃目录 + 归档批次(workflow/archive/<批次>/<type>/,2026-10-09;B10a 自源仓随动回灌)——
236
+ // 归档件仍可检索(防复发不丢历史),标签用相对 workflow/ 的路径,活跃目录标签与旧口径逐字一致。
237
+ // 缓存键/失效语义随动:归档件按自身路径独立入缓存键,git mv 后旧活跃路径键由 saveCache
238
+ // 的 stat 清扫回收([mtimeMs,size] 逐文件失效不变);可检索子集提取函数未变 → 口径指纹不变。
239
+ const dirs = [path.join('workflow', type)];
240
+ const arcRoot = path.join('workflow', 'archive');
241
+ if (fs.existsSync(arcRoot)) {
242
+ for (const batch of fs.readdirSync(arcRoot)) {
243
+ const d = path.join(arcRoot, batch, type);
244
+ if (fs.existsSync(d)) dirs.push(d);
245
+ }
246
+ }
247
+ for (const dir of dirs) {
248
+ if (!fs.existsSync(dir)) continue;
249
+ const rel = path.relative('workflow', dir).split(path.sep).join('/');
250
+ for (const f of fs.readdirSync(dir)) {
237
251
  if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
238
252
  const p = path.join(dir, f);
239
253
  const e = cachedEntry(p, 'wf', () => {
@@ -252,10 +266,11 @@ function searchWorkflow(words) {
252
266
  const kb = e.st[1] / 1024;
253
267
  const active = ACTIVE_STATUS.includes(status);
254
268
  results.push({
255
- file: `${type}/${f}`, title: e.title || '', status, level: meta['级别'] || '—', module,
269
+ file: `${rel}/${f}`, title: e.title || '', status, level: meta['级别'] || '—', module,
256
270
  total: hits.total, shown: hits.shown, kb,
257
271
  score: (hits.total / Math.sqrt(Math.max(kb, 0.1))) * (active ? 1.3 : 1),
258
272
  });
273
+ }
259
274
  }
260
275
  }
261
276
  return results.sort((a, b) => b.score - a.score);
@@ -258,6 +258,39 @@ const files = (out) => (out.match(/^📄 (workflow\/\S+)/gm) || []).map((l) => l
258
258
  fs.rmSync(root, { recursive: true, force: true });
259
259
  }
260
260
 
261
+ // ---- 场景 13:归档扩面(B10a 自源仓随动回灌)——workflow/archive/<批次>/<type>/ 参与检索 ----
262
+ // 归档件命中带归档路径标签;无归档语料时行为与旧口径逐字一致;缓存键随动(归档件独立入键)
263
+ {
264
+ const root = mkfix();
265
+ // 造归档批次:与活跃件同词命中(含终态件 frontmatter,可命中)
266
+ fs.mkdirSync(path.join(root, 'workflow', 'archive', '2026-09', 'plans'), { recursive: true });
267
+ fs.writeFileSync(path.join(root, 'workflow', 'archive', '2026-09', 'plans', '2026-09-01-arch.md'),
268
+ doc('状态: done\n级别: L1\n模块: purchase', 'PLAN — 归档件', [['目标', '- 归档批次里的编码模板历史档']]), 'utf8');
269
+ const r = run(root, ['--scope', 'workflow', '编码模板']);
270
+ check('场景 13a 归档件参与检索且带归档路径标签',
271
+ r.status === 0 && files(r.stdout).some((f) => f === 'workflow/archive/2026-09/plans/2026-09-01-arch.md'), r.stdout);
272
+ check('场景 13b 活跃目录标签与旧口径逐字一致(不带 archive 段)',
273
+ files(r.stdout).some((f) => f === 'workflow/plans/2026-09-20-ccc.md'), files(r.stdout).join('\n'));
274
+ // 归档件状态 done → --status active 时被状态过滤排除(过滤口径对归档件照常生效)
275
+ const a = run(root, ['--scope', 'workflow', '--status', 'active', '编码模板']);
276
+ check('场景 13c --status active 排除终态归档件',
277
+ !a.stdout.includes('2026-09-01-arch.md') && a.stdout.includes('2026-09-21-bbb.md'), a.stdout);
278
+ // 缓存随动:warm 复跑与冷扫逐字节一致(归档件已入缓存键)
279
+ const warm = run(root, ['--scope', 'workflow', '编码模板']);
280
+ check('场景 13d 归档件入缓存后 warm ≡ 前次输出(逐字节)', warm.status === 0 && warm.stdout === r.stdout,
281
+ `warm:\n${warm.stdout}\nfirst:\n${r.stdout}`);
282
+ fs.rmSync(root, { recursive: true, force: true });
283
+ }
284
+
285
+ // ---- 场景 14:无归档语料 → 行为不变(对照锚——扩面不得扰动既有输出)----
286
+ {
287
+ const root = mkfix(); // mkfix 不建 workflow/archive/
288
+ const before = run(root, ['--scope', 'workflow', '编码模板']);
289
+ check('场景 14 无 archive 目录时输出照旧(3 篇活跃件命中)',
290
+ before.status === 0 && files(before.stdout).length === 3 && !before.stdout.includes('archive/'), before.stdout);
291
+ fs.rmSync(root, { recursive: true, force: true });
292
+ }
293
+
261
294
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
262
295
  if (fail) {
263
296
  console.log(`\n${USAGE}`);
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ // lane-surface-audit.mjs — L2 触达面闭集防漂移审计(advisory 工具,恒 exit 0;2026-10-13 自
3
+ // Shipyard.Material 参考实现回灌,源仓沿革留痕在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
4
+ // 问题:lane-surfaces.txt 的 ERE 闭集靠人工维护——新共享契约面若未登记,判低拦截对新面静默失效。
5
+ // 本审计反着问:**近窗口内 L2/L3 任务实际反复触及、却无任何文件被面清单命中的目录** = 候选漏登面,
6
+ // 供人工评估补登(不建议自动写清单——高频触及 ≠ 契约面,机器不猜语义)。
7
+ // 归因代理:提交触及某入口/事故文档的提交 = 该任务的落地提交(三件套随代码同一提交纪律),
8
+ // 取这些提交里的代码文件(剔 workflow/、.agents/、.githooks/、wiki/、根 AGENTS.md)作「任务触及面」。
9
+ // 已知粗度:任务中途未触文档的纯代码提交会被漏计(审计低招,不是执法——宁可漏报不误报)。
10
+ // 判据:目录签名(dirname 前 3 段)被 ≥ --min(默认 3)个**不同** L2/L3 文档触及,且该签名下
11
+ // 观测到的文件无一命中 ERE → 候选。被命中的文件所在目录不进候选(已覆盖)。
12
+ // 用法:node .agents/scripts/lane-surface-audit.mjs [--since <YYYY-MM-DD>] [--min <N>] [--root <dir>]
13
+ // --since 缺省 = 今天−30 天。输出候选清单;无候选打印一行「无漂移信号」。
14
+ // 时机:月度手跑(登记位 = .agents/lane-surfaces.txt 文件头);改共享契约频繁期可随时手跑。
15
+ // 测试:node .agents/scripts/lane-surface-audit.test.mjs(git fixture 注入)
16
+ import fs from 'node:fs';
17
+ import path from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
19
+
20
+ const argv = process.argv.slice(2);
21
+ const argOf = (k) => {
22
+ const i = argv.indexOf(k);
23
+ return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
24
+ };
25
+ const ROOT = argOf('--root') || (() => {
26
+ const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' });
27
+ return r.status === 0 ? String(r.stdout).trim() : '';
28
+ })() || process.cwd();
29
+ const SINCE = argOf('--since') || new Date(Date.now() - 30 * 86400_000).toISOString().slice(0, 10);
30
+ const MIN = Number(argOf('--min')) || 3;
31
+
32
+ const gitOut = (args) => {
33
+ const r = spawnSync('git', ['-c', 'core.quotepath=off', ...args], { cwd: ROOT, encoding: 'utf8' });
34
+ return r.status === 0 ? String(r.stdout || '') : '';
35
+ };
36
+
37
+ // 面清单(解析同 check-lane-surface.mjs:逐行 ERE,# 注释与空行忽略——段标题行同为注释,不参与命中)
38
+ const loadPatterns = () => {
39
+ const file = path.join(ROOT, '.agents', 'lane-surfaces.txt');
40
+ if (!fs.existsSync(file)) return [];
41
+ return fs.readFileSync(file, 'utf8')
42
+ .split(/\r?\n/)
43
+ .map((l) => l.trim())
44
+ .filter((l) => l && !l.startsWith('#'))
45
+ .map((l) => { try { return new RegExp(l); } catch { return null; } })
46
+ .filter(Boolean);
47
+ };
48
+ const patterns = loadPatterns();
49
+ const isCovered = (p) => patterns.some((re) => re.test(p));
50
+
51
+ // frontmatter 读取(CRLF 容忍,同 check-lane-surface.fmOf)
52
+ const fmOf = (file) => {
53
+ const fm = {};
54
+ let lines;
55
+ try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); } catch { return fm; }
56
+ if (!/^---\s*$/.test(lines[0] || '')) return fm;
57
+ for (let i = 1; i < lines.length; i++) {
58
+ if (/^---\s*$/.test(lines[i])) break;
59
+ const m = lines[i].match(/^([^:]+):\s*(.*)$/);
60
+ if (m) fm[m[1].trim()] = m[2].trim();
61
+ }
62
+ return fm;
63
+ };
64
+
65
+ // 1) 窗口内 L2/L3 文档(任何状态——审计覆盖已关单历史;日期键 intent=日期 / incident=发现)
66
+ const docs = [];
67
+ for (const [dir, dateKey] of [['intents', '日期'], ['incidents', '发现']]) {
68
+ const dirPath = path.join(ROOT, 'workflow', dir);
69
+ if (!fs.existsSync(dirPath)) continue;
70
+ for (const f of fs.readdirSync(dirPath).sort()) {
71
+ if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
72
+ const fm = fmOf(path.join(dirPath, f));
73
+ if ((fm['级别'] || '') !== 'L2' && (fm['级别'] || '') !== 'L3') continue;
74
+ if ((fm[dateKey] || '') < SINCE) continue;
75
+ docs.push(`workflow/${dir}/${f}`);
76
+ }
77
+ }
78
+ if (!docs.length) {
79
+ console.log(`[lane-audit] 窗口(${SINCE} 起)内无 L2/L3 文档——无审计面。`);
80
+ process.exit(0);
81
+ }
82
+
83
+ // 2) 一遍 git log 取 (commit → files) 与文档↔提交关联(--name-only 块解析;doc 路径含非 ASCII 须 quotepath=off)
84
+ const log = gitOut(['log', '--name-only', '--format=%H', '--', ...docs]);
85
+ if (!log) {
86
+ console.error('[lane-audit] git log 不可用(非 git 环境或无历史)——跳过审计。');
87
+ process.exit(0);
88
+ }
89
+ const filesByDoc = new Map(); // doc → Set(代码文件)
90
+ let commit = '';
91
+ const commitsForDoc = new Map(); // doc → Set(commit)
92
+ for (const line of log.split(/\r?\n/)) {
93
+ if (!line.trim()) continue;
94
+ if (/^[0-9a-f]{40}$/.test(line)) { commit = line; continue; }
95
+ const p = line.replace(/"/g, ''); // quotepath=off 已关转义,防御性去引号
96
+ if (docs.includes(p)) {
97
+ if (!commitsForDoc.has(p)) commitsForDoc.set(p, new Set());
98
+ commitsForDoc.get(p).add(commit);
99
+ }
100
+ }
101
+ for (const [doc, commits] of commitsForDoc) {
102
+ const set = new Set();
103
+ for (const c of commits) {
104
+ for (const f of gitOut(['show', '--name-only', '--format=', c]).split(/\r?\n/).filter(Boolean)) {
105
+ const fp = f.replace(/"/g, '');
106
+ if (/^(workflow|\.agents|\.githooks|wiki)\//.test(fp) || /\.md$/.test(fp) || fp === 'AGENTS.md') continue;
107
+ set.add(fp);
108
+ }
109
+ }
110
+ filesByDoc.set(doc, set);
111
+ }
112
+
113
+ // 3) 目录签名聚合(dirname 前 3 段);仅聚合「未被 ERE 命中」的文件;按不同文档数计
114
+ const sigOf = (fp) => {
115
+ const parts = path.posix.dirname(fp.replace(/\\/g, '/')).split('/');
116
+ return parts.slice(0, 3).join('/');
117
+ };
118
+ const docsBySig = new Map(); // sig → Set(doc)
119
+ const samplesBySig = new Map(); // sig → Set(file)
120
+ for (const [doc, files] of filesByDoc) {
121
+ for (const fp of files) {
122
+ if (isCovered(fp)) continue;
123
+ const sig = sigOf(fp);
124
+ if (!docsBySig.has(sig)) { docsBySig.set(sig, new Set()); samplesBySig.set(sig, new Set()); }
125
+ docsBySig.get(sig).add(doc);
126
+ samplesBySig.get(sig).add(fp);
127
+ }
128
+ }
129
+ const candidates = [...docsBySig.entries()]
130
+ .filter(([, ds]) => ds.size >= MIN)
131
+ .sort((a, b) => b[1].size - a[1].size);
132
+
133
+ if (!candidates.length) {
134
+ console.log(`[lane-audit] ✅ 无漂移信号——窗口(${SINCE} 起)${docs.length} 份 L2/L3 文档的反复触及目录均被面清单覆盖(或未达阈值 ${MIN})。`);
135
+ process.exit(0);
136
+ }
137
+ console.log(`[lane-audit] ⚠️ 候选漏登面 ${candidates.length} 个(窗口 ${SINCE} 起 / 阈值 ≥${MIN} 个不同 L2/L3 任务 / ERE 均未命中)——人工评估是否补登 .agents/lane-surfaces.txt:`);
138
+ for (const [sig, ds] of candidates) {
139
+ const samples = [...samplesBySig.get(sig)].slice(0, 3).join(' ');
140
+ console.log(` - ${sig}(${ds.size} 个任务;示例 ${samples})`);
141
+ }
142
+ console.log(' 判据提醒:仅「一处定义、多处消费」的契约/规则面才应登记(判据单源 .agents/scripts/level-criteria.mjs 的 L2_SURFACES)——高频触及 ≠ 契约面。');
143
+ process.exit(0);
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+ // lane-surface-audit.test.mjs — 闭集审计 fixture 套件(2026-10-13 自 Shipyard.Material 参考实现回灌)
3
+ // 方法:临时 git 仓构造「L2 文档 + 同笔代码提交」历史,断言候选目录判定(阈值/ERE 覆盖排除/无候选静默)。
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { spawnSync } from 'node:child_process';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
11
+ const SCRIPT = path.join(SCRIPT_DIR, 'lane-surface-audit.mjs');
12
+
13
+ let pass = 0;
14
+ let fail = 0;
15
+ const check = (desc, cond, detail = '') => {
16
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
17
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
18
+ };
19
+
20
+ const mkrepo = () => {
21
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lane-audit-test-'));
22
+ const g = (a) => spawnSync('git', ['-c', 'core.quotepath=off', ...a], { cwd: d, encoding: 'utf8' });
23
+ g(['init', '-q', '-b', 'main']); g(['config', 'user.email', 't@t']); g(['config', 'user.name', 't']);
24
+ for (const s of ['intents', 'incidents']) fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
25
+ fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
26
+ w(d, '.agents/lane-surfaces.txt', '## product\n^backend/shared/\n');
27
+ const commit = (msg) => { g(['add', '-A']); g(['commit', '-q', '-m', msg]); };
28
+ return { d, g, commit };
29
+ };
30
+ const w = (root, rel, content) => { fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); };
31
+ const intent = (name, level, date) => `---\n状态: done\n级别: ${level}\n日期: ${date}\n模块: pipeline\n---\n# INTENT — ${name}\n`;
32
+ const run = (root, ...args) => {
33
+ const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
34
+ return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
35
+ };
36
+
37
+ // 场景 1:3 个不同 L2 任务反复触及未覆盖目录 → 候选;已覆盖目录(backend/shared)不进候选
38
+ {
39
+ const { d, commit } = mkrepo();
40
+ for (let i = 1; i <= 3; i++) {
41
+ w(d, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
42
+ w(d, `backend/material/Shipyard.Material.Application/Services/S${i}.cs`, `// ${i}\n`);
43
+ w(d, 'backend/shared/Covered.cs', `// c${i}\n`); // 同任务也触已覆盖面
44
+ commit(`task ${i}`);
45
+ }
46
+ const r = run(d, '--since', '2026-10-01');
47
+ check('场景 1:未覆盖目录 3 任务 → 候选列出且提示人工评估;已覆盖目录不出现',
48
+ r.code === 0 && r.out.includes('候选漏登面 1 个') && r.out.includes('backend/material/Shipyard.Material.Application') && r.out.includes('lane-surfaces.txt') && !r.out.includes('backend/shared/'), r.out);
49
+ }
50
+ // 场景 2:阈值未达(2 个任务)→ 无候选静默
51
+ {
52
+ const { d, commit } = mkrepo();
53
+ for (let i = 1; i <= 2; i++) {
54
+ w(d, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
55
+ w(d, `backend/material/Shipyard.Material.Application/Services/S${i}.cs`, `// ${i}\n`);
56
+ commit(`task ${i}`);
57
+ }
58
+ const r = run(d, '--since', '2026-10-01');
59
+ check('场景 2:2 任务未达阈值 3 → 无漂移信号', r.code === 0 && r.out.includes('无漂移信号'), r.out);
60
+ }
61
+ // 场景 3:L1 文档不计数(级别过滤)+ --min 覆盖(2 任务 + min 2 → 候选)
62
+ {
63
+ const { d, commit } = mkrepo();
64
+ for (let i = 1; i <= 2; i++) {
65
+ w(d, `workflow/intents/2026-10-0${i}-l${i}.md`, intent(`l${i}`, 'L1', `2026-10-0${i}`));
66
+ w(d, 'frontend/src/views/Some/V.vue', `// ${i}\n`);
67
+ commit(`l ${i}`);
68
+ }
69
+ const r1 = run(d, '--since', '2026-10-01');
70
+ check('场景 3a:纯 L1 不入审计面(无 L2/L3 文档)', r1.code === 0 && r1.out.includes('无 L2/L3 文档'), r1.out);
71
+ const { d: d2, commit: c2 } = mkrepo();
72
+ for (let i = 1; i <= 2; i++) {
73
+ w(d2, `workflow/intents/2026-10-0${i}-t${i}.md`, intent(`t${i}`, 'L2', `2026-10-0${i}`));
74
+ w(d2, 'frontend/src/views/Some/V.vue', `// ${i}\n`);
75
+ c2(`t ${i}`);
76
+ }
77
+ const r2 = run(d2, '--since', '2026-10-01', '--min', '2');
78
+ check('场景 3b:--min 2 时 2 任务 → 候选', r2.code === 0 && r2.out.includes('frontend/src/views'), r2.out);
79
+ }
80
+ // 场景 4:incident 等价(发现日期键)入审计面
81
+ {
82
+ const { d, commit } = mkrepo();
83
+ for (let i = 1; i <= 3; i++) {
84
+ w(d, `workflow/incidents/2026-10-0${i}-inc${i}.md`, `---\n状态: closed\n级别: L3\n发现: 2026-10-0${i}\n模块: pipeline\n---\n# INCIDENT\n`);
85
+ w(d, `db/procs/P${i}.sql`, `-- ${i}\n`);
86
+ commit(`inc ${i}`);
87
+ }
88
+ const r = run(d, '--since', '2026-10-01');
89
+ check('场景 4:incident(发现键)×3 → db/procs 候选', r.code === 0 && r.out.includes('db/procs'), r.out);
90
+ }
91
+
92
+ console.log(`\n${fail ? `❌ ${fail} failed` : '✅ all passed'}(${pass} passed)`);
93
+ process.exit(fail ? 1 : 0);