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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// gen-workflow-dashboard.mjs — workflow 仪表盘(2026-10-08,intent workflow-dashboard,L1)
|
|
3
|
+
// 一键生成 workflow/DASHBOARD.md:头部红绿灯(质量门 + 关单时长带)+ 时长分布 + 质量月度行。
|
|
4
|
+
// 观测面非门禁:红灯是「看」的不是「拦」的(gen-workflow-metrics 同教训——快照拿去做漂移校验只会常红);
|
|
5
|
+
// 想看就跑一行命令,与 INDEX.md「状态变更后重跑」同模式,不挂钩子(post-commit 自动重生成有脏工作区问题)。
|
|
6
|
+
// 时长口径:done 态 intents 的立项日(frontmatter 日期)→ confirmations.jsonl 该 doc **最早** done 行 ts
|
|
7
|
+
// (多次 done 取首关;无台账行的存量单诚实跳过不虚构)。真实分布基准(2026-10-08 本仓 43 单):P50=0 / P90=7 / max=8。
|
|
8
|
+
// 复用:require('./agg-delegations.cjs') 纯函数(readLedger / metrics / gateMonth / expansionVerdict)——
|
|
9
|
+
// 质量判据零重复(N3 教训);readLedger 的 root 显式参数 + soft 模式为本批使能导出(原签名行为不变)。
|
|
10
|
+
// isMain 守卫 + 纯函数导出(board-kb-p1 先例):closeDurations / pickPct / durationVerdict / normalizeForCheck 供测试 import 直测。
|
|
11
|
+
// 用法:node .agents/scripts/gen-workflow-dashboard.mjs [--dry-run | --check]
|
|
12
|
+
// --check 不写盘;与磁盘不一致时 exit 1 并打印差异(供 check-loop 检查 11 漂移告警,2026-10-08 selfmeasure-and-modularize)
|
|
13
|
+
// 两道归一缺一不可(均照 gen-workflow-index.mjs 先例):
|
|
14
|
+
// ① 行尾:fresh clone 检出 CRLF、生成段恒 LF——直接字符串相等会在克隆环境假阳性「漂移」(2026-09-30 该坑已在
|
|
15
|
+
// gen-workflow-index 踩过,本件照抄 normEol);
|
|
16
|
+
// ② 「生成于 <ISO>」行:每次运行必变,不归一则门禁恒红(不可消退噪声,违反 audit-gate-hardening P3 教训)。
|
|
17
|
+
// 测试:node templates/_agents/scripts/gen-workflow-dashboard.test.mjs
|
|
18
|
+
import fs from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createRequire } from 'node:module';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
26
|
+
const agg = require('./agg-delegations.cjs');
|
|
27
|
+
|
|
28
|
+
// 关单时长控制带(默认带,数据说话后调):P50 ≤ 1 天 且 P90 ≤ 10 天为绿——本仓实测 P50=0/P90=7 在带内,
|
|
29
|
+
// 留 7 天 P90 余量覆盖跨周单;不带配置文件(YAGNI,真需要项目级调带时再外置)。
|
|
30
|
+
export const DURATION_BAND = { p50Max: 1, p90Max: 10 };
|
|
31
|
+
|
|
32
|
+
// closeDurations:done intents 关单天数(纯函数供测试)——doneMap: Map<rel, 最早 done 行 ISO ts>
|
|
33
|
+
export function closeDurations(intents, doneMap) {
|
|
34
|
+
const days = [];
|
|
35
|
+
let skipped = 0;
|
|
36
|
+
for (const it of intents) {
|
|
37
|
+
const ts = doneMap.get(it.rel);
|
|
38
|
+
const t = ts === undefined ? NaN : Date.parse(ts);
|
|
39
|
+
const d0 = Date.parse(`${it.date}T00:00:00Z`);
|
|
40
|
+
if (!Number.isFinite(t) || !Number.isFinite(d0)) { skipped++; continue; }
|
|
41
|
+
days.push({ rel: it.rel, days: Math.max(0, Math.floor((t - d0) / 86400000)) }); // floor=日历天差:同日关门 0.625 天仍记 0(round 会记 1)
|
|
42
|
+
}
|
|
43
|
+
days.sort((a, b) => a.days - b.days || a.rel.localeCompare(b.rel));
|
|
44
|
+
return { days, skipped };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// pickPct:百分位(升序数组;与 repo 内实测探针同口径 floor(len*p) 截断)
|
|
48
|
+
export function pickPct(sortedNums, p) {
|
|
49
|
+
if (!sortedNums.length) return null;
|
|
50
|
+
return sortedNums[Math.min(sortedNums.length - 1, Math.floor(sortedNums.length * p))];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// durationVerdict:时长带判定(三态:无样本 null / 带内 / 超带)
|
|
54
|
+
export function durationVerdict(p50, p90) {
|
|
55
|
+
if (p50 === null) return { ok: null, text: '⚪ 暂无关单样本(无带台账 done 行的 done intent)' };
|
|
56
|
+
const ok = p50 <= DURATION_BAND.p50Max && p90 <= DURATION_BAND.p90Max;
|
|
57
|
+
return { ok, text: ok ? '✅ 带内' : '❌ 超带' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// fmField:frontmatter 单行「键: 值」最小提取(check-loop fmGet 无 isMain 守卫不可 import——本件够用)
|
|
61
|
+
function fmField(text, key) {
|
|
62
|
+
const m = String(text).match(new RegExp(`^${key}: (.*)$`, 'm'));
|
|
63
|
+
return m ? m[1].trim() : '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// collectDoneIntents:done 态 intents(排除 _TEMPLATE)→ [{rel, date}]
|
|
67
|
+
function collectDoneIntents(root) {
|
|
68
|
+
const dir = path.join(root, 'workflow', 'intents');
|
|
69
|
+
const out = [];
|
|
70
|
+
if (!fs.existsSync(dir)) return out;
|
|
71
|
+
for (const f of fs.readdirSync(dir)) {
|
|
72
|
+
if (!f.endsWith('.md') || f.startsWith('_TEMPLATE')) continue;
|
|
73
|
+
const text = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
74
|
+
if (fmField(text, '状态') !== 'done') continue;
|
|
75
|
+
const date = fmField(text, '日期');
|
|
76
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
|
|
77
|
+
out.push({ rel: `workflow/intents/${f}`, date });
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// collectDoneMap:台账 earliest done 行(坏行容忍,doc 正斜杠根相对——沿检查 14/15 口径)
|
|
83
|
+
function collectDoneMap(root) {
|
|
84
|
+
const doneMap = new Map();
|
|
85
|
+
const ledgerPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
86
|
+
if (!fs.existsSync(ledgerPath)) return doneMap;
|
|
87
|
+
for (const line of fs.readFileSync(ledgerPath, 'utf8').split(/\r?\n/)) {
|
|
88
|
+
if (!line.trim()) continue;
|
|
89
|
+
try {
|
|
90
|
+
const e = JSON.parse(line);
|
|
91
|
+
if (e && e.stage === 'done' && typeof e.doc === 'string' && e.doc.startsWith('workflow/intents/')) {
|
|
92
|
+
const prev = doneMap.get(e.doc);
|
|
93
|
+
if (prev === undefined || e.ts < prev) doneMap.set(e.doc, e.ts);
|
|
94
|
+
}
|
|
95
|
+
} catch { /* 坏行容忍跳过 */ }
|
|
96
|
+
}
|
|
97
|
+
return doneMap;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// qualityRows:月度质量行(判据全部复用 agg——本函数只做分组与格式化)
|
|
101
|
+
export function qualityRows(root) {
|
|
102
|
+
const rows = agg.readLedger(root, { soft: true });
|
|
103
|
+
const byMonth = {};
|
|
104
|
+
for (const r of rows) (byMonth[r.date.slice(0, 7)] ||= []).push(r);
|
|
105
|
+
const months = Object.keys(byMonth).sort();
|
|
106
|
+
const verdictMap = new Map();
|
|
107
|
+
const monthOkMap = new Map();
|
|
108
|
+
const out = [];
|
|
109
|
+
for (const ym of months) {
|
|
110
|
+
const m = agg.metrics(byMonth[ym]);
|
|
111
|
+
const { items, monthOk } = agg.gateMonth(m);
|
|
112
|
+
monthOkMap.set(ym, monthOk);
|
|
113
|
+
const [y, mo] = ym.split('-').map(Number);
|
|
114
|
+
const pd = new Date(Date.UTC(y, mo - 2, 1));
|
|
115
|
+
const prev = `${pd.getUTCFullYear()}-${String(pd.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
116
|
+
const verdict = agg.expansionVerdict(monthOk, monthOkMap.get(prev), !months.includes(prev), items);
|
|
117
|
+
verdictMap.set(ym, verdict);
|
|
118
|
+
const pct = (x) => (Number.isFinite(x) ? `${Math.round(x * 100)}%` : '—');
|
|
119
|
+
const avg = Number.isFinite(m.avgRework) ? m.avgRework.toFixed(2) : '—';
|
|
120
|
+
// 门禁噪声返工单列(2026-10-08 selfmeasure-and-modularize):设计返工与噪声返工分列可诊断——
|
|
121
|
+
// 「平均返工」把两者平均在一起时看不出该改门禁还是改设计。
|
|
122
|
+
out.push(`| ${ym} | ${m.total} | ${pct(m.passRate)} | ${m.reworkSum} | ${m.reworkNoiseSum} | ${avg} | ${pct(m.fallbackRate)} | ${verdict} |`);
|
|
123
|
+
}
|
|
124
|
+
return { rows: out, verdictMap, months };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// gateRoiLines:门禁 ROI 节的行(2026-10-08 gate-roi-metrics)
|
|
128
|
+
// 口径声明 + 采集/查看入口。**刻意不把运行数据(次数 / 耗时 / 噪声率)铺进 DASHBOARD**——
|
|
129
|
+
// 本文件被检查 11(--check)覆盖,而采集数据每跑一次门禁就变一次:铺进去等于
|
|
130
|
+
// 「每次采集必产一条漂移 WARN」=不可消除噪声(audit-gate-hardening P3 的反面教材)。
|
|
131
|
+
// 连「有无数据」也不做分支(cache 已 gitignore → 新克隆必然无 cache,一采集文案就翻转 = 又一次漂移):
|
|
132
|
+
// 降级语义交由 CLI 承担——agg-gate-stats.mjs 空数据时自己打印「未采集——跑 check-loop --gate-stats」。
|
|
133
|
+
// 故本节恒为静态文案:**任何时候生成本文件,字节恒等**(测试 ⑤ 的两条 --check exit 0 钉的就是这条)。
|
|
134
|
+
export function gateRoiLines() {
|
|
135
|
+
return [
|
|
136
|
+
'- 采集:`node .agents/scripts/check-loop.mjs --gate-stats`(数据落 `.agents/cache/gate-stats.jsonl`,该目录已 gitignore;不带 flag 时零写入、输出逐字节不变)',
|
|
137
|
+
'- 看数值:`node .agents/scripts/agg-gate-stats.mjs [--days 30]`——输出成本排序 + 两类决策依据:噪声率 top(该收窄或该修)、死检查(该拆或该降级);无采集数据时该命令自己提示「未采集」并给出采集命令',
|
|
138
|
+
'- 口径:耗时**含子进程时间**(检查 11 调两个生成器 `--check`、检查 13 调 `sh rule-budget.sh`)——门禁真实成本,不做剔除;样本 <5 次时只作线索,不作决策依据',
|
|
139
|
+
'- 本节为何不铺数值:本文件被检查 11 漂移门覆盖,运行数据每跑一次门禁即变一次,铺进来会让每次采集都产一条漂移 WARN(不可消除噪声)',
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const USAGE = '用法:node .agents/scripts/gen-workflow-dashboard.mjs [--dry-run | --check]';
|
|
144
|
+
|
|
145
|
+
// normalizeForCheck:--check 比对前的双归一(纯函数供测试直测)——行尾 CRLF→LF + 「生成于 <ISO>」行抹平。
|
|
146
|
+
// 导出是为了让测试不必 spawn 子进程即可断言「仅时间戳不同 → 归一后相等」(门禁恒红的直接防线)。
|
|
147
|
+
export function normalizeForCheck(s) {
|
|
148
|
+
return String(s)
|
|
149
|
+
.replace(/\r\n/g, '\n')
|
|
150
|
+
.replace(/生成于 \d{4}-\d{2}-\d{2}T[\d:.]+Z/g, '生成于 <TS>');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function firstDiffLine(a, b) {
|
|
154
|
+
const la = a.split('\n');
|
|
155
|
+
const lb = b.split('\n');
|
|
156
|
+
for (let i = 0; i < Math.max(la.length, lb.length); i++) {
|
|
157
|
+
if (la[i] !== lb[i]) return i + 1;
|
|
158
|
+
}
|
|
159
|
+
return -1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function main() {
|
|
163
|
+
const args = process.argv.slice(2);
|
|
164
|
+
const dry = args.includes('--dry-run');
|
|
165
|
+
const check = args.includes('--check');
|
|
166
|
+
const intents = collectDoneIntents(ROOT);
|
|
167
|
+
const doneMap = collectDoneMap(ROOT);
|
|
168
|
+
const { days, skipped } = closeDurations(intents, doneMap);
|
|
169
|
+
const nums = days.map((d) => d.days);
|
|
170
|
+
const p50 = pickPct(nums, 0.5);
|
|
171
|
+
const p90 = pickPct(nums, 0.9);
|
|
172
|
+
const mx = nums.length ? nums[nums.length - 1] : null;
|
|
173
|
+
const dv = durationVerdict(p50, p90);
|
|
174
|
+
const q = qualityRows(ROOT);
|
|
175
|
+
const curYm = q.months[q.months.length - 1];
|
|
176
|
+
const qVerdict = curYm ? q.verdictMap.get(curYm) : '⚪ 台账暂无数据(delegations.md 无结果表行)';
|
|
177
|
+
|
|
178
|
+
const slowest = days.slice(-3).reverse().map((d) => `${d.rel.split('/').pop()}=${d.days}天`).join('、') || '—';
|
|
179
|
+
const stat = (x) => (x === null ? '—' : `${x} 天`);
|
|
180
|
+
const lines = [
|
|
181
|
+
'# workflow 仪表盘(生成物,勿手改)',
|
|
182
|
+
'',
|
|
183
|
+
`> 生成器 \`node .agents/scripts/gen-workflow-dashboard.mjs\`;生成于 ${new Date().toISOString()}`,
|
|
184
|
+
'> 观测面非门禁:红灯提示看,不拦提交;想看就跑一行命令(与 INDEX.md「状态变更后重跑」同模式)。',
|
|
185
|
+
'',
|
|
186
|
+
'## 红绿灯',
|
|
187
|
+
'',
|
|
188
|
+
`- ${qVerdict}|质量门(门槛权威 = workflow/delegations.md §并发扩容门槛:一次通过率 ≥90%、月度返工 0、无主兜底、样本 ≥5)${curYm ? `——当月 ${curYm}` : ''}`,
|
|
189
|
+
`- ${dv.text}|关单时长带(P50 ≤ ${DURATION_BAND.p50Max} 天 且 P90 ≤ ${DURATION_BAND.p90Max} 天):P50=${stat(p50)} / P90=${stat(p90)} / max=${stat(mx)}(样本 ${nums.length},无台账行跳过 ${skipped})`,
|
|
190
|
+
'',
|
|
191
|
+
'## 关单时长(done intents:立项日 → 台账最早 done 行)',
|
|
192
|
+
'',
|
|
193
|
+
'| 指标 | 值 |',
|
|
194
|
+
'|------|----|',
|
|
195
|
+
`| P50 | ${stat(p50)} |`,
|
|
196
|
+
`| P90 | ${stat(p90)} |`,
|
|
197
|
+
`| max | ${stat(mx)} |`,
|
|
198
|
+
`| 样本数 | ${nums.length} |`,
|
|
199
|
+
`| 无台账行跳过(存量单) | ${skipped} |`,
|
|
200
|
+
'',
|
|
201
|
+
`最慢 3 单:${slowest}`,
|
|
202
|
+
'',
|
|
203
|
+
'## 质量 / 返工(月度,判据复用 agg-delegations)',
|
|
204
|
+
'',
|
|
205
|
+
'| 月份 | 有效任务 | 一次通过率 | 设计返工 | 门禁噪声返工 | 平均返工 | 主兜底占比 | 扩容门判定 |',
|
|
206
|
+
'|------|----------|------------|---------|-------------|--------|------------|------------|',
|
|
207
|
+
...(q.rows.length ? q.rows : ['| — | — | — | — | — | — | — | 台账暂无数据 |']),
|
|
208
|
+
'',
|
|
209
|
+
'## 门禁 ROI(门禁自身的度量)',
|
|
210
|
+
'',
|
|
211
|
+
...gateRoiLines(),
|
|
212
|
+
'',
|
|
213
|
+
];
|
|
214
|
+
const out = lines.join('\n');
|
|
215
|
+
const target = path.join(ROOT, 'workflow', 'DASHBOARD.md');
|
|
216
|
+
// --check 优先于写盘、--dry-run 优先于 --check(显式不写盘意图最高):比对模式不改任何盘面。
|
|
217
|
+
if (check) {
|
|
218
|
+
let disk;
|
|
219
|
+
try {
|
|
220
|
+
disk = fs.readFileSync(target, 'utf8');
|
|
221
|
+
} catch {
|
|
222
|
+
console.error(`[check] ❌ ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'} 不存在或不可读——跑 node .agents/scripts/gen-workflow-dashboard.mjs 生成`);
|
|
223
|
+
process.exitCode = 1;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
const a = normalizeForCheck(out);
|
|
227
|
+
const b = normalizeForCheck(disk);
|
|
228
|
+
if (a === b) {
|
|
229
|
+
console.log(`[check] ✅ ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'} 与盘面一致(行尾与「生成于」时间戳已归一)`);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const ln = firstDiffLine(a, b);
|
|
233
|
+
console.error(`[check] ❌ ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'} 与当前事实不一致(首个差异在第 ${ln} 行)——跑 node .agents/scripts/gen-workflow-dashboard.mjs 重新生成`);
|
|
234
|
+
process.exitCode = 1;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (dry) {
|
|
238
|
+
console.log(out);
|
|
239
|
+
console.log(`(--dry-run:未写盘。${USAGE})`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
243
|
+
fs.writeFileSync(target, out, 'utf8');
|
|
244
|
+
console.log(`✅ 已生成 ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'}`);
|
|
245
|
+
console.log(` 红绿灯:${qVerdict.split('|')[0].trim()} 质量门 | ${dv.text} 关单时长带(P50=${stat(p50)} / P90=${stat(p90)},样本 ${nums.length})`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('gen-workflow-dashboard.mjs');
|
|
249
|
+
if (isMain) main();
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// gen-workflow-dashboard.test.mjs — workflow 仪表盘测试(2026-10-08 workflow-dashboard,L1)
|
|
2
|
+
// 两层:①纯函数直测(closeDurations 四例 / pickPct / durationVerdict 三态 / qualityRows 复用 agg 同值对账);
|
|
3
|
+
// ②端到端 spawn 一例(CHECK_LOOP_ROOT 注入 fixture,断言 DASHBOARD.md 红绿灯行与结构)。
|
|
4
|
+
// fixture:临时 root(intents + confirmations.jsonl + delegations.md 两表)。
|
|
5
|
+
// 用法:node templates/_agents/scripts/gen-workflow-dashboard.test.mjs
|
|
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, pathToFileURL } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const { closeDurations, pickPct, durationVerdict, qualityRows, DURATION_BAND, normalizeForCheck, gateRoiLines } =
|
|
14
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs')).href);
|
|
15
|
+
const agg = (await import(pathToFileURL(path.join(SCRIPT_DIR, 'agg-delegations.cjs')).href)).default
|
|
16
|
+
?? null; // .cjs 经 import 具名空间取——见下方 createRequire 兜底
|
|
17
|
+
import { createRequire } from 'node:module';
|
|
18
|
+
const require2 = createRequire(import.meta.url);
|
|
19
|
+
const aggCjs = require2(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
20
|
+
|
|
21
|
+
let pass = 0, fail = 0;
|
|
22
|
+
const check = (name, cond, detail = '') => {
|
|
23
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
24
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
25
|
+
};
|
|
26
|
+
const W = (root, rel, content) => { fs.mkdirSync(path.join(root, path.dirname(rel)), { recursive: true }); fs.writeFileSync(path.join(root, rel), content); };
|
|
27
|
+
const INTENT = (fm) => `---\n${fm}\n---\n# INTENT — x\n`;
|
|
28
|
+
|
|
29
|
+
// ---- ① closeDurations 四例 ----
|
|
30
|
+
{
|
|
31
|
+
const intents = [
|
|
32
|
+
{ rel: 'workflow/intents/2026-10-01-same.md', date: '2026-10-01' }, // 同日 → 0
|
|
33
|
+
{ rel: 'workflow/intents/2026-09-20-cross.md', date: '2026-09-20' }, // 跨 7 天
|
|
34
|
+
{ rel: 'workflow/intents/2026-09-01-multi.md', date: '2026-09-01' }, // 多次 done → 取最早(=14 天,非 3 天)
|
|
35
|
+
{ rel: 'workflow/intents/2026-09-05-noledger.md', date: '2026-09-05' }, // 无台账行 → 跳过
|
|
36
|
+
{ rel: 'workflow/intents/2026-09-10-baddate.md', date: 'not-a-date' }, // 坏日期 → 跳过
|
|
37
|
+
];
|
|
38
|
+
const doneMap = new Map([
|
|
39
|
+
['workflow/intents/2026-10-01-same.md', '2026-10-01T15:00:00.000Z'],
|
|
40
|
+
['workflow/intents/2026-09-20-cross.md', '2026-09-27T12:00:00.000Z'],
|
|
41
|
+
['workflow/intents/2026-09-01-multi.md', '2026-09-04T00:00:00.000Z'], // 后发生的 done 行
|
|
42
|
+
]);
|
|
43
|
+
// 最早 done 行由 collectDoneMap 保证——此处直测取值;「取最早」在 ② 端到端断言
|
|
44
|
+
doneMap.set('workflow/intents/2026-09-01-multi.md', '2026-09-04T00:00:00.000Z');
|
|
45
|
+
const { days, skipped } = closeDurations(intents, doneMap);
|
|
46
|
+
const byRel = new Map(days.map((d) => [d.rel, d.days]));
|
|
47
|
+
check('时长:同日关单 = 0 天', byRel.get('workflow/intents/2026-10-01-same.md') === 0, JSON.stringify(days));
|
|
48
|
+
check('时长:跨 7 天 = 7', byRel.get('workflow/intents/2026-09-20-cross.md') === 7);
|
|
49
|
+
check('时长:doneMap 值即最早行 → 3 天(最早判定在 collectDoneMap,端到端覆盖)', byRel.get('workflow/intents/2026-09-01-multi.md') === 3);
|
|
50
|
+
check('时长:无台账行 + 坏日期 → skipped=2 不虚构', skipped === 2 && !byRel.has('workflow/intents/2026-09-05-noledger.md'), `skipped=${skipped}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---- ② pickPct / durationVerdict 三态 ----
|
|
54
|
+
{
|
|
55
|
+
check('pickPct:43 样本 P50=0 / P90=7(真实分布回归基准)',
|
|
56
|
+
pickPct([...Array(26).fill(0), ...Array(9).fill(1), 7, 8, 8, 8, 8], 0.5) === 0
|
|
57
|
+
&& pickPct([...Array(26).fill(0), ...Array(9).fill(1), 7, 8, 8, 8, 8], 0.9) === 8,
|
|
58
|
+
`p50=${pickPct([0, 0, 1], 0.5)}`);
|
|
59
|
+
check('pickPct:空 → null', pickPct([], 0.5) === null);
|
|
60
|
+
check('时长带:P50=0/P90=7 → 带内;P50=2 → 超带;null → 无样本',
|
|
61
|
+
durationVerdict(0, 7).ok === true && durationVerdict(2, 5).ok === false && durationVerdict(null, null).ok === null,
|
|
62
|
+
JSON.stringify([durationVerdict(0, 7), durationVerdict(2, 5), durationVerdict(null, null)]));
|
|
63
|
+
check('时长带常量:P50≤1 且 P90≤10(默认带声明)', DURATION_BAND.p50Max === 1 && DURATION_BAND.p90Max === 10);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---- ③ qualityRows 复用 agg 同值对账 + 端到端 spawn ----
|
|
67
|
+
{
|
|
68
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-'));
|
|
69
|
+
// delegations.md:2026-09 二十单全「一次通过」(委派 ≥ 样本护栏 minSample=20)→ 口径项全过
|
|
70
|
+
const head = '## 委派结果\n\n| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |\n|------|----------|------------|------|------|\n';
|
|
71
|
+
const rows9 = [...Array(20)].map((_, i) => `| 2026-09-${String(i + 1).padStart(2, '0')} | x | y${i} | 一次通过 | 2026-09-a${i} |\n`).join('');
|
|
72
|
+
W(root, 'workflow/delegations.md', `${head}${rows9}`);
|
|
73
|
+
// intents + 台账:same=0 / multi 两行 done 取最早(09-01 → 09-03=2 天,第二行 09-10 不采纳)
|
|
74
|
+
W(root, 'workflow/intents/2026-09-01-multi.md', INTENT('状态: done\n级别: L1\n日期: 2026-09-01\n模块: pipeline'));
|
|
75
|
+
W(root, 'workflow/intents/2026-10-01-same.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
76
|
+
W(root, 'workflow/intents/2026-09-05-noledger.md', INTENT('状态: done\n级别: L1\n日期: 2026-09-05\n模块: pipeline'));
|
|
77
|
+
W(root, 'workflow/intents/2026-09-06-open.md', INTENT('状态: approved\n级别: L1\n日期: 2026-09-06\n模块: pipeline')); // 非 done 不计
|
|
78
|
+
W(root, '.agents/confirmations.jsonl', [
|
|
79
|
+
JSON.stringify({ ts: '2026-09-02T00:00:00.000Z', doc: 'workflow/intents/2026-09-01-multi.md', stage: 'done' }),
|
|
80
|
+
JSON.stringify({ ts: '2026-09-10T00:00:00.000Z', doc: 'workflow/intents/2026-09-01-multi.md', stage: 'done' }),
|
|
81
|
+
JSON.stringify({ ts: '2026-10-01T20:00:00.000Z', doc: 'workflow/intents/2026-10-01-same.md', stage: 'done' }),
|
|
82
|
+
JSON.stringify({ ts: '2026-09-02T00:00:00.000Z', doc: 'workflow/intents/2026-09-01-multi.md', stage: 'approved' }), // 非 done 行不混
|
|
83
|
+
'not-json-bad-line',
|
|
84
|
+
].join('\n') + '\n');
|
|
85
|
+
|
|
86
|
+
// 纯函数层:qualityRows 与 agg 直算同值
|
|
87
|
+
const q = qualityRows(root);
|
|
88
|
+
check('qualityRows:月份行数与台账月份一致(2026-09 一行)', q.rows.length === 1 && q.rows[0].startsWith('| 2026-09 |'), JSON.stringify(q.rows));
|
|
89
|
+
const m = aggCjs.metrics(aggCjs.readLedger(root, { soft: true }));
|
|
90
|
+
check('qualityRows:复用 agg 同值(total=20)', m.total === 20, `total=${m.total}`);
|
|
91
|
+
check('qualityRows:单月门全过 → 判定「本月达标」(首月连续性 1/2 属 expansionVerdict 语义)',
|
|
92
|
+
q.verdictMap.get('2026-09').includes('达标'), q.verdictMap.get('2026-09'));
|
|
93
|
+
|
|
94
|
+
// 端到端:spawn 真脚本(CHECK_LOOP_ROOT 注入)→ DASHBOARD.md 结构断言
|
|
95
|
+
const r = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs')], {
|
|
96
|
+
cwd: root, encoding: 'utf8',
|
|
97
|
+
env: { ...process.env, CHECK_LOOP_ROOT: root },
|
|
98
|
+
});
|
|
99
|
+
const dash = fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
100
|
+
check('端到端:exit 0 + 生成 DASHBOARD.md', r.status === 0 && fs.existsSync(path.join(root, 'workflow', 'DASHBOARD.md')), `status=${r.status} ${(r.stdout || '') + (r.stderr || '')}`);
|
|
101
|
+
check('端到端:红绿灯行质量门「达标」+ 时长带 ✅', /达标[^]*质量门/.test(dash) && /✅[^]*关单时长带/.test(dash), dash.slice(0, 500));
|
|
102
|
+
check('端到端:时长取最早 done 行且 floor 日历天(multi=1 天;第二行 9 天不采纳;同日 0 天)',
|
|
103
|
+
dash.includes('| P50 | 1 天 |') && dash.includes('| max | 1 天 |') && /最慢 3 单:2026-09-01-multi\.md=1天、2026-10-01-same\.md=0天/.test(dash), dash);
|
|
104
|
+
check('端到端:无台账行跳过计数(noledger=1)+ 生成于戳 + 勿手改头', dash.includes('无台账行跳过(存量单) | 1 |') && dash.includes('生成于') && dash.includes('勿手改'), dash);
|
|
105
|
+
check('端到端:非 done intent 不入时长样本(open 不出现)', !dash.includes('2026-09-06-open'), dash);
|
|
106
|
+
// 幂等:重跑字节不变
|
|
107
|
+
const before = fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
108
|
+
spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs')], { cwd: root, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
109
|
+
check('端到端:重跑幂等(生成于戳同 ms 内可能变——除戳行外字节一致)',
|
|
110
|
+
before.split('\n').filter((l) => !l.includes('生成于')).join('\n') === dash.split('\n').filter((l) => !l.includes('生成于')).join('\n'));
|
|
111
|
+
|
|
112
|
+
// 空台账装户:无 delegations.md / 无 confirmations.jsonl → 优雅降级不炸
|
|
113
|
+
const empty = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-empty-'));
|
|
114
|
+
W(empty, 'workflow/intents/2026-10-01-x.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
115
|
+
const r2 = spawnSync(process.execPath, [path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs')], {
|
|
116
|
+
cwd: empty, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: empty },
|
|
117
|
+
});
|
|
118
|
+
const dash2 = fs.readFileSync(path.join(empty, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
119
|
+
check('端到端:空台账装户 → exit 0 + ⚪ 降级(质量门无数据 / 时长无样本 / 跳过 1)',
|
|
120
|
+
r2.status === 0 && dash2.includes('⚪') && dash2.includes('暂无关单样本') && dash2.includes('跳过(存量单) | 1 |'), `status=${r2.status} ${dash2.slice(0, 300)}`);
|
|
121
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
122
|
+
fs.rmSync(empty, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ---- ④ --check 漂移门(2026-10-08 selfmeasure-and-modularize)----
|
|
126
|
+
// 归一(CRLF + 「生成于」戳)不过则门禁恒红 = 不可消退噪声(audit-gate-hardening P3),故纯函数层先钉死。
|
|
127
|
+
{
|
|
128
|
+
const base = '# dash\n> 生成于 2026-10-07T13:56:00.376Z\n| P50 | 0 天 |\n';
|
|
129
|
+
check('--check 归一:仅「生成于」戳不同 → 归一后相等(否则门禁恒红)',
|
|
130
|
+
normalizeForCheck(base) === normalizeForCheck(base.replace('2026-10-07T13:56:00.376Z', '2026-10-08T00:00:00.000Z')));
|
|
131
|
+
check('--check 归一:CRLF 盘面 vs LF 生成段 → 相等(gen-workflow-index 2026-09-30 同坑先例)',
|
|
132
|
+
normalizeForCheck(base) === normalizeForCheck(base.replace(/\n/g, '\r\n')));
|
|
133
|
+
check('--check 归一:内容真漂移仍不等(归一不得吞真差异)',
|
|
134
|
+
normalizeForCheck(base) !== normalizeForCheck(base.replace('| P50 | 0 天 |', '| P50 | 7 天 |')));
|
|
135
|
+
|
|
136
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-check-'));
|
|
137
|
+
W(root, 'workflow/intents/2026-10-01-x.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
138
|
+
const gen = path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs');
|
|
139
|
+
const runCheck = () => spawnSync(process.execPath, [gen, '--check'], {
|
|
140
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: root },
|
|
141
|
+
});
|
|
142
|
+
const rMiss = runCheck();
|
|
143
|
+
check('--check 端到端:盘面缺失 → exit 1 + 提示生成', rMiss.status === 1 && /不存在或不可读/.test(rMiss.stderr || ''), `status=${rMiss.status}`);
|
|
144
|
+
spawnSync(process.execPath, [gen], { cwd: root, env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
145
|
+
const rOk = runCheck();
|
|
146
|
+
check('--check 端到端:刚生成 → exit 0', rOk.status === 0 && /一致/.test(rOk.stdout || ''), `status=${rOk.status} ${rOk.stdout || ''}${rOk.stderr || ''}`);
|
|
147
|
+
fs.appendFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), '\n<!-- drift -->\n');
|
|
148
|
+
const rDrift = runCheck();
|
|
149
|
+
check('--check 端到端:内容漂移 → exit 1 + 差异行号 + 修复命令',
|
|
150
|
+
rDrift.status === 1 && /首个差异在第 \d+ 行/.test(rDrift.stderr || '') && /gen-workflow-dashboard\.mjs 重新生成/.test(rDrift.stderr || ''), `status=${rDrift.status} ${rDrift.stderr || ''}`);
|
|
151
|
+
const noWrite = fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
152
|
+
check('--check 端到端:不写盘(漂移态下盘面保持原样)', noWrite.includes('<!-- drift -->'));
|
|
153
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- ⑤ 门禁 ROI 节(2026-10-08 gate-roi-metrics)----
|
|
157
|
+
// 本节存在的核心断言不是「有数值」,而是「**不因运行数据而漂移」**:DASHBOARD 被检查 11 --check 覆盖,
|
|
158
|
+
// 采集数值每跑一次门禁就变一次——铺进去等于每次采集必报一条漂移 WARN(不可消除噪声)。
|
|
159
|
+
{
|
|
160
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-roi-'));
|
|
161
|
+
W(root, 'workflow/intents/2026-10-01-x.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
162
|
+
const gen = path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs');
|
|
163
|
+
const run = (...args) => spawnSync(process.execPath, [gen, ...args], { cwd: root, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
164
|
+
const dashOf = () => fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
165
|
+
|
|
166
|
+
// 无采集态(先于造 cache 生成):节恒为静态文案,故与有无数据无关
|
|
167
|
+
const rl = gateRoiLines();
|
|
168
|
+
check('门禁ROI:节内容给出采集命令 + 看数值命令 + 无数据时 CLI 自带「未采集」提示(优雅降级不落空节)',
|
|
169
|
+
rl.join('\n').includes('check-loop.mjs --gate-stats') && rl.join('\n').includes('agg-gate-stats.mjs') && rl.join('\n').includes('未采集'), rl.join('\n'));
|
|
170
|
+
check('门禁ROI:节内容不含任何耗时/次数运行数值(防检查 11 每次采集报漂移的设计核心)',
|
|
171
|
+
!/\d+\s*(ms|s)\b/.test(rl.join('\n')) && !/运行\s*\d/.test(rl.join('\n')), rl.join('\n'));
|
|
172
|
+
run();
|
|
173
|
+
const dash0 = dashOf();
|
|
174
|
+
check('门禁ROI:生成物含该节 + 耗时口径显式声明「含子进程」防误读',
|
|
175
|
+
dash0.includes('## 门禁 ROI') && dash0.includes('含子进程时间'), dash0.slice(-800));
|
|
176
|
+
|
|
177
|
+
// 采集数据从无到有 → DASHBOARD 必须仍一致:这是本节设计的核心钉子
|
|
178
|
+
const cacheRel = path.join(root, '.agents', 'cache', 'gate-stats.jsonl');
|
|
179
|
+
fs.mkdirSync(path.dirname(cacheRel), { recursive: true });
|
|
180
|
+
fs.writeFileSync(cacheRel, JSON.stringify({ ts: '2026-10-08T00:00:00Z', segs: [{ id: '8', warns: 3, ms: 11809 }], totalMs: 11809 }) + '\n');
|
|
181
|
+
check('门禁ROI:采集数据「从无到有」后 --check 仍 exit 0', run('--check').status === 0, `status=${run('--check').status}`);
|
|
182
|
+
fs.appendFileSync(cacheRel, JSON.stringify({ ts: '2026-10-08T01:00:00Z', segs: [{ id: '8', warns: 9, ms: 99 }], totalMs: 99 }) + '\n');
|
|
183
|
+
check('门禁ROI:采集数据「再变一次」后 --check 仍 exit 0 且盘面字节恒等(零新增漂移源)',
|
|
184
|
+
run('--check').status === 0 && dashOf() === dash0, `status=${run('--check').status}`);
|
|
185
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
189
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -69,6 +69,25 @@ export const POLICIES = {
|
|
|
69
69
|
riskLevelSince: '2026-10-01',
|
|
70
70
|
verifySince: '2026-10-07', // 测试绿机器凭证生效日(2026-10-07 verify-evidence);v1-v4 无此键 → 前置与对账整体跳过
|
|
71
71
|
},
|
|
72
|
+
// 版本 6 = 版本 5 的全部键 + verifyDocSince(测试绿凭证与被证明对象的绑定日,2026-10-08 verify-doc-binding)。
|
|
73
|
+
// 背景(2026-10-08 verify-doc-binding):v5 的凭证行只记 {ts, exitCode, suite, passed, failed},
|
|
74
|
+
// **没有字段能承载「它是为哪一单落的」**,而两个消费方(confirm-doc 置 done 前置 / 检查 8 对账)
|
|
75
|
+
// 都只问「全仓任意一份 24h 内绿行」→ 跑一次 verify 就能给全仓任意一单的「测试绿」声明背书,
|
|
76
|
+
// 这是自证式验证的教科书形态(证据与被证明对象零绑定)。
|
|
77
|
+
// 本版本只加一个开关键:**缺键(v1-v5)时两个消费方维持 v5 的全局行为逐字不变**(存量装户与存量
|
|
78
|
+
// fixture 零变化、零新增告警);本仓升 v6 后才按 doc 精确匹配。抄全键仍是铁律(见 v2 注释)。
|
|
79
|
+
6: {
|
|
80
|
+
moduleSince: '2026-09-22',
|
|
81
|
+
check14Since: '2026-09-26',
|
|
82
|
+
confirmDocsEffective: '2026-09-27',
|
|
83
|
+
confirmIncidentsEffective: '2026-09-28',
|
|
84
|
+
bindingTs: '2026-09-28',
|
|
85
|
+
check17UnclosedAfter: '0.8.0',
|
|
86
|
+
stageGateSince: '2026-09-29',
|
|
87
|
+
riskLevelSince: '2026-10-01',
|
|
88
|
+
verifySince: '2026-10-07',
|
|
89
|
+
verifyDocSince: '2026-10-08', // 凭证按 doc 绑定生效日;v1-v5 无此键 → 两个消费方维持 v5 全局行为
|
|
90
|
+
},
|
|
72
91
|
};
|
|
73
92
|
|
|
74
93
|
export function loadKitPolicy(root) {
|
|
@@ -90,17 +109,26 @@ export function auditEnabled(policy) {
|
|
|
90
109
|
return policy.audit !== false;
|
|
91
110
|
}
|
|
92
111
|
|
|
93
|
-
// ---- 测试绿凭证判定(2026-10-07 verify-evidence
|
|
112
|
+
// ---- 测试绿凭证判定(2026-10-07 verify-evidence;2026-10-08 verify-doc-binding 增 doc 绑定)----
|
|
94
113
|
// 单源(confirm-doc done 前置与 check-loop 检查 8 对账共用——防两处窗口字面量漂移,N3 教训):
|
|
95
|
-
// hasFreshVerifyLine(lines, now):verifications.jsonl 的行数组里是否存在「ts 距 now ≤ 24h 且
|
|
114
|
+
// hasFreshVerifyLine(lines, now, doc):verifications.jsonl 的行数组里是否存在「ts 距 now ≤ 24h 且
|
|
96
115
|
// exitCode === 0」的绿行。坏行(非 JSON / 缺 ts)跳过不抛;窗口 24h = 覆盖「跑 verify → 跨会话/
|
|
97
116
|
// 隔夜 → 次日关单」节奏(spec 2026-10-07-verify-evidence 论证)。
|
|
98
|
-
|
|
117
|
+
//
|
|
118
|
+
// doc 绑定(2026-10-08 verify-doc-binding,第三参):
|
|
119
|
+
// doc 为空/缺省 → 逻辑与 2026-10-07 版**逐字相同**(全量行扫描)——v1-v5 装户走这条,零变化;
|
|
120
|
+
// doc 非空 → 额外要求该行 `e.doc === doc`(**精确等值**,不做前缀/子串/形态匹配——
|
|
121
|
+
// 沿 check8-digit-sha-misfire「文本启发式须可被实证兜底,禁以形态抢先分类」纪律)。
|
|
122
|
+
// 为什么必须精确:放宽成「包含即认」等于给跨单背书开后门,那正是本单要消灭的漏洞。
|
|
123
|
+
// 不带 doc 落账的行(CI 场景)**不给任何单背书**——doc 为 null 时 e.doc === doc 恒假。
|
|
124
|
+
export function hasFreshVerifyLine(lines, now = Date.now(), doc = null) {
|
|
125
|
+
const byDoc = typeof doc === 'string' && doc !== '';
|
|
99
126
|
for (const line of lines || []) {
|
|
100
127
|
if (!line || !line.trim()) continue;
|
|
101
128
|
try {
|
|
102
129
|
const e = JSON.parse(line);
|
|
103
130
|
if (e && e.exitCode === 0 && typeof e.ts === 'string') {
|
|
131
|
+
if (byDoc && e.doc !== doc) continue;
|
|
104
132
|
const t = Date.parse(e.ts);
|
|
105
133
|
if (Number.isFinite(t) && now - t <= 24 * 60 * 60 * 1000) return true;
|
|
106
134
|
}
|
|
@@ -14,7 +14,13 @@
|
|
|
14
14
|
// 且**不落测试绿凭证**(凭证语义 = 测试真跑绿,跳过即无凭证,防假绿),并提示用
|
|
15
15
|
// --test-cmd "<项目测试命令>" 指定;显式 --test-cmd 不受此判定影响(2026-10-07 papercut)。
|
|
16
16
|
// 测试:node templates/_agents/scripts/verify.test.mjs(--test-cmd 注入假命令 + CHECK_LOOP_ROOT 注入夹具,不触真实 npm test 与 workflow)
|
|
17
|
-
//
|
|
17
|
+
// --doc 凭证绑定(2026-10-08 verify-doc-binding):落账行增 `doc` 字段,写入**相对仓库根的 posix 形态**
|
|
18
|
+
// (win32 反斜杠 / posix 斜杠 / 绝对路径三种写法经 resolve+relative+分隔符归一后落同一值),使凭证
|
|
19
|
+
// 与被证明的那一单绑定。不带 --doc 时仍落行但**不写该键**——CI(kit-ci.yml)无单次上下文,照常记录
|
|
20
|
+
// 事实;这样的行不给任何单背书(消费方按 doc 精确匹配时 e.doc 为 undefined 恒不命中)。
|
|
21
|
+
// 用法:node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"] [--doc <本单 workflow 路径>]
|
|
22
|
+
// (--test-cmd 用于测试注入,或在无根 package.json 的装户仓指定项目测试命令;
|
|
23
|
+
// --doc 把凭证绑到本单,如 --doc workflow/intents/2026-10-08-x.md)
|
|
18
24
|
import fs from 'node:fs';
|
|
19
25
|
import { spawnSync } from 'node:child_process';
|
|
20
26
|
import path from 'node:path';
|
|
@@ -27,6 +33,22 @@ const testCmd = explicitTestCmd ?? 'npm test';
|
|
|
27
33
|
const checkLoop = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
28
34
|
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
29
35
|
|
|
36
|
+
// --doc 解析(沿 --test-cmd 同款取下一 argv 的写法;缺值报用法错误 exit 1 与 --test-cmd 对称)。
|
|
37
|
+
// 归一基准 = **ROOT**(不是 cwd)——消费方两处都以仓库根为基准(check-loop 检查 8 用
|
|
38
|
+
// path.relative(ROOT, intent);confirm-doc 的 docRelOf 用 path.resolve(root, doc))。若这里按 cwd
|
|
39
|
+
// 解析而进程不在仓库根(CI / 钩子 / CHECK_LOOP_ROOT 注入夹具),落下的 doc 会是跨目录长路径,
|
|
40
|
+
// 凭证永远匹配不上——**基准必须三处同源**。
|
|
41
|
+
// 不校验目标文件是否存在:凭证记录的是「跑了什么」而非「被证明对象是否有效」,存在性校验属裁决,
|
|
42
|
+
// 越界(沿本件「只编排不裁决」语义)。
|
|
43
|
+
if (args.includes('--doc') && !args[args.indexOf('--doc') + 1]) {
|
|
44
|
+
console.error('verify: --doc 缺值——用法 node .agents/scripts/verify.mjs [--test-cmd "<shell 命令>"] [--doc <本单 workflow 路径>]');
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
const docArg = args.includes('--doc') ? args[args.indexOf('--doc') + 1] : null;
|
|
48
|
+
const docRel = docArg
|
|
49
|
+
? path.relative(ROOT, path.resolve(ROOT, docArg)).split(path.sep).join('/')
|
|
50
|
+
: null;
|
|
51
|
+
|
|
30
52
|
// 装户仓判定:默认命令只在**当前目录**有 package.json 时才跑(npm test 亦在该目录执行)。
|
|
31
53
|
// 跳过时后续不落测试绿凭证(见文末落账段)。
|
|
32
54
|
const hasRootPackageJson = fs.existsSync(path.join(process.cwd(), 'package.json'));
|
|
@@ -66,9 +88,12 @@ try {
|
|
|
66
88
|
const entry = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
|
|
67
89
|
if (m) { entry.passed = Number(m[1]); entry.failed = Number(m[2]); }
|
|
68
90
|
if (process.env.PIPELINE_RUN_ID) entry.runId = process.env.PIPELINE_RUN_ID;
|
|
91
|
+
// doc 键:未传 --doc 时**不写该键**(不是写 null)——消费方按 doc 精确匹配,undefined 与 null 都恒不命中,
|
|
92
|
+
// 但缺键让「本行没有绑定对象」这件事在台账盘面上直接可读。
|
|
93
|
+
if (docRel) entry.doc = docRel;
|
|
69
94
|
fs.mkdirSync(path.join(ROOT, '.agents'), { recursive: true });
|
|
70
95
|
fs.appendFileSync(path.join(ROOT, '.agents', 'verifications.jsonl'), `${JSON.stringify(entry)}\n`);
|
|
71
|
-
console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ? `(PASS ${entry.passed} / FAIL ${entry.failed})` : ''}`);
|
|
96
|
+
console.log(`[verify] 🧾 测试绿凭证已落账 verifications.jsonl${entry.passed !== undefined ? `(PASS ${entry.passed} / FAIL ${entry.failed})` : ''}${docRel ? ` 绑定本单 ${docRel}` : '(未带 --doc:不给任何单背书)'}`);
|
|
72
97
|
}
|
|
73
98
|
} catch (e) {
|
|
74
99
|
console.error(`[verify] ⚠️ 凭证落账失败(${e.code || e.message})——不阻断,关单门前置将按无凭证告警`);
|
|
@@ -132,6 +132,59 @@ const run = (argv, opts = {}) => spawnSync(process.execPath, [VERIFY, ...argv],
|
|
|
132
132
|
fs.rmSync(bare, { recursive: true, force: true });
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// ---- 场景 8:--doc 凭证绑定(2026-10-08 verify-doc-binding)----
|
|
136
|
+
// ① 带 --doc → 末行含 doc 且为规范化 posix 相对路径;② 不带 → 末行**无 doc 键**(不是 null);
|
|
137
|
+
// ③ win32 反斜杠 / posix / 绝对路径三种写法落同一值(归一是本单的地基,错一处=凭证永远匹配不上);
|
|
138
|
+
// ④ 缺值 → exit 1 用法错误;⑤ 跳过步骤 1 时即便带 --doc 仍不落账(防假绿语义不得被 --doc 破坏)。
|
|
139
|
+
{
|
|
140
|
+
const readLast = (root) => {
|
|
141
|
+
const p = path.join(root, '.agents', 'verifications.jsonl');
|
|
142
|
+
if (!fs.existsSync(p)) return null;
|
|
143
|
+
const lines = fs.readFileSync(p, 'utf8').trim().split('\n').filter(Boolean);
|
|
144
|
+
return lines.length ? JSON.parse(lines[lines.length - 1]) : null;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const rootA = mkfix();
|
|
148
|
+
const rel = 'workflow/intents/2026-10-08-demo.md';
|
|
149
|
+
const rA = run(['--test-cmd', `node ${mkCmd(0, '合计: PASS 3 / FAIL 0\n')}`, '--doc', rel],
|
|
150
|
+
{ env: { ...process.env, CHECK_LOOP_ROOT: rootA } });
|
|
151
|
+
const eA = readLast(rootA);
|
|
152
|
+
check('场景 8①:带 --doc → 末行含 doc 且为 posix 相对路径',
|
|
153
|
+
rA.status === 0 && eA && eA.doc === rel && rA.stdout.includes('绑定本单'), `exit=${rA.status} entry=${JSON.stringify(eA)}`);
|
|
154
|
+
|
|
155
|
+
const rootB = mkfix();
|
|
156
|
+
run(['--test-cmd', `node ${mkCmd(0)}`], { env: { ...process.env, CHECK_LOOP_ROOT: rootB } });
|
|
157
|
+
const eB = readLast(rootB);
|
|
158
|
+
check('场景 8②:不带 --doc → 仍落行但**无 doc 键**(CI 路径不退化;此行不给任何单背书)',
|
|
159
|
+
eB && eB.exitCode === 0 && !('doc' in eB) && eB.suite === 'npm test', JSON.stringify(eB));
|
|
160
|
+
|
|
161
|
+
// 归一:cwd 设为仓库根时,win32 反斜杠 / posix / 绝对路径三种写法须落同一值
|
|
162
|
+
const rootC = mkfix();
|
|
163
|
+
const abs = path.join(rootC, rel);
|
|
164
|
+
const winStyle = rel.split('/').join(path.sep);
|
|
165
|
+
const rC1 = run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', winStyle], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
|
|
166
|
+
const eC1 = readLast(rootC);
|
|
167
|
+
const rC2 = run(['--test-cmd', `node ${mkCmd(0)}`, '--doc', abs], { cwd: rootC, env: { ...process.env, CHECK_LOOP_ROOT: rootC } });
|
|
168
|
+
const eC2 = readLast(rootC);
|
|
169
|
+
check('场景 8③:反斜杠 / posix / 绝对路径三种写法落同一 doc 值(归一是地基)',
|
|
170
|
+
eC1 && eC1.doc === rel && eC2 && eC2.doc === rel, `win=${JSON.stringify(eC1 && eC1.doc)} abs=${JSON.stringify(eC2 && eC2.doc)}`);
|
|
171
|
+
|
|
172
|
+
const rootD = mkfix();
|
|
173
|
+
const rD = run(['--test-cmd', `node ${mkCmd(0)}`, '--doc'], { env: { ...process.env, CHECK_LOOP_ROOT: rootD } });
|
|
174
|
+
check('场景 8④:--doc 缺值 → exit 1 + 用法提示',
|
|
175
|
+
rD.status === 1 && /--doc 缺值/.test(rD.stderr), `exit=${rD.status} ${rD.stderr}`);
|
|
176
|
+
|
|
177
|
+
const rootE = mkfix();
|
|
178
|
+
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'verify-bare-doc-'));
|
|
179
|
+
const rE = run(['--doc', rel], { cwd: bare, env: { ...process.env, CHECK_LOOP_ROOT: rootE } });
|
|
180
|
+
check('场景 8⑤:步骤 1 跳过时即便带 --doc 仍不落账(防假绿语义不被 --doc 破坏)',
|
|
181
|
+
rE.status === 0 && !fs.existsSync(path.join(rootE, '.agents', 'verifications.jsonl')) && rE.stdout.includes('不落测试绿凭证'),
|
|
182
|
+
`exit=${rE.status} ${rE.stdout}`);
|
|
183
|
+
|
|
184
|
+
for (const r of [rootA, rootB, rootC, rootD, rootE]) fs.rmSync(r, { recursive: true, force: true });
|
|
185
|
+
fs.rmSync(bare, { recursive: true, force: true });
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
136
189
|
if (fail) {
|
|
137
190
|
console.log('\n用法:node templates/_agents/scripts/verify.test.mjs');
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# kit-ci —— agentic-flow-kit 装户远端门(GitHub Actions 薄模板,2026-10-07 adopter-ci-github)
|
|
2
|
+
# 语义:本地 git hooks 可被 --no-verify / 未配 hooksPath 的克隆绕过——本工作流在 GitHub runner 上复跑
|
|
3
|
+
# 统一入口 node .agents/scripts/verify.mjs(测试【有根 package.json 才跑】+ check-loop),任一非零即红;
|
|
4
|
+
# 不在 YAML 里复制门禁逻辑(本地/远端单源,防口径漂移)。
|
|
5
|
+
# 本文件归装户自持(owned):触发分支 / node 版本 / 测试命令可自改,flow-kit sync 永不覆盖。
|
|
6
|
+
# 装户仓工程在子目录(根无 package.json)时 verify 会跳过测试——改用
|
|
7
|
+
# node .agents/scripts/verify.mjs --test-cmd "<项目测试命令>"
|
|
8
|
+
# 非 GitHub 托管(TFS / Azure DevOps / Jenkins…)可删除本文件:等价远端门 = 同入口命令 + 服务端分支策略。
|
|
9
|
+
# ⚠️ 真正的「门」在服务端:Settings → Branches → branch protection 勾「要求 PR + 状态检查绿才可合并」
|
|
10
|
+
# ——该配置带不进仓库文件,一次性人工动作,机器不校验。
|
|
11
|
+
name: kit-ci
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
pull_request:
|
|
16
|
+
jobs:
|
|
17
|
+
gate:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v4
|
|
21
|
+
- uses: actions/setup-node@v4
|
|
22
|
+
with:
|
|
23
|
+
node-version: 22 # 按项目 engines 调整(kit 引擎要求 >=18)
|
|
24
|
+
- name: 安装依赖(有根 package.json 才跑;私有源在此加 registry 配置)
|
|
25
|
+
if: hashFiles('package.json') != ''
|
|
26
|
+
run: npm ci || npm install
|
|
27
|
+
- name: 远端门(测试 + 闭环校验)
|
|
28
|
+
run: node .agents/scripts/verify.mjs
|