agentic-flow-kit 1.7.16 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +11 -7
  2. package/modules/hosts/claude/commands/wf-build.md +1 -1
  3. package/modules/hosts/claude/commands/wf-maintain.md +1 -1
  4. package/modules/hosts/claude/commands/wf-new-task.md +9 -8
  5. package/modules/hosts/claude/commands/wf-plan.md +1 -1
  6. package/modules/hosts/claude/commands/wf-review.md +2 -2
  7. package/modules/hosts/claude/commands/wf-test.md +1 -1
  8. package/modules/hosts/codex/commands/wf-build.md +1 -1
  9. package/modules/hosts/codex/commands/wf-maintain.md +1 -1
  10. package/modules/hosts/codex/commands/wf-new-task.md +9 -8
  11. package/modules/hosts/codex/commands/wf-plan.md +1 -1
  12. package/modules/hosts/codex/commands/wf-review.md +2 -2
  13. package/modules/hosts/codex/commands/wf-test.md +1 -1
  14. package/modules/hosts/cursor/commands/wf-build.md +1 -1
  15. package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
  16. package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
  17. package/modules/hosts/cursor/commands/wf-plan.md +1 -1
  18. package/modules/hosts/cursor/commands/wf-review.md +2 -2
  19. package/modules/hosts/cursor/commands/wf-test.md +1 -1
  20. package/modules/hosts/opencode/commands/wf-build.md +1 -1
  21. package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
  22. package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
  23. package/modules/hosts/opencode/commands/wf-plan.md +1 -1
  24. package/modules/hosts/opencode/commands/wf-review.md +2 -2
  25. package/modules/hosts/opencode/commands/wf-test.md +1 -1
  26. package/modules/hosts/trae/commands/wf-build.md +1 -1
  27. package/modules/hosts/trae/commands/wf-maintain.md +1 -1
  28. package/modules/hosts/trae/commands/wf-new-task.md +9 -8
  29. package/modules/hosts/trae/commands/wf-plan.md +1 -1
  30. package/modules/hosts/trae/commands/wf-review.md +2 -2
  31. package/modules/hosts/trae/commands/wf-test.md +1 -1
  32. package/package.json +1 -1
  33. package/src/cli.mjs +6 -2
  34. package/src/doctor.mjs +11 -2
  35. package/src/executor.mjs +25 -8
  36. package/src/executor.test.mjs +4 -1
  37. package/src/fresh-init.test.mjs +1 -1
  38. package/src/init.mjs +20 -3
  39. package/src/profiles.mjs +7 -3
  40. package/src/rule-budget.test.mjs +2 -2
  41. package/src/stack-profile.test.mjs +6 -0
  42. package/src/validate.mjs +6 -1
  43. package/src/validate.test.mjs +1 -1
  44. package/templates/.gitattributes +4 -1
  45. package/templates/AGENTS.md +2 -2
  46. package/templates/_agents/commands/build.md +1 -1
  47. package/templates/_agents/commands/maintain.md +1 -1
  48. package/templates/_agents/commands/new-task.md +9 -8
  49. package/templates/_agents/commands/plan.md +1 -1
  50. package/templates/_agents/commands/review.md +2 -2
  51. package/templates/_agents/commands/test.md +1 -1
  52. package/templates/_agents/engine-lock.json +130 -42
  53. package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
  54. package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
  55. package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
  56. package/templates/_agents/lane-surfaces.txt +4 -1
  57. package/templates/_agents/level-hints.txt +1 -1
  58. package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
  59. package/templates/_agents/mechanism-registry.md +27 -0
  60. package/templates/_agents/rule-budgets.txt +2 -2
  61. package/templates/_agents/scripts/agg-delegations.cjs +21 -3
  62. package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
  63. package/templates/_agents/scripts/agg-judgments.mjs +80 -0
  64. package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
  65. package/templates/_agents/scripts/archive-sweep.mjs +233 -0
  66. package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
  67. package/templates/_agents/scripts/check-attribution.mjs +37 -0
  68. package/templates/_agents/scripts/check-convergence.mjs +119 -0
  69. package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
  70. package/templates/_agents/scripts/check-funnel.mjs +56 -0
  71. package/templates/_agents/scripts/check-hygiene.mjs +5 -5
  72. package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
  73. package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
  74. package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
  75. package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
  76. package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
  77. package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
  78. package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
  79. package/templates/_agents/scripts/check-loop.mjs +215 -34
  80. package/templates/_agents/scripts/check-loop.test.mjs +582 -49
  81. package/templates/_agents/scripts/check-prejudgment.mjs +64 -0
  82. package/templates/_agents/scripts/check-registry.mjs +45 -0
  83. package/templates/_agents/scripts/check-scope.mjs +96 -0
  84. package/templates/_agents/scripts/check-scope.test.mjs +96 -0
  85. package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
  86. package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
  87. package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
  88. package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
  89. package/templates/_agents/scripts/confirm-doc.mjs +114 -38
  90. package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
  91. package/templates/_agents/scripts/edit-face-check.mjs +5 -3
  92. package/templates/_agents/scripts/fill-intent.mjs +5 -3
  93. package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
  94. package/templates/_agents/scripts/fill-plan.mjs +10 -6
  95. package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
  96. package/templates/_agents/scripts/fill-spec.mjs +6 -3
  97. package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
  98. package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
  99. package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
  100. package/templates/_agents/scripts/gate-checklist.mjs +6 -0
  101. package/templates/_agents/scripts/gate-seg.mjs +1 -1
  102. package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
  103. package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
  104. package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
  105. package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
  106. package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
  107. package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
  108. package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
  109. package/templates/_agents/scripts/git-core.mjs +45 -0
  110. package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
  111. package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
  112. package/templates/_agents/scripts/kb-search.mjs +19 -4
  113. package/templates/_agents/scripts/kb-search.test.mjs +33 -0
  114. package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
  115. package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
  116. package/templates/_agents/scripts/ledger-shards.mjs +307 -0
  117. package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
  118. package/templates/_agents/scripts/level-criteria.mjs +9 -10
  119. package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
  120. package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
  121. package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
  122. package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
  123. package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
  124. package/templates/_agents/scripts/pipeline-run.mjs +6 -22
  125. package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
  126. package/templates/_agents/scripts/policy.mjs +28 -0
  127. package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
  128. package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
  129. package/templates/_agents/scripts/rewrite-file.mjs +133 -0
  130. package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
  131. package/templates/_agents/scripts/rule-budget.mjs +83 -0
  132. package/templates/_agents/scripts/run-tests.mjs +99 -0
  133. package/templates/_agents/scripts/run-tests.test.mjs +105 -0
  134. package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
  135. package/templates/_agents/scripts/stage-gates.mjs +34 -12
  136. package/templates/_agents/scripts/trust-mode.mjs +4 -4
  137. package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
  138. package/templates/_agents/scripts/verify.mjs +135 -58
  139. package/templates/_agents/scripts/verify.test.mjs +156 -47
  140. package/templates/_agents/scripts/wf-journal.mjs +8 -4
  141. package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
  142. package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
  143. package/templates/_agents/scripts-test-exempt.txt +1 -0
  144. package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
  145. package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
  146. package/templates/_agents/skills/review-method/SKILL.md +55 -0
  147. package/templates/_agents/workflow-enums.txt +8 -1
  148. package/templates/_githooks/pre-commit +110 -90
  149. package/templates/_githooks/pre-merge-commit +27 -23
  150. package/templates/_githooks/pre-push +4 -3
  151. package/templates/_github/workflows/kit-ci.yml +4 -3
  152. package/templates/workflow/README.md +14 -7
  153. package/templates/workflow/incidents/_TEMPLATE.md +4 -1
  154. package/templates/workflow/intents/_TEMPLATE.md +4 -2
  155. package/templates/workflow/papercuts.md +2 -0
  156. package/templates/workflow/plans/_TEMPLATE.md +10 -5
  157. package/templates/workflow/regression-checklist.md +1 -0
  158. package/templates/workflow/specs/_TEMPLATE.md +17 -1
  159. package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
  160. package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
  161. package/templates/_agents/scripts/rule-budget.sh +0 -74
@@ -0,0 +1,339 @@
1
+ #!/usr/bin/env node
2
+ // ledger-shards.test.mjs — 分片读写单源 fixture(2026-10-13 ledger-shard-ledgers,自源仓回灌)
3
+ // 覆盖:数据行判定(含节限定)/ 月提取 / 主台账∪分片读 / 迁移幂等 / 追加 / 表头自带 / fail-loud
4
+ // kit 适配:表头签名用 kit 模板形态(delegations 末列「合入结论」、regression-checklist「incident | 条目 | 落点」);
5
+ // S22 钉住 kit 适配(分片渲染须出 kit 表头、不出源仓表头——防再同步漂移)。
6
+ import fs from 'node:fs';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import {
10
+ LEDGERS, isDataRow, inScope, monthOf, readLedger, readLedgerText,
11
+ splitMain, appendRows, shardPathFor, renderShard,
12
+ } from './ledger-shards.mjs';
13
+
14
+ let pass = 0;
15
+ let fail = 0;
16
+ const check = (desc, cond, detail = '') => {
17
+ if (cond) { pass++; console.log(`PASS ${desc}`); }
18
+ else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
19
+ };
20
+ const mkroot = () => {
21
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ledger-shards-'));
22
+ fs.mkdirSync(path.join(root, 'workflow'), { recursive: true });
23
+ return root;
24
+ };
25
+ const wf = (root, ...p) => path.join(root, 'workflow', ...p);
26
+
27
+ // ---- S1 数据行判定:表格式(delegations/papercuts)----
28
+ {
29
+ check('S1a delegations 数据行命中', isDataRow('delegations', '| 2026-10-01 | ui-verifier | 任务 | 一次通过 | 备注 |'));
30
+ check('S1b delegations 表头排除', !isDataRow('delegations', '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |'));
31
+ check('S1c delegations 分隔行排除', !isDataRow('delegations', '|------|----------------|------------|------|------|----------|'));
32
+ check('S1d papercuts 数据行命中', isDataRow('papercuts', '| 2026-10-11 | `.agents/x.mjs` | 症状 | 拟修 |'));
33
+ check('S1e papercuts 表头排除', !isDataRow('papercuts', '| 日期 | 位置 | 症状 | 拟修 |'));
34
+ check('S1f 说明行不命中', !isDataRow('papercuts', '> 说明文字,非数据行'));
35
+ }
36
+
37
+ // ---- S2 节限定:regression-checklist 只迁「防复发验证」节 ----
38
+ {
39
+ check('S2a 防复发验证节在范围内', inScope('regression-checklist', '防复发验证'));
40
+ check('S2b 模块节不在范围内', !inScope('regression-checklist', '回归必过'));
41
+ check('S2c 无 section 限定的台账恒在范围', inScope('delegations', '任意节') && inScope('papercuts', ''));
42
+ check('S2d 防复发数据行命中', isDataRow('regression-checklist', '| 2026-09-11 某事故 | 条目 | 落点 |'));
43
+ check('S2e 防复发表头排除', !isDataRow('regression-checklist', '| incident | 条目 | 落点 |'));
44
+ }
45
+
46
+ // ---- S3 月提取 ----
47
+ {
48
+ check('S3a 表格式取首列日期月', monthOf('delegations', '| 2026-10-01 | x |') === '2026-10');
49
+ check('S3b 无日期返回空', monthOf('delegations', '| 无日期 | x |') === '');
50
+ }
51
+
52
+ // ---- S4 读:主台账 ∪ 分片 ----
53
+ {
54
+ const root = mkroot();
55
+ fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
56
+ fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n\n| 日期 | 位置 | 症状 | 拟修 |\n|------|------|------|------|\n| 2026-10-01 | 主表行 | s | f |\n', 'utf8');
57
+ fs.writeFileSync(wf(root, 'papercuts.d', '2026-09.md'), renderShard('papercuts', []) + '| 2026-09-01 | 分片行 | s | f |\n', 'utf8');
58
+ const r = readLedger(root, 'papercuts');
59
+ check('S4a 主台账 + 分片都读到', r.main.includes('主表行') && r.shards.length === 1 && r.all.includes('分片行'));
60
+ check('S4b all 含两侧', r.all.includes('主表行') && r.all.includes('分片行'));
61
+ check('S4c readLedgerText 等价 all', readLedgerText(root, 'papercuts') === r.all);
62
+ fs.rmSync(root, { recursive: true, force: true });
63
+ }
64
+
65
+ // ---- S5 读:分片按文件名排序(月序 = 时间序)----
66
+ {
67
+ const root = mkroot();
68
+ fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
69
+ fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n', 'utf8');
70
+ fs.writeFileSync(wf(root, 'papercuts.d', '2026-10.md'), '| 2026-10-01 | 十月 | s | f |\n', 'utf8');
71
+ fs.writeFileSync(wf(root, 'papercuts.d', '2026-09.md'), '| 2026-09-01 | 九月 | s | f |\n', 'utf8');
72
+ const r = readLedger(root, 'papercuts');
73
+ check('S5 分片按文件名排序(09 在 10 前)', r.shards[0].file === '2026-09.md' && r.shards[1].file === '2026-10.md');
74
+ fs.rmSync(root, { recursive: true, force: true });
75
+ }
76
+
77
+ // ---- S6 读:无分片目录 → 等价原读法(向后兼容)----
78
+ {
79
+ const root = mkroot();
80
+ fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n| 2026-10-01 | 行 | s | f |\n', 'utf8');
81
+ const r = readLedger(root, 'papercuts');
82
+ check('S6 无分片目录 → shards 空、all = main', r.shards.length === 0 && r.all === r.main);
83
+ fs.rmSync(root, { recursive: true, force: true });
84
+ }
85
+
86
+ // ---- S7 迁移:主台账数据行按月迁出,非数据行保留 ----
87
+ {
88
+ const root = mkroot();
89
+ fs.writeFileSync(wf(root, 'papercuts.md'), [
90
+ '# 台账', '', '> 说明', '',
91
+ '| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
92
+ '| 2026-09-01 | 九月行 | s | f |',
93
+ '| 2026-10-01 | 十月行 | s | f |',
94
+ '| 2026-10-02 | 十月行2 | s | f |', '',
95
+ ].join('\n'), 'utf8');
96
+ const r = splitMain(root, 'papercuts');
97
+ check('S7a 迁移计数正确', r.moved['2026-09'] === 1 && r.moved['2026-10'] === 2, JSON.stringify(r.moved));
98
+ const main = fs.readFileSync(wf(root, 'papercuts.md'), 'utf8');
99
+ check('S7b 主台账保留说明与表头、无数据行', main.includes('# 台账') && main.includes('| 日期 |') && !main.includes('九月行') && !main.includes('十月行'));
100
+ check('S7c 分片自带表头', fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8').includes('| 日期 | 位置 | 症状 | 拟修 |'));
101
+ check('S7d 分片含数据行', fs.readFileSync(wf(root, 'papercuts.d', '2026-10.md'), 'utf8').includes('十月行2'));
102
+ // 迁移后读回:数据不丢
103
+ const back = readLedgerText(root, 'papercuts');
104
+ check('S7e 迁移后读回数据完整', back.includes('九月行') && back.includes('十月行') && back.includes('十月行2'));
105
+ fs.rmSync(root, { recursive: true, force: true });
106
+ }
107
+
108
+ // ---- S8 迁移幂等:二次 split 无数据行可迁 ----
109
+ {
110
+ const root = mkroot();
111
+ fs.writeFileSync(wf(root, 'papercuts.md'), '| 日期 | 位置 | 症状 | 拟修 |\n|------|------|------|------|\n| 2026-10-01 | 行 | s | f |\n', 'utf8');
112
+ splitMain(root, 'papercuts');
113
+ const r2 = splitMain(root, 'papercuts');
114
+ check('S8 二次迁移 0 行(幂等)', Object.keys(r2.moved).length === 0, JSON.stringify(r2.moved));
115
+ fs.rmSync(root, { recursive: true, force: true });
116
+ }
117
+
118
+ // ---- S9 节限定迁移:regression-checklist 只迁防复发节 ----
119
+ {
120
+ const root = mkroot();
121
+ fs.writeFileSync(wf(root, 'regression-checklist.md'), [
122
+ '# 回归清单', '',
123
+ '## 回归必过', '',
124
+ '| 模块 | 条目 | 依据 |', '|------|------|------|',
125
+ '| 模块条目(不迁) | x | y |', '',
126
+ '## 防复发验证', '',
127
+ '| incident | 条目 | 落点 |', '|----------|------|------|',
128
+ '| 2026-09-11 某事故 | 条目 | 落点 |', '',
129
+ ].join('\n'), 'utf8');
130
+ const r = splitMain(root, 'regression-checklist');
131
+ check('S9a 只迁防复发节 1 行', r.moved['2026-09'] === 1, JSON.stringify(r.moved));
132
+ const main = fs.readFileSync(wf(root, 'regression-checklist.md'), 'utf8');
133
+ check('S9b 模块节条目留守', main.includes('| 模块条目(不迁) | x | y |'));
134
+ check('S9c 防复发数据行已迁出', !main.includes('2026-09-11 某事故'));
135
+ fs.rmSync(root, { recursive: true, force: true });
136
+ }
137
+
138
+ // ---- S10 appendRows:追加进指定月分片,缺表头则补 ----
139
+ {
140
+ const root = mkroot();
141
+ appendRows(root, 'papercuts', ['| 2026-10-13 | 新行 | s | f |']);
142
+ const p = shardPathFor(root, 'papercuts', '2026-10');
143
+ check('S10a 分片文件已建', fs.existsSync(p));
144
+ const txt = fs.readFileSync(p, 'utf8');
145
+ check('S10b 自带表头 + 数据行', txt.includes('| 日期 | 位置 | 症状 | 拟修 |') && txt.includes('新行'));
146
+ appendRows(root, 'papercuts', ['| 2026-10-14 | 第二行 | s | f |']);
147
+ const txt2 = fs.readFileSync(p, 'utf8');
148
+ check('S10c 二次追加不重复表头', (txt2.match(/\| 日期 \| 位置/g) || []).length === 1 && txt2.includes('第二行'));
149
+ fs.rmSync(root, { recursive: true, force: true });
150
+ }
151
+
152
+ // ---- S11 appendRows:显式 month 覆盖(无日期行)----
153
+ {
154
+ const root = mkroot();
155
+ appendRows(root, 'papercuts', ['| 无日期行 | s | f |'], { month: '2026-10' });
156
+ check('S11 显式 month 生效', fs.existsSync(shardPathFor(root, 'papercuts', '2026-10')));
157
+ fs.rmSync(root, { recursive: true, force: true });
158
+ }
159
+
160
+ // ---- S12 fail-loud:无法确定月 → 抛(不静默吞)----
161
+ {
162
+ const root = mkroot();
163
+ let thrown = null;
164
+ try { appendRows(root, 'papercuts', ['| 无日期行 | s | f |']); } catch (e) { thrown = e; }
165
+ check('S12 无日期且未传 month → 抛错', thrown !== null && /无法确定分片月/.test(thrown.message));
166
+ fs.rmSync(root, { recursive: true, force: true });
167
+ }
168
+
169
+ // ---- S13 fail-loud:分片目录读取异常 → 抛 ----
170
+ {
171
+ const root = mkroot();
172
+ fs.writeFileSync(wf(root, 'papercuts.md'), '# 台账\n', 'utf8');
173
+ fs.mkdirSync(wf(root, 'papercuts.d'), { recursive: true });
174
+ fs.mkdirSync(wf(root, 'papercuts.d', 'bad.md')); // 目录冒充分片 → readFileSync EISDIR
175
+ let thrown = null;
176
+ try { readLedger(root, 'papercuts'); } catch (e) { thrown = e; }
177
+ check('S13 分片读失败 → 抛(不静默吞)', thrown !== null && /分片读取失败/.test(thrown.message));
178
+ fs.rmSync(root, { recursive: true, force: true });
179
+ }
180
+
181
+ // ---- S14 台账清单单源:三个台账都在册 ----
182
+ {
183
+ check('S14 三台账在册', Object.keys(LEDGERS).sort().join(',') === 'delegations,papercuts,regression-checklist');
184
+ }
185
+
186
+ // ---- S15 delegations 双表分组:分片须两张表头都带且行归入正确表 ----
187
+ // (2026-10-13 源仓迁移实测踩中:只带一张表头 → splitTables 把另一张的行按错表解析,240 任务塌成 100)
188
+ {
189
+ const root = mkroot();
190
+ fs.writeFileSync(wf(root, 'delegations.md'), [
191
+ '# 台账', '',
192
+ '## 委派结果', '',
193
+ '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
194
+ '|------|----------------|------------|------|------|----------|',
195
+ '| 2026-09-01 | ui-verifier | 委派任务 | 一次通过 | 备注 | |',
196
+ '',
197
+ '## 自做任务结果', '',
198
+ '| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |',
199
+ '|------|------------|------|------|----------|',
200
+ '| 2026-09-02 | 自做任务 | 一次通过 | 备注 | |',
201
+ '',
202
+ ].join('\n'), 'utf8');
203
+ splitMain(root, 'delegations');
204
+ const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
205
+ check('S15a 分片含委派表头(kit 形态)', shard.includes('| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |'));
206
+ check('S15b 分片含自做表头(kit 形态)', shard.includes('| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |'));
207
+ // 委派行须在委派表头之后、自做表头之前
208
+ const iDeleg = shard.indexOf('被委派方(模型)');
209
+ const iSelf = shard.indexOf('| 日期 | 任务一句话');
210
+ const iRow = shard.indexOf('委派任务');
211
+ check('S15c 委派行归入委派表(表头之后、自做表头之前)', iDeleg < iRow && iRow < iSelf, `deleg=${iDeleg} row=${iRow} self=${iSelf}`);
212
+ fs.rmSync(root, { recursive: true, force: true });
213
+ }
214
+
215
+ // ---- S16 renderShard 分组:有行时不出空表头 ----
216
+ {
217
+ const onlyDeleg = renderShard('delegations', [{ line: '| 2026-09-01 | a | b | c | d | e |', section: '委派结果' }]);
218
+ check('S16a 只有委派行 → 不出自做表头', onlyDeleg.includes('被委派方(模型)') && !onlyDeleg.includes('| 日期 | 任务一句话'));
219
+ const onlySelf = renderShard('delegations', [{ line: '| 2026-09-01 | b | c | d | e |', section: '自做任务结果' }]);
220
+ check('S16b 只有自做行 → 不出委派表头', !onlySelf.includes('被委派方(模型)') && onlySelf.includes('| 日期 | 任务一句话'));
221
+ const empty = renderShard('delegations', []);
222
+ check('S16c 无行 → 出全部表头(空分片骨架)', empty.includes('被委派方(模型)') && empty.includes('| 日期 | 任务一句话'));
223
+ }
224
+
225
+ // ---- S17 幂等:分片已存在时重跑 split 不重复追加 ----
226
+ // (2026-10-13 源仓实测踩中:无去重时二次 split 把 58 行重复写入,120 行 vs 62 行)
227
+ {
228
+ const root = mkroot();
229
+ fs.writeFileSync(wf(root, 'papercuts.md'), [
230
+ '| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
231
+ '| 2026-09-01 | 行A | s | f |',
232
+ '| 2026-09-02 | 行B | s | f |', '',
233
+ ].join('\n'), 'utf8');
234
+ splitMain(root, 'papercuts');
235
+ const after1 = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
236
+ // 模拟「主台账被还原后重跑」(分片仍在)
237
+ fs.writeFileSync(wf(root, 'papercuts.md'), [
238
+ '| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
239
+ '| 2026-09-01 | 行A | s | f |',
240
+ '| 2026-09-02 | 行B | s | f |', '',
241
+ ].join('\n'), 'utf8');
242
+ splitMain(root, 'papercuts');
243
+ const after2 = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
244
+ check('S17 重跑 split 不重复追加(幂等)', after1 === after2, `len1=${after1.length} len2=${after2.length}`);
245
+ const rows = after2.split(/\r?\n/).filter((l) => /^\| 20/.test(l));
246
+ check('S17b 行数仍为 2', rows.length === 2, `rows=${rows.length}`);
247
+ fs.rmSync(root, { recursive: true, force: true });
248
+ }
249
+
250
+ // ---- S18 appendRows 幂等:同行走两次只落一次 ----
251
+ {
252
+ const root = mkroot();
253
+ appendRows(root, 'papercuts', ['| 2026-10-13 | 同行 | s | f |']);
254
+ appendRows(root, 'papercuts', ['| 2026-10-13 | 同行 | s | f |']);
255
+ const txt = fs.readFileSync(shardPathFor(root, 'papercuts', '2026-10'), 'utf8');
256
+ const rows = txt.split(/\r?\n/).filter((l) => /^\| 20/.test(l));
257
+ check('S18 同行走两次只落一次', rows.length === 1, `rows=${rows.length}`);
258
+ fs.rmSync(root, { recursive: true, force: true });
259
+ }
260
+
261
+ // ---- S19 嵌入 `|` 的行不丢(2026-10-13 源仓实测踩中:4 行因列数虚高被 renderShard 丢弃)----
262
+ {
263
+ const root = mkroot();
264
+ const weird = '| 2026-09-17 | `.agents/x.mjs` | 正则 `[:=]\\s*["\']?` 含字面竖线 | 拟修 |';
265
+ fs.writeFileSync(wf(root, 'papercuts.md'), [
266
+ '| 日期 | 位置 | 症状 | 拟修 |', '|------|------|------|------|',
267
+ '| 2026-09-01 | 正常行 | s | f |',
268
+ weird, '',
269
+ ].join('\n'), 'utf8');
270
+ splitMain(root, 'papercuts');
271
+ const shard = fs.readFileSync(wf(root, 'papercuts.d', '2026-09.md'), 'utf8');
272
+ check('S19a 嵌入竖线的行不丢', shard.includes('含字面竖线'), shard.slice(0, 200));
273
+ check('S19b 正常行也在', shard.includes('正常行'));
274
+ fs.rmSync(root, { recursive: true, force: true });
275
+ }
276
+
277
+ // ---- S20 多表按节归属:委派行(含嵌入竖线)仍归委派表 ----
278
+ {
279
+ const root = mkroot();
280
+ const weirdDeleg = '| 2026-09-01 | ui-verifier | 任务含 `a|b` 竖线 | 一次通过 | 备注 | |';
281
+ fs.writeFileSync(wf(root, 'delegations.md'), [
282
+ '## 委派结果', '',
283
+ '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
284
+ '|------|----------------|------------|------|------|----------|',
285
+ weirdDeleg, '',
286
+ '## 自做任务结果', '',
287
+ '| 日期 | 任务一句话 | 结果 | 备注 | 合入结论 |',
288
+ '|------|------------|------|------|----------|',
289
+ '| 2026-09-02 | 自做任务 | 一次通过 | 备注 | |', '',
290
+ ].join('\n'), 'utf8');
291
+ splitMain(root, 'delegations');
292
+ const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
293
+ const iDeleg = shard.indexOf('被委派方(模型)');
294
+ const iSelf = shard.indexOf('| 日期 | 任务一句话');
295
+ const iWeird = shard.indexOf('任务含');
296
+ check('S20 嵌入竖线的委派行仍归委派表', iDeleg < iWeird && iWeird < iSelf, `deleg=${iDeleg} weird=${iWeird} self=${iSelf}`);
297
+ fs.rmSync(root, { recursive: true, force: true });
298
+ }
299
+
300
+ // ---- S21 分片须带节标题(wf-journal.delegationSectionRows 的节门)----
301
+ // (2026-10-13 源仓实测踩中:只带表头 → 节门读侧读到 0 行 → 静默漏报)
302
+ {
303
+ const root = mkroot();
304
+ fs.writeFileSync(wf(root, 'delegations.md'), [
305
+ '## 委派结果', '',
306
+ '| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
307
+ '|------|----------------|------------|------|------|----------|',
308
+ '| 2026-09-01 | ui-verifier | 委派任务 | 一次通过 | 备注 | |', '',
309
+ ].join('\n'), 'utf8');
310
+ splitMain(root, 'delegations');
311
+ const shard = fs.readFileSync(wf(root, 'delegations.d', '2026-09.md'), 'utf8');
312
+ check('S21a 分片带 `## 委派结果` 节标题', /^##\s+委派结果\s*$/m.test(shard), shard.slice(0, 120));
313
+ // 复刻 wf-journal.delegationSectionRows 的节门判据(不 break 形态)
314
+ let inSec = false;
315
+ let rows = 0;
316
+ for (const line of shard.split(/\r?\n/)) {
317
+ if (/^##\s+/.test(line)) { inSec = /^##\s+委派结果\s*$/.test(line); continue; }
318
+ if (!inSec || !/^\s*\|/.test(line)) continue;
319
+ const cells = line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((c) => c.trim());
320
+ if (!cells.length || cells.every((c) => /^[-: ]*$/.test(c))) continue;
321
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(cells[0])) continue;
322
+ rows++;
323
+ }
324
+ check('S21b 节门口径能读到分片行', rows === 1, `rows=${rows}`);
325
+ fs.rmSync(root, { recursive: true, force: true });
326
+ }
327
+
328
+ // ---- S22 kit 适配钉子:分片渲染出 kit 表头、不出源仓表头 ----
329
+ // (kit delegations 两表末列 =「合入结论」、regression-checklist 表头 =「incident | 条目 | 落点」;
330
+ // 再自源仓同步本模块时若把源仓表头带回来,此组先红)
331
+ {
332
+ const deleg = renderShard('delegations', []);
333
+ check('S22a delegations 分片骨架出「合入结论」表头', deleg.includes('合入结论') && !deleg.includes('返工原因'));
334
+ const regr = renderShard('regression-checklist', []);
335
+ check('S22b regression-checklist 分片骨架出 kit 表头', regr.includes('| incident | 条目 | 落点 |') && !regr.includes('来源 incident'));
336
+ }
337
+
338
+ console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
339
+ process.exit(fail ? 1 : 0);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // level-criteria.mjs — 风险级别判定的**机器单源**(2026-10-13 flow-kit-route)
3
3
  //
4
- // 为什么有它:风险级别(L0-L3)此前只以**散文**形态存在于 `new-task.md`「级别判断」节——
4
+ // 为什么有它:风险级别(L1-L3;2026-10-13 L0 退役,自源仓收敛系列 1 回灌)此前只以**散文**形态存在于 `new-task.md`「级别判断」节——
5
5
  // L2 四类触达面、L3 结构面、负例、自查句全都写给人读。后果有二:
6
6
  // ① 定级是纯自由心证,AI 每次判级都可能不同,门禁无从校验「判低」是否守约;
7
7
  // ② 想让 CLI 接管路由(`flow-kit route`),就没有可执行的判据可调,只能复读 md。
@@ -19,13 +19,12 @@
19
19
  import fs from 'node:fs';
20
20
  import path from 'node:path';
21
21
 
22
- export const LEVELS = ['L0', 'L1', 'L2', 'L3'];
22
+ export const LEVELS = ['L1', 'L2', 'L3'];
23
23
  export const HINTS_REL = '.agents/level-hints.txt';
24
24
 
25
25
  // 档位语义(new-task.md 引用本段,不复述——单源纪律)
26
26
  export const LEVEL_MEANING = {
27
- L0: '文档/样式微调(无行为影响)——直接 commit,豁免 intent',
28
- L1: '实现级改动(未命中 L2/L3)——快车道:轻量 intent/plan + 实现',
27
+ L1: '实现级改动(未命中 L2/L3)——一份 plan 即入口(协作道,不另立 intent)',
29
28
  L2: '规则/契约(一处改、多处依赖)——入口文档 + spec + plan 三件套',
30
29
  L3: '数据与运行时结构(schema / 迁移 SQL / DI 链 / 认证与中间件管线)——同 L2 + spec 新会话独立复核',
31
30
  };
@@ -71,10 +70,10 @@ export function maxLevel(a, b) {
71
70
  // parseLevelHints:读 hints 清单。行格式 `L3 <ERE>`,ERE 为 grep -E 兼容正则,匹配仓库相对路径(/ 分隔)。
72
71
  // 非法档位 / 语法错误的行 → fail-loud 抛错(防静默空洞:写错配置比没配置更危险——没配置会提示,写错会误判)。
73
72
  // 三条收紧(2026-10-13 executor-review,全部来自实测复现,不是假想):
74
- // ① **只收 L2 / L3**。本清单语义是「哪些路径一碰就至少是 L2/L3」,L0/L1 行不构成任何门槛——
73
+ // ① **只收 L2 / L3**。本清单语义是「哪些路径一碰就至少是 L2/L3」,L1 行不构成任何门槛——
75
74
  // 而此前接受 L0 会被提交面判低门当真:`L0 ^docs/` 会让纯文档提交被硬拦,修法还让人
76
- // 「升级至 L0 及以上」(L0 即最低档,荒谬)。写这种行的人本意多半是「这里是 L0 区」,
77
- // 那正是不该登记的面——故按写错配置处理,fail-loud 点名。
75
+ // 「升级至 L0 及以上」(L0 即最低档,荒谬;该档位已随 2026-10-13 L0 退役整体消失)。
76
+ // 写这种行的人本意多半是「这里是低风险区」,那正是不该登记的面——故按写错配置处理,fail-loud 点名。
78
77
  // ② **行内 `#` 注释 fail-loud**。ERE 取整行剩余部分(`(\S.*)$`),所以 `L3 ^a\.sql$ # 注释`
79
78
  // 会把注释编进正则:若路径含 `$` 锚点,该条**永不命中**且无任何报错——路径下限与提交面兜底
80
79
  // 同时静默失效,而 `--check` 还报「N 条有效」。这正是随包模板原先示例的写法(评审实测)。
@@ -92,7 +91,7 @@ export function parseLevelHints(text) {
92
91
  const re = m[2];
93
92
  if (level !== 'L2' && level !== 'L3') {
94
93
  errs.push(`第 ${i + 1} 行:档位只能是 L2 或 L3(实际 ${level})——本清单只登记「一碰就至少是中/高风险面」的路径下限;`
95
- + `L0/L1 不构成门槛,登记了只会让提交面判低门对无关改动出手。若本意是「这里是低风险区」,那就不该登记`);
94
+ + `L1 不构成门槛(L0 已退役),登记了只会让提交面判低门对无关改动出手。若本意是「这里是低风险区」,那就不该登记`);
96
95
  return;
97
96
  }
98
97
  // 行内注释检测:空白后的未转义 #。ERE 里的 `#` 若被转义(\#)或前面不是空白(形如 `^src/#old/`)则放行。
@@ -171,7 +170,7 @@ export function routeAdvice(level, _ctx = {}) {
171
170
  lines.push(`【L3 结构面】`);
172
171
  for (const s of L3_SURFACES) lines.push(` · ${s}`);
173
172
  }
174
- if (L === 'L1' || L === 'L0') {
173
+ if (L === 'L1') {
175
174
  lines.push('');
176
175
  lines.push(`【负例 · 命中即不得升 L2/L3】`);
177
176
  for (const s of L1_NEGATIVES) lines.push(` · ${s}`);
@@ -220,7 +219,7 @@ if (process.argv[1] && path.resolve(process.argv[1]).endsWith(path.join('scripts
220
219
  const r = checkHintsFile(path.resolve(file));
221
220
  if (r.ok === false) {
222
221
  console.error(`[level-hints 配置有误] ${file}\n${String(r.error).split('\n').map((l) => ` ${l}`).join('\n')}`);
223
- console.error(' 修法:按 `<档位> <ERE>` 格式修正(档位 ∈ L0-L3,ERE 为 grep -E 兼容正则)——'
222
+ console.error(' 修法:按 `<档位> <ERE>` 格式修正(档位 ∈ L2|L3,ERE 为 grep -E 兼容正则)——'
224
223
  + '写错配置比没配置危险:没配置会提示补配置,写错会误判档位');
225
224
  process.exit(1);
226
225
  }
@@ -29,14 +29,14 @@ const throwsCode = (fn, code) => {
29
29
 
30
30
  // ============ 场景 1:档位闭集与排序原语 ============
31
31
  {
32
- check('S1 档位闭集为 L0-L3', JSON.stringify(C.LEVELS) === JSON.stringify(['L0', 'L1', 'L2', 'L3']));
33
- check('S1 rankOf 大小写不敏感', C.rankOf('l2') === 2 && C.rankOf('L3') === 3 && C.rankOf('L0') === 0);
34
- check('S1 rankOf 非法档返回 null', C.rankOf('L9') === null && C.rankOf('') === null && C.rankOf(undefined) === null);
32
+ check('S1 档位闭集为 L1-L3(L0 已退役)', JSON.stringify(C.LEVELS) === JSON.stringify(['L1', 'L2', 'L3']));
33
+ check('S1 rankOf 大小写不敏感', C.rankOf('l2') === 1 && C.rankOf('L3') === 2);
34
+ check('S1 rankOf 非法档返回 null(含已退役的 L0)', C.rankOf('L9') === null && C.rankOf('L0') === null && C.rankOf('') === null && C.rankOf(undefined) === null);
35
35
  check('S1 maxLevel 取高', C.maxLevel('L1', 'L3') === 'L3' && C.maxLevel('L3', 'L1') === 'L3');
36
36
  check('S1 maxLevel 同档幂等', C.maxLevel('L2', 'L2') === 'L2');
37
37
  check('S1 maxLevel 单边非法时取合法侧', C.maxLevel('L9', 'L2') === 'L2' && C.maxLevel('L1', 'L9') === 'L1');
38
38
  check('S1 maxLevel 双边非法返回空串(不假装判得出)', C.maxLevel('L9', 'X') === '');
39
- check('S1 maxLevel 小写输入归一为大写', C.maxLevel('l0', 'l2') === 'L2');
39
+ check('S1 maxLevel 小写输入归一为大写', C.maxLevel('l1', 'l2') === 'L2');
40
40
  }
41
41
 
42
42
  // ============ 场景 2:hints 解析 —— 注释/空行/CRLF/档位前缀 ============
@@ -160,8 +160,10 @@ const throwsCode = (fn, code) => {
160
160
  const l3 = C.routeAdvice('L3', { hintsConfigured: true });
161
161
  check('S7 L3 同时输出 L2 触达面与 L3 结构面', C.L2_SURFACES.every((s) => l3.includes(s)) && C.L3_SURFACES.every((s) => l3.includes(s)));
162
162
 
163
+ // L0 已退役(2026-10-13 自源仓收敛系列 1 回灌):不在 LEVELS 闭集 → routeAdvice 只报「未知档」,
164
+ // 不再有语义行、也不产生下溢(下溢概念随 L0 退役消失)。
163
165
  const l0 = C.routeAdvice('L0', { hintsConfigured: true });
164
- check('S7 L0 归一为语义行(无下溢)', l0.includes('建议级别:L0'));
166
+ check('S7 已退役的 L0 → 无语义行(未知档),不产生下溢', l0.includes('建议级别:L0 —— (未知档)') && !l0.includes('【负例'));
165
167
 
166
168
  // 2026-10-13 executor-review:hints 未配置 / 低于下限两条**状态**信息已移出 routeAdvice,
167
169
  // 改由 route 的降级块(executor.mjs degradations)单一输出——原实现把同一事实在文本面打两遍。
@@ -0,0 +1,141 @@
1
+ #!/usr/bin/env node
2
+ // papercut-debt-controller.mjs — 回路控制器(2026-10-07 papercut-debt 回路 Phase D;B10a 自
3
+ // Shipyard.Material 回灌——排序键与 scope gate 逐字对齐,出处沿革留痕在包源仓 git 历史与 CHANGELOG)
4
+ // 用途:把传感器的测量变成"本轮修哪一条"。确定性选取,无随机、无 LLM 判断。
5
+ // 口径单源:.agents/loop/papercut-debt/DESIGN.md §3(改排序键先改设计文件)。
6
+ // 排序键(依次比较):① 命中次数降序(台账文件头自身规则:第 2 次命中须升级为 docs 修复)
7
+ // ② 日期降序(近期痛点更可能仍成立) ③ 原行序升序(完全平手时保可复现)
8
+ // Scope gate:拟修明确指向 backend/ frontend/ tests/ wiki/ sqlcmd/ db_tool,或含 数据库/DDL/DROP 字样的条目本轮跳过
9
+ // (执行器边界=文档面,见 DESIGN §4)。跳过的条目照列,供人工决定是否单独授权。
10
+ // 单轮只取 1 条:独立复核有成本(L2/L3 须独立复核),一次给 3 条审不动。
11
+ // 用法:node .agents/scripts/papercut-debt-controller.mjs [--json] [--top <n>] [--root <仓库根>]
12
+ // 退出码:0 正常(含"无目标"与"全部被 scope gate 拦");2 = 测量不可用(debt 未知)。
13
+ import fs from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
18
+ const argv = process.argv.slice(2);
19
+ const argOf = (k) => { const i = argv.indexOf(k); return i >= 0 && i + 1 < argv.length ? argv[i + 1] : ''; };
20
+ const asJson = argv.includes('--json');
21
+ const TOP = Math.max(1, Number(argOf('--top')) || 3);
22
+ const ROOT = path.resolve(argOf('--root') || path.join(SCRIPT_DIR, '..', '..'));
23
+
24
+ // Scope gate 关键词:命中即视为「要改源码 / 连库」,超出文档面授权
25
+ const OUT_OF_SCOPE = /(^|[^\w/])(backend|frontend|tests|wiki)\/|sqlcmd|db_tool\.py|数据库|DDL|\bDROP\b/i;
26
+ // 「同上 X」承接上一行的路径,无法独立判定存在性——需人读后定性,不作为自动目标
27
+ const STALE_RE = /^同上两处|^同上/;
28
+
29
+ // 口径单源在传感器(settled = ✅ 或 ⏸带理由;stale 判定也在传感器)。
30
+ // 本脚本刻意**不复制**任何判定正则,而是 import 传感器复用其扫描面——两处各判一次必然漂移
31
+ // (首轮实证:传感器按 ✅ 判债、控制器也按 ✅ 选目标,⏸ 暂缓条目被反复选中导致空转——
32
+ // 留痕在包源仓 git 历史与 CHANGELOG)。
33
+ // 传感器 main() 仅在直接执行时运行,import 时只导出 scan()。
34
+ import { scan as sensorScan } from './papercut-debt-sensor.mjs';
35
+
36
+ function measure() {
37
+ const ledger = path.join(ROOT, 'workflow', 'papercuts.md');
38
+ if (!fs.existsSync(ledger)) throw new Error(`台账不存在: ${ledger}`);
39
+ // 复用传感器解析面(含 ✅ / ⏸带理由 / 命中次数 / stale 判定),此处只做排序与 scope gate。
40
+ const rows = sensorScan(ROOT).map((r) => ({
41
+ ...r,
42
+ // scope gate 需读全文:location + proposedFix(拟修列可能含管道符,传感器已并回)
43
+ fullText: `${r.location} ${r.proposedFix}`,
44
+ }));
45
+ return { ledger, rows };
46
+ }
47
+
48
+ function rank(a, b) {
49
+ if (a.hits !== b.hits) return b.hits - a.hits;
50
+ if (a.date !== b.date) return a.date < b.date ? 1 : -1; // 日期降序
51
+ return a.line - b.line; // 原行序升序
52
+ }
53
+
54
+ function gateReason(r) {
55
+ const m = r.fullText.match(OUT_OF_SCOPE);
56
+ if (!m) return null;
57
+ return `拟修触及源码/数据库面(命中「${(m[2] || m[1] || m[0]).trim()}」)`;
58
+ }
59
+
60
+ function main() {
61
+ let m;
62
+ try {
63
+ m = measure();
64
+ } catch (e) {
65
+ console.error(`[ERROR] papercut-debt-controller: 测量不可用——${e.message}`);
66
+ if (asJson) console.log(JSON.stringify({ error: e.message, target: null }, null, 2));
67
+ process.exit(2);
68
+ }
69
+
70
+ const unresolved = m.rows.filter((r) => !r.settled);
71
+ const debt = unresolved.length;
72
+ const inScope = [];
73
+ const skipped = [];
74
+ const needsHuman = [];
75
+ for (const r of unresolved) {
76
+ const why = gateReason(r);
77
+ if (why) { skipped.push({ ...r, skipReason: why }); continue; }
78
+ // stale 与「同上 X」承接行:需要人读后定性(已失效 / 或需重定位),不作为自动修复目标
79
+ const isStale = r.stale;
80
+ const isDangling = STALE_RE.test(r.location);
81
+ if (isStale || isDangling) {
82
+ needsHuman.push({ ...r, needReason: isStale ? '位置列路径已不存在,需确认是否已失效' : '「同上」承接行,路径需从上条解析后才能判定' });
83
+ continue;
84
+ }
85
+ inScope.push(r);
86
+ }
87
+ inScope.sort(rank);
88
+ const target = inScope[0] || null;
89
+ const queue = inScope.slice(0, TOP);
90
+
91
+ if (asJson) {
92
+ console.log(JSON.stringify({
93
+ ledger: path.relative(ROOT, m.ledger).split(path.sep).join('/'),
94
+ debt, total: m.rows.length,
95
+ target, queue,
96
+ skippedOutOfScope: skipped,
97
+ needsHuman,
98
+ achieved: debt === 0,
99
+ }, null, 2));
100
+ return;
101
+ }
102
+
103
+ console.log(`控制器 —— debt ${debt} · 在选 ${inScope.length} · scope-gate 拦下 ${skipped.length} · 待人工定性 ${needsHuman.length}`);
104
+ if (debt === 0) {
105
+ console.log('✅ debt = 0,设定点已达成——本轮无目标,执行器不应动手。');
106
+ return;
107
+ }
108
+ if (!target) {
109
+ console.log('⚠️ 所有未升级条目均被 scope gate 拦下(执行器边界=文档面)——需人工决定是否单独授权。');
110
+ for (const s of skipped) console.log(` L${s.line} [${s.date}] ${s.location} —— ${s.skipReason}`);
111
+ return;
112
+ }
113
+
114
+ console.log('');
115
+ console.log('本轮目标(top-1):');
116
+ console.log(` 行号 L${target.line}(workflow/papercuts.md)`);
117
+ console.log(` 日期 ${target.date}`);
118
+ console.log(` 命中次数 ${target.hits}`);
119
+ console.log(` 位置 ${target.location}`);
120
+ console.log(` 症状 ${target.symptom.slice(0, 200)}${target.symptom.length > 200 ? '…' : ''}`);
121
+ console.log(` 拟修 ${target.proposedFix.slice(0, 300)}${target.proposedFix.length > 300 ? '…' : ''}`);
122
+ console.log('');
123
+ console.log(`后续候选(本轮不做,供你改指令时直接点名):`);
124
+ for (const q of queue.slice(1)) {
125
+ console.log(` L${q.line} [${q.date}] hits=${q.hits} ${q.location}`);
126
+ }
127
+ if (skipped.length) {
128
+ console.log('');
129
+ console.log('已按 scope gate 跳过(要改源码/连库,需你单独授权):');
130
+ for (const s of skipped) console.log(` L${s.line} [${s.date}] ${s.location} —— ${s.skipReason}`);
131
+ }
132
+ if (needsHuman.length) {
133
+ console.log('');
134
+ console.log('待人工定性(不作为自动目标——请读后定性:判失效则标 ⏸ 暂缓+理由,判仍成立则补出可执行拟修):');
135
+ for (const h of needsHuman) console.log(` L${h.line} [${h.date}] ${h.location} —— ${h.needReason}`);
136
+ }
137
+ console.log('');
138
+ console.log('下一步:按 .agents/skills/papercut-fix/SKILL.md 执行(改完跑验证门,debt 应恰好 -1)。');
139
+ }
140
+
141
+ main();