agentic-flow-kit 1.6.0 → 1.7.15
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.
- package/README.md +40 -6
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +2 -2
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +2 -2
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +2 -2
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +17 -2
- package/src/doctor.mjs +99 -11
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +7 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/render.mjs +5 -0
- package/src/rule-budget.test.mjs +103 -0
- package/src/run-tests.mjs +16 -9
- package/src/sync-hosts.mjs +4 -2
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/AGENTS.md +3 -1
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +2 -2
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -3
- package/templates/_agents/commands/sync-hosts.md +1 -3
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
- package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
- package/templates/_agents/scripts/check-ledger-invariant.mjs +18 -0
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +16 -0
- package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
- package/templates/_agents/scripts/check-loop.mjs +247 -5
- package/templates/_agents/scripts/check-loop.test.mjs +367 -2
- package/templates/_agents/scripts/confirm-doc.mjs +50 -3
- package/templates/_agents/scripts/confirm-doc.test.mjs +93 -1
- package/templates/_agents/scripts/doctor.test.mjs +251 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -2
- package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/pipeline-run.mjs +1 -1
- package/templates/_agents/scripts/policy.mjs +49 -0
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
- package/templates/_agents/scripts/stage-gates.mjs +48 -1
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +12 -4
- package/templates/_agents/scripts/verify.test.mjs +30 -3
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/* agg-delegations: 量化证据聚合
|
|
3
3
|
* 读 workflow/delegations.md 两张结果表(按表头签名识别:委派表含「被委派方」列、自做表=日期+任务一句话;不依赖 ## 节标题)+ 扫 workflow/incidents/ 按月计数,
|
|
4
|
-
* 输出各月指标(一次通过率 / 平均返工次数 /
|
|
4
|
+
* 输出各月指标(一次通过率 / 平均返工次数 / 主兜底占比 / **产出接受率**)、并发扩容门判定、可粘贴的月度快照行。
|
|
5
5
|
* 结果取值:一次通过 | 返工×N | 返工×N(门禁噪声)| 主兜底 | 返工待修(未闭环,不计入率)。
|
|
6
6
|
* 门禁噪声档(2026-10-08 selfmeasure-and-modularize):剥离「返工由门禁面而非设计面触发」的形态——
|
|
7
7
|
* 扩容门第 3 项只对设计返工判 =0,门禁噪声返工单列观测。存量行零回填(旧四档解析逐条不变)。
|
|
@@ -58,6 +58,25 @@ function parseResult(raw) {
|
|
|
58
58
|
return { kind: 'unknown', rework: 0, reworkNoise: 0 };
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// parseVerdict:合入结论列解析(2026-10-12 merge-accept-rate)
|
|
62
|
+
// 三档:合入 / 返工后合入 / 未合入;空与未知一律 return null(**不计入接受率分母**——
|
|
63
|
+
// 存量行零回填是硬约定,未记录 ≠ 未合入,混进去会凭空造出一个低接受率)。
|
|
64
|
+
// 判据边界:结论由人工/独立复核写入,机器不判真伪(同 --delegated 信任边界);
|
|
65
|
+
// 本项是**观测不是门槛**,不进 gateMonth(理由见 workflow/delegations.md 头部)。
|
|
66
|
+
// 与结果列(过程质量)刻意分开:结果列量「跑了几遍」,本列量「人愿不愿意合」——
|
|
67
|
+
// METR 2026-03 实测两者落差约 24 个百分点,混列即被前者盖过后者。
|
|
68
|
+
function parseVerdict(raw) {
|
|
69
|
+
const v = (raw || '').trim();
|
|
70
|
+
if (/^合入$/.test(v)) return 'accepted';
|
|
71
|
+
if (/^返工后合入$/.test(v)) return 'accepted-after-rework';
|
|
72
|
+
if (/^未合入$/.test(v)) return 'rejected';
|
|
73
|
+
return null; // 留空 / 未登记取值 = 未记录(不进分母)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ACCEPT_MIN_SAMPLE:接受率最小样本护栏(样本 <5 不出率,沿 agg 既有护栏口径——
|
|
77
|
+
// 样本过小的比率是噪声不是信号,且一旦进门禁就会产出不可消退的告警)。
|
|
78
|
+
const ACCEPT_MIN_SAMPLE = 5;
|
|
79
|
+
|
|
61
80
|
// splitTables:按表头签名识别两张结果表——节标题只服务人类阅读,表头才是数据边界
|
|
62
81
|
// (2026-09-24 修复:曾按 `## 委派结果` 节标题定位,节结构漂移时已有记录被静默读成「台账为空」)
|
|
63
82
|
// ⚠️ 互引(2026-10-08 papercuts-cleanup-batch 修 2):check-loop.mjs delegationResultRows 已对齐本签名
|
|
@@ -91,18 +110,20 @@ function readLedger(root = ROOT, opts = {}) {
|
|
|
91
110
|
date: c[0],
|
|
92
111
|
actor: c[1],
|
|
93
112
|
result: c[3] || '',
|
|
113
|
+
verdict: c[5] || '',
|
|
94
114
|
}));
|
|
95
115
|
const selfDone = parseTableRows(tables.self.join('\n')).map((c) => ({
|
|
96
116
|
scope: '自做',
|
|
97
117
|
date: c[0],
|
|
98
118
|
actor: '主智能体',
|
|
99
119
|
result: c[2] || '',
|
|
120
|
+
verdict: c[4] || '',
|
|
100
121
|
}));
|
|
101
122
|
const rows = delegated.concat(selfDone);
|
|
102
123
|
// 零数据行时区分「台账真空」与「结构漂移」:有日期开头的数据行却识别不到表 → fail-loud,不再静默「台账为空」
|
|
103
124
|
if (!rows.length && /^\s*\|\s*\d{4}-\d{2}-\d{2}\s*\|/m.test(text)) {
|
|
104
125
|
if (opts.soft) return [];
|
|
105
|
-
fail(`台账结构漂移:${ledger} 存在日期开头的数据行但未识别到结果表(委派表表头须含「被委派方」列,自做表表头须为「日期 | 任务一句话 | 结果 |
|
|
126
|
+
fail(`台账结构漂移:${ledger} 存在日期开头的数据行但未识别到结果表(委派表表头须含「被委派方」列,自做表表头须为「日期 | 任务一句话 | 结果 | 备注」——末列「合入结论」可缺省,存量表不要求补)`);
|
|
106
127
|
}
|
|
107
128
|
return rows;
|
|
108
129
|
}
|
|
@@ -142,6 +163,18 @@ function metrics(rows) {
|
|
|
142
163
|
if (p.kind === 'rework-noise' || p.kind === 'rework-mixed') reworkNoiseSum += p.reworkNoise;
|
|
143
164
|
}
|
|
144
165
|
const delegatedValid = valid.filter((r) => r.scope === '委派');
|
|
166
|
+
// 接受率(2026-10-12 merge-accept-rate):分母 = **已记录合入结论**的行数(parseVerdict 非 null),
|
|
167
|
+
// 分子 = accepted + accepted-after-rework。留空行不计入——存量行零回填,未记录 ≠ 未合入。
|
|
168
|
+
// 分母不足 ACCEPT_MIN_SAMPLE 时 acceptRate = null(输出 '—'),不给小样本比率。
|
|
169
|
+
let verdictRecorded = 0, accepted = 0, rejected = 0;
|
|
170
|
+
for (const r of valid) {
|
|
171
|
+
const v = parseVerdict(r.verdict);
|
|
172
|
+
if (!v) continue;
|
|
173
|
+
verdictRecorded += 1;
|
|
174
|
+
if (v === 'accepted' || v === 'accepted-after-rework') accepted += 1;
|
|
175
|
+
if (v === 'rejected') rejected += 1;
|
|
176
|
+
}
|
|
177
|
+
const acceptRate = verdictRecorded >= ACCEPT_MIN_SAMPLE ? accepted / verdictRecorded : null;
|
|
145
178
|
return {
|
|
146
179
|
total: valid.length,
|
|
147
180
|
pending,
|
|
@@ -154,9 +187,20 @@ function metrics(rows) {
|
|
|
154
187
|
delegatedValid: delegatedValid.length,
|
|
155
188
|
fallback,
|
|
156
189
|
fallbackRate: delegatedValid.length ? fallback / delegatedValid.length : null,
|
|
190
|
+
verdictRecorded,
|
|
191
|
+
accepted,
|
|
192
|
+
rejected,
|
|
193
|
+
acceptRate,
|
|
157
194
|
};
|
|
158
195
|
}
|
|
159
196
|
|
|
197
|
+
// fmtAccept:接受率展示(分母为 0 或未过护栏 → '—' 并注明原因,不打 0%)
|
|
198
|
+
function fmtAccept(m) {
|
|
199
|
+
if (!m.verdictRecorded) return '—(合入结论零记录)';
|
|
200
|
+
if (m.acceptRate == null) return `—(样本 ${m.verdictRecorded}/${ACCEPT_MIN_SAMPLE})`;
|
|
201
|
+
return `${(m.acceptRate * 100).toFixed(0)}%(${m.accepted}/${m.verdictRecorded})`;
|
|
202
|
+
}
|
|
203
|
+
|
|
160
204
|
function fmtRate(x, digits = 0) {
|
|
161
205
|
return x == null ? '—' : `${(x * 100).toFixed(digits)}%`;
|
|
162
206
|
}
|
|
@@ -211,16 +255,17 @@ function main() {
|
|
|
211
255
|
const prevYm = (() => { const [y, mo] = ym.split('-').map(Number); const d = new Date(Date.UTC(y, mo - 2, 1)); return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, '0')}`; })();
|
|
212
256
|
const verdict = expansionVerdict(monthOk, monthResults.get(prevYm), !months.includes(prevYm), items);
|
|
213
257
|
console.log(`## ${ym}`);
|
|
214
|
-
console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${m.pass}(${fmtRate(m.passRate)})|设计返工 ${m.reworkSum} 次 / 门禁噪声返工 ${m.reworkNoiseSum} 次(合计返工 ${m.reworkSum + m.reworkNoiseSum},平均 ${fmtNum(m.avgRework)})|主兜底 ${m.fallback}/${m.delegatedValid}(${fmtRate(m.fallbackRate)}
|
|
258
|
+
console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${m.pass}(${fmtRate(m.passRate)})|设计返工 ${m.reworkSum} 次 / 门禁噪声返工 ${m.reworkNoiseSum} 次(合计返工 ${m.reworkSum + m.reworkNoiseSum},平均 ${fmtNum(m.avgRework)})|主兜底 ${m.fallback}/${m.delegatedValid}(${fmtRate(m.fallbackRate)})|产出接受率 ${fmtAccept(m)}|incident ${incidents[ym] || 0} 起`);
|
|
259
|
+
console.log(` 接受率说明(观测项,非门槛):已记录合入结论 ${m.verdictRecorded} 行|合入 ${m.accepted}|未合入 ${m.rejected}|留空未记录行不入分母`);
|
|
215
260
|
for (const i of items) console.log(` 门${i.no}[${i.kind}] ${i.ok === null ? '[人工]' : i.ok ? '✅' : '❌'} ${i.desc}`);
|
|
216
261
|
console.log(` 扩容门判定:${verdict}\n`);
|
|
217
|
-
snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
|
|
262
|
+
snapshotRows.push(`| ${ym} | ${m.total} | ${fmtRate(m.passRate)} | ${fmtNum(m.avgRework)} | ${fmtRate(m.fallbackRate)} | ${incidents[ym] || 0} | ${fmtAccept(m)} | ${verdict} | 样本含待修${m.pending}${m.unknown ? `、未知${m.unknown}` : ''} |`);
|
|
218
263
|
}
|
|
219
264
|
|
|
220
265
|
if (!argMonth && rows.length) {
|
|
221
266
|
const m = metrics(rows);
|
|
222
267
|
console.log(`## 全量累计`);
|
|
223
|
-
console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${fmtRate(m.passRate)}|平均返工 ${fmtNum(m.avgRework)}|主兜底 ${fmtRate(m.fallbackRate)}|incident 合计 ${Object.values(incidents).reduce((a, b) => a + b, 0)} 起\n`);
|
|
268
|
+
console.log(` 有效任务 ${m.total}(待修 ${m.pending}${m.unknown ? `、未知结果 ${m.unknown}` : ''})|一次通过 ${fmtRate(m.passRate)}|平均返工 ${fmtNum(m.avgRework)}|主兜底 ${fmtRate(m.fallbackRate)}|产出接受率 ${fmtAccept(m)}|incident 合计 ${Object.values(incidents).reduce((a, b) => a + b, 0)} 起\n`);
|
|
224
269
|
}
|
|
225
270
|
|
|
226
271
|
console.log(`## 可粘贴快照行(粘到 workflow/delegations.md §月度聚合快照)`);
|
|
@@ -235,5 +280,5 @@ function main() {
|
|
|
235
280
|
if (require.main === module) {
|
|
236
281
|
main();
|
|
237
282
|
} else {
|
|
238
|
-
module.exports = { parseResult, metrics, gateMonth, expansionVerdict, GATE, readLedger };
|
|
283
|
+
module.exports = { parseResult, parseVerdict, metrics, gateMonth, expansionVerdict, fmtAccept, GATE, ACCEPT_MIN_SAMPLE, readLedger };
|
|
239
284
|
}
|
|
@@ -91,7 +91,7 @@ const runAgg = (root) => spawnSync(process.execPath, [path.join(root, '.agents',
|
|
|
91
91
|
// 纯函数经 module.exports require(isMain 守卫后 import 不执行 main)
|
|
92
92
|
import { createRequire } from 'node:module';
|
|
93
93
|
const requireCjs = createRequire(import.meta.url);
|
|
94
|
-
const { metrics, gateMonth, expansionVerdict, parseResult } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
94
|
+
const { metrics, gateMonth, expansionVerdict, parseResult, parseVerdict, fmtAccept, ACCEPT_MIN_SAMPLE } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
95
95
|
|
|
96
96
|
// 场景 4:审查反例——30 任务、2 主兜底、平均返工 0.25(旧口径四门全过,声明口径下必须 ❌)
|
|
97
97
|
{
|
|
@@ -194,6 +194,84 @@ const { metrics, gateMonth, expansionVerdict, parseResult } = requireCjs(path.jo
|
|
|
194
194
|
withDesign.ok === false, JSON.stringify(withDesign));
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// 场景 8:产出接受率(2026-10-12 merge-accept-rate)
|
|
198
|
+
// 动机(METR 2026-03):agent 生成的 SWE-bench 通过 PR,维护者合并判定比自动评分低约 24 个百分点、
|
|
199
|
+
// 约一半不会被合入——「自动门全绿」与「人愿意合」是两个维度,混列必被前者盖过后者。
|
|
200
|
+
{
|
|
201
|
+
check('场景 8:parseVerdict 三档 + 留空/乱填落 null(未记录 ≠ 未合入)',
|
|
202
|
+
parseVerdict('合入') === 'accepted' && parseVerdict('返工后合入') === 'accepted-after-rework'
|
|
203
|
+
&& parseVerdict('未合入') === 'rejected'
|
|
204
|
+
&& parseVerdict('') === null && parseVerdict(undefined) === null && parseVerdict('已上线') === null,
|
|
205
|
+
JSON.stringify([parseVerdict('合入'), parseVerdict('返工后合入'), parseVerdict('未合入'), parseVerdict(''), parseVerdict('已上线')]));
|
|
206
|
+
|
|
207
|
+
// 反例钉子:一次通过率 100% 但接受率低 —— 这正是混列会掩盖的形态
|
|
208
|
+
const rows = [
|
|
209
|
+
...Array.from({ length: 8 }, () => ({ scope: '自做', result: '一次通过', verdict: '合入' })),
|
|
210
|
+
{ scope: '自做', result: '一次通过', verdict: '未合入' },
|
|
211
|
+
{ scope: '自做', result: '一次通过', verdict: '返工后合入' },
|
|
212
|
+
{ scope: '自做', result: '一次通过' }, // 留空 = 未记录,不入分母
|
|
213
|
+
];
|
|
214
|
+
const m = metrics(rows);
|
|
215
|
+
check('场景 8:一次通过率 100% 但接受率 90%(8 合入 + 1 返工后合入 / 10 已记录)',
|
|
216
|
+
Math.abs(m.passRate - 1) < 1e-9 && Math.abs(m.acceptRate - 0.9) < 1e-9,
|
|
217
|
+
JSON.stringify({ passRate: m.passRate, acceptRate: m.acceptRate }));
|
|
218
|
+
check('场景 8:留空行不入分母(verdictRecorded=10 而非 11)',
|
|
219
|
+
m.verdictRecorded === 10 && m.accepted === 9 && m.rejected === 1, JSON.stringify(m));
|
|
220
|
+
check('场景 8:接受率不影响任何扩容门项(观测项非门槛项——METR 缺口不靠加门堵)',
|
|
221
|
+
gateMonth(m).items.length === 6, JSON.stringify(gateMonth(m).items.map((i) => i.no)));
|
|
222
|
+
|
|
223
|
+
// 样本护栏:不足 ACCEPT_MIN_SAMPLE 条不出率(给小样本比率 = 给噪声开门)
|
|
224
|
+
check('场景 8:护栏阈值 = 5 且与脚本常量同源(文档写死一处会漂)', ACCEPT_MIN_SAMPLE === 5, String(ACCEPT_MIN_SAMPLE));
|
|
225
|
+
const tiny = metrics([{ scope: '自做', result: '一次通过', verdict: '未合入' }]);
|
|
226
|
+
check('场景 8:样本 <5 → acceptRate=null 且 fmtAccept 注明样本数',
|
|
227
|
+
tiny.acceptRate === null && fmtAccept(tiny).includes(`样本 1/${ACCEPT_MIN_SAMPLE}`), `${String(tiny.acceptRate)} | ${fmtAccept(tiny)}`);
|
|
228
|
+
check('场景 8:零记录 → fmtAccept 显式说明而非打 0%',
|
|
229
|
+
fmtAccept(metrics([{ scope: '自做', result: '一次通过' }])).includes('零记录'), fmtAccept(metrics([{ scope: '自做', result: '一次通过' }])));
|
|
230
|
+
|
|
231
|
+
// 存量兼容:无 verdict 字段的行(旧行对象)零影响
|
|
232
|
+
const legacy = metrics([{ scope: '自做', result: '一次通过' }, { scope: '委派', result: '返工×1' }]);
|
|
233
|
+
check('场景 8:存量行(无 verdict 键)零影响——verdictRecorded=0 / 一次通过率与旧口径一致',
|
|
234
|
+
legacy.verdictRecorded === 0 && legacy.total === 2 && Math.abs(legacy.passRate - 0.5) < 1e-9,
|
|
235
|
+
JSON.stringify(legacy));
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 场景 9:表结构兼容 —— 末列缺省不误判(存量表不要求补列)
|
|
239
|
+
{
|
|
240
|
+
const root = mkfix([
|
|
241
|
+
'# 量化证据台账(fixture)',
|
|
242
|
+
'',
|
|
243
|
+
'## 委派结果',
|
|
244
|
+
'',
|
|
245
|
+
'| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 |',
|
|
246
|
+
'|------|----------------|------------|------|------|',
|
|
247
|
+
'| 2026-09-23 | executor(子代理) | 旧表结构行(无合入结论列) | 一次通过 | fixture |',
|
|
248
|
+
'',
|
|
249
|
+
].join('\n'));
|
|
250
|
+
const r = runAgg(root);
|
|
251
|
+
check('场景 9:存量五列表 → exit 0 且正常入账', r.status === 0 && /有效任务 1(/.test(r.stdout), `exit=${r.status}\n${r.stdout}${r.stderr}`);
|
|
252
|
+
check('场景 9:合入结论零记录 → 接受率显示「零记录」', r.stdout.includes('零记录'), r.stdout);
|
|
253
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
254
|
+
|
|
255
|
+
const root2 = mkfix([
|
|
256
|
+
'# 量化证据台账(fixture)',
|
|
257
|
+
'',
|
|
258
|
+
'## 委派结果',
|
|
259
|
+
'',
|
|
260
|
+
'| 日期 | 被委派方(模型) | 任务一句话 | 结果 | 备注 | 合入结论 |',
|
|
261
|
+
'|------|----------------|------------|------|------|----------|',
|
|
262
|
+
...Array.from({ length: 8 }, (_, i) => `| 2026-10-0${i + 1} | executor(子代理) | 新表结构行 | 一次通过 | fixture | 合入 |`),
|
|
263
|
+
'| 2026-10-09 | executor(子代理) | 新表结构行 | 一次通过 | fixture | 未合入 |',
|
|
264
|
+
'| 2026-10-10 | executor(子代理) | 新表结构行 | 一次通过 | fixture | 返工后合入 |',
|
|
265
|
+
'',
|
|
266
|
+
].join('\n'));
|
|
267
|
+
const r2 = runAgg(root2);
|
|
268
|
+
check('场景 9:六列表 → 末列正确解析(已记录 10 / 合入 9 / 未合入 1)',
|
|
269
|
+
r2.status === 0 && /已记录合入结论 10 行|合入 9|未合入 1/.test(r2.stdout), `exit=${r2.status}\n${r2.stdout}${r2.stderr}`);
|
|
270
|
+
check('场景 9:快照行含接受率列(90%(9/10)——样本过护栏才出率)',
|
|
271
|
+
/\|\s*90%(9\/10)\s*\|/.test(r2.stdout), r2.stdout);
|
|
272
|
+
fs.rmSync(root2, { recursive: true, force: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
197
275
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
198
276
|
if (fail) {
|
|
199
277
|
console.log(`\n${USAGE}`);
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-engine-integrity.mjs — 门禁本体完整性门(2026-10-09 P0-A:破「裁判给自己判无罪」)
|
|
3
|
+
//
|
|
4
|
+
// 要解决的缺陷(审查结论 P0 第一条,本仓一手证据):
|
|
5
|
+
// ① `.agents/` 脚本属 new-task.md §5「docs 级豁免 → 直接 commit」,而 check-lane-surface 检查 A 在
|
|
6
|
+
// 「无活跃入口」时只 advisory —— 两条组合 = 改弱 check-loop.mjs 里某道门可无入口直接提交;
|
|
7
|
+
// ② CI 的机器门跑的是**被测分支自己的** `.agents/scripts/check-loop.mjs`(ci.yml:42)——被改坏的
|
|
8
|
+
// 裁判验证自己的篡改,无外部参照,CI 全绿。
|
|
9
|
+
//
|
|
10
|
+
// 本门的做法:把「门禁本体当前的硬拦面」**冻结进一个入库基线** `.agents/engine-lock.json`,
|
|
11
|
+
// 判定 = HEAD 盘面向量 ↔ 基线逐项比对(哈希 + 硬拦单位数)。它不是签名、也不宣称不可伪造——
|
|
12
|
+
// 它把「静默改弱」变成「必须显式声明的改弱」:
|
|
13
|
+
// · 硬拦单位数**下降**(删 blocker / 删 exit 1 / 从面清单删行 / 从 CI 删一次门调用)→ 无条件 BLOCK;
|
|
14
|
+
// · 内容变了但硬拦面没缩 → 须有一份**活跃 L2/L3 入口文档正文点名该文件**,否则 BLOCK;
|
|
15
|
+
// · 基线本身在受检面内(engine-lock.json 已入 control-surfaces,装户径与包源径两面),
|
|
16
|
+
// 改基线同样要过入口门。
|
|
17
|
+
// 于是绕过路径从「编辑一个文件」抬到「编辑两个文件 + 立一份 L2 文档 + 落一条确认台账 + 提交」——
|
|
18
|
+
// 每一环都在 git 历史与台账里留痕,可事后对质。诚实边界:`--no-verify` / 不挂 hooksPath /
|
|
19
|
+
// 删掉本脚本仍可绕过本地门,兜底 = CI 复跑(本脚本被 ci.yml 调用)+ 分支保护;本地信任边界内
|
|
20
|
+
// 不可机器防这一点不掩盖(沿 policy.mjs#ledgerChainHash 与 README 确认门口径)。
|
|
21
|
+
//
|
|
22
|
+
// 可选外部锚两条(都是「不受被测对象控制的参照物」,缺省不启用=无外部副本/无历史时不误报):
|
|
23
|
+
// --against <目录> 逐字比对包源件与外部副本(装户/发布仓:node_modules/agentic-flow-kit)
|
|
24
|
+
// --against-ref <ref> 跨提交锚(破 CI 自指的关键一环):从**历史 ref** 读基线(git show <ref>:<lock>),
|
|
25
|
+
// 与盘面硬拦单位数做单调比较。本地门只比「盘面 ↔ 同分支基线」——恶意分支可同时改弱门禁并 --update
|
|
26
|
+
// 重写基线,两者自洽、CI 全绿(「裁判给自己判无罪」的第二形态)。跨提交锚的参照物在**祖先提交**里,
|
|
27
|
+
// 改它要 rewrite history + 强推(分支保护拦住),CI 由此获得一个不受被测分支控制的参照。
|
|
28
|
+
// 合法收缩(退役一道门)不是一步死路:本地层宽松档(正文提及即认),锚层**严格档**——须活跃
|
|
29
|
+
// L2/L3 入口在 §改动面 段落点名该文件(正文顺手提及不豁免;否则存量无关 L2 文档可替任意改弱背书)。
|
|
30
|
+
// ref 不可解析 / 该 ref 无基线(首个入账提交)→ 跳过并报 ℹ️(锚缺失不误报)。
|
|
31
|
+
//
|
|
32
|
+
// 判据原则(沿 check-lane-surface):只用一手事实——盘面字节哈希、可数单位、文档正文点名;
|
|
33
|
+
// 不从时间戳反推过程。
|
|
34
|
+
// 用法:node check-engine-integrity.mjs [--root <dir>] [--update] [--against <dir>] [--against-ref <ref>] [--quiet]
|
|
35
|
+
// --update 按当前盘面重生成基线(合法演进的最后一步;本身是控制面改动,须与入口文档同批提交)
|
|
36
|
+
// 退出码:0 通过(含基线缺失=未启用);1 阻断(stderr 出 [标签] 与修法);2 用法错误(如 --against 目录不存在)
|
|
37
|
+
import fs from 'node:fs';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import { createHash } from 'node:crypto';
|
|
40
|
+
import { spawnSync } from 'node:child_process';
|
|
41
|
+
import { laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
42
|
+
import { loadKitPolicy } from './policy.mjs';
|
|
43
|
+
|
|
44
|
+
const argv = process.argv.slice(2);
|
|
45
|
+
const argOf = (k) => {
|
|
46
|
+
const i = argv.indexOf(k);
|
|
47
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
|
|
48
|
+
};
|
|
49
|
+
const QUIET = argv.includes('--quiet');
|
|
50
|
+
const UPDATE = argv.includes('--update');
|
|
51
|
+
|
|
52
|
+
const ROOT = argOf('--root') || (() => {
|
|
53
|
+
const r = spawnSync('git', ['-C', process.cwd(), 'rev-parse', '--show-toplevel'], { encoding: 'utf8' });
|
|
54
|
+
return r.status === 0 ? String(r.stdout || '').trim() : process.cwd();
|
|
55
|
+
})();
|
|
56
|
+
// 引擎基线路径双布局(装户端内容清理后):盘面已有基线按其径读;两处皆无时按**仓库布局**定
|
|
57
|
+
// 落点——.agents/ 在 = 装户径;否则 templates/_agents/ 在 = 包源径(--update 才不会重建已清理的装户目录)。
|
|
58
|
+
// ⚠️ 包源径在 sync 下发面内(render.mjs 只排除 .agents/cache):包源基线的键是 templates/_agents/scripts/…
|
|
59
|
+
// 路径,装户盘面是 .agents/scripts/…——一旦把包源基线入库,sync 会把它下发进每个装户的 .agents/,
|
|
60
|
+
// 命名空间错配 → 全员误报「门禁件消失 / 未入基线」。纯包源仓**不得入库包源基线**;要引擎仓自证
|
|
61
|
+
// 完整性,须把基线放到 sync 扫描面之外(如仓库根)再接线 CI,且其豁免出口(L2 入口)在纯包源仓
|
|
62
|
+
// 无 workflow/ 的情况下无处可立——这是设计约束,不是待修缺陷。
|
|
63
|
+
const LOCK_CANDIDATES = ['.agents/engine-lock.json', 'templates/_agents/engine-lock.json'];
|
|
64
|
+
const resolveLock = () => {
|
|
65
|
+
for (const rel of LOCK_CANDIDATES) {
|
|
66
|
+
if (fs.existsSync(path.join(ROOT, rel))) return { rel, abs: path.join(ROOT, rel) };
|
|
67
|
+
}
|
|
68
|
+
const fallback = fs.existsSync(path.join(ROOT, '.agents'))
|
|
69
|
+
? LOCK_CANDIDATES[0]
|
|
70
|
+
: LOCK_CANDIDATES[1];
|
|
71
|
+
return { rel: fallback, abs: path.join(ROOT, fallback) };
|
|
72
|
+
};
|
|
73
|
+
const { rel: LOCK_REL, abs: LOCK_ABS } = resolveLock();
|
|
74
|
+
|
|
75
|
+
// ---- 受检面(单源:本函数;基线与比对同一份清单,防「基线少记一项」的静默空洞)----
|
|
76
|
+
const gateFiles = () => {
|
|
77
|
+
const out = new Set();
|
|
78
|
+
const add = (rel) => { if (fs.existsSync(path.join(ROOT, rel)) && fs.statSync(path.join(ROOT, rel)).isFile()) out.add(rel); };
|
|
79
|
+
const addDir = (relDir, filter = () => true) => {
|
|
80
|
+
const abs = path.join(ROOT, relDir);
|
|
81
|
+
if (!fs.existsSync(abs)) return;
|
|
82
|
+
for (const f of fs.readdirSync(abs, { withFileTypes: true })) {
|
|
83
|
+
if (!f.isFile()) continue;
|
|
84
|
+
const rel = `${relDir}/${f.name}`;
|
|
85
|
+
if (filter(rel)) add(rel);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
addDir('templates/_agents/scripts', (r) => /\.mjs$/.test(r) && !r.endsWith('.test.mjs'));
|
|
89
|
+
addDir('.agents/scripts', (r) => /\.mjs$/.test(r) && !r.endsWith('.test.mjs'));
|
|
90
|
+
addDir('templates/_githooks');
|
|
91
|
+
addDir('.githooks');
|
|
92
|
+
addDir('.agents/hooks');
|
|
93
|
+
addDir('.github/workflows', (r) => r.endsWith('.yml'));
|
|
94
|
+
addDir('templates/_github/workflows', (r) => r.endsWith('.yml'));
|
|
95
|
+
for (const f of ['lane-surfaces.txt', 'control-surfaces.txt', 'workflow-enums.txt', 'workflow-modules.txt',
|
|
96
|
+
'scripts-test-exempt.txt', 'rule-budgets.txt', 'metric-claims.txt']) {
|
|
97
|
+
add(`.agents/${f}`);
|
|
98
|
+
add(`templates/_agents/${f}`);
|
|
99
|
+
}
|
|
100
|
+
add('AGENTS.md');
|
|
101
|
+
add('templates/AGENTS.md');
|
|
102
|
+
return [...out].sort();
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// LF 归一 sha(口径同 src/sync.mjs#shaText:CRLF 检出与 LF 克隆同值,基线跨平台稳定)
|
|
106
|
+
const shaOf = (abs) => createHash('sha256')
|
|
107
|
+
.update(Buffer.from(fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n'), 'utf8')).digest('hex');
|
|
108
|
+
|
|
109
|
+
// 硬拦单位数:文件里「这道门会拦下来」的可数形态。判据刻意保守(只数结构性标志,不数注释/文案)——
|
|
110
|
+
// 目的是抓「删掉一道拦人的分支」,不是抓「改写了一句提示」。
|
|
111
|
+
const hardUnits = (rel, text) => {
|
|
112
|
+
const count = (re) => (text.match(re) || []).length;
|
|
113
|
+
if (/\.m?c?js$/.test(rel) && !rel.endsWith('.test.mjs')) return count(/blockers\.push\(/g) + count(/process\.exit\(1\)/g);
|
|
114
|
+
if (/^(\.githooks|templates\/_githooks)\//.test(rel) || /\.sh$/.test(rel) || rel.endsWith('local-pre-commit')) {
|
|
115
|
+
return count(/\bexit\s+1\b/g);
|
|
116
|
+
}
|
|
117
|
+
if (rel.endsWith('.yml')) return count(/node [^\s]*scripts\/[^\s]*\.(mjs|cjs|sh)/g); // CI 里每道门的调用
|
|
118
|
+
if (/\.(txt|json)$/.test(rel)) {
|
|
119
|
+
return text.split(/\r?\n/).filter((l) => l.trim() && !l.trim().startsWith('#')).length; // 面清单:行即判据
|
|
120
|
+
}
|
|
121
|
+
if (rel.endsWith('AGENTS.md')) return count(/^[-#]/gm); // 常驻规则面条目(协议面)
|
|
122
|
+
return 0;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const buildLock = () => {
|
|
126
|
+
const files = {};
|
|
127
|
+
for (const rel of gateFiles()) {
|
|
128
|
+
const abs = path.join(ROOT, rel);
|
|
129
|
+
files[rel] = { sha256: shaOf(abs), hard: hardUnits(rel, fs.readFileSync(abs, 'utf8')) };
|
|
130
|
+
}
|
|
131
|
+
return { schema: 1, generatedAt: new Date().toISOString(), files };
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const readJson = (p) => {
|
|
135
|
+
try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return null; }
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// 活跃 L2/L3(泳道 high)入口集合:一次扫描,宽松/严格两档判据共用同一份盘面事实
|
|
139
|
+
const HIGH_ENTRIES = (() => {
|
|
140
|
+
const ACTIVE = { intent: new Set(['draft', 'approved']), incident: new Set(['open', 'fixed']) };
|
|
141
|
+
const fmGet = (lines, key) => {
|
|
142
|
+
for (let i = 1; i < lines.length; i++) {
|
|
143
|
+
if (/^---\s*$/.test(lines[i])) break;
|
|
144
|
+
const m = lines[i].match(new RegExp(`^${key}:\\s*(.*)$`));
|
|
145
|
+
if (m) return m[1].trim();
|
|
146
|
+
}
|
|
147
|
+
return '';
|
|
148
|
+
};
|
|
149
|
+
const pol = loadKitPolicy(ROOT);
|
|
150
|
+
const cache = [];
|
|
151
|
+
for (const [dir, kind] of [['intents', 'intent'], ['incidents', 'incident']]) {
|
|
152
|
+
const dp = path.join(ROOT, 'workflow', dir);
|
|
153
|
+
if (!fs.existsSync(dp)) continue;
|
|
154
|
+
for (const f of fs.readdirSync(dp)) {
|
|
155
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
156
|
+
const abs = path.join(dp, f);
|
|
157
|
+
let lines;
|
|
158
|
+
try { lines = fs.readFileSync(abs, 'utf8').replace(/\r\n/g, '\n').split('\n'); } catch { continue; }
|
|
159
|
+
if (!/^---\s*$/.test(lines[0] || '')) continue;
|
|
160
|
+
const st = fmGet(lines, '状态');
|
|
161
|
+
if (!ACTIVE[kind].has(st)) continue;
|
|
162
|
+
const level = fmGet(lines, '级别');
|
|
163
|
+
const lane = kind === 'intent'
|
|
164
|
+
? laneOfEntry(level, fmGet(lines, 'risk_level'), pol.riskLevelSince, fmGet(lines, '日期'))
|
|
165
|
+
: laneOfDoc(level);
|
|
166
|
+
if (lane !== 'high') continue;
|
|
167
|
+
cache.push({ rel: `workflow/${dir}/${f}`, text: lines.join('\n') });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return cache;
|
|
171
|
+
})();
|
|
172
|
+
|
|
173
|
+
// 宽松档(本地层):整条 rel 或 basename 出现在正文即认——口径同检查 8 的「声明改动面 ↔ 提交文件」
|
|
174
|
+
// 模糊匹配,宁松不假阳性;精确性由 confirm-doc 指纹与台账承担。
|
|
175
|
+
const entryNamesFile = (fileRel) => {
|
|
176
|
+
const base = path.basename(fileRel);
|
|
177
|
+
return HIGH_ENTRIES.find((e) => e.text.includes(fileRel) || e.text.includes(base)) || null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// 严格档(仅跨提交锚用):入口须在**声明改动面**段落点名该文件,而非正文任意一处提及。
|
|
181
|
+
// 为什么分层:本地层的模糊放行代价低(内容漂移本就允许「声明过的演进」);锚层拦的是
|
|
182
|
+
// 「硬拦单位数下降」——若一句顺手提及就能豁免,本仓实测三份**与本批无关的存量 L2 incident**
|
|
183
|
+
// (2026-09-28-metric-claim-gate / 2026-10-03-plan-confirm-boiler 正文含 check-loop.mjs,
|
|
184
|
+
// 2026-09-26-managed-ledger-adopt 含 pre-commit)即可替任意改弱行为背书,锚等于没有。
|
|
185
|
+
// 严格档的误拦修法就是协议本意:把该文件写进入口的 §改动面。
|
|
186
|
+
const entryDeclaresFile = (() => {
|
|
187
|
+
const SECTION = /^[^\n]{0,40}改动面/;
|
|
188
|
+
const declared = (text) => {
|
|
189
|
+
const lines = text.split('\n');
|
|
190
|
+
const out = [];
|
|
191
|
+
for (let i = 0; i < lines.length; i++) {
|
|
192
|
+
if (/^---\s*$/.test(lines[i]) || !SECTION.test(lines[i])) continue;
|
|
193
|
+
// 含标题行本身(「§改动面 点名 x.mjs」是常见的一行式声明)与标题后到下一个 heading 前的全部正文
|
|
194
|
+
for (let j = i; j < lines.length && (j === i || !/^\s*#{1,6}\s/.test(lines[j])); j++) out.push(lines[j]);
|
|
195
|
+
}
|
|
196
|
+
return out.join('\n');
|
|
197
|
+
};
|
|
198
|
+
return (fileRel) => {
|
|
199
|
+
const base = path.basename(fileRel);
|
|
200
|
+
return HIGH_ENTRIES.find((e) => { const s = declared(e.text); return s.includes(fileRel) || s.includes(base); }) || null;
|
|
201
|
+
};
|
|
202
|
+
})();
|
|
203
|
+
|
|
204
|
+
if (UPDATE) {
|
|
205
|
+
fs.mkdirSync(path.dirname(LOCK_ABS), { recursive: true });
|
|
206
|
+
const lock = buildLock();
|
|
207
|
+
const prev = readJson(LOCK_ABS);
|
|
208
|
+
const changed = prev ? Object.keys(lock.files).filter((k) => !prev.files[k] || prev.files[k].sha256 !== lock.files[k].sha256) : Object.keys(lock.files);
|
|
209
|
+
const removed = prev ? Object.keys(prev.files).filter((k) => !lock.files[k]) : [];
|
|
210
|
+
fs.writeFileSync(LOCK_ABS, JSON.stringify(lock, null, 2) + '\n');
|
|
211
|
+
console.log(`引擎基线已更新:${LOCK_REL}(受检面 ${Object.keys(lock.files).length} 个文件)`);
|
|
212
|
+
if (changed.length) console.log(` 本次入账为改动件:${changed.length} 个`);
|
|
213
|
+
if (removed.length) console.log(` 本次移除(文件已不在盘):${removed.join(' ')}`);
|
|
214
|
+
console.log(' ⚠️ 基线更新本身是控制面改动:须与点名这些文件的 L2 入口文档同批提交(否则检查 C / 本门默认模式会拦)');
|
|
215
|
+
process.exit(0);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const lock = readJson(LOCK_ABS);
|
|
219
|
+
const problems = [];
|
|
220
|
+
const current = {};
|
|
221
|
+
for (const rel of gateFiles()) current[rel] = { sha256: shaOf(path.join(ROOT, rel)), hard: hardUnits(rel, fs.readFileSync(path.join(ROOT, rel), 'utf8')) };
|
|
222
|
+
|
|
223
|
+
// ---- 跨提交锚(先于本地基线判定:恶意分支把盘面 engine-lock.json 删掉也逃不掉这一层——参照物读自 git 对象库)----
|
|
224
|
+
if (argOf('--against-ref')) {
|
|
225
|
+
const ref = argOf('--against-ref');
|
|
226
|
+
// 两候选径都试(装户径 / 包源径)——纯包源仓的基线在 templates/_agents/ 下,锚须读得到
|
|
227
|
+
let anchor = null;
|
|
228
|
+
for (const rel of LOCK_CANDIDATES) {
|
|
229
|
+
const r = spawnSync('git', ['-C', ROOT, 'show', `${ref}:${rel}`], { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
230
|
+
try { anchor = JSON.parse(String(r.stdout || '')); } catch { /* 该候选径在此 ref 无基线 → 试下一个 */ }
|
|
231
|
+
if (anchor && anchor.files) break;
|
|
232
|
+
anchor = null;
|
|
233
|
+
}
|
|
234
|
+
if (!anchor) {
|
|
235
|
+
if (!QUIET) console.log(`ℹ️ 跨提交锚缺失(${ref} 无 ${LOCK_REL})——该层跳过(首个入账提交/浅克隆)`);
|
|
236
|
+
} else {
|
|
237
|
+
for (const [rel, was] of Object.entries(anchor.files)) {
|
|
238
|
+
const got = current[rel];
|
|
239
|
+
if (!got) { problems.push(`[门禁件消失(跨提交锚)] ${rel} 在 ${ref} 的基线内但盘面不存在——删除一道门等于禁用该检查`); continue; }
|
|
240
|
+
if (typeof was.hard !== 'number' || got.hard >= was.hard) continue;
|
|
241
|
+
const e = entryDeclaresFile(rel);
|
|
242
|
+
if (!e) {
|
|
243
|
+
problems.push(`[硬拦面收缩(跨提交锚)] ${rel} 硬拦单位数 ${was.hard}(见 ${ref})→ 盘面 ${got.hard},减少 ${was.hard - got.hard}——祖先提交是参照物,本分支改不动它;确需退役该判据须有活跃 L2/L3 入口在**声明改动面**段落点名该文件(正文顺手提及不算——存量无关文档会替任意改弱行为背书)`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!lock || !lock.files) {
|
|
250
|
+
if (!QUIET) console.log(`ℹ️ 引擎基线未启用(无 ${LOCK_REL})——跑 node .agents/scripts/check-engine-integrity.mjs --update 生成(跨提交锚与外部副本锚照常执行)`);
|
|
251
|
+
} else {
|
|
252
|
+
for (const [rel, want] of Object.entries(lock.files)) {
|
|
253
|
+
const got = current[rel];
|
|
254
|
+
const name = path.basename(rel);
|
|
255
|
+
if (!got) { problems.push(`[门禁件消失] ${rel} 在基线内但盘面不存在——删除一道门等于禁用该检查`); continue; }
|
|
256
|
+
if (got.sha256 === want.sha256) continue;
|
|
257
|
+
if (typeof want.hard === 'number' && got.hard < want.hard) {
|
|
258
|
+
problems.push(`[硬拦面收缩] ${rel} 硬拦单位数 ${want.hard} → ${got.hard}(减少 ${want.hard - got.hard})——删 blocker/exit/判据行的结构性形态,无论是否声明均拦`);
|
|
259
|
+
} else {
|
|
260
|
+
const e = entryNamesFile(rel);
|
|
261
|
+
if (!e) problems.push(`[控制面改动无入口] ${rel} 内容与基线不符(${name} 硬拦单位数 ${want.hard} → ${got.hard})——无活跃 L2/L3 入口正文点名该文件`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
for (const rel of Object.keys(current)) {
|
|
265
|
+
if (!lock.files[rel]) problems.push(`[门禁件未入基线] ${rel} 是新出现的受检文件——合法新增判据须跑 --update 并把基线更新与入口文档同批提交`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (argOf('--against')) {
|
|
270
|
+
const ext = path.resolve(argOf('--against'));
|
|
271
|
+
if (!fs.existsSync(ext)) { console.error(`用法:--against 目录不存在(${ext})`); process.exit(2); }
|
|
272
|
+
const extProblems = [];
|
|
273
|
+
for (const rel of Object.keys(current)) {
|
|
274
|
+
const extAbs = path.join(ext, rel);
|
|
275
|
+
if (!fs.existsSync(extAbs)) continue; // 外部副本不含该件(如装户无 templates/)——不判
|
|
276
|
+
if (shaOf(extAbs) !== current[rel].sha256) extProblems.push(` ${rel} 与外部已知良好副本失配`);
|
|
277
|
+
}
|
|
278
|
+
if (extProblems.length) problems.push(`[外部锚失配] 受检面与 ${argOf('--against')} 逐字比对不一致(外部裁判不认本次改动面):\n${extProblems.join('\n')}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (problems.length) {
|
|
282
|
+
console.error(`引擎完整性 — HARD-BLOCK:\n\n${problems.map((p) => `- ${p}`).join('\n')}\n`);
|
|
283
|
+
console.error(` 修法:确属门禁演进 → 立 L2 入口(正文 §改动面 点名上述文件)+ 判据更新后跑 node .agents/scripts/check-engine-integrity.mjs --update,三者同批提交`);
|
|
284
|
+
console.error(' 修法:非演进(误改/被他人改动)→ git diff 核对并回退,勿改基线绕过(基线本身在控制面清单内)');
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
if (!QUIET) console.log(`✅ 引擎完整性:受检面 ${Object.keys(current).length} 个文件(硬拦面无收缩${lock && lock.files ? '、盘面与基线一致' : '、本地基线未启用'})`);
|
|
288
|
+
process.exit(0);
|