agentic-flow-kit 1.7.16 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -7
- package/modules/hosts/claude/commands/wf-build.md +1 -1
- package/modules/hosts/claude/commands/wf-maintain.md +1 -1
- package/modules/hosts/claude/commands/wf-new-task.md +9 -8
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-build.md +1 -1
- package/modules/hosts/codex/commands/wf-maintain.md +1 -1
- package/modules/hosts/codex/commands/wf-new-task.md +9 -8
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-build.md +1 -1
- package/modules/hosts/cursor/commands/wf-maintain.md +1 -1
- package/modules/hosts/cursor/commands/wf-new-task.md +9 -8
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-build.md +1 -1
- package/modules/hosts/opencode/commands/wf-maintain.md +1 -1
- package/modules/hosts/opencode/commands/wf-new-task.md +9 -8
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-build.md +1 -1
- package/modules/hosts/trae/commands/wf-maintain.md +1 -1
- package/modules/hosts/trae/commands/wf-new-task.md +9 -8
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +6 -2
- package/src/doctor.mjs +11 -2
- package/src/executor.mjs +25 -8
- package/src/executor.test.mjs +4 -1
- package/src/fresh-init.test.mjs +1 -1
- package/src/init.mjs +20 -3
- package/src/profiles.mjs +7 -3
- package/src/rule-budget.test.mjs +2 -2
- package/src/stack-profile.test.mjs +6 -0
- package/src/validate.mjs +6 -1
- package/src/validate.test.mjs +1 -1
- package/templates/.gitattributes +4 -1
- package/templates/AGENTS.md +2 -2
- package/templates/_agents/commands/build.md +1 -1
- package/templates/_agents/commands/maintain.md +1 -1
- package/templates/_agents/commands/new-task.md +9 -8
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/engine-lock.json +130 -42
- package/templates/_agents/hooks/check-index-incremental.mjs +48 -0
- package/templates/_agents/hooks/check-pairing-incremental.mjs +66 -0
- package/templates/_agents/hooks/check-wiki-ledger.mjs +30 -0
- package/templates/_agents/lane-surfaces.txt +4 -1
- package/templates/_agents/level-hints.txt +1 -1
- package/templates/_agents/loop/papercut-debt/DESIGN.md +112 -0
- package/templates/_agents/mechanism-registry.md +27 -0
- package/templates/_agents/rule-budgets.txt +2 -2
- package/templates/_agents/scripts/agg-delegations.cjs +21 -3
- package/templates/_agents/scripts/agg-gate-stats.mjs +3 -3
- package/templates/_agents/scripts/agg-judgments.mjs +80 -0
- package/templates/_agents/scripts/agg-judgments.test.mjs +45 -0
- package/templates/_agents/scripts/archive-sweep.mjs +233 -0
- package/templates/_agents/scripts/archive-sweep.test.mjs +129 -0
- package/templates/_agents/scripts/check-attribution.mjs +37 -0
- package/templates/_agents/scripts/check-convergence.mjs +119 -0
- package/templates/_agents/scripts/check-engine-integrity.mjs +4 -1
- package/templates/_agents/scripts/check-funnel.mjs +56 -0
- package/templates/_agents/scripts/check-hygiene.mjs +5 -5
- package/templates/_agents/scripts/check-index-incremental.test.mjs +73 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +2 -2
- package/templates/_agents/scripts/check-lane-surface.test.mjs +5 -5
- package/templates/_agents/scripts/check-ledger-invariant.mjs +403 -152
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +433 -3
- package/templates/_agents/scripts/check-loop-rev.test.mjs +8 -5
- package/templates/_agents/scripts/check-loop-unit.test.mjs +8 -0
- package/templates/_agents/scripts/check-loop.mjs +215 -34
- package/templates/_agents/scripts/check-loop.test.mjs +570 -49
- package/templates/_agents/scripts/check-prejudgment.mjs +59 -0
- package/templates/_agents/scripts/check-registry.mjs +45 -0
- package/templates/_agents/scripts/check-scope.mjs +96 -0
- package/templates/_agents/scripts/check-scope.test.mjs +96 -0
- package/templates/_agents/scripts/check-scripts-syntax.mjs +61 -0
- package/templates/_agents/scripts/check-scripts-syntax.test.mjs +72 -0
- package/templates/_agents/scripts/check-spec-verif.mjs +47 -0
- package/templates/_agents/scripts/compact-ledgers.mjs +3 -3
- package/templates/_agents/scripts/confirm-doc.mjs +114 -38
- package/templates/_agents/scripts/confirm-doc.test.mjs +180 -4
- package/templates/_agents/scripts/edit-face-check.mjs +5 -3
- package/templates/_agents/scripts/fill-intent.mjs +5 -3
- package/templates/_agents/scripts/fill-intent.test.mjs +19 -1
- package/templates/_agents/scripts/fill-plan.mjs +10 -6
- package/templates/_agents/scripts/fill-plan.test.mjs +19 -3
- package/templates/_agents/scripts/fill-spec.mjs +6 -3
- package/templates/_agents/scripts/fill-spec.test.mjs +12 -4
- package/templates/_agents/scripts/fixture-scripts.mjs +2 -2
- package/templates/_agents/scripts/fixture-scripts.test.mjs +6 -1
- package/templates/_agents/scripts/gate-checklist.mjs +6 -0
- package/templates/_agents/scripts/gate-seg.mjs +1 -1
- package/templates/_agents/scripts/gen-effective-policy.mjs +107 -0
- package/templates/_agents/scripts/gen-effective-policy.test.mjs +100 -0
- package/templates/_agents/scripts/gen-verify-evidence.mjs +107 -0
- package/templates/_agents/scripts/gen-verify-evidence.test.mjs +84 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -3
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +2 -1
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +3 -3
- package/templates/_agents/scripts/git-core.mjs +45 -0
- package/templates/_agents/scripts/judgment-ledger.mjs +65 -0
- package/templates/_agents/scripts/judgment-ledger.test.mjs +129 -0
- package/templates/_agents/scripts/kb-search.mjs +19 -4
- package/templates/_agents/scripts/kb-search.test.mjs +33 -0
- package/templates/_agents/scripts/lane-surface-audit.mjs +143 -0
- package/templates/_agents/scripts/lane-surface-audit.test.mjs +93 -0
- package/templates/_agents/scripts/ledger-shards.mjs +307 -0
- package/templates/_agents/scripts/ledger-shards.test.mjs +339 -0
- package/templates/_agents/scripts/level-criteria.mjs +9 -10
- package/templates/_agents/scripts/level-criteria.test.mjs +7 -5
- package/templates/_agents/scripts/papercut-debt-controller.mjs +141 -0
- package/templates/_agents/scripts/papercut-debt-controller.test.mjs +127 -0
- package/templates/_agents/scripts/papercut-debt-sensor.mjs +170 -0
- package/templates/_agents/scripts/papercut-debt-sensor.test.mjs +146 -0
- package/templates/_agents/scripts/pipeline-run.mjs +6 -22
- package/templates/_agents/scripts/pipeline-run.test.mjs +22 -42
- package/templates/_agents/scripts/policy.mjs +28 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.mjs +110 -0
- package/templates/_agents/scripts/resolve-merge-conflicts.test.mjs +87 -0
- package/templates/_agents/scripts/rewrite-file.mjs +133 -0
- package/templates/_agents/scripts/rewrite-file.test.mjs +148 -0
- package/templates/_agents/scripts/rule-budget.mjs +83 -0
- package/templates/_agents/scripts/run-tests.mjs +99 -0
- package/templates/_agents/scripts/run-tests.test.mjs +105 -0
- package/templates/_agents/scripts/solidify-task.test.mjs +1 -1
- package/templates/_agents/scripts/stage-gates.mjs +34 -12
- package/templates/_agents/scripts/trust-mode.mjs +4 -4
- package/templates/_agents/scripts/trust-mode.test.mjs +1 -1
- package/templates/_agents/scripts/verify.mjs +135 -58
- package/templates/_agents/scripts/verify.test.mjs +156 -47
- package/templates/_agents/scripts/wf-journal.mjs +8 -4
- package/templates/_agents/scripts/workflow-board-server.test.mjs +2 -2
- package/templates/_agents/scripts/workflow-enums.test.mjs +14 -5
- package/templates/_agents/scripts-test-exempt.txt +1 -0
- package/templates/_agents/skills/papercut-fix/SKILL.md +60 -0
- package/templates/_agents/skills/papercut-fix/references/response-template.md +81 -0
- package/templates/_agents/workflow-enums.txt +8 -1
- package/templates/_githooks/pre-commit +110 -90
- package/templates/_githooks/pre-merge-commit +27 -23
- package/templates/_githooks/pre-push +4 -3
- package/templates/_github/workflows/kit-ci.yml +4 -3
- package/templates/workflow/README.md +14 -7
- package/templates/workflow/incidents/_TEMPLATE.md +4 -1
- package/templates/workflow/intents/_TEMPLATE.md +4 -2
- package/templates/workflow/papercuts.md +2 -0
- package/templates/workflow/plans/_TEMPLATE.md +10 -5
- package/templates/workflow/regression-checklist.md +1 -0
- package/templates/workflow/specs/_TEMPLATE.md +17 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +0 -43
- package/templates/_agents/hooks/check-wiki-ledger.sh +0 -18
- package/templates/_agents/scripts/rule-budget.sh +0 -74
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gen-verify-evidence.test.mjs — 验证凭证摘要生成器 fixture 套件(2026-10-13 自 Shipyard.Material
|
|
3
|
+
// 参考实现回灌时随件移植;台账口径改 kit 绿行形状 suite:'npm test' + 绑定单 doc 列)
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { spawnSync } from 'node:child_process';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
const SCRIPT = path.join(SCRIPT_DIR, 'gen-verify-evidence.mjs');
|
|
12
|
+
|
|
13
|
+
let pass = 0;
|
|
14
|
+
let fail = 0;
|
|
15
|
+
const check = (desc, cond, detail = '') => {
|
|
16
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
17
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
const mkfix = (rows) => {
|
|
20
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-evidence-test-'));
|
|
21
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
22
|
+
if (rows) fs.writeFileSync(path.join(d, '.agents', 'verifications.jsonl'), rows.map((r) => JSON.stringify(r)).join('\n') + '\n');
|
|
23
|
+
return d;
|
|
24
|
+
};
|
|
25
|
+
const run = (root, ...args) => {
|
|
26
|
+
const r = spawnSync(process.execPath, [SCRIPT, '--root', root, ...args], { encoding: 'utf8' });
|
|
27
|
+
return { code: r.status, out: `${r.stdout || ''}${r.stderr || ''}` };
|
|
28
|
+
};
|
|
29
|
+
// kit 绿行形状(verify.mjs 落账单源):suite 'npm test'、passed/failed = 套件计数、doc = --doc 绑定单
|
|
30
|
+
const GREEN = (headShort, ts, extra = {}) => ({ ts, exitCode: 0, suite: 'npm test', gates: 7, passed: 45, failed: 0, headShort, ...extra });
|
|
31
|
+
|
|
32
|
+
// 场景 1:绿行渲染 + 同 SHA 去重取最新 + 失败行/无 SHA 行/坏行排除
|
|
33
|
+
{
|
|
34
|
+
const d = mkfix([
|
|
35
|
+
GREEN('abc1234', '2026-10-09T08:34:08Z'),
|
|
36
|
+
GREEN('abc1234', '2026-10-09T09:14:44Z'), // 同 SHA 更新 ts → 只留此行
|
|
37
|
+
GREEN('def5678', '2026-10-09T10:00:00Z', { runId: 'PIPE-1', doc: 'workflow/intents/<日期>-<主题>.md' }),
|
|
38
|
+
{ ts: '2026-10-09T11:00:00Z', exitCode: 1, suite: 'check-loop', note: 'exit 1' }, // 失败行
|
|
39
|
+
{ ts: '2026-10-09T12:00:00Z', exitCode: 0, suite: 'npm test', gates: 7 }, // 无 headShort(非 git 环境/旧 schema)
|
|
40
|
+
'{broken json', // 坏行
|
|
41
|
+
]);
|
|
42
|
+
const r = run(d);
|
|
43
|
+
const doc = fs.readFileSync(path.join(d, 'workflow', 'verify-evidence.md'), 'utf8');
|
|
44
|
+
check('场景 1:只渲染绿行且同 SHA 取最新(abc1234 只出现一次且为 09:14)',
|
|
45
|
+
r.code === 0 && (doc.match(/\| `abc1234`/g) || []).length === 1 && doc.includes('2026-10-09T09:14:44Z') && doc.includes('`def5678`') && doc.includes('PIPE-1') && !doc.includes('2026-10-09T11:00:00Z') && !doc.includes('broken'),
|
|
46
|
+
doc);
|
|
47
|
+
check('场景 1b:门数、套件计数与绑定单列渲染', doc.includes('| 7 | PASS 45 / FAIL 0 |') && doc.includes('workflow/intents/<日期>-<主题>.md'), doc);
|
|
48
|
+
}
|
|
49
|
+
// 场景 2:无台账 → 空表骨架(新 clone 首跑)
|
|
50
|
+
{
|
|
51
|
+
const d = mkfix(null);
|
|
52
|
+
const r = run(d);
|
|
53
|
+
const doc = fs.readFileSync(path.join(d, 'workflow', 'verify-evidence.md'), 'utf8');
|
|
54
|
+
check('场景 2:无台账生成空表骨架', r.code === 0 && doc.includes('# 验证绿凭证摘要') && !doc.includes('`abc'), doc);
|
|
55
|
+
}
|
|
56
|
+
// 场景 3:--check 一致/漂移/再生成恢复
|
|
57
|
+
{
|
|
58
|
+
const d = mkfix([GREEN('aaa1111', '2026-10-09T08:00:00Z')]);
|
|
59
|
+
run(d);
|
|
60
|
+
check('场景 3a:生成后 --check 一致', run(d, '--check').code === 0);
|
|
61
|
+
fs.writeFileSync(path.join(d, '.agents', 'verifications.jsonl'), JSON.stringify(GREEN('bbb2222', '2026-10-09T09:00:00Z')) + '\n');
|
|
62
|
+
const r2 = run(d, '--check');
|
|
63
|
+
check('场景 3b:台账新增绿行 → --check 漂移 exit 1', r2.code === 1 && r2.out.includes('关单时跑'), r2.out);
|
|
64
|
+
run(d);
|
|
65
|
+
check('场景 3c:再生成后恢复一致', run(d, '--check').code === 0);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 场景 4:追加保全——本地台账缺某 SHA 时,仓内既有行不被抹掉(跨机/换 worktree 不失历史)
|
|
69
|
+
{
|
|
70
|
+
const d = mkfix([GREEN('aaa1111', '2026-10-09T08:00:00Z'), GREEN('ccc3333', '2026-10-09T09:00:00Z')]);
|
|
71
|
+
run(d);
|
|
72
|
+
const before = fs.readFileSync(path.join(d, 'workflow', 'verify-evidence.md'), 'utf8');
|
|
73
|
+
// 换 worktree / 换机:本地台账被覆盖为只含 ddd4444
|
|
74
|
+
fs.writeFileSync(path.join(d, '.agents', 'verifications.jsonl'), JSON.stringify(GREEN('ddd4444', '2026-10-09T10:00:00Z')) + '\n');
|
|
75
|
+
const r = run(d);
|
|
76
|
+
const doc = fs.readFileSync(path.join(d, 'workflow', 'verify-evidence.md'), 'utf8');
|
|
77
|
+
check('场景 4:台账换机后仓内既有行保全(aaa1111/ccc3333 仍在,ddd4444 补入)',
|
|
78
|
+
r.code === 0 && doc.includes('`aaa1111`') && doc.includes('`ccc3333`') && doc.includes('`ddd4444`') && before.includes('`aaa1111`'),
|
|
79
|
+
doc);
|
|
80
|
+
check('场景 4b:保全后 --check 一致(幂等)', run(d, '--check').code === 0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
84
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -20,6 +20,9 @@ import path from 'node:path';
|
|
|
20
20
|
import { fileURLToPath } from 'node:url';
|
|
21
21
|
import { createRequire } from 'node:module';
|
|
22
22
|
import { readLedgerRows } from './ledger-loader.mjs';
|
|
23
|
+
// 委派台账分片感知读(2026-10-13 ledger-shard-ledgers 回灌):合并文本(主台账 ∪ delegations.d/*.md)
|
|
24
|
+
// 由本件经 readLedgerText 取(读侧单源),经 opts.text 注入 agg.readLedger——.cjs 库模式无法自装载 .mjs。
|
|
25
|
+
import { readLedgerText } from './ledger-shards.mjs';
|
|
23
26
|
|
|
24
27
|
const require = createRequire(import.meta.url);
|
|
25
28
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -96,7 +99,9 @@ function collectDoneMap(root) {
|
|
|
96
99
|
|
|
97
100
|
// qualityRows:月度质量行(判据全部复用 agg——本函数只做分组与格式化)
|
|
98
101
|
export function qualityRows(root) {
|
|
99
|
-
|
|
102
|
+
// 分片感知(2026-10-13 ledger-shard-ledgers):text = 主台账 ∪ delegations.d/*.md;空文本(无台账
|
|
103
|
+
// 装户)→ rows [],soft 优雅降级语义不变。
|
|
104
|
+
const rows = agg.readLedger(root, { soft: true, text: readLedgerText(root, 'delegations') });
|
|
100
105
|
const byMonth = {};
|
|
101
106
|
for (const r of rows) (byMonth[r.date.slice(0, 7)] ||= []).push(r);
|
|
102
107
|
const months = Object.keys(byMonth).sort();
|
|
@@ -130,9 +135,9 @@ export function qualityRows(root) {
|
|
|
130
135
|
// 故本节恒为静态文案:**任何时候生成本文件,字节恒等**(测试 ⑤ 的两条 --check exit 0 钉的就是这条)。
|
|
131
136
|
export function gateRoiLines() {
|
|
132
137
|
return [
|
|
133
|
-
'- 采集:`node .agents/scripts/check-loop.mjs --gate-stats
|
|
138
|
+
'- 采集:`node .agents/scripts/check-loop.mjs --all --gate-stats`(--all 全段成本面——2026-10-13 分道后裸跑只记硬门段;数据落 `.agents/cache/gate-stats.jsonl`,该目录已 gitignore;不带 flag 时零写入、输出逐字节不变)',
|
|
134
139
|
'- 看数值:`node .agents/scripts/agg-gate-stats.mjs [--days 30]`——输出成本排序 + 两类决策依据:噪声率 top(该收窄或该修)、死检查(该拆或该降级);无采集数据时该命令自己提示「未采集」并给出采集命令',
|
|
135
|
-
'- 口径:耗时**含子进程时间**(检查 11 调两个生成器 `--check`、检查 13 调 `
|
|
140
|
+
'- 口径:耗时**含子进程时间**(检查 11 调两个生成器 `--check`、检查 13 调 `rule-budget.mjs`)——门禁真实成本,不做剔除;样本 <5 次时只作线索,不作决策依据',
|
|
136
141
|
'- 本节为何不铺数值:本文件被检查 11 漂移门覆盖,运行数据每跑一次门禁即变一次,铺进来会让每次采集都产一条漂移 WARN(不可消除噪声)',
|
|
137
142
|
];
|
|
138
143
|
}
|
|
@@ -163,8 +163,9 @@ const INTENT = (fm) => `---\n${fm}\n---\n# INTENT — x\n`;
|
|
|
163
163
|
|
|
164
164
|
// 无采集态(先于造 cache 生成):节恒为静态文案,故与有无数据无关
|
|
165
165
|
const rl = gateRoiLines();
|
|
166
|
+
// 采集命令随 2026-10-13 分道带 --all(全段成本面;裸 --gate-stats 只记硬门段)
|
|
166
167
|
check('门禁ROI:节内容给出采集命令 + 看数值命令 + 无数据时 CLI 自带「未采集」提示(优雅降级不落空节)',
|
|
167
|
-
rl.join('\n').includes('check-loop.mjs --gate-stats') && rl.join('\n').includes('agg-gate-stats.mjs') && rl.join('\n').includes('未采集'), rl.join('\n'));
|
|
168
|
+
rl.join('\n').includes('check-loop.mjs --all --gate-stats') && rl.join('\n').includes('agg-gate-stats.mjs') && rl.join('\n').includes('未采集'), rl.join('\n'));
|
|
168
169
|
check('门禁ROI:节内容不含任何耗时/次数运行数值(防检查 11 每次采集报漂移的设计核心)',
|
|
169
170
|
!/\d+\s*(ms|s)\b/.test(rl.join('\n')) && !/运行\s*\d/.test(rl.join('\n')), rl.join('\n'));
|
|
170
171
|
run();
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// 目的:把「语料膨胀 + 常驻面体积」变成可观测趋势——每月(或按需)跑一次,往 workflow/metrics.md 追加/更新一行。
|
|
5
5
|
// 口径:文档 = 磁盘(对照 gen-workflow-index / 看板,排除 _TEMPLATE.md);常驻面 = `.agents/rule-budgets.txt`
|
|
6
6
|
// 各条实测(单源,含预算上限),括号内给「预算占用峰值项」。
|
|
7
|
-
// 字节口径声明(p2-batch2):本脚本用工作区 statSync 字节(CRLF 检出态会偏高),rule-budget.
|
|
7
|
+
// 字节口径声明(p2-batch2):本脚本用工作区 statSync 字节(CRLF 检出态会偏高),rule-budget.mjs 用
|
|
8
8
|
// 索引 LF 归一字节——趋势观测可对比,但与门禁判定数值存在系统性差异,勿互相替用。
|
|
9
9
|
// 不做 --check / 不挂门禁:快照是历史记录——任何文档改动都会让「当前值」漂移,拿它做漂移校验只会常红;
|
|
10
|
-
// 规则面的机器约束由 pre-commit 的 rule-budget.
|
|
10
|
+
// 规则面的机器约束由 pre-commit 的 rule-budget.mjs 负责,本脚本只负责「看见趋势」。
|
|
11
11
|
// 测试:node .agents/scripts/gen-workflow-metrics.test.mjs(fixture 回归,须全绿)
|
|
12
12
|
import fs from 'node:fs';
|
|
13
13
|
import path from 'node:path';
|
|
@@ -115,7 +115,7 @@ function scanSurface() {
|
|
|
115
115
|
max = sum;
|
|
116
116
|
label = `${pat}(合计)`;
|
|
117
117
|
} else if (pat.includes('*')) {
|
|
118
|
-
// 词表约定 glob = 基名内单个 `*`(<前缀>*<后缀>),目录段不支持通配——与 rule-budget.
|
|
118
|
+
// 词表约定 glob = 基名内单个 `*`(<前缀>*<后缀>),目录段不支持通配——与 rule-budget.mjs 的 glob 展开同口径。
|
|
119
119
|
// 多 `*` / 目录段含 `*` 出词表:警告跳过,不做「只按后缀 endsWith」的放宽式匹配(2026-09-24 收口:
|
|
120
120
|
// 曾按首个 * 取后缀,`pl*.md` 会误吃目录内全部 .md、`a*b` 会误吃一切 b 结尾文件)
|
|
121
121
|
const stars = pat.split('*').length - 1;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// git-core.mjs — 引擎 git 调用单源(2026-10-10 收敛系列 5:合并 check-loop-kit spawnGit/gitOut 与
|
|
2
|
+
// stage-gates gitRun 的「同口径两份实现」(该债由 stage-gates 头注登记、本单偿清))。
|
|
3
|
+
// 语义(取两侧并集,全部调用方共享):
|
|
4
|
+
// - 参数数组直 spawn(不走 shell——Windows cmd 不认单引号);git.exe 显式解析(Windows spawn 不补扩展名)
|
|
5
|
+
// - CHECK_LOOP_GIT 环境变量注入钩子(测试替身:指向不存在路径可稳定触发 spawn 异常)
|
|
6
|
+
// - 瞬时类错误(EAGAIN/EPERM/EMFILE/EINTR)单次重试;重试终败响亮出账(进程级去重恰一条)——
|
|
7
|
+
// 「非 git 仓」(status≠0、无 error)仍是合法装户语义、静默降级
|
|
8
|
+
// - 只读查询记忆化:同参(含 cwd)复用结果;带 error 的基础设施失败不缓存(保留重试语义)
|
|
9
|
+
// - maxBuffer 32MB(pickaxe/name-only 大输出)
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
export const GIT = process.env.CHECK_LOOP_GIT || (process.platform === 'win32' ? 'git.exe' : 'git');
|
|
13
|
+
|
|
14
|
+
const GIT_TRANSIENT = new Set(['EAGAIN', 'EPERM', 'EMFILE', 'EINTR']);
|
|
15
|
+
const GIT_RESULT_CACHE = new Map(); // 键 = cwd + NUL + args.join(NUL)(多仓安全:confirm-doc 等 root 参数调用方)
|
|
16
|
+
let infraWarned = false;
|
|
17
|
+
function warnInfra(args, code) {
|
|
18
|
+
if (infraWarned) return;
|
|
19
|
+
infraWarned = true;
|
|
20
|
+
console.error(`git-core: git 探测异常 ${args[0]}(${code})——按非 git 仓降级,检查面可能收窄`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function spawnGit(args, cwd) {
|
|
24
|
+
const cacheKey = cwd + '\u0000' + args.join('\u0000');
|
|
25
|
+
if (GIT_RESULT_CACHE.has(cacheKey)) return GIT_RESULT_CACHE.get(cacheKey);
|
|
26
|
+
const opts = { cwd, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 };
|
|
27
|
+
let r = spawnSync(GIT, args, opts);
|
|
28
|
+
if (r.error) {
|
|
29
|
+
if (GIT_TRANSIENT.has(r.error.code)) r = spawnSync(GIT, args, opts);
|
|
30
|
+
if (r.error) warnInfra(args, r.error.code || r.error.message);
|
|
31
|
+
}
|
|
32
|
+
if (!r.error) GIT_RESULT_CACHE.set(cacheKey, r);
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function gitOut(args, cwd, { ok = () => true } = {}) {
|
|
37
|
+
const r = spawnGit(args, cwd);
|
|
38
|
+
if (r.error || r.status !== 0 || !ok(r)) return null;
|
|
39
|
+
return r.stdout || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --rev 模式 ROOT 切到临时 worktree 后调:缓存键含 cwd,本不必整体作废;保留接口防御未来键形变化
|
|
43
|
+
export function clearGitCache() {
|
|
44
|
+
GIT_RESULT_CACHE.clear();
|
|
45
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// judgment-ledger.mjs — check-loop 判定流水落账(2026-10-11 judgment-ledger,灰度消费端数据底座)
|
|
2
|
+
// 背景:机制月度对账原为「人工 grep check-loop 输出」;advisory→hard 升格与退役判定
|
|
3
|
+
// 均缺 per-rule 判定流水(时间戳+结论)。本模块把每次 check-loop 真实运行追加为一行 per-rule 计数
|
|
4
|
+
// 到 .agents/judgments.jsonl(本地,gitignore——「本机机器事实不跨机共享」口径,同 verifications.jsonl)。
|
|
5
|
+
// 消费:node .agents/scripts/agg-judgments.mjs(月度×规则计数);mechanism-registry 月度对账直接查账
|
|
6
|
+
// (机制登记表模板 .agents/mechanism-registry.md)。
|
|
7
|
+
// 诚实边界:非签名链(同 verify-evidence)——本地追加可被改,事后对质靠 git 历史与确认台账。
|
|
8
|
+
// fail-open:落账失败只打 stderr,不改变门禁裁决(观测面不得挡住裁决面)。
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
|
|
13
|
+
export const LEDGER_REL = path.join('.agents', 'judgments.jsonl');
|
|
14
|
+
|
|
15
|
+
// tallyJudgments(entries):'- [WARN 标签] …' → {rule,level:'WARN'};'- [标签] …' → {rule,level:'BLOCK'};
|
|
16
|
+
// 无括号标签行 → {rule:'未标注', level:按来源}。输出按 rule 字典序的 {rule,level,count} 数组(确定性,测试可断言)。
|
|
17
|
+
export function tallyJudgments(blockers = [], warnings = []) {
|
|
18
|
+
const tally = new Map();
|
|
19
|
+
const add = (raw, fallbackLevel) => {
|
|
20
|
+
const m = String(raw).match(/^\s*-\s*\[([^\]]+)\]/);
|
|
21
|
+
let rule = '未标注';
|
|
22
|
+
let level = fallbackLevel;
|
|
23
|
+
if (m) {
|
|
24
|
+
const tag = m[1].trim();
|
|
25
|
+
if (/^WARN\b/.test(tag)) { level = 'WARN'; rule = tag.replace(/^WARN\s*/, ''); }
|
|
26
|
+
else { rule = tag; }
|
|
27
|
+
}
|
|
28
|
+
const key = `${level}\u0000${rule}`;
|
|
29
|
+
const cur = tally.get(key) || { rule, level, count: 0 };
|
|
30
|
+
cur.count++;
|
|
31
|
+
tally.set(key, cur);
|
|
32
|
+
};
|
|
33
|
+
for (const b of blockers || []) add(b, 'BLOCK');
|
|
34
|
+
for (const w of warnings || []) add(w, 'WARN');
|
|
35
|
+
return [...tally.values()].sort((a, b) => (a.level + a.rule).localeCompare(b.level + b.rule));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const shortRev = (root) => {
|
|
39
|
+
try {
|
|
40
|
+
const r = spawnSync('git', ['rev-parse', '--short', 'HEAD'], { cwd: root, encoding: 'utf8' });
|
|
41
|
+
return r.status === 0 ? String(r.stdout).trim() || null : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// recordRun({ root, mode, blockers, warnings, now }):追加一行运行判定;守卫 fixture(CHECK_LOOP_ROOT)
|
|
48
|
+
// 与显式关闭(CHECK_LOOP_NO_LEDGER);任何失败 fail-open(stderr 一行,不 throw、不改 exit code)。
|
|
49
|
+
export function recordRun({ root, mode, blockers, warnings, now = new Date() }) {
|
|
50
|
+
try {
|
|
51
|
+
if (process.env.CHECK_LOOP_ROOT || process.env.CHECK_LOOP_NO_LEDGER) return;
|
|
52
|
+
const row = {
|
|
53
|
+
ts: now.toISOString(),
|
|
54
|
+
mode: mode || 'hard',
|
|
55
|
+
rev: shortRev(root),
|
|
56
|
+
blockers: (blockers || []).length,
|
|
57
|
+
warnings: (warnings || []).length,
|
|
58
|
+
rules: tallyJudgments(blockers, warnings),
|
|
59
|
+
};
|
|
60
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
61
|
+
fs.appendFileSync(path.join(root, LEDGER_REL), `${JSON.stringify(row)}\n`);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`judgment-ledger: 落账失败(不阻断门禁)——${err.code || err.message}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// judgment-ledger.test.mjs — 判定流水落账套件(2026-10-11 judgment-ledger)
|
|
3
|
+
// 纯函数 tallyJudgments 进程内断言 + recordRun 落账行为(临时根目录 + 环境守卫)。
|
|
4
|
+
// 守卫口径:CHECK_LOOP_ROOT(fixture 模式)/ CHECK_LOOP_NO_LEDGER 任一在即不落账——
|
|
5
|
+
// 进程内切换环境变量须保存/恢复,防污染同进程后续用例。
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { tallyJudgments, recordRun, LEDGER_REL } from './judgment-ledger.mjs';
|
|
12
|
+
|
|
13
|
+
let pass = 0;
|
|
14
|
+
let fail = 0;
|
|
15
|
+
const check = (desc, cond, detail = '') => {
|
|
16
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
17
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ---- S1 tallyJudgments:标签解析 / 级别分流 / 计数 / 确定性排序 ----
|
|
21
|
+
{
|
|
22
|
+
const out = tallyJudgments(
|
|
23
|
+
['- [配对断裂] incident 缺 plan:x.md', '- [配对断裂] incident 缺 plan:y.md', '- [回路断档] 选「是」未指明 intent'],
|
|
24
|
+
['- [WARN 三件套不全] x.md 缺之 2', '- [WARN 归因缺字段] x.md 缺「来源:」', '- [WARN 三件套不全] y.md 缺之 3', '无标签散行'],
|
|
25
|
+
);
|
|
26
|
+
check('S1 BLOCK/WARN 分流计数', out.some((r) => r.rule === '配对断裂' && r.level === 'BLOCK' && r.count === 2)
|
|
27
|
+
&& out.some((r) => r.rule === '回路断档' && r.level === 'BLOCK' && r.count === 1)
|
|
28
|
+
&& out.some((r) => r.rule === '三件套不全' && r.level === 'WARN' && r.count === 2)
|
|
29
|
+
&& out.some((r) => r.rule === '归因缺字段' && r.level === 'WARN' && r.count === 1), JSON.stringify(out));
|
|
30
|
+
check('S1 无标签行归「未标注」WARN', out.some((r) => r.rule === '未标注' && r.level === 'WARN' && r.count === 1), JSON.stringify(out));
|
|
31
|
+
const keys = out.map((r) => r.level + r.rule);
|
|
32
|
+
// 与实现同款 localeCompare 排序比对——被测不变量是「确定性」而非特定 collation(中文 locale 与码位排序不同)
|
|
33
|
+
const sorted = [...keys].sort((a, b) => a.localeCompare(b));
|
|
34
|
+
check('S1 输出按 level+rule 排序(确定性)', JSON.stringify(keys) === JSON.stringify(sorted), JSON.stringify(keys));
|
|
35
|
+
check('S1 空入参 → 空数组', tallyJudgments([], []).length === 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---- S2 recordRun:真落账(临时根,rev 为 null 因非 git 目录)----
|
|
39
|
+
{
|
|
40
|
+
const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
|
|
41
|
+
delete process.env.CHECK_LOOP_ROOT;
|
|
42
|
+
delete process.env.CHECK_LOOP_NO_LEDGER;
|
|
43
|
+
try {
|
|
44
|
+
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-'));
|
|
45
|
+
const now = new Date('2026-10-11T01:02:03.000Z');
|
|
46
|
+
recordRun({ root: T, mode: 'hygiene', blockers: ['- [配对断裂] a'], warnings: ['- [WARN 归因缺字段] b'], now });
|
|
47
|
+
const p = path.join(T, LEDGER_REL);
|
|
48
|
+
const rows = fs.existsSync(p) ? fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean).map((l) => JSON.parse(l)) : [];
|
|
49
|
+
check('S2 追加一行且字段齐(ts/mode/rev/计数/rules)',
|
|
50
|
+
rows.length === 1 && rows[0].ts === '2026-10-11T01:02:03.000Z' && rows[0].mode === 'hygiene'
|
|
51
|
+
&& rows[0].blockers === 1 && rows[0].warnings === 1
|
|
52
|
+
&& Array.isArray(rows[0].rules) && rows[0].rules.length === 2, JSON.stringify(rows));
|
|
53
|
+
recordRun({ root: T, mode: 'hard', blockers: [], warnings: [], now });
|
|
54
|
+
const lines = fs.readFileSync(p, 'utf8').split(/\r?\n/).filter(Boolean);
|
|
55
|
+
check('S2 二次调用追加不覆盖(append-only)', lines.length === 2, String(lines.length));
|
|
56
|
+
} finally {
|
|
57
|
+
if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot;
|
|
58
|
+
if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- S3 双守卫:CHECK_LOOP_ROOT / CHECK_LOOP_NO_LEDGER 任一在 → 不落账 ----
|
|
63
|
+
{
|
|
64
|
+
const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
|
|
65
|
+
try {
|
|
66
|
+
for (const guard of ['CHECK_LOOP_ROOT', 'CHECK_LOOP_NO_LEDGER']) {
|
|
67
|
+
delete process.env.CHECK_LOOP_ROOT;
|
|
68
|
+
delete process.env.CHECK_LOOP_NO_LEDGER;
|
|
69
|
+
process.env[guard] = '1';
|
|
70
|
+
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-guard-'));
|
|
71
|
+
recordRun({ root: T, mode: 'hard', blockers: [], warnings: [] });
|
|
72
|
+
check(`S3 ${guard} 在 → 不落账`, !fs.existsSync(path.join(T, LEDGER_REL)));
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot; else delete process.env.CHECK_LOOP_ROOT;
|
|
76
|
+
if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger; else delete process.env.CHECK_LOOP_NO_LEDGER;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- S4 落账失败 fail-open(root 指向文件而非目录 → append 必败,只 stderr 不 throw)----
|
|
81
|
+
{
|
|
82
|
+
const saved = { loopRoot: process.env.CHECK_LOOP_ROOT, noLedger: process.env.CHECK_LOOP_NO_LEDGER };
|
|
83
|
+
delete process.env.CHECK_LOOP_ROOT;
|
|
84
|
+
delete process.env.CHECK_LOOP_NO_LEDGER;
|
|
85
|
+
try {
|
|
86
|
+
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-bad-'));
|
|
87
|
+
const filePath = path.join(T, '.agents'); // 占住 .agents 为普通文件 → mkdirSync 必败
|
|
88
|
+
fs.writeFileSync(filePath, 'not a dir');
|
|
89
|
+
let threw = false;
|
|
90
|
+
try {
|
|
91
|
+
recordRun({ root: T, mode: 'hard', blockers: [], warnings: [] });
|
|
92
|
+
} catch {
|
|
93
|
+
threw = true;
|
|
94
|
+
}
|
|
95
|
+
check('S4 落账失败不 throw(fail-open)', threw === false);
|
|
96
|
+
} finally {
|
|
97
|
+
if (saved.loopRoot !== undefined) process.env.CHECK_LOOP_ROOT = saved.loopRoot; else delete process.env.CHECK_LOOP_ROOT;
|
|
98
|
+
if (saved.noLedger !== undefined) process.env.CHECK_LOOP_NO_LEDGER = saved.noLedger; else delete process.env.CHECK_LOOP_NO_LEDGER;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- S5 端到端:真实 check-loop.mjs fixture 运行不落账(CHECK_LOOP_ROOT 守卫在 spawn 态同样生效)----
|
|
103
|
+
{
|
|
104
|
+
const T = fs.mkdtempSync(path.join(os.tmpdir(), 'jledger-e2e-'));
|
|
105
|
+
for (const s of ['intents', 'specs', 'plans', 'incidents']) fs.mkdirSync(path.join(T, 'workflow', s), { recursive: true });
|
|
106
|
+
fs.mkdirSync(path.join(T, '.agents'), { recursive: true });
|
|
107
|
+
// 枚举单源随夹具内联(kit 契约:.agents/workflow-enums.txt,缺文件 fail-loud)——键集沿 verify.test.mjs mkfix 同款
|
|
108
|
+
fs.writeFileSync(path.join(T, '.agents', 'workflow-enums.txt'), [
|
|
109
|
+
'doc.status.all=draft approved done superseded cancelled',
|
|
110
|
+
'doc.status.confirmed=approved done superseded cancelled',
|
|
111
|
+
'doc.status.active=draft approved',
|
|
112
|
+
'doc.status.terminal=done superseded cancelled',
|
|
113
|
+
'doc.status.abandoned=superseded cancelled',
|
|
114
|
+
'incident.status.all=open fixed closed',
|
|
115
|
+
'incident.status.active=open',
|
|
116
|
+
'level.all=L1 L2 L3',
|
|
117
|
+
'',
|
|
118
|
+
].join('\n'), 'utf8');
|
|
119
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
120
|
+
// 裸跑=默认硬门(2026-10-13 分道,kit 与源仓同构 hard/hygiene/all 三面)——落账守卫断言与运行面无关,裸跑即可
|
|
121
|
+
const r = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'check-loop.mjs')],
|
|
122
|
+
{ env: { ...process.env, CHECK_LOOP_ROOT: T }, cwd: T, encoding: 'utf8' });
|
|
123
|
+
check('S5 fixture 态 spawn 运行不落账(守卫在子进程同样生效)',
|
|
124
|
+
!fs.existsSync(path.join(T, LEDGER_REL)), `exit=${r.status} ${r.stderr || ''}`);
|
|
125
|
+
fs.rmSync(T, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
129
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// 语料与口径
|
|
6
6
|
// workflow(流程留痕):只搜 frontmatter + H1 + 指定节(背景 / 目标 / 根因 / 复盘三件套 / 验收标准 / 影响面),
|
|
7
7
|
// 默认全状态可命中、活跃(draft/approved/open)排序 ×1.3 优先;输出带「节名 + 行号」便于节级读取。
|
|
8
|
+
// 语料面含归档批次 workflow/archive/<批次>/<type>/(B10a 自源仓随动回灌)——归档件仍可检索(防复发不丢历史)。
|
|
8
9
|
// wiki(领域知识):沿用 2026-09-21 降噪口径——默认跳过 .json/.csv、*.visual-check.* 与 >180KB bulk 文件。
|
|
9
10
|
// 两侧排序均为 命中数/√文件KB(防大文件靠体量霸榜);`-n` 为每语料展示上限。
|
|
10
11
|
// 结构缓存(2026-09-23 intent kb-search-structure-cache):可检索子集预提取进 .agents/cache/kb-index.json,
|
|
@@ -231,9 +232,22 @@ function saveCache() {
|
|
|
231
232
|
function searchWorkflow(words) {
|
|
232
233
|
const results = [];
|
|
233
234
|
for (const type of (typeFilter || DOC_TYPES)) {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
235
|
+
// 活跃目录 + 归档批次(workflow/archive/<批次>/<type>/,2026-10-09;B10a 自源仓随动回灌)——
|
|
236
|
+
// 归档件仍可检索(防复发不丢历史),标签用相对 workflow/ 的路径,活跃目录标签与旧口径逐字一致。
|
|
237
|
+
// 缓存键/失效语义随动:归档件按自身路径独立入缓存键,git mv 后旧活跃路径键由 saveCache
|
|
238
|
+
// 的 stat 清扫回收([mtimeMs,size] 逐文件失效不变);可检索子集提取函数未变 → 口径指纹不变。
|
|
239
|
+
const dirs = [path.join('workflow', type)];
|
|
240
|
+
const arcRoot = path.join('workflow', 'archive');
|
|
241
|
+
if (fs.existsSync(arcRoot)) {
|
|
242
|
+
for (const batch of fs.readdirSync(arcRoot)) {
|
|
243
|
+
const d = path.join(arcRoot, batch, type);
|
|
244
|
+
if (fs.existsSync(d)) dirs.push(d);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
for (const dir of dirs) {
|
|
248
|
+
if (!fs.existsSync(dir)) continue;
|
|
249
|
+
const rel = path.relative('workflow', dir).split(path.sep).join('/');
|
|
250
|
+
for (const f of fs.readdirSync(dir)) {
|
|
237
251
|
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
238
252
|
const p = path.join(dir, f);
|
|
239
253
|
const e = cachedEntry(p, 'wf', () => {
|
|
@@ -252,10 +266,11 @@ function searchWorkflow(words) {
|
|
|
252
266
|
const kb = e.st[1] / 1024;
|
|
253
267
|
const active = ACTIVE_STATUS.includes(status);
|
|
254
268
|
results.push({
|
|
255
|
-
file: `${
|
|
269
|
+
file: `${rel}/${f}`, title: e.title || '', status, level: meta['级别'] || '—', module,
|
|
256
270
|
total: hits.total, shown: hits.shown, kb,
|
|
257
271
|
score: (hits.total / Math.sqrt(Math.max(kb, 0.1))) * (active ? 1.3 : 1),
|
|
258
272
|
});
|
|
273
|
+
}
|
|
259
274
|
}
|
|
260
275
|
}
|
|
261
276
|
return results.sort((a, b) => b.score - a.score);
|
|
@@ -258,6 +258,39 @@ const files = (out) => (out.match(/^📄 (workflow\/\S+)/gm) || []).map((l) => l
|
|
|
258
258
|
fs.rmSync(root, { recursive: true, force: true });
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
// ---- 场景 13:归档扩面(B10a 自源仓随动回灌)——workflow/archive/<批次>/<type>/ 参与检索 ----
|
|
262
|
+
// 归档件命中带归档路径标签;无归档语料时行为与旧口径逐字一致;缓存键随动(归档件独立入键)
|
|
263
|
+
{
|
|
264
|
+
const root = mkfix();
|
|
265
|
+
// 造归档批次:与活跃件同词命中(含终态件 frontmatter,可命中)
|
|
266
|
+
fs.mkdirSync(path.join(root, 'workflow', 'archive', '2026-09', 'plans'), { recursive: true });
|
|
267
|
+
fs.writeFileSync(path.join(root, 'workflow', 'archive', '2026-09', 'plans', '2026-09-01-arch.md'),
|
|
268
|
+
doc('状态: done\n级别: L1\n模块: purchase', 'PLAN — 归档件', [['目标', '- 归档批次里的编码模板历史档']]), 'utf8');
|
|
269
|
+
const r = run(root, ['--scope', 'workflow', '编码模板']);
|
|
270
|
+
check('场景 13a 归档件参与检索且带归档路径标签',
|
|
271
|
+
r.status === 0 && files(r.stdout).some((f) => f === 'workflow/archive/2026-09/plans/2026-09-01-arch.md'), r.stdout);
|
|
272
|
+
check('场景 13b 活跃目录标签与旧口径逐字一致(不带 archive 段)',
|
|
273
|
+
files(r.stdout).some((f) => f === 'workflow/plans/2026-09-20-ccc.md'), files(r.stdout).join('\n'));
|
|
274
|
+
// 归档件状态 done → --status active 时被状态过滤排除(过滤口径对归档件照常生效)
|
|
275
|
+
const a = run(root, ['--scope', 'workflow', '--status', 'active', '编码模板']);
|
|
276
|
+
check('场景 13c --status active 排除终态归档件',
|
|
277
|
+
!a.stdout.includes('2026-09-01-arch.md') && a.stdout.includes('2026-09-21-bbb.md'), a.stdout);
|
|
278
|
+
// 缓存随动:warm 复跑与冷扫逐字节一致(归档件已入缓存键)
|
|
279
|
+
const warm = run(root, ['--scope', 'workflow', '编码模板']);
|
|
280
|
+
check('场景 13d 归档件入缓存后 warm ≡ 前次输出(逐字节)', warm.status === 0 && warm.stdout === r.stdout,
|
|
281
|
+
`warm:\n${warm.stdout}\nfirst:\n${r.stdout}`);
|
|
282
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---- 场景 14:无归档语料 → 行为不变(对照锚——扩面不得扰动既有输出)----
|
|
286
|
+
{
|
|
287
|
+
const root = mkfix(); // mkfix 不建 workflow/archive/
|
|
288
|
+
const before = run(root, ['--scope', 'workflow', '编码模板']);
|
|
289
|
+
check('场景 14 无 archive 目录时输出照旧(3 篇活跃件命中)',
|
|
290
|
+
before.status === 0 && files(before.stdout).length === 3 && !before.stdout.includes('archive/'), before.stdout);
|
|
291
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
292
|
+
}
|
|
293
|
+
|
|
261
294
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
262
295
|
if (fail) {
|
|
263
296
|
console.log(`\n${USAGE}`);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// lane-surface-audit.mjs — L2 触达面闭集防漂移审计(advisory 工具,恒 exit 0;2026-10-13 自
|
|
3
|
+
// Shipyard.Material 参考实现回灌,源仓沿革留痕在包源仓 git 历史与 CHANGELOG——装户径不引实例路径)
|
|
4
|
+
// 问题:lane-surfaces.txt 的 ERE 闭集靠人工维护——新共享契约面若未登记,判低拦截对新面静默失效。
|
|
5
|
+
// 本审计反着问:**近窗口内 L2/L3 任务实际反复触及、却无任何文件被面清单命中的目录** = 候选漏登面,
|
|
6
|
+
// 供人工评估补登(不建议自动写清单——高频触及 ≠ 契约面,机器不猜语义)。
|
|
7
|
+
// 归因代理:提交触及某入口/事故文档的提交 = 该任务的落地提交(三件套随代码同一提交纪律),
|
|
8
|
+
// 取这些提交里的代码文件(剔 workflow/、.agents/、.githooks/、wiki/、根 AGENTS.md)作「任务触及面」。
|
|
9
|
+
// 已知粗度:任务中途未触文档的纯代码提交会被漏计(审计低招,不是执法——宁可漏报不误报)。
|
|
10
|
+
// 判据:目录签名(dirname 前 3 段)被 ≥ --min(默认 3)个**不同** L2/L3 文档触及,且该签名下
|
|
11
|
+
// 观测到的文件无一命中 ERE → 候选。被命中的文件所在目录不进候选(已覆盖)。
|
|
12
|
+
// 用法:node .agents/scripts/lane-surface-audit.mjs [--since <YYYY-MM-DD>] [--min <N>] [--root <dir>]
|
|
13
|
+
// --since 缺省 = 今天−30 天。输出候选清单;无候选打印一行「无漂移信号」。
|
|
14
|
+
// 时机:月度手跑(登记位 = .agents/lane-surfaces.txt 文件头);改共享契约频繁期可随时手跑。
|
|
15
|
+
// 测试:node .agents/scripts/lane-surface-audit.test.mjs(git fixture 注入)
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { spawnSync } from 'node:child_process';
|
|
19
|
+
|
|
20
|
+
const argv = process.argv.slice(2);
|
|
21
|
+
const argOf = (k) => {
|
|
22
|
+
const i = argv.indexOf(k);
|
|
23
|
+
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : '';
|
|
24
|
+
};
|
|
25
|
+
const ROOT = argOf('--root') || (() => {
|
|
26
|
+
const r = spawnSync('git', ['rev-parse', '--show-toplevel'], { cwd: process.cwd(), encoding: 'utf8' });
|
|
27
|
+
return r.status === 0 ? String(r.stdout).trim() : '';
|
|
28
|
+
})() || process.cwd();
|
|
29
|
+
const SINCE = argOf('--since') || new Date(Date.now() - 30 * 86400_000).toISOString().slice(0, 10);
|
|
30
|
+
const MIN = Number(argOf('--min')) || 3;
|
|
31
|
+
|
|
32
|
+
const gitOut = (args) => {
|
|
33
|
+
const r = spawnSync('git', ['-c', 'core.quotepath=off', ...args], { cwd: ROOT, encoding: 'utf8' });
|
|
34
|
+
return r.status === 0 ? String(r.stdout || '') : '';
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// 面清单(解析同 check-lane-surface.mjs:逐行 ERE,# 注释与空行忽略——段标题行同为注释,不参与命中)
|
|
38
|
+
const loadPatterns = () => {
|
|
39
|
+
const file = path.join(ROOT, '.agents', 'lane-surfaces.txt');
|
|
40
|
+
if (!fs.existsSync(file)) return [];
|
|
41
|
+
return fs.readFileSync(file, 'utf8')
|
|
42
|
+
.split(/\r?\n/)
|
|
43
|
+
.map((l) => l.trim())
|
|
44
|
+
.filter((l) => l && !l.startsWith('#'))
|
|
45
|
+
.map((l) => { try { return new RegExp(l); } catch { return null; } })
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
};
|
|
48
|
+
const patterns = loadPatterns();
|
|
49
|
+
const isCovered = (p) => patterns.some((re) => re.test(p));
|
|
50
|
+
|
|
51
|
+
// frontmatter 读取(CRLF 容忍,同 check-lane-surface.fmOf)
|
|
52
|
+
const fmOf = (file) => {
|
|
53
|
+
const fm = {};
|
|
54
|
+
let lines;
|
|
55
|
+
try { lines = fs.readFileSync(file, 'utf8').split(/\r?\n/); } catch { return fm; }
|
|
56
|
+
if (!/^---\s*$/.test(lines[0] || '')) return fm;
|
|
57
|
+
for (let i = 1; i < lines.length; i++) {
|
|
58
|
+
if (/^---\s*$/.test(lines[i])) break;
|
|
59
|
+
const m = lines[i].match(/^([^:]+):\s*(.*)$/);
|
|
60
|
+
if (m) fm[m[1].trim()] = m[2].trim();
|
|
61
|
+
}
|
|
62
|
+
return fm;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// 1) 窗口内 L2/L3 文档(任何状态——审计覆盖已关单历史;日期键 intent=日期 / incident=发现)
|
|
66
|
+
const docs = [];
|
|
67
|
+
for (const [dir, dateKey] of [['intents', '日期'], ['incidents', '发现']]) {
|
|
68
|
+
const dirPath = path.join(ROOT, 'workflow', dir);
|
|
69
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
70
|
+
for (const f of fs.readdirSync(dirPath).sort()) {
|
|
71
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
72
|
+
const fm = fmOf(path.join(dirPath, f));
|
|
73
|
+
if ((fm['级别'] || '') !== 'L2' && (fm['级别'] || '') !== 'L3') continue;
|
|
74
|
+
if ((fm[dateKey] || '') < SINCE) continue;
|
|
75
|
+
docs.push(`workflow/${dir}/${f}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!docs.length) {
|
|
79
|
+
console.log(`[lane-audit] 窗口(${SINCE} 起)内无 L2/L3 文档——无审计面。`);
|
|
80
|
+
process.exit(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2) 一遍 git log 取 (commit → files) 与文档↔提交关联(--name-only 块解析;doc 路径含非 ASCII 须 quotepath=off)
|
|
84
|
+
const log = gitOut(['log', '--name-only', '--format=%H', '--', ...docs]);
|
|
85
|
+
if (!log) {
|
|
86
|
+
console.error('[lane-audit] git log 不可用(非 git 环境或无历史)——跳过审计。');
|
|
87
|
+
process.exit(0);
|
|
88
|
+
}
|
|
89
|
+
const filesByDoc = new Map(); // doc → Set(代码文件)
|
|
90
|
+
let commit = '';
|
|
91
|
+
const commitsForDoc = new Map(); // doc → Set(commit)
|
|
92
|
+
for (const line of log.split(/\r?\n/)) {
|
|
93
|
+
if (!line.trim()) continue;
|
|
94
|
+
if (/^[0-9a-f]{40}$/.test(line)) { commit = line; continue; }
|
|
95
|
+
const p = line.replace(/"/g, ''); // quotepath=off 已关转义,防御性去引号
|
|
96
|
+
if (docs.includes(p)) {
|
|
97
|
+
if (!commitsForDoc.has(p)) commitsForDoc.set(p, new Set());
|
|
98
|
+
commitsForDoc.get(p).add(commit);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [doc, commits] of commitsForDoc) {
|
|
102
|
+
const set = new Set();
|
|
103
|
+
for (const c of commits) {
|
|
104
|
+
for (const f of gitOut(['show', '--name-only', '--format=', c]).split(/\r?\n/).filter(Boolean)) {
|
|
105
|
+
const fp = f.replace(/"/g, '');
|
|
106
|
+
if (/^(workflow|\.agents|\.githooks|wiki)\//.test(fp) || /\.md$/.test(fp) || fp === 'AGENTS.md') continue;
|
|
107
|
+
set.add(fp);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
filesByDoc.set(doc, set);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3) 目录签名聚合(dirname 前 3 段);仅聚合「未被 ERE 命中」的文件;按不同文档数计
|
|
114
|
+
const sigOf = (fp) => {
|
|
115
|
+
const parts = path.posix.dirname(fp.replace(/\\/g, '/')).split('/');
|
|
116
|
+
return parts.slice(0, 3).join('/');
|
|
117
|
+
};
|
|
118
|
+
const docsBySig = new Map(); // sig → Set(doc)
|
|
119
|
+
const samplesBySig = new Map(); // sig → Set(file)
|
|
120
|
+
for (const [doc, files] of filesByDoc) {
|
|
121
|
+
for (const fp of files) {
|
|
122
|
+
if (isCovered(fp)) continue;
|
|
123
|
+
const sig = sigOf(fp);
|
|
124
|
+
if (!docsBySig.has(sig)) { docsBySig.set(sig, new Set()); samplesBySig.set(sig, new Set()); }
|
|
125
|
+
docsBySig.get(sig).add(doc);
|
|
126
|
+
samplesBySig.get(sig).add(fp);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const candidates = [...docsBySig.entries()]
|
|
130
|
+
.filter(([, ds]) => ds.size >= MIN)
|
|
131
|
+
.sort((a, b) => b[1].size - a[1].size);
|
|
132
|
+
|
|
133
|
+
if (!candidates.length) {
|
|
134
|
+
console.log(`[lane-audit] ✅ 无漂移信号——窗口(${SINCE} 起)${docs.length} 份 L2/L3 文档的反复触及目录均被面清单覆盖(或未达阈值 ${MIN})。`);
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
console.log(`[lane-audit] ⚠️ 候选漏登面 ${candidates.length} 个(窗口 ${SINCE} 起 / 阈值 ≥${MIN} 个不同 L2/L3 任务 / ERE 均未命中)——人工评估是否补登 .agents/lane-surfaces.txt:`);
|
|
138
|
+
for (const [sig, ds] of candidates) {
|
|
139
|
+
const samples = [...samplesBySig.get(sig)].slice(0, 3).join(' ');
|
|
140
|
+
console.log(` - ${sig}(${ds.size} 个任务;示例 ${samples})`);
|
|
141
|
+
}
|
|
142
|
+
console.log(' 判据提醒:仅「一处定义、多处消费」的契约/规则面才应登记(判据单源 .agents/scripts/level-criteria.mjs 的 L2_SURFACES)——高频触及 ≠ 契约面。');
|
|
143
|
+
process.exit(0);
|