agentic-flow-kit 1.2.2 → 1.4.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 +10 -4
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/fresh-init.test.mjs +10 -0
- package/src/init.mjs +1 -1
- package/src/profiles.mjs +12 -6
- package/src/sync.mjs +30 -26
- package/src/sync.test.mjs +82 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/rule-budgets.txt +3 -1
- package/templates/_agents/scripts/agg-delegations.cjs +56 -17
- package/templates/_agents/scripts/agg-delegations.test.mjs +76 -1
- package/templates/_agents/scripts/agg-gate-stats.mjs +113 -0
- package/templates/_agents/scripts/agg-gate-stats.test.mjs +95 -0
- package/templates/_agents/scripts/check-hygiene.mjs +167 -0
- package/templates/_agents/scripts/check-hygiene.test.mjs +182 -0
- package/templates/_agents/scripts/check-ledger.mjs +60 -16
- package/templates/_agents/scripts/check-ledger.test.mjs +70 -0
- package/templates/_agents/scripts/check-loop-unit.test.mjs +95 -0
- package/templates/_agents/scripts/check-loop.mjs +1119 -1097
- package/templates/_agents/scripts/check-loop.test.mjs +200 -1
- package/templates/_agents/scripts/confirm-doc.mjs +27 -6
- package/templates/_agents/scripts/confirm-doc.test.mjs +35 -0
- package/templates/_agents/scripts/gate-seg.mjs +54 -0
- package/templates/_agents/scripts/gate-seg.test.mjs +112 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +249 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +189 -0
- package/templates/_agents/scripts/policy.mjs +31 -3
- package/templates/_agents/scripts/verify.mjs +27 -2
- package/templates/_agents/scripts/verify.test.mjs +53 -0
- package/templates/_github/workflows/kit-ci.yml +28 -0
|
@@ -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 } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
94
|
+
const { metrics, gateMonth, expansionVerdict, parseResult } = requireCjs(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
95
95
|
|
|
96
96
|
// 场景 4:审查反例——30 任务、2 主兜底、平均返工 0.25(旧口径四门全过,声明口径下必须 ❌)
|
|
97
97
|
{
|
|
@@ -119,6 +119,81 @@ const { metrics, gateMonth, expansionVerdict } = requireCjs(path.join(SCRIPT_DIR
|
|
|
119
119
|
check('场景 5:本月未达标 → ❌ 且列出未达标项号', expansionVerdict(false, true, false, [{ no: 3, ok: false }, { no: 4, ok: false }]).match(/❌.*3\+4/));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
// 场景 6:门禁噪声返工拆分(2026-10-08 selfmeasure-and-modularize)
|
|
123
|
+
// 动机:本仓质量门连续两月红(55% / 61% < 90%),根因是门禁噪声与设计返工同判第 3 项 → 指标不指示改进方向。
|
|
124
|
+
{
|
|
125
|
+
check('场景 6:parseResult 识别门禁噪声档(kind=rework-noise;2026-10-08 rework-attribution-split 更正:rework 分量归 0——噪声不是设计返工;外部行为 metrics/第 3 项不变)',
|
|
126
|
+
parseResult('返工×2(门禁噪声)').kind === 'rework-noise' && parseResult('返工×2(门禁噪声)').reworkNoise === 2
|
|
127
|
+
&& parseResult('返工×2(门禁噪声)').rework === 0, JSON.stringify(parseResult('返工×2(门禁噪声)')));
|
|
128
|
+
check('场景 6:存量四档解析逐条不变(向后兼容钉子)',
|
|
129
|
+
parseResult('一次通过').kind === 'pass' && parseResult('返工×2').kind === 'rework'
|
|
130
|
+
&& parseResult('返工×2').rework === 2 && parseResult('主兜底').kind === 'fallback'
|
|
131
|
+
&& parseResult('返工待修').kind === 'pending' && parseResult('乱填').kind === 'unknown');
|
|
132
|
+
check('场景 6:新旧形态互不误吞($ 锚定;半角括号不认)',
|
|
133
|
+
parseResult('返工×1(门禁噪声)').kind !== 'rework' && parseResult('返工×1(门禁噪声)').kind === 'unknown');
|
|
134
|
+
|
|
135
|
+
const rows = [
|
|
136
|
+
{ scope: '自做', result: '返工×2(门禁噪声)' }, // 噪声
|
|
137
|
+
{ scope: '自做', result: '返工×3(门禁噪声)' }, // 噪声
|
|
138
|
+
{ scope: '自做', result: '一次通过' },
|
|
139
|
+
];
|
|
140
|
+
const m = metrics(rows);
|
|
141
|
+
check('场景 6:双列累计分离(设计返工 0 / 噪声 5)',
|
|
142
|
+
m.reworkSum === 0 && m.reworkNoiseSum === 5, JSON.stringify({ reworkSum: m.reworkSum, noise: m.reworkNoiseSum }));
|
|
143
|
+
check('场景 6:平均返工仍含噪声(快照行口径不变,delegations.md 表结构零改动)',
|
|
144
|
+
Math.abs(m.avgRework - 5 / 3) < 1e-9, String(m.avgRework));
|
|
145
|
+
const g = gateMonth(m);
|
|
146
|
+
const i3 = g.items.find((i) => i.no === 3);
|
|
147
|
+
check('场景 6:扩容门第 3 项只对设计返工判 ok=true(噪声不计入)', i3.ok === true, JSON.stringify(i3));
|
|
148
|
+
check('场景 6:噪声数字在描述里显式可见(不隐藏)', /门禁噪声返工 5 次已剥离单列/.test(i3.desc), i3.desc);
|
|
149
|
+
|
|
150
|
+
// 反向:设计返工不得因存在噪声档而被放过
|
|
151
|
+
const mixed = metrics([{ scope: '自做', result: '返工×1' }, { scope: '自做', result: '返工×1(门禁噪声)' }]);
|
|
152
|
+
check('场景 6:反例——设计返工 1 + 噪声 1 → 门 3 ❌(噪声标签不能洗白设计返工)',
|
|
153
|
+
gateMonth(mixed).items.find((i) => i.no === 3).ok === false, JSON.stringify(gateMonth(mixed).items[2]));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 场景 7:行内双值归因(2026-10-08 rework-attribution-split)
|
|
157
|
+
// 动机:存量逐行审计(29 行含返工)证实单值格式无法表达混合归因——一行只记一个返工总数时,
|
|
158
|
+
// 「1 次设计 + 1 次噪声」只能整行倒向某一侧。行内双值是作者写入时确定已知的归因事实,不反推。
|
|
159
|
+
{
|
|
160
|
+
const mixed = parseResult('返工×1 + 门禁噪声×1');
|
|
161
|
+
check('场景 7:行内双值解析出两个分量(kind=rework-mixed)',
|
|
162
|
+
mixed.kind === 'rework-mixed' && mixed.rework === 1 && mixed.reworkNoise === 1, JSON.stringify(mixed));
|
|
163
|
+
check('场景 7:宽容空格(无空格变体亦可解析)',
|
|
164
|
+
parseResult('返工×2+门禁噪声×3').rework === 2 && parseResult('返工×2+门禁噪声×3').reworkNoise === 3);
|
|
165
|
+
check('场景 7:四个返工格式互不误吞',
|
|
166
|
+
parseResult('返工×2').kind === 'rework' && parseResult('返工×2').reworkNoise === 0
|
|
167
|
+
&& parseResult('返工×2(门禁噪声)').kind === 'rework-noise'
|
|
168
|
+
&& parseResult('返工×2(门禁噪声)').rework === 0
|
|
169
|
+
&& parseResult('一次通过').kind === 'pass' && parseResult('乱填').kind === 'unknown');
|
|
170
|
+
check('场景 7:非法数字落 unknown(记录本身有问题,不混进待修)',
|
|
171
|
+
parseResult('返工×a + 门禁噪声×1').kind === 'unknown');
|
|
172
|
+
|
|
173
|
+
// 不变量①:任务总数不虚增——一行仍是一个任务,两个数字不拆成两个任务
|
|
174
|
+
const rows = [
|
|
175
|
+
{ scope: '自做', result: '返工×1 + 门禁噪声×2' },
|
|
176
|
+
{ scope: '自做', result: '返工×1 + 门禁噪声×2' },
|
|
177
|
+
{ scope: '自做', result: '一次通过' },
|
|
178
|
+
];
|
|
179
|
+
const m = metrics(rows);
|
|
180
|
+
check('场景 7:不变量①——total 按行计 1(行内双值不虚增任务数 / 不抬高 passRate 分母)',
|
|
181
|
+
m.total === 3, `total=${m.total}`);
|
|
182
|
+
check('场景 7:双列准确累计(设计返工 2 / 门禁噪声 4)',
|
|
183
|
+
m.reworkSum === 2 && m.reworkNoiseSum === 4, JSON.stringify({ r: m.reworkSum, n: m.reworkNoiseSum }));
|
|
184
|
+
check('场景 7:一次通过率分母不变(1/3,与若拆成两行的结果不同——这是选行内双值而非拆行的理由)',
|
|
185
|
+
Math.abs(m.passRate - 1 / 3) < 1e-9, String(m.passRate));
|
|
186
|
+
|
|
187
|
+
// 混合行的门 3 判定:设计返工 0 + 噪声 4 → ok;设计返工 2 → ❌
|
|
188
|
+
const onlyNoise = metrics([{ scope: '自做', result: '返工×0 + 门禁噪声×3' }, { scope: '自做', result: '一次通过' }]);
|
|
189
|
+
const i3ok = gateMonth(onlyNoise).items.find((i) => i.no === 3);
|
|
190
|
+
check('场景 7:纯噪声混合行 → 门 3 ✅ 且描述带噪声数',
|
|
191
|
+
i3ok.ok === true && /门禁噪声返工 3 次已剥离单列/.test(i3ok.desc), JSON.stringify(i3ok));
|
|
192
|
+
const withDesign = gateMonth(m).items.find((i) => i.no === 3);
|
|
193
|
+
check('场景 7:含设计返工的混合行 → 门 3 ❌(噪声分量不能洗白设计分量)',
|
|
194
|
+
withDesign.ok === false, JSON.stringify(withDesign));
|
|
195
|
+
}
|
|
196
|
+
|
|
122
197
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
123
198
|
if (fail) {
|
|
124
199
|
console.log(`\n${USAGE}`);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// agg-gate-stats.mjs — 门禁 ROI 聚合(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 读 .agents/cache/gate-stats.jsonl(check-loop --gate-stats 落的运行时数据)→ 按检查 ID 聚合
|
|
3
|
+
// 运行次数 / 命中次数 / 拦截数 / 警告数 / 平均耗时 → 输出表 + 两类决策依据:
|
|
4
|
+
// ① 噪声率 top N(警告数 ÷ 命中次数,降序)——该收窄或该修的候选;
|
|
5
|
+
// ② 死检查(声明了但从未命中)——该拆或该降级的候选。
|
|
6
|
+
//
|
|
7
|
+
// **为什么需要它**(本件存在的理由):本仓门禁 16→20 项期间,2026-10 一次通过率 61%→56%——
|
|
8
|
+
// 门禁数与质量指标负相关,但此前无任何数据能回答「该加门还是该拆门」(metrics/DASHBOARD 全部指标
|
|
9
|
+
// 100% 指向产出物、0 条指向门禁自身)。加门是焦虑驱动的唯一理性反应,因为拆门没有依据。
|
|
10
|
+
//
|
|
11
|
+
// 耗时口径**含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 sh rule-budget.sh)——
|
|
12
|
+
// 门禁真实成本本就要含子进程,不做剔除;输出中显式标注防误读为「纯计算耗时」。
|
|
13
|
+
// 坏行容忍跳过(与 confirmations.jsonl / delegations 台账同容错口径)。
|
|
14
|
+
// 用法:node .agents/scripts/agg-gate-stats.mjs [--days 30]
|
|
15
|
+
// 测试:node templates/_agents/scripts/agg-gate-stats.test.mjs
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
22
|
+
const STATS_FILE = path.join(ROOT, '.agents', 'cache', 'gate-stats.jsonl');
|
|
23
|
+
|
|
24
|
+
// readRows:读 jsonl,坏行跳过不抛
|
|
25
|
+
export function readRows(file = STATS_FILE) {
|
|
26
|
+
let text = '';
|
|
27
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { return []; }
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const line of text.split('\n')) {
|
|
30
|
+
if (!line.trim()) continue;
|
|
31
|
+
try {
|
|
32
|
+
const e = JSON.parse(line);
|
|
33
|
+
if (e && Array.isArray(e.segs)) out.push(e);
|
|
34
|
+
} catch { /* 坏行跳过 */ }
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// aggregate:按 id 聚合(pure,供测试直测)。days = 统计窗口(天),null = 全量。
|
|
40
|
+
// runs = 该段被记录到的运行次数;hits = 至少命中一次(warns+blocks>0)的运行次数;
|
|
41
|
+
// warns / blocks = 累计条数;msSum / avgMs = 累计与平均耗时。
|
|
42
|
+
export function aggregate(rows, now = Date.now(), days = null) {
|
|
43
|
+
const cutoff = days ? now - days * 86400000 : null;
|
|
44
|
+
const by = new Map();
|
|
45
|
+
let runs = 0;
|
|
46
|
+
for (const r of rows) {
|
|
47
|
+
if (cutoff) {
|
|
48
|
+
const t = Date.parse(r.ts || '');
|
|
49
|
+
if (!Number.isFinite(t) || t < cutoff) continue;
|
|
50
|
+
}
|
|
51
|
+
runs++;
|
|
52
|
+
for (const s of r.segs || []) {
|
|
53
|
+
if (!s || !s.id) continue;
|
|
54
|
+
if (!by.has(s.id)) by.set(s.id, { id: s.id, label: s.label || '', runs: 0, hits: 0, warns: 0, blocks: 0, msSum: 0 });
|
|
55
|
+
const a = by.get(s.id);
|
|
56
|
+
a.runs++;
|
|
57
|
+
a.label = s.label || a.label;
|
|
58
|
+
a.warns += Number(s.warns) || 0;
|
|
59
|
+
a.blocks += Number(s.blocks) || 0;
|
|
60
|
+
a.msSum += Number(s.ms) || 0;
|
|
61
|
+
if ((Number(s.warns) || 0) + (Number(s.blocks) || 0) > 0) a.hits++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const list = [...by.values()].map((a) => ({
|
|
65
|
+
...a,
|
|
66
|
+
avgMs: a.runs ? Math.round(a.msSum / a.runs) : 0,
|
|
67
|
+
noiseRate: a.hits ? a.warns / a.hits : 0, // 每次命中的平均警告条数(0 = 从不产噪声)
|
|
68
|
+
}));
|
|
69
|
+
list.sort((x, y) => y.msSum - x.msSum); // 默认按累计耗时降序——成本视角优先
|
|
70
|
+
const noiseTop = [...list].filter((a) => a.warns > 0).sort((x, y) => y.noiseRate - x.noiseRate);
|
|
71
|
+
const dead = list.filter((a) => a.hits === 0);
|
|
72
|
+
const costTotal = list.reduce((a, s) => a + s.msSum, 0);
|
|
73
|
+
return { runs, list, noiseTop, dead, costTotal };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function fmtMs(ms) {
|
|
77
|
+
return ms >= 1000 ? `${(ms / 1000).toFixed(1)}s` : `${ms}ms`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function main() {
|
|
81
|
+
const i = process.argv.indexOf('--days');
|
|
82
|
+
const days = i >= 0 ? Number(process.argv[i + 1]) : 30;
|
|
83
|
+
const rows = readRows();
|
|
84
|
+
const { runs, list, noiseTop, dead, costTotal } = aggregate(rows, Date.now(), Number.isFinite(days) ? days : null);
|
|
85
|
+
|
|
86
|
+
if (!runs || !list.length) {
|
|
87
|
+
console.log('门禁 ROI:未采集——跑 `node .agents/scripts/check-loop.mjs --gate-stats` 后重试');
|
|
88
|
+
console.log('(数据落 .agents/cache/gate-stats.jsonl,该目录已 gitignore;首次采集后本表才有内容)');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log(`# 门禁 ROI 聚合(窗口 ${days} 天 / 有效样本 ${runs} 次运行)\n`);
|
|
93
|
+
console.log('耗时口径**含子进程时间**(检查 11 调生成器 --check、检查 13 调 sh rule-budget.sh)——门禁真实成本,不做剔除。\n');
|
|
94
|
+
console.log('| 检查 | 名称 | 命中/运行 | 拦截 | 警告 | 累计耗时 | 均耗时 | 每次命中警告数 |');
|
|
95
|
+
console.log('|------|------|-----------|------|------|----------|--------|----------------|');
|
|
96
|
+
for (const a of list) {
|
|
97
|
+
console.log(`| ${a.id} | ${a.label} | ${a.hits}/${a.runs} | ${a.blocks} | ${a.warns} | ${fmtMs(a.msSum)} | ${fmtMs(a.avgMs)} | ${a.noiseRate.toFixed(1)} |`);
|
|
98
|
+
}
|
|
99
|
+
console.log(`\n累计门禁耗时 ${fmtMs(costTotal)}(${runs} 次运行,均 ${fmtMs(Math.round(costTotal / runs))}/次)`);
|
|
100
|
+
|
|
101
|
+
console.log(`\n## ① 噪声率 top(警告数 ÷ 命中次数)——收窄或修的候选`);
|
|
102
|
+
if (noiseTop.length) for (const a of noiseTop) console.log(` 检查 ${a.id} ${a.label}:${a.noiseRate.toFixed(1)} 条/次命中(累计 ${a.warns} 条警告)`);
|
|
103
|
+
else console.log(' (无段产出过警告)');
|
|
104
|
+
|
|
105
|
+
console.log(`\n## ② 死检查(声明了但从未命中)——拆或降级的候选`);
|
|
106
|
+
if (dead.length) for (const a of dead) console.log(` 检查 ${a.id} ${a.label}:${a.runs} 次运行零命中,累计耗时 ${fmtMs(a.msSum)}`);
|
|
107
|
+
else console.log(' (每段都至少命中过一次)');
|
|
108
|
+
|
|
109
|
+
if (runs < 5) console.log(`\n⚠️ 样本仅 ${runs} 次运行——**结论待样本积累**(建议 ≥30 次 / ≥30 天后再据此改判据;此刻只作线索不作决策依据)`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('agg-gate-stats.mjs');
|
|
113
|
+
if (isMain) main();
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// agg-gate-stats.test.mjs — 门禁 ROI 聚合测试(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 覆盖:聚合数学 / 窗口过滤 / 坏行容错 / 死检查识别 / 噪声率排序 / 按耗时降序 / 空数据降级。
|
|
3
|
+
// 用法:node templates/_agents/scripts/agg-gate-stats.test.mjs
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const { aggregate, readRows } =
|
|
11
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'agg-gate-stats.mjs')).href);
|
|
12
|
+
|
|
13
|
+
let pass = 0, fail = 0;
|
|
14
|
+
const check = (name, cond, detail = '') => {
|
|
15
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
16
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const NOW = Date.parse('2026-10-08T00:00:00.000Z');
|
|
20
|
+
const run = (ts, segs) => ({ ts, segs });
|
|
21
|
+
const seg = (id, warns, blocks, ms, label = `检查${id}`) => ({ id, label, warns, blocks, ms });
|
|
22
|
+
|
|
23
|
+
// ---- 1. 聚合数学 ----
|
|
24
|
+
{
|
|
25
|
+
const rows = [
|
|
26
|
+
run('2026-10-07T00:00:00.000Z', [seg('8', 0, 0, 10000, '验收'), seg('4', 2, 0, 20, '引用')]),
|
|
27
|
+
run('2026-10-08T00:00:00.000Z', [seg('8', 0, 0, 12000, '验收'), seg('4', 1, 0, 30, '引用'), seg('3', 0, 1, 5, 'incidents')]),
|
|
28
|
+
];
|
|
29
|
+
const a = aggregate(rows, NOW, null);
|
|
30
|
+
check('聚合:runs 计 2', a.runs === 2, `runs=${a.runs}`);
|
|
31
|
+
const s8 = a.list.find((x) => x.id === '8');
|
|
32
|
+
check('聚合:按 id 累计(检查 8 = 2 次运行 / 均耗时 11000ms)',
|
|
33
|
+
s8.runs === 2 && s8.avgMs === 11000 && s8.msSum === 22000, JSON.stringify(s8));
|
|
34
|
+
const s4 = a.list.find((x) => x.id === '4');
|
|
35
|
+
check('聚合:警告累计与命中次数(检查 4 = 命中 2 / 警告 3 / 噪声率 1.5)',
|
|
36
|
+
s4.hits === 2 && s4.warns === 3 && s4.noiseRate === 1.5, JSON.stringify(s4));
|
|
37
|
+
const s3 = a.list.find((x) => x.id === '3');
|
|
38
|
+
check('聚合:blocks 计入命中(检查 3 拦截 1 次 → hits=1)', s3.hits === 1 && s3.blocks === 1, JSON.stringify(s3));
|
|
39
|
+
check('聚合:默认按累计耗时降序(成本视角优先)', a.list[0].id === '8', a.list.map((x) => x.id).join(','));
|
|
40
|
+
check('聚合:costTotal = 各段 ms 之和', a.costTotal === 22000 + 50 + 5, String(a.costTotal));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- 2. 窗口过滤 ----
|
|
44
|
+
{
|
|
45
|
+
const rows = [
|
|
46
|
+
run('2026-01-01T00:00:00.000Z', [seg('8', 0, 0, 99999, '验收')]),
|
|
47
|
+
run('2026-10-07T00:00:00.000Z', [seg('8', 0, 0, 1000, '验收')]),
|
|
48
|
+
];
|
|
49
|
+
const a = aggregate(rows, NOW, 30);
|
|
50
|
+
check('窗口:30 天外的运行被排除(runs=1,耗时不含 99999)',
|
|
51
|
+
a.runs === 1 && a.costTotal === 1000, `runs=${a.runs} cost=${a.costTotal}`);
|
|
52
|
+
const b = aggregate(rows, NOW, null);
|
|
53
|
+
check('窗口:days=null 取全量(runs=2)', b.runs === 2, `runs=${b.runs}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---- 3. 死检查与噪声率排序 ----
|
|
57
|
+
{
|
|
58
|
+
const rows = [
|
|
59
|
+
run('2026-10-07T00:00:00.000Z', [
|
|
60
|
+
seg('8', 0, 0, 5000, '验收'), // 贵但从不命中 → 死检查
|
|
61
|
+
seg('17', 0, 0, 4000, '发版树'), // 贵且从不命中 → 死检查
|
|
62
|
+
seg('11', 5, 0, 100, '漂移'), // 高噪声
|
|
63
|
+
seg('4', 1, 0, 10, '引用'), // 低噪声
|
|
64
|
+
]),
|
|
65
|
+
];
|
|
66
|
+
const a = aggregate(rows, NOW, null);
|
|
67
|
+
const deadIds = a.dead.map((x) => x.id).sort();
|
|
68
|
+
check('死检查:识别「声明了但从未命中」(8/17),且按耗时排序(8 在前)',
|
|
69
|
+
deadIds.join(',') === '17,8' || deadIds.join(',') === '8,17', deadIds.join(','));
|
|
70
|
+
check('噪声率 top:11(5.0)> 4(1.0)', a.noiseTop.map((x) => x.id).join(',') === '11,4', a.noiseTop.map((x) => x.id).join(','));
|
|
71
|
+
check('死检查不进 noiseTop(从不命中 ≠ 高噪声)', !a.noiseTop.some((x) => x.id === '8'));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- 4. 坏行容错 + 空数据 ----
|
|
75
|
+
{
|
|
76
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-gatestats-'));
|
|
77
|
+
const f = path.join(root, 'gate-stats.jsonl');
|
|
78
|
+
fs.writeFileSync(f, [
|
|
79
|
+
'not-json-bad-line',
|
|
80
|
+
JSON.stringify(run('2026-10-07T00:00:00.000Z', [seg('8', 0, 0, 100)])),
|
|
81
|
+
'',
|
|
82
|
+
JSON.stringify({ ts: '2026-10-07T00:00:00.000Z' }), // 无 segs
|
|
83
|
+
].join('\n') + '\n');
|
|
84
|
+
const rows = readRows(f);
|
|
85
|
+
check('坏行容错:非 JSON / 无 segs 的行跳过不抛(其余 1 行入账)', rows.length === 1 && rows[0].segs.length === 1);
|
|
86
|
+
check('坏行容错:文件不存在 → 空数组不抛', readRows(path.join(root, 'nope.jsonl')).length === 0);
|
|
87
|
+
check('空数据降级:aggregate 返回零值而非抛', aggregate([], NOW, 30).runs === 0 && aggregate([], NOW, 30).list.length === 0);
|
|
88
|
+
// 段体缺字段容错
|
|
89
|
+
const r2 = aggregate([{ ts: '2026-10-07T00:00:00.000Z', segs: [{ id: '8' }, null, { warns: 1 }] }], NOW, null);
|
|
90
|
+
check('段体容错:缺 ms / null 段 / 缺 id 段不抛', r2.runs === 1 && r2.list.find((x) => x.id === '8').avgMs === 0);
|
|
91
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
95
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// check-hygiene.mjs — check-loop 卫生类检查模块(2026-10-08 selfmeasure-and-modularize,L2)
|
|
2
|
+
// 承载 check-loop.mjs 的检查 2 / 9 / 11 / 12 / 13(拆分先例 = check-metric-claims.mjs 承载检查 16):
|
|
3
|
+
// 2. 模板字段占位符残留 [warning]
|
|
4
|
+
// 9. 文件名英文 kebab-case [warning]
|
|
5
|
+
// 11. 生成物漂移(INDEX + DASHBOARD)[warning] ← 本次扩 DASHBOARD(原仅 INDEX)
|
|
6
|
+
// 12. frontmatter「模块:」合法性 [warning]
|
|
7
|
+
// 13. 常驻面体积预算 [warning]
|
|
8
|
+
//
|
|
9
|
+
// 为什么是这五个:它们无跨检查共享状态——判据只依赖 ctx 里既有的 helper(docFiles / linesOf / fmGet /
|
|
10
|
+
// isTracked / readdirOrNull)与 policy,不消费其他检查的中间产物。耦合高的检查 1/5(共享文档循环)、
|
|
11
|
+
// 8(证据核验)、15(指纹对账)留后续批次,拆它们需先解耦,不与本批混(2026-10-08-checkloop-importable
|
|
12
|
+
// 教训:1600+ 行变换与行为修复混批 diff 巨大且不可审)。
|
|
13
|
+
//
|
|
14
|
+
// **判定零复刻铁律**:ctx 里每个 helper 都从 check-loop.mjs 原样传入,模块内**不得**重新实现
|
|
15
|
+
// docFiles / fmGet / linesOf / isTracked——它们各自承载不可漂移的语义(docFiles 的「仓库模式只扫
|
|
16
|
+
// tracked(HEAD)」、fmGet 的 frontmatter 受限子集、linesOf 的读异常响亮出账 + 按空文档降级)。复刻即造口径分叉。
|
|
17
|
+
//
|
|
18
|
+
// 输出契约:本模块**只返回文案数组**,不打印、不 exit、不改 ctx。调用方 `warnings.push(...ret)`——
|
|
19
|
+
// check-loop 在 `audit:false`(init 新装)下把 `warnings.push` 替换为 no-op,卫生项因此不出账(硬规则语义保持)。
|
|
20
|
+
// 检查 11 的生成器 `--check` 口径同样不复刻(调子进程,判据单源在生成器内)。
|
|
21
|
+
//
|
|
22
|
+
// 测试:node templates/_agents/scripts/check-hygiene.test.mjs(fixture root 注入,不触真实 workflow/)
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import { spawnSync } from 'node:child_process';
|
|
26
|
+
import { gateSeg, finishSegs, makeCollector, appendSegs } from './gate-seg.mjs';
|
|
27
|
+
|
|
28
|
+
// 门禁 ROI 插桩(2026-10-08 gate-roi-metrics):本模块承载的检查 2/9/11/12/13 同样需要计入门禁成本
|
|
29
|
+
// 统计——尤其检查 11(调两个生成器 --check 的 spawn 子进程)与检查 13(调 sh rule-budget.sh)是耗时大户,
|
|
30
|
+
// 不插桩就等于把最贵的两段排除在度量之外。collector 由 check-loop 传入(它持有全局 warnings/blockers),
|
|
31
|
+
// 未传时跳过插桩(纯函数直测场景零副作用)。判据单源见 gate-seg.mjs。
|
|
32
|
+
|
|
33
|
+
export function runCheckHygiene(ctx) {
|
|
34
|
+
const { root: ROOT, WF, DOC_DIRS, docFiles, linesOf, fmGet, isTracked, readdirOrNull, kitPolicy } = ctx;
|
|
35
|
+
const warnings = [];
|
|
36
|
+
// ⚠️ 插桩记在**本模块的局部数组**上,不是 ctx.gateStats 的全局数组——本模块产出的是局部 warnings,
|
|
37
|
+
// 由 check-loop 在返回后才 spread 进全局;执行期间全局长度不动,直接共用游标会导致确定的错账
|
|
38
|
+
// (本模块 5 段恒记 0 命中,全部增量被误记到模块之后的第一个段即检查 3 头上;实测已抓到该症状)。
|
|
39
|
+
// 末尾用 appendSegs 把结算好的段按序并入全局收集器——它会先结算模块前的那一段(如检查 1)再清游标。
|
|
40
|
+
const localStats = makeCollector(warnings, []); // 本模块只产 warnings,不产 blockers
|
|
41
|
+
const markGate = (id, label) => { if (ctx.gateStats) gateSeg(localStats, id, label); };
|
|
42
|
+
|
|
43
|
+
// --- 2. 模板字段占位符残留 [warning](排除运行时文件名格式与协议样板,口径沿 sh 版)---
|
|
44
|
+
markGate('2', '模板字段占位符残留');
|
|
45
|
+
{
|
|
46
|
+
const phRe = /YYYY-MM-DD|<主题>|<日期 主题>|L0 \/ L1 \/ L2 \/ L3|draft \/ approved \/ done|open \/ fixed \/ closed/;
|
|
47
|
+
const boilerRe = /\.\.\/specs\/[A-Za-z0-9-]*\.md|写明如何满足|防复发验证|_YYYY-MM-DD\.|format\('YYYY-MM-DD'\)|value-format="YYYY-MM-DD"|确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)/;
|
|
48
|
+
// plan 确认节样板豁免(2026-10-03 plan-confirm-boiler):plan 模板「确认与复核」节自带
|
|
49
|
+
// 「确认结果:approved(YYYY-MM-DD 用户对话内确认);done(YYYY-MM-DD 关单,随入口文档置终态)」
|
|
50
|
+
// 样板句,未回填不等于漏填占位(确认由 confirm-doc 机器兜底)——精确豁免整句,真实占位(如
|
|
51
|
+
// `日期: YYYY-MM-DD`)照拦。
|
|
52
|
+
// 命名约定豁免:`<主题>` 与 `.md` 同行 = 在描述文件命名规则(如 `.agents/workflows/<主题>.md`、
|
|
53
|
+
// 「复制本模板为 YYYY-MM-DD-<主题>.md」),非未填占位符;真未填的占位(标题 `# INTENT — <主题>`、
|
|
54
|
+
// `日期: YYYY-MM-DD`)不含 .md,仍照拦(2026-09-25-wf-runtime incident 记录的误报口径)
|
|
55
|
+
const isNamingConv = (line) => line.includes('<主题>') && /\.md/.test(line);
|
|
56
|
+
// 样例引用豁免(2026-10-02 caliber-convergence 假阳性消除,先例=检查 18 行尾归一):命中位于行内代码
|
|
57
|
+
// (反引号包裹)或围栏代码块内 = 引用样例而非未填占位符,不报;正文裸占位符照报——真占位符均为
|
|
58
|
+
// 裸文本(模板正文无反引号包裹),检测面不缩。
|
|
59
|
+
// 日期显示格式豁免(2026-10-06 check2-datetime-literal-exempt):`YYYY-MM-DD HH:mm(:ss)` 是功能
|
|
60
|
+
// 自实现的显示格式描述(文档叙述「匹配时间列显示为 YYYY-MM-DD HH:mm」属常态——cancel-export
|
|
61
|
+
// intent/plan 实证 2 条 advisory、2026-10-06 loop-audit intent 现场复现),非未填占位——行内剔除
|
|
62
|
+
// 后再判;真占位(`日期: YYYY-MM-DD`,无时间粒度后缀)不含该形态仍拦。只豁免实证形态,变体(斜杠日期
|
|
63
|
+
// /十二进制等)出现假阳性再扩。
|
|
64
|
+
const stripSamples = (lines) => {
|
|
65
|
+
let inFence = false;
|
|
66
|
+
return lines.map((line) => {
|
|
67
|
+
if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; return ''; }
|
|
68
|
+
if (inFence) return '';
|
|
69
|
+
return line.replace(/`[^`]*`/g, '').replace(/YYYY-MM-DD\s+HH:mm(:ss)?/g, '');
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
const groups = new Map(); // file -> [ "行号:内容" ](首现序)
|
|
73
|
+
for (const sub of DOC_DIRS) {
|
|
74
|
+
for (const f of docFiles(sub)) {
|
|
75
|
+
const lines = linesOf(f) || [];
|
|
76
|
+
const hits = [];
|
|
77
|
+
stripSamples(lines).forEach((line, i) => {
|
|
78
|
+
if (phRe.test(line) && !boilerRe.test(line) && !isNamingConv(line)) hits.push(`${i + 1}:${lines[i]}`);
|
|
79
|
+
});
|
|
80
|
+
if (hits.length) groups.set(f, hits);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const [f, hits] of groups) {
|
|
84
|
+
warnings.push(`- [WARN 模板未填] ${path.relative(ROOT, f).split(path.sep).join('/')} 含模板占位符:\n${hits.join('\n')}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// --- 9. 文件名英文 kebab-case [warning] ---
|
|
89
|
+
markGate('9', '文件名 kebab-case');
|
|
90
|
+
for (const sub of DOC_DIRS) {
|
|
91
|
+
const dir = path.join(ROOT, WF, sub);
|
|
92
|
+
for (const f of readdirOrNull(dir) || []) {
|
|
93
|
+
if (!f.endsWith('.md') || f === '_TEMPLATE.md') continue;
|
|
94
|
+
const abs = path.join(dir, f);
|
|
95
|
+
if (!isTracked(abs)) continue;
|
|
96
|
+
if (!/^[\x20-\x7e]*$/.test(f)) {
|
|
97
|
+
warnings.push(`- [WARN 文件名非英文] 文件名含非 ASCII 字符,应为英文 kebab-case:${WF}/${sub}/${f}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// --- 11. 生成物漂移 [warning](调生成器 --check,口径单一不复刻渲染)---
|
|
103
|
+
markGate('11', '生成物漂移(INDEX+DASHBOARD)');
|
|
104
|
+
// ⚠️ 位置:本段须排在 12/13 之前——check-loop 输出按 warnings 插入序,行序是稳定输出契约
|
|
105
|
+
// (pre-push / pre-commit 消费者 + doctor 按 WARN 行计数);原序为 2 → 9 → 11 → 12 → 13,勿调换。
|
|
106
|
+
// 两个生成物各自独立出账:漂移来源不同、修复命令不同,混成一条会让装户猜该跑哪个。
|
|
107
|
+
// 装户跳过语义(防常驻噪声,audit-gate-hardening P3 教训):
|
|
108
|
+
// ① 生成器脚本不存在(旧装副本)→ 跳过;
|
|
109
|
+
// ② DASHBOARD.md 不存在(从未生成过)→ 跳过——**新装用户不得因未生成过看板而每次 push 吃 WARN**;
|
|
110
|
+
// (INDEX.md 不设此前置:init 必落该文件,不存在本身即异常)
|
|
111
|
+
for (const [label, genName, docRel] of [
|
|
112
|
+
['索引漂移', 'gen-workflow-index.mjs', 'workflow/INDEX.md'],
|
|
113
|
+
['仪表盘漂移', 'gen-workflow-dashboard.mjs', 'workflow/DASHBOARD.md'],
|
|
114
|
+
]) {
|
|
115
|
+
const gen = path.join(ROOT, '.agents', 'scripts', genName);
|
|
116
|
+
if (!fs.existsSync(gen)) continue;
|
|
117
|
+
if (label === '仪表盘漂移' && !fs.existsSync(path.join(ROOT, docRel))) continue;
|
|
118
|
+
const r = spawnSync(process.execPath, [gen, '--check'], { cwd: ROOT, encoding: 'utf8' });
|
|
119
|
+
if (r.status !== 0) {
|
|
120
|
+
const first = `${r.stdout || ''}${r.stderr || ''}`.split('\n')[0];
|
|
121
|
+
warnings.push(`- [WARN ${label}] ${docRel} 与当前事实不一致——跑 node .agents/scripts/${genName} 重新生成(${first})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// --- 12. frontmatter「模块:」合法性 [warning](词表单源;缺字段只对 2026-09-22 起新建提示)---
|
|
126
|
+
markGate('12', '模块枚举合法性');
|
|
127
|
+
{
|
|
128
|
+
const modsFile = path.join(ROOT, '.agents', 'workflow-modules.txt');
|
|
129
|
+
if (fs.existsSync(modsFile)) {
|
|
130
|
+
const mods = linesOf(modsFile).filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.trim());
|
|
131
|
+
const vocab = new Set(mods);
|
|
132
|
+
for (const sub of ['intents', 'specs', 'plans', 'incidents']) {
|
|
133
|
+
for (const doc of docFiles(sub)) {
|
|
134
|
+
const base = path.basename(doc);
|
|
135
|
+
const mod = fmGet(doc, '模块');
|
|
136
|
+
let d = fmGet(doc, '日期') || fmGet(doc, '发现');
|
|
137
|
+
if (!d) d = /^\d{4}-\d{2}-\d{2}$/.test(base.slice(0, 10)) ? base.slice(0, 10) : '';
|
|
138
|
+
if (mod) {
|
|
139
|
+
if (!vocab.has(mod)) warnings.push(`- [WARN 模块元数据] ${base}「模块: ${mod}」不在词表(见 .agents/workflow-modules.txt)`);
|
|
140
|
+
} else if (/^\d{4}-\d{2}-\d{2}$/.test(d) && d >= kitPolicy.moduleSince) {
|
|
141
|
+
warnings.push(`- [WARN 模块元数据] ${base} 缺「模块:」字段(${kitPolicy.moduleSince} 起新建文档必填)`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// --- 13. 常驻面体积预算 [warning](判定单源 rule-budget.sh;无 sh 环境静默跳过——advisory,pre-commit 硬拦兜底)---
|
|
149
|
+
markGate('13', '常驻面体积预算');
|
|
150
|
+
{
|
|
151
|
+
const rb = path.join(ROOT, '.agents', 'scripts', 'rule-budget.sh');
|
|
152
|
+
const budgets = path.join(ROOT, '.agents', 'rule-budgets.txt');
|
|
153
|
+
if (fs.existsSync(rb) && fs.existsSync(budgets)) {
|
|
154
|
+
const r = spawnSync('sh', [rb, '--all'], { cwd: ROOT, encoding: 'utf8' });
|
|
155
|
+
if (!r.error && r.status !== 0) {
|
|
156
|
+
const head = `${r.stdout || ''}${r.stderr || ''}`.split('\n').slice(0, 5).map((l) => ' ' + l).join('\n');
|
|
157
|
+
warnings.push(`- [WARN 常驻面超限] 常驻面体积超预算(先删除或下沉被取代条目再增——一进一出):\n${head}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// 并入全局收集器(仅显式插桩时):把本模块 5 段按序插到「模块前的那一段」之后,
|
|
163
|
+
// 游标由 appendSegs 清空 → check-loop 随后的 markGate('3', …) 从零起算,Δ 天然正确。
|
|
164
|
+
if (ctx.gateStats) appendSegs(ctx.gateStats, finishSegs(localStats));
|
|
165
|
+
|
|
166
|
+
return warnings;
|
|
167
|
+
}
|