agentic-flow-kit 1.7.0 → 1.7.16

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 (162) hide show
  1. package/README.md +81 -9
  2. package/modules/gates/README.md +1 -0
  3. package/modules/gates/test-protect/README.md +23 -0
  4. package/modules/gates/test-protect/check-test-protect.mjs +64 -0
  5. package/modules/gates/test-protect/local-pre-commit.line +1 -0
  6. package/modules/hosts/README.md +57 -0
  7. package/modules/hosts/claude/commands/wf-build.md +5 -24
  8. package/modules/hosts/claude/commands/wf-deploy.md +1 -0
  9. package/modules/hosts/claude/commands/wf-design.md +7 -6
  10. package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
  11. package/modules/hosts/claude/commands/wf-new-task.md +42 -21
  12. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  13. package/modules/hosts/claude/commands/wf-review.md +1 -1
  14. package/modules/hosts/claude/commands/wf-sync-hosts.md +8 -16
  15. package/modules/hosts/claude/commands/wf-test.md +5 -4
  16. package/modules/hosts/codex/commands/wf-build.md +5 -24
  17. package/modules/hosts/codex/commands/wf-deploy.md +1 -0
  18. package/modules/hosts/codex/commands/wf-design.md +7 -6
  19. package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
  20. package/modules/hosts/codex/commands/wf-new-task.md +42 -21
  21. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  22. package/modules/hosts/codex/commands/wf-review.md +1 -1
  23. package/modules/hosts/codex/commands/wf-sync-hosts.md +8 -16
  24. package/modules/hosts/codex/commands/wf-test.md +5 -4
  25. package/modules/hosts/cursor/commands/wf-build.md +5 -24
  26. package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
  27. package/modules/hosts/cursor/commands/wf-design.md +7 -6
  28. package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
  29. package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
  30. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  31. package/modules/hosts/cursor/commands/wf-review.md +1 -1
  32. package/modules/hosts/cursor/commands/wf-sync-hosts.md +8 -16
  33. package/modules/hosts/cursor/commands/wf-test.md +5 -4
  34. package/modules/hosts/opencode/commands/wf-build.md +5 -24
  35. package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
  36. package/modules/hosts/opencode/commands/wf-design.md +7 -6
  37. package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
  38. package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
  39. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  40. package/modules/hosts/opencode/commands/wf-review.md +1 -1
  41. package/modules/hosts/opencode/commands/wf-sync-hosts.md +8 -16
  42. package/modules/hosts/opencode/commands/wf-test.md +5 -4
  43. package/modules/hosts/trae/commands/wf-build.md +5 -24
  44. package/modules/hosts/trae/commands/wf-deploy.md +1 -0
  45. package/modules/hosts/trae/commands/wf-design.md +7 -6
  46. package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
  47. package/modules/hosts/trae/commands/wf-new-task.md +42 -21
  48. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  49. package/modules/hosts/trae/commands/wf-review.md +1 -1
  50. package/modules/hosts/trae/commands/wf-sync-hosts.md +8 -16
  51. package/modules/hosts/trae/commands/wf-test.md +5 -4
  52. package/package.json +2 -2
  53. package/src/add-gate.mjs +25 -1
  54. package/src/add-gate.test.mjs +103 -0
  55. package/src/cli.mjs +59 -5
  56. package/src/doctor.mjs +136 -12
  57. package/src/executor.mjs +245 -0
  58. package/src/executor.test.mjs +292 -0
  59. package/src/gates.test.mjs +17 -0
  60. package/src/init.mjs +14 -1
  61. package/src/init.test.mjs +19 -0
  62. package/src/instance-ref.test.mjs +62 -0
  63. package/src/jscpd-drift.test.mjs +144 -0
  64. package/src/profiles.mjs +3 -0
  65. package/src/render.mjs +6 -1
  66. package/src/rule-budget.test.mjs +103 -0
  67. package/src/sync-hosts.mjs +41 -45
  68. package/src/sync-hosts.test.mjs +67 -56
  69. package/src/trae-hooks.test.mjs +15 -3
  70. package/src/validate.mjs +72 -0
  71. package/src/validate.test.mjs +95 -0
  72. package/templates/.gitattributes +8 -0
  73. package/templates/AGENTS.md +2 -1
  74. package/templates/_agents/commands/build.md +5 -24
  75. package/templates/_agents/commands/deploy.md +1 -0
  76. package/templates/_agents/commands/design.md +7 -6
  77. package/templates/_agents/commands/gate-checklist.md +1 -3
  78. package/templates/_agents/commands/new-task.md +42 -21
  79. package/templates/_agents/commands/plan.md +1 -1
  80. package/templates/_agents/commands/review.md +1 -1
  81. package/templates/_agents/commands/sync-hosts.md +8 -16
  82. package/templates/_agents/commands/test.md +5 -4
  83. package/templates/_agents/control-surfaces.txt +53 -0
  84. package/templates/_agents/engine-lock.json +246 -0
  85. package/templates/_agents/lane-surfaces.txt +1 -1
  86. package/templates/_agents/level-hints.txt +30 -0
  87. package/templates/_agents/scripts/agg-delegations.cjs +51 -6
  88. package/templates/_agents/scripts/agg-delegations.test.mjs +84 -4
  89. package/templates/_agents/scripts/check-engine-integrity.mjs +217 -0
  90. package/templates/_agents/scripts/check-engine-integrity.test.mjs +215 -0
  91. package/templates/_agents/scripts/check-lane-surface.mjs +78 -24
  92. package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
  93. package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
  94. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
  95. package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
  96. package/templates/_agents/scripts/check-loop.mjs +255 -45
  97. package/templates/_agents/scripts/check-loop.test.mjs +330 -15
  98. package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
  99. package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
  100. package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
  101. package/templates/_agents/scripts/confirm-doc.mjs +54 -12
  102. package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
  103. package/templates/_agents/scripts/doctor.test.mjs +294 -1
  104. package/templates/_agents/scripts/draft-sediment.mjs +178 -0
  105. package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
  106. package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
  107. package/templates/_agents/scripts/fill-intent.mjs +2 -2
  108. package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
  109. package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
  110. package/templates/_agents/scripts/gate-checklist.mjs +6 -3
  111. package/templates/_agents/scripts/gate-checklist.test.mjs +2 -5
  112. package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
  113. package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
  114. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +10 -13
  115. package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -30
  116. package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
  117. package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
  118. package/templates/_agents/scripts/kb-search.mjs +87 -12
  119. package/templates/_agents/scripts/kb-search.test.mjs +83 -0
  120. package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
  121. package/templates/_agents/scripts/ledger-loader.mjs +108 -0
  122. package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
  123. package/templates/_agents/scripts/ledger-prune.mjs +96 -0
  124. package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
  125. package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
  126. package/templates/_agents/scripts/level-criteria.mjs +232 -0
  127. package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
  128. package/templates/_agents/scripts/pipeline-run.mjs +5 -3
  129. package/templates/_agents/scripts/policy.mjs +47 -0
  130. package/templates/_agents/scripts/solidify-task.mjs +1 -1
  131. package/templates/_agents/scripts/stage-gates.mjs +52 -10
  132. package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
  133. package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
  134. package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
  135. package/templates/_agents/scripts/verify.mjs +1 -1
  136. package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
  137. package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
  138. package/templates/_agents/scripts/workflows-check.mjs +1 -1
  139. package/templates/_agents/scripts-test-exempt.txt +3 -1
  140. package/templates/_agents/skills/wiki/SKILL.md +5 -3
  141. package/templates/_agents/workflows/pipeline-closing.md +4 -5
  142. package/templates/_githooks/pre-commit +3 -34
  143. package/templates/_githooks/pre-push +3 -3
  144. package/templates/wiki/INDEX.md +44 -1
  145. package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
  146. package/templates/workflow/README.md +11 -0
  147. package/templates/workflow/delegations.md +5 -4
  148. package/templates/workflow/incidents/_TEMPLATE.md +2 -0
  149. package/templates/workflow/intents/_TEMPLATE.md +3 -0
  150. package/templates/workflow/plans/_TEMPLATE.md +4 -0
  151. package/templates/workflow/specs/_TEMPLATE.md +23 -0
  152. package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -91
  153. package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -91
  154. package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -91
  155. package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -91
  156. package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -92
  157. package/templates/_agents/commands/source-sync-check.md +0 -105
  158. package/templates/_agents/scripts/check-ledger.mjs +0 -109
  159. package/templates/_agents/scripts/check-ledger.test.mjs +0 -197
  160. package/templates/_agents/scripts/source-sync-check.mjs +0 -156
  161. package/templates/_agents/scripts/source-sync-check.test.mjs +0 -281
  162. package/templates/_agents/workflows/source-sync-repair.md +0 -30
@@ -13,7 +13,7 @@ import { fileURLToPath } from 'node:url';
13
13
  import { computeFingerprint } from './confirm-doc.mjs';
14
14
  import { POLICIES, loadKitPolicy } from './policy.mjs';
15
15
  import { entryConfirmed, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
16
- import { parseAddedDatesLog, addedDatesWithCache } from './check-loop.mjs';
16
+ import { parseAddedDatesLog, addedDatesWithCache, scenarioIdsOf, extractThenByScenario, sectionHasBody, sedimentLandingIsShell } from './check-loop.mjs';
17
17
 
18
18
  const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
19
19
  const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
@@ -28,6 +28,10 @@ const check = (desc, cond, detail = '') => {
28
28
  else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
29
29
  };
30
30
 
31
+ // 夹具闭包工具(2026-10-12 fixture-closure-rootfix):本文件此前 5 处手写依赖枚举,2026-10-12 那批
32
+ // 台账改动漏拷 ledger-loader.mjs 就假绿过(看板漂移门恒绿)。改为按入口自动求传递闭包——新增依赖自动跟随。
33
+ const { copyScriptClosure, copyAgentsFiles } = await import('./fixture-scripts.mjs');
34
+
31
35
  // mkfix:空 fixture 根(四目录 + 枚举单源内联 8 键——fixture 自包含,测消费逻辑而非当前词表值)
32
36
  const ENUMS_FIXTURE = [
33
37
  'doc.status.all=draft approved done superseded cancelled',
@@ -383,9 +387,8 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
383
387
  {
384
388
  const T = mkfix();
385
389
  fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
386
- fs.copyFileSync(path.join(SCRIPT_DIR, 'gen-workflow-index.mjs'), path.join(T, '.agents', 'scripts', 'gen-workflow-index.mjs'));
387
- fs.copyFileSync(path.join(SCRIPT_DIR, 'workflow-enums.mjs'), path.join(T, '.agents', 'scripts', 'workflow-enums.mjs'));
388
- fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
390
+ copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs']);
391
+ copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
389
392
  w(T, 'workflow/intents/2026-09-22-mod-ok.md', INTENT('mod ok', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline', '\n## 验收标准(可测试)\n- [ ] 用例通过\n'));
390
393
  w(T, 'workflow/plans/2026-09-22-mod-ok.md', PLAN('mod ok', '状态: approved\n级别: L1\n模块: pipeline'));
391
394
  spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
@@ -412,9 +415,8 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
412
415
  {
413
416
  const T = mkfix();
414
417
  fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
415
- fs.copyFileSync(path.join(SCRIPT_DIR, 'gen-workflow-index.mjs'), path.join(T, '.agents', 'scripts', 'gen-workflow-index.mjs'));
416
- fs.copyFileSync(path.join(SCRIPT_DIR, 'workflow-enums.mjs'), path.join(T, '.agents', 'scripts', 'workflow-enums.mjs'));
417
- fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
418
+ copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs']);
419
+ copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
418
420
  w(T, 'workflow/intents/2026-09-22-drift.md', INTENT('drift', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
419
421
  w(T, 'workflow/plans/2026-09-22-drift.md', PLAN('drift', '状态: approved\n级别: L1\n模块: pipeline'));
420
422
  spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
@@ -430,10 +432,10 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
430
432
  const mkDashFix = () => {
431
433
  const T = mkfix();
432
434
  fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
433
- for (const f of ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs', 'agg-delegations.cjs', 'workflow-enums.mjs']) {
434
- fs.copyFileSync(path.join(SCRIPT_DIR, f), path.join(T, '.agents', 'scripts', f));
435
- }
436
- fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
435
+ // 看板/索引生成器的台账读取依赖(ledger-loader 等)由闭包自动带入——此前手写枚举漏拷过,
436
+ // 表现是「import 失败 → 看板静默不生成 → 漂移门恒绿」,即假绿比红危险(2026-10-12 已修,见 fixture-scripts.mjs)
437
+ copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs']);
438
+ copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
437
439
  w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
438
440
  w(T, 'workflow/plans/2026-09-22-dash.md', PLAN('dash', '状态: approved\n级别: L1\n模块: pipeline'));
439
441
  spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
@@ -460,7 +462,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
460
462
  const T = mkfix();
461
463
  fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
462
464
  fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
463
- fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'rule-budgets.txt'), path.join(T, '.agents', 'rule-budgets.txt'));
465
+ copyAgentsFiles(path.join(T, '.agents'), ['rule-budgets.txt']);
464
466
  // 超限样本自校准:从拷入的预算表读 AGENTS.md 上限(预算可上调,硬编码样本会随上调失效——2026-10-06-v114-backflow-batch)
465
467
  const agLimit = Number(fs.readFileSync(path.join(T, '.agents', 'rule-budgets.txt'), 'utf8').split(/\r?\n/).find((l) => l.startsWith('AGENTS.md ')).trim().split(' ').pop());
466
468
  w(T, 'AGENTS.md', '# AGENTS 小体积 fixture\n');
@@ -488,7 +490,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
488
490
  const T = mkfix();
489
491
  fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
490
492
  fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
491
- fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'rule-budgets.txt'), path.join(T, '.agents', 'rule-budgets.txt'));
493
+ copyAgentsFiles(path.join(T, '.agents'), ['rule-budgets.txt']);
492
494
  const agLimit = Number(fs.readFileSync(path.join(T, '.agents', 'rule-budgets.txt'), 'utf8').split(/\r?\n/).find((l) => l.startsWith('AGENTS.md ')).trim().split(' ').pop());
493
495
  gitInit(T);
494
496
  w(T, 'AGENTS.md', 'x'.repeat(agLimit + 512));
@@ -2919,8 +2921,11 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
2919
2921
  writeLedger(T, [tampered, row2]);
2920
2922
  let r = run(T);
2921
2923
  let out = outOf(r);
2924
+ // 定位措辞随 2026-10-12 ledger-shard-write 由「第 N 行(物理)」改为 `rel:行号`——
2925
+ // 台账视图含主台账 + 分片后,单一行号跨来源无法定位,必须带来源。首断行号仍须精确指向第 1 行。
2922
2926
  check('场景 21a 链断裂:中间行篡改 → hard 台账链断裂(指首断行号)',
2923
- r.status !== 0 && out.includes('台账链断裂') && /第 1 行/.test(out), `exit=${r.status}\n${out.slice(-400)}`);
2927
+ r.status !== 0 && out.includes('台账链断裂') && /\.agents\/confirmations\.jsonl:1\b/.test(out),
2928
+ `exit=${r.status}\n${out.slice(-400)}`);
2924
2929
  // 反证还原:篡改行恢复原 quote → 链复通(断言非恒绿)
2925
2930
  writeLedger(T, [row1, row2]);
2926
2931
  r = run(T);
@@ -3023,5 +3028,315 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
3023
3028
  fs.rmSync(cacheP, { force: true });
3024
3029
  }
3025
3030
 
3031
+ // ---- 场景 23:场景覆盖子判据(2026-10-10 scenario-coverage,灰度 warning)----
3032
+ // scenarioIdsOf 纯函数 + 端到端(spec 有「验收场景」节 → done intent 须逐场景引用;缺 = warning 不阻断)
3033
+ {
3034
+ const d = fs.mkdtempSync(path.join(os.tmpdir(), 'scen-ids-'));
3035
+ fs.writeFileSync(path.join(d, 's1.md'),
3036
+ '---\n状态: draft\n---\n# SPEC\n## 功能行为\n### S0 不该被提取(节外)\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n## 数据流\n### S3 也不提取(节外)\n');
3037
+ const ids = scenarioIdsOf(path.join(d, 's1.md'));
3038
+ check('场景 24a scenarioIdsOf:只提「验收场景」节内 ID,按序去重', ids.join(',') === 'S1,S2', JSON.stringify(ids));
3039
+ fs.writeFileSync(path.join(d, 's2.md'), '---\n状态: draft\n---\n# SPEC\n## 功能行为\n无场景节\n');
3040
+ check('场景 24b scenarioIdsOf:无「验收场景」节 → 空数组', scenarioIdsOf(path.join(d, 's2.md')).length === 0);
3041
+ check('场景 24c scenarioIdsOf:文件不可读 → 空数组(fail-open)', scenarioIdsOf(path.join(d, 'nope.md')).length === 0);
3042
+ fs.rmSync(d, { recursive: true, force: true });
3043
+ }
3044
+ {
3045
+ // 端到端:spec 两场景 + intent 全引用 → exit 0 且无「场景未覆盖」warning
3046
+ const T = mkfix();
3047
+ w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n');
3048
+ w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
3049
+ '\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:冒烟 checkout.json)\n- [x] 场景:S2 退款到账(证据:refund.test.js)\n'));
3050
+ w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
3051
+ const r = run(T);
3052
+ check('场景 24d spec 有场景且 intent 全引用 → exit 0 无告警', r.status === 0 && !outOf(r).includes('场景未覆盖'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
3053
+ }
3054
+ {
3055
+ // 端到端:缺 S2 引用 → warning(灰度,不阻断:exit 仍 0)
3056
+ const T = mkfix();
3057
+ w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n### S2 退款\n- GIVEN a\n');
3058
+ w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
3059
+ '\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:冒烟 checkout.json)\n'));
3060
+ w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
3061
+ const r = run(T);
3062
+ check('场景 24e 缺场景引用 → warning 且不阻断(exit 0)',
3063
+ r.status === 0 && outOf(r).includes('场景未覆盖') && outOf(r).includes('缺 S2'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3064
+ }
3065
+ {
3066
+ // 端到端:spec 无场景节 → 零变化(不出现「场景未覆盖」)
3067
+ const T = mkfix();
3068
+ w(T, 'workflow/specs/2026-09-12-sc.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — sc\n## 功能行为\n普通 spec\n');
3069
+ w(T, 'workflow/intents/2026-09-12-sc.md', INTENT('sc', '状态: done\n级别: L1\n日期: 2026-09-12',
3070
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
3071
+ w(T, 'workflow/plans/2026-09-12-sc.md', PLAN('sc', '状态: done\n级别: L1'));
3072
+ const r = run(T);
3073
+ check('场景 24f spec 无场景节 → 零变化(无场景告警,exit 0)', r.status === 0 && !outOf(r).includes('场景未覆盖'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
3074
+ }
3075
+
3076
+ // ---- 场景 25:沉淀追踪子判据(2026-10-10 sediment-tracking,灰度 warning)----
3077
+ // sectionHasBody 纯函数 + 端到端(done intent 含影响面/触达红线 且 frontmatter 无「沉淀」→ WARN 未沉淀;
3078
+ // incident closed 且三件套齐全 → 同款;「沉淀: 无」豁免;路径断档 → WARN 沉淀断档)
3079
+ {
3080
+ check('场景 25a sectionHasBody:标题后有实质行 → true',
3081
+ sectionHasBody(['## 影响面', '- 页面 A', ''], '影响面') === true);
3082
+ check('场景 25b sectionHasBody:光标题(模板未填)→ false',
3083
+ sectionHasBody(['## 影响面', '<页面 / 接口 / 数据范围>', ''], '影响面') === false);
3084
+ check('场景 25c sectionHasBody:无该节 → false',
3085
+ sectionHasBody(['## 目标', '做 X'], '影响面') === false);
3086
+ check('场景 25d sectionHasBody:前缀匹配 + 节外有同名行不算',
3087
+ sectionHasBody(['## 目标', '提影响面', '## 影响面与边界', '- 内容'], '影响面') === true);
3088
+ }
3089
+ {
3090
+ // intent 侧:done + 影响面有内容 + 无沉淀字段 → WARN 未沉淀(不阻断)
3091
+ const T = mkfix();
3092
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12',
3093
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A / 接口 B\n'));
3094
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3095
+ const r = run(T);
3096
+ check('场景 25e done intent 高价值无沉淀 → WARN 未沉淀且不阻断',
3097
+ r.status === 0 && outOf(r).includes('未沉淀') && outOf(r).includes('2026-09-12-sed.md'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3098
+ }
3099
+ {
3100
+ // 「沉淀: 无」显式豁免 → 无未沉淀告警
3101
+ const T = mkfix();
3102
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: 无',
3103
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
3104
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3105
+ const r = run(T);
3106
+ check('场景 25f 「沉淀: 无」显式豁免 → 无未沉淀告警',
3107
+ r.status === 0 && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3108
+ }
3109
+ {
3110
+ // 沉淀字段指向不存在的文件 → WARN 沉淀断档
3111
+ const T = mkfix();
3112
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/不存在.md',
3113
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
3114
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3115
+ const r = run(T);
3116
+ check('场景 25g 沉淀字段指向缺失文件 → WARN 沉淀断档',
3117
+ r.status === 0 && outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3118
+ }
3119
+ {
3120
+ // intent 无影响面/触达红线 → 零变化(不出现未沉淀告警)
3121
+ const T = mkfix();
3122
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12',
3123
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
3124
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3125
+ const r = run(T);
3126
+ check('场景 25h intent 无高价值节 → 零变化', r.status === 0 && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
3127
+ }
3128
+ {
3129
+ // incident 侧:closed 且三件套齐全 且无沉淀 → WARN 未沉淀
3130
+ const T = mkfix();
3131
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3132
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
3133
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3134
+ const r = run(T);
3135
+ check('场景 25i closed incident 三件套齐全无沉淀 → WARN 未沉淀',
3136
+ r.status === 0 && outOf(r).includes('未沉淀') && outOf(r).includes('2026-09-12-inc.md'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3137
+ }
3138
+ {
3139
+ // incident 三件套不全 → 沉淀子判据不触发(不出未沉淀,只出三件套不全)
3140
+ const T = mkfix();
3141
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3142
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n1. 结构性修复\n - 修复 commit:abc1234\n - 是否需要新 intent:\n - 否 → 理由:单点修复\n`);
3143
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3144
+ const r = run(T);
3145
+ check('场景 25j incident 三件套不全 → 只告三件套不全,不出未沉淀',
3146
+ r.status === 0 && outOf(r).includes('三件套不全') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3147
+ }
3148
+
3149
+ // ---- 场景 25 续:落点真实性(2026-10-11 landing-reality,P1-3,灰度 warning)----
3150
+ // sedimentLandingIsShell 纯函数 + 端到端(落点只有标题/占位 → WARN 落点空壳;带「来源:」frontmatter
3151
+ // 或 ≥3 行非占位正文 → 不出;不存在 → 仍 WARN 沉淀断档(回归);指向目录(读取失败)→ 空壳并注明读取失败)
3152
+ {
3153
+ check('场景 25k sedimentLandingIsShell:只有标题+占位(空壳)→ true',
3154
+ sedimentLandingIsShell('---\n标题: 某主题\n---\n# 某主题\n<状态:待整理>\n') === true);
3155
+ check('场景 25l sedimentLandingIsShell:frontmatter 有「来源:」→ false',
3156
+ sedimentLandingIsShell('---\n来源: workflow/incidents/2026-09-12-inc.md\n---\n# 某主题\n') === false);
3157
+ check('场景 25m sedimentLandingIsShell:非占位正文 ≥3 行 → false',
3158
+ sedimentLandingIsShell('# 某主题\n- 要点一\n- 要点二\n- 要点三\n') === false);
3159
+ check('场景 25n sedimentLandingIsShell:非占位正文仅 2 行 → true',
3160
+ sedimentLandingIsShell('# 某主题\n- 要点一\n- 要点二\n') === true);
3161
+ check('场景 25o sedimentLandingIsShell:读取失败(null)→ true',
3162
+ sedimentLandingIsShell(null) === true);
3163
+ }
3164
+ {
3165
+ // incident 侧:closed + 三件套齐全 + 沉淀指向只有标题/占位的文件 → WARN 落点空壳(不阻断)
3166
+ const T = mkfix();
3167
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3168
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/shell.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
3169
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3170
+ fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
3171
+ w(T, 'wiki/项目规范/shell.md', '---\n标题: 某主题沉淀\n---\n# 某主题沉淀\n<状态:待整理>\n');
3172
+ const r = run(T);
3173
+ check('场景 25p incident 沉淀落点为标题/占位空壳 → WARN 落点空壳',
3174
+ r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('2026-09-12-inc.md')
3175
+ && !outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3176
+ }
3177
+ {
3178
+ // intent 侧对称:done + 影响面有内容 + 沉淀指向空壳文件 → WARN 落点空壳(不阻断)
3179
+ const T = mkfix();
3180
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/shell.md',
3181
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
3182
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3183
+ fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
3184
+ w(T, 'wiki/项目规范/shell.md', '# 某主题沉淀\n<待整理>\n');
3185
+ const r = run(T);
3186
+ check('场景 25q intent 沉淀落点为标题/占位空壳 → WARN 落点空壳',
3187
+ r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('2026-09-12-sed.md')
3188
+ && !outOf(r).includes('沉淀断档') && !outOf(r).includes('未沉淀'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3189
+ }
3190
+ {
3191
+ // 落点带「来源:」frontmatter → 非空壳(无落点空壳告警)
3192
+ const T = mkfix();
3193
+ w(T, 'workflow/intents/2026-09-12-sed.md', INTENT('sed', '状态: done\n级别: L1\n日期: 2026-09-12\n沉淀: wiki/项目规范/real.md',
3194
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n## 影响面\n- 页面 A\n'));
3195
+ w(T, 'workflow/plans/2026-09-12-sed.md', PLAN('sed', '状态: done\n级别: L1'));
3196
+ fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
3197
+ w(T, 'wiki/项目规范/real.md', '---\n来源: workflow/incidents/2026-09-12-inc.md\n---\n# 某主题沉淀\n');
3198
+ const r = run(T);
3199
+ check('场景 25r 沉淀落点带来源 frontmatter → 不出落点空壳',
3200
+ r.status === 0 && !outOf(r).includes('落点空壳') && !outOf(r).includes('沉淀断档'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3201
+ }
3202
+ {
3203
+ // 落点有 ≥3 行非占位正文 → 非空壳(incident 侧非空壳路径)
3204
+ const T = mkfix();
3205
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3206
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/real.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
3207
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3208
+ fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
3209
+ w(T, 'wiki/项目规范/real.md', '---\n标题: 某主题沉淀\n---\n# 某主题沉淀\n- 要点一\n- 要点二\n- 要点三\n');
3210
+ const r = run(T);
3211
+ check('场景 25s 沉淀落点有 ≥3 行非占位正文 → 不出落点空壳',
3212
+ r.status === 0 && !outOf(r).includes('落点空壳') && !outOf(r).includes('沉淀断档'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3213
+ }
3214
+ {
3215
+ // 回归:沉淀指向不存在的文件 → 仍是 WARN 沉淀断档(incident 侧;intent 侧由既有 25g 锚定)
3216
+ const T = mkfix();
3217
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3218
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范/不存在.md\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
3219
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3220
+ const r = run(T);
3221
+ check('场景 25t 沉淀指向缺失文件 → 仍 WARN 沉淀断档(不出落点空壳)',
3222
+ r.status === 0 && outOf(r).includes('沉淀断档') && !outOf(r).includes('落点空壳'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3223
+ }
3224
+ {
3225
+ // 落点指向目录(readFileSync 抛异常)→ 按空壳出账并在文案注明读取失败
3226
+ const T = mkfix();
3227
+ w(T, 'workflow/incidents/2026-09-12-inc.md',
3228
+ `---\n状态: closed\n级别: L1\n发现: 2026-09-12\n沉淀: wiki/项目规范\n---\n# INCIDENT — inc\n## 时间线\n- 用户确认:草稿过目通过(2026-09-12)\n## 复盘三件套(缺一不可)\n\n${三件套(false)}`);
3229
+ w(T, 'workflow/plans/2026-09-12-inc.md', PLAN('inc', '状态: done\n级别: L1'));
3230
+ fs.mkdirSync(path.join(T, 'wiki', '项目规范'), { recursive: true });
3231
+ const r = run(T);
3232
+ check('场景 25u 沉淀落点读取失败(目录)→ WARN 落点空壳并注明读取失败',
3233
+ r.status === 0 && outOf(r).includes('落点空壳') && outOf(r).includes('读取失败'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3234
+ }
3235
+
3236
+ // ---- 场景 92:THEN 断言抽取(2026-10-11 then-assertion,P2-2,灰度 warning)----
3237
+ // extractThenByScenario 纯函数 + 端到端(声明场景缺 THEN 行 = 空壳场景 → WARN 场景无断言,聚合一条;
3238
+ // 验收场景节本身可选——节不存在零变化)
3239
+ {
3240
+ const spec = [
3241
+ '## 验收场景(可选)',
3242
+ '<说明行,不属于任何场景块>',
3243
+ '### S1 折扣',
3244
+ '- GIVEN 购物车有商品 THEN 不误抽(行中段 THEN 字样不算断言)',
3245
+ '- WHEN 结算',
3246
+ '- THEN 折扣金额入账',
3247
+ ' - THEN 重复结算幂等(缩进变体也认)',
3248
+ '### S2 退款',
3249
+ 'THEN: 退款单生成(无连字符带冒号也认)',
3250
+ 'THEN 审计流水落库(无连字符空格也认)',
3251
+ ].join('\n');
3252
+ const m = extractThenByScenario(spec);
3253
+ check('场景 92a extractThenByScenario:- THEN / 缩进 / THEN: / THEN 变体均抽取',
3254
+ m.get('S1').length === 2 && m.get('S2').length === 2
3255
+ && m.get('S1')[0] === '- THEN 折扣金额入账' && m.get('S2')[0] === 'THEN: 退款单生成(无连字符带冒号也认)',
3256
+ JSON.stringify([...m]));
3257
+ check('场景 92b extractThenByScenario:GIVEN 行中段 THEN 字样不误抽',
3258
+ !m.get('S1').some((s) => s.includes('GIVEN')), JSON.stringify(m.get('S1')));
3259
+ check('场景 92c extractThenByScenario:无验收场景节 → 空 Map(节外 THEN 不认)',
3260
+ extractThenByScenario('# SPEC\n## 功能行为\n- THEN 节外不认\n').size === 0);
3261
+ check('场景 92d extractThenByScenario:非字符串(读取失败)→ 空 Map(不可判定不误报)',
3262
+ extractThenByScenario(null).size === 0);
3263
+ }
3264
+ {
3265
+ // 端到端:全场景带 THEN → 无「场景无断言」
3266
+ const T = mkfix();
3267
+ w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- WHEN y\n- THEN z\n### S2 退款\n- GIVEN a\n- THEN b\n');
3268
+ w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
3269
+ '\n## 验收标准(可测试)\n- [x] 场景:S1 折扣生效(证据:checkout.json)\n- [x] 场景:S2 退款到账(证据:refund.test.js)\n'));
3270
+ w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
3271
+ const r = run(T);
3272
+ check('场景 92e 全场景带 THEN → 无场景无断言告警(exit 0)',
3273
+ r.status === 0 && !outOf(r).includes('场景无断言'), `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3274
+ }
3275
+ {
3276
+ // 端到端:S2/S3 缺 THEN → 聚合一条 WARN,S1(有断言)不在列
3277
+ const T = mkfix();
3278
+ w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n### S2 退款\n- GIVEN a\n### S3 对账\n- WHEN c\n');
3279
+ w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
3280
+ '\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S2(证据:b.log)\n- [x] 场景:S3(证据:c.log)\n'));
3281
+ w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
3282
+ const r = run(T);
3283
+ check('场景 92f 场景缺 THEN → 聚合 WARN 且只列缺断言的 ID(S2、S3)',
3284
+ r.status === 0 && outOf(r).includes('场景无断言') && outOf(r).includes('缺 THEN 断言:S2、S3'),
3285
+ `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3286
+ }
3287
+ {
3288
+ // 端到端:spec 无验收场景节 → THEN 对账不适用(零变化)
3289
+ const T = mkfix();
3290
+ w(T, 'workflow/specs/2026-09-12-thn.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — thn\n## 功能行为\n普通 spec,无场景节\n');
3291
+ w(T, 'workflow/intents/2026-09-12-thn.md', INTENT('thn', '状态: done\n级别: L1\n日期: 2026-09-12',
3292
+ '\n## 验收标准(可测试)\n- [x] 用例通过(证据:jest 全绿)\n'));
3293
+ w(T, 'workflow/plans/2026-09-12-thn.md', PLAN('thn', '状态: done\n级别: L1'));
3294
+ const r = run(T);
3295
+ check('场景 92g spec 无验收场景节 → 零场景告警(exit 0)',
3296
+ r.status === 0 && !outOf(r).includes('场景无断言') && !outOf(r).includes('场景引用断链'),
3297
+ `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
3298
+ }
3299
+
3300
+ // ---- 场景 93:引用断链反向对账(2026-10-11 dangling-ref,P2-1,灰度 warning)----
3301
+ // 端到端(与「场景未覆盖」共用同一次解析):验收标准引用 spec 未声明的场景 ID → WARN 场景引用断链
3302
+ {
3303
+ // 引用 S1(已声明且覆盖)+ S9(未声明)→ 只出断链,不出未覆盖
3304
+ const T = mkfix();
3305
+ w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n');
3306
+ w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
3307
+ '\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S9 幻影场景(证据:ghost.log)\n'));
3308
+ w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
3309
+ const r = run(T);
3310
+ check('场景 93a 引用 spec 未声明的 S9 → WARN 场景引用断链(不阻断)',
3311
+ r.status === 0 && outOf(r).includes('场景引用断链') && outOf(r).includes('S9'),
3312
+ `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3313
+ check('场景 93b 已覆盖的 S1 不触发未覆盖(两方向互不误伤)',
3314
+ !outOf(r).includes('场景未覆盖'), outOf(r).slice(0, 400));
3315
+ }
3316
+ {
3317
+ // 引用与声明完全一致 → 无断链
3318
+ const T = mkfix();
3319
+ w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 验收场景(可选)\n### S1 折扣\n- GIVEN x\n- THEN z\n### S2 退款\n- GIVEN a\n- THEN b\n');
3320
+ w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
3321
+ '\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n- [x] 场景:S2(证据:b.log)\n'));
3322
+ w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
3323
+ const r = run(T);
3324
+ check('场景 93c 引用与声明一致 → 无断链无未覆盖(exit 0)',
3325
+ r.status === 0 && !outOf(r).includes('场景引用断链') && !outOf(r).includes('场景未覆盖'),
3326
+ `exit=${r.status}\n${outOf(r).slice(0, 400)}`);
3327
+ }
3328
+ {
3329
+ // spec 无场景节但 intent 引用 S1 → 同样算断链(引用了不存在的场景)
3330
+ const T = mkfix();
3331
+ w(T, 'workflow/specs/2026-09-12-dr.md', '---\n状态: approved\n级别: L1\n---\n# SPEC — dr\n## 功能行为\n普通 spec,无场景节\n');
3332
+ w(T, 'workflow/intents/2026-09-12-dr.md', INTENT('dr', '状态: done\n级别: L1\n日期: 2026-09-12',
3333
+ '\n## 验收标准(可测试)\n- [x] 场景:S1(证据:a.log)\n'));
3334
+ w(T, 'workflow/plans/2026-09-12-dr.md', PLAN('dr', '状态: done\n级别: L1'));
3335
+ const r = run(T);
3336
+ check('场景 93d spec 无场景节且引用 S1 → 断链 WARN(exit 0)',
3337
+ r.status === 0 && outOf(r).includes('场景引用断链') && outOf(r).includes('S1'),
3338
+ `exit=${r.status}\n${outOf(r).slice(0, 500)}`);
3339
+ }
3340
+
3026
3341
  console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
3027
- process.exit(fail ? 1 : 0);
3342
+ process.exit(fail ? 1 : 0);
@@ -17,14 +17,16 @@
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
19
  import { createRequire } from 'node:module';
20
+ import { readLedgerLines } from './ledger-loader.mjs';
20
21
 
21
22
  export function runCheck16(ctx) {
22
23
  const { ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings } = ctx;
23
24
  const mcFile = path.join(ROOT, '.agents', 'metric-claims.txt');
24
25
  if (fs.existsSync(mcFile)) {
25
26
  const ledgerLines = () => {
26
- const p = path.join(ROOT, '.agents', 'confirmations.jsonl');
27
- const ls = (linesOf(p) || []).filter((l) => l.trim());
27
+ // 读法收敛 ledger-loader(2026-10-12 ledger-loader-p1):本指标按**原始文本行**计数
28
+ // (total / 含 "batch" 字段的行数),故用 lines 视图而非 rows 视图。
29
+ const ls = readLedgerLines(ROOT);
28
30
  return { total: ls.length, withBatch: ls.filter((l) => /"batch"/.test(l)).length };
29
31
  };
30
32
  // 内置取数器:全部本地确定性、零网络。**装户零配置即用**(不依赖任何装户文件)。
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env node
2
+ // compact-ledgers.mjs — 台账分片归并器(2026-10-12 ledger-shard-write 配套)
3
+ // 用途:里程碑节点或发布前,把 `.agents/confirmations.d/*.jsonl` 的分片确认行并回主台账
4
+ // `.agents/confirmations.jsonl` 并清理分片。`--shard` 写入的分片若长期不归并,会让台账面无限碎片化,
5
+ // 且分片的历史不受主台账的前缀不变量约束(见 check-ledger-invariant 已知边界)——本脚本就是收敛动作。
6
+ //
7
+ // 保持 append-only 前缀不变量:主台账既有行**严格保序不动**,分片新行只追加到末尾——这正是
8
+ // check-ledger-invariant 的「旧 blob 是新 blob 的逐字节前缀」所要求的形态。绝不做重排/去重改写。
9
+ //
10
+ // 安全设计(三条,缺一条都可能损坏主台账):
11
+ // ① 默认 dry-run:只报告「将并入 N 行」,不动盘。必须显式 execute 才写。
12
+ // ② 原子覆写:同目录临时文件 + rename。中途崩溃 = 主台账原样、分片未删,可安全重跑。
13
+ // ③ 幂等:并集按整行文本去重(复用 git-merge-jsonl 的同款判据),重复执行结果不变。
14
+ // 任一分片读取失败 → 立即中止,主台账与全部分片保持原样。
15
+ //
16
+ // 用法:node .agents/scripts/compact-ledgers.mjs [--root <仓库根>] [--execute]
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { mergeJsonlLines } from './git-merge-jsonl.mjs';
20
+ import { LEDGER_REL, SHARD_DIR_REL } from './ledger-loader.mjs';
21
+
22
+ const linesOf = (t) => (t || '').replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim().length > 0);
23
+
24
+ // compactLedgers(root, { execute }):纯函数形态,供测试直测。
25
+ // 返回 { ok, skipped?, reason?, added, before, after, shards, shardLines, error? }
26
+ export function compactLedgers(root = process.cwd(), { execute = false } = {}) {
27
+ const mainLedger = path.join(root, ...LEDGER_REL.split('/'));
28
+ const shardDir = path.join(root, ...SHARD_DIR_REL.split('/'));
29
+
30
+ if (!fs.existsSync(shardDir) || !fs.statSync(shardDir).isDirectory()) {
31
+ return { ok: true, skipped: true, reason: '无分片目录', added: 0, before: 0, after: 0, shards: 0, shardLines: 0 };
32
+ }
33
+ const shardFiles = fs.readdirSync(shardDir).filter((f) => f.endsWith('.jsonl')).sort();
34
+ if (shardFiles.length === 0) {
35
+ return { ok: true, skipped: true, reason: '分片目录无 .jsonl', added: 0, before: 0, after: 0, shards: 0, shardLines: 0 };
36
+ }
37
+
38
+ const mainBefore = fs.existsSync(mainLedger) ? fs.readFileSync(mainLedger, 'utf8') : '';
39
+ const mainBeforeLines = linesOf(mainBefore);
40
+
41
+ let shardCombined = '';
42
+ let shardLineCount = 0;
43
+ for (const f of shardFiles) {
44
+ try {
45
+ const t = fs.readFileSync(path.join(shardDir, f), 'utf8');
46
+ shardLineCount += linesOf(t).length;
47
+ shardCombined += '\n' + t;
48
+ } catch (err) {
49
+ // fail-soft 中止:主台账与全部分片保持原样(修好重跑即可——并集按行去重,重跑安全)
50
+ return { ok: false, error: `读取分片失败: ${f} (${err.message})`, added: 0, before: mainBeforeLines.length, after: mainBeforeLines.length, shards: shardFiles.length, shardLines: shardLineCount };
51
+ }
52
+ }
53
+
54
+ const merged = mergeJsonlLines(mainBefore, shardCombined);
55
+ const added = linesOf(merged).filter((l) => !mainBeforeLines.includes(l)).length;
56
+ const after = linesOf(merged).length;
57
+
58
+ if (!execute) {
59
+ return { ok: true, skipped: true, reason: 'dry-run', added, before: mainBeforeLines.length, after, shards: shardFiles.length, shardLines: shardLineCount };
60
+ }
61
+
62
+ const tmpMain = `${mainLedger}.tmp`;
63
+ try {
64
+ fs.writeFileSync(tmpMain, merged, 'utf8');
65
+ fs.renameSync(tmpMain, mainLedger);
66
+ } catch (err) {
67
+ try { fs.unlinkSync(tmpMain); } catch { /* tmp 可能未创建或删除失败 */ }
68
+ return { ok: false, error: `主台账写入失败: ${err.message}`, added: 0, before: mainBeforeLines.length, after: mainBeforeLines.length, shards: shardFiles.length, shardLines: shardLineCount };
69
+ }
70
+
71
+ for (const f of shardFiles) {
72
+ try { fs.unlinkSync(path.join(shardDir, f)); } catch { /* 残留分片不影响正确性,下轮再清 */ }
73
+ }
74
+ try { if (fs.readdirSync(shardDir).length === 0) fs.rmdirSync(shardDir); } catch { /* 忽略 */ }
75
+
76
+ return { ok: true, added, before: mainBeforeLines.length, after, shards: shardFiles.length, shardLines: shardLineCount };
77
+ }
78
+
79
+ const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('compact-ledgers.mjs');
80
+ if (isMain) {
81
+ const argv = process.argv.slice(2);
82
+ const i = argv.indexOf('--root');
83
+ const root = path.resolve(i >= 0 && argv[i + 1] ? argv[i + 1] : process.cwd());
84
+ const execute = argv.includes('--execute');
85
+ const r = compactLedgers(root, { execute });
86
+
87
+ if (!r.ok) { console.error(`[compact-ledgers] ${r.error}`); console.error('[compact-ledgers] 已中止:主台账与分片保持原样,修好重跑即可。'); process.exit(1); }
88
+ if (r.skipped) { console.log(`[compact-ledgers] ${r.reason},无需归并。`); process.exit(0); }
89
+ if (!execute) {
90
+ console.log(`[compact-ledgers] dry-run:将并入 ${r.added} 行(分片共 ${r.shardLines} 行 / ${r.shards} 个文件,其中 ${r.shardLines - r.added} 行主台账已有),主台账 ${r.before} → ${r.after} 行`);
91
+ console.log('[compact-ledgers] 加 --execute 执行(未加则不动盘)。');
92
+ process.exit(0);
93
+ }
94
+ console.log(`[compact-ledgers] ✅ 归并完成:并入 ${r.added} 行,主台账 ${r.before} → ${r.after} 行;${r.shards} 个分片已清理。`);
95
+ console.log('[compact-ledgers] 提醒:归并产生的台账变更请随下一笔提交入库——历史前缀不变量按提交判,不入库等于没生效。');
96
+ process.exit(0);
97
+ }