agentic-flow-kit 1.2.2 → 1.3.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/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/rule-budgets.txt +3 -1
- package/templates/_agents/scripts/agg-delegations.cjs +15 -5
- 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 +1134 -1084
- package/templates/_agents/scripts/check-loop.test.mjs +86 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +182 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +126 -0
- package/templates/_github/workflows/kit-ci.yml +28 -0
|
@@ -17,7 +17,7 @@ import { entryConfirmed, MARK_RE, laneOfEntry, laneOfDoc } from './stage-gates.m
|
|
|
17
17
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
19
19
|
const GIT = process.platform === 'win32' ? 'git.exe' : 'git'; // Windows spawn 不补 .exe 扩展名(gitInit/gitCommitAll/场景 43/44/47 共用)
|
|
20
|
-
//
|
|
20
|
+
// 注意(2026-10-08 checkloop-importable 后):check-loop.mjs 已有 isMain 主守卫,可 import(runCheckLoop/四纯零件,直测见 check-loop-unit.test.mjs)——本套件保持端到端 spawn 作为行为回归网,
|
|
21
21
|
// 不可从其 import 函数直测(2026-10-04 review-fix-batch 实证;可 import 化记 papercuts 待 L2 重构)
|
|
22
22
|
|
|
23
23
|
let pass = 0;
|
|
@@ -743,6 +743,24 @@ const evidenceFixture = (slug, declared, createDeclared) => {
|
|
|
743
743
|
check('git 基础设施异常:基础设施异常不冒充业务裁决(无证据无关误报)', !outOf(r).includes('证据无关'), outOf(r));
|
|
744
744
|
rmfix(T);
|
|
745
745
|
}
|
|
746
|
+
{
|
|
747
|
+
// 纯数字短 sha × 通道坏死(2026-10-07 v1.3.0 Release CI ubuntu 实证):fixture 短 sha 恰全数字
|
|
748
|
+
//(`--short` 7 位 ≈4%/run)时,rev-parse 实证分流在 CHECK_LOOP_GIT 整体坏死下不可能——
|
|
749
|
+
// 纯数字 text 豁免曾把 fail-closed 漏成 exit 0。守卫后:通道坏死(spawn 异常)→ forged 拦;
|
|
750
|
+
// 对照:通道健康时纯数字假 sha → rev-parse 正常回答不存在 → text 豁免语义保持(misfire 防线不回归)。
|
|
751
|
+
const T = mkfix();
|
|
752
|
+
gitInit(T);
|
|
753
|
+
w(T, 'workflow/plans/2026-09-26-pe7.md', PLAN('pe7', '状态: done\n级别: L1\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
754
|
+
w(T, 'workflow/intents/2026-09-26-pe7.md', INTENT('pe7', '状态: done\n级别: L1\n日期: 2026-09-26', '\n## 验收标准(可测试)\n- [x] 演练穿越(证据:1234567)\n'));
|
|
755
|
+
gitCommitAll(T, 'docs(x): 纯数字证据通道坏死场景');
|
|
756
|
+
const rDead = run(T, { env: { CHECK_LOOP_GIT: 'definitely-not-git-xyz-2026' } });
|
|
757
|
+
check('git 基础设施异常:纯数字 sha × 通道坏死 → forged 拦不豁免(实证不可能即 fail-closed)',
|
|
758
|
+
rDead.status === 1 && outOf(rDead).includes('证据伪造'), `exit=${rDead.status}\n${outOf(rDead)}`);
|
|
759
|
+
const rAlive = run(T);
|
|
760
|
+
check('git 基础设施异常:纯数字假 sha × 通道健康 → text 豁免语义保持(misfire 防线不回归)',
|
|
761
|
+
rAlive.status === 0 && outOf(rAlive).includes('证据豁免 text'), `exit=${rAlive.status}\n${outOf(rAlive)}`);
|
|
762
|
+
rmfix(T);
|
|
763
|
+
}
|
|
746
764
|
{
|
|
747
765
|
const T = mkfix();
|
|
748
766
|
gitInit(T);
|
|
@@ -2703,5 +2721,72 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2703
2721
|
rmfix(T2);
|
|
2704
2722
|
}
|
|
2705
2723
|
|
|
2724
|
+
// ---- 检查 18 表头签名识别(2026-10-08 papercuts-cleanup-batch 修 2):节标题漂移不再静默停解析 ----
|
|
2725
|
+
// 修前按 `## 委派结果|自做任务结果` 精确节标题切段,装户把标题写成「委派结果表」即静默停解析该表
|
|
2726
|
+
// → L2/L3 done 回流无告警;修后按表头签名(与 agg-delegations.cjs splitTables 同口径)识别。
|
|
2727
|
+
{
|
|
2728
|
+
const T = mkfix();
|
|
2729
|
+
const gap = '2026-09-12-drift.md';
|
|
2730
|
+
w(T, `workflow/intents/${gap}`, INTENT('drift', '状态: done\n级别: L2\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
2731
|
+
w(T, `workflow/specs/${gap}`, SPEC('drift', '状态: approved\n级别: L2\n日期: 2026-09-12'));
|
|
2732
|
+
w(T, `workflow/plans/${gap}`, PLAN('drift', '状态: approved\n级别: L2\n日期: 2026-09-12'));
|
|
2733
|
+
const ledgerWarn = (r) => outOf(r).split('\n').some((l) => l.includes('委派台账') && l.includes(`intents/${gap}`));
|
|
2734
|
+
// 正例:节标题漂移(「委派结果表」≠精确节名)而表头不动 → 仍按表头签名解析,警告消失
|
|
2735
|
+
w(T, 'workflow/delegations.md', '## 委派结果表\n\n| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |\n|------|----------|------------|------|------|\n| 2026-09-12 | x | y | 一次通过 | 2026-09-12-drift |\n');
|
|
2736
|
+
let r = run(T);
|
|
2737
|
+
check('检查18 节标题漂移而表头不动 → 仍按表头签名解析,警告消失(修 2 核心场景)',
|
|
2738
|
+
r.status === 0 && !ledgerWarn(r), `exit=${r.status}\n${outOf(r)}`);
|
|
2739
|
+
// 负例:表头也漂移(新格式无签名列)→ 数据行不收集 → 警告仍在(识别不过宽)
|
|
2740
|
+
w(T, 'workflow/delegations.md', '## 委派结果表\n\n| 日期 | 承办 | 说明 |\n|------|------|------|\n| 2026-09-12 | x | 2026-09-12-drift |\n');
|
|
2741
|
+
r = run(T);
|
|
2742
|
+
check('检查18 表头也漂移(无签名列)→ 不收集 → 警告仍在(识别不过宽负例)',
|
|
2743
|
+
r.status === 0 && ledgerWarn(r), `exit=${r.status}\n${outOf(r)}`);
|
|
2744
|
+
rmfix(T);
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// ---- 检查 14 台账 OR(2026-10-08 papercuts-cleanup-batch 修 3):git 历史无 approved 行而台账有 → 不再误报 ----
|
|
2748
|
+
// 两跳确认同批提交时 git 历史不出现行首「状态: approved」(2026-09-28-adopter-derivers 实证)——
|
|
2749
|
+
// confirmations.jsonl 已证明两跳均走;判据改 OR 并集(git 判据保留,台账为第二通道)。
|
|
2750
|
+
{
|
|
2751
|
+
const T = mkfix();
|
|
2752
|
+
gitInit(T);
|
|
2753
|
+
const base = '2026-09-23-confirm-ledger';
|
|
2754
|
+
w(T, `workflow/incidents/${base}.md`, `---\n状态: closed\n级别: L1\n发现: 2026-09-23\n模块: material\n---\n# INCIDENT — confirm ledger\n\n${三件套(false)}`);
|
|
2755
|
+
w(T, `workflow/plans/${base}.md`, PLAN('confirm ledger', '状态: done\n级别: L1\n模块: material'));
|
|
2756
|
+
gitCommitAll(T, 'test'); // 两跳同批:git 历史无行首 approved(修前必 WARN)
|
|
2757
|
+
w(T, '.agents/confirmations.jsonl', `${JSON.stringify({ ts: '2026-09-23T00:00:00Z', doc: `workflow/plans/${base}.md`, stage: 'approved', fingerprint: 'x', prev: 'draft', source: 'tty' })}\n`);
|
|
2758
|
+
let r = run(T);
|
|
2759
|
+
// exit 1 属检查 15 指纹 hard 同场(台账有该 doc 行即受管,fixture 无真实指纹必拦——预期);
|
|
2760
|
+
// 本场景只断言检查 14 的 WARN 确认态缺失不再出账(台账 OR 兜住)
|
|
2761
|
+
check('检查14 git 历史无 approved 行而台账有 → 台账 OR 兜住不误报(修 3 核心场景)',
|
|
2762
|
+
!outOf(r).includes('WARN 确认态缺失'), `exit=${r.status}\n${outOf(r)}`);
|
|
2763
|
+
// 台账行 doc 不指向该文档 → 仍 WARN(OR 不弱化原判据)
|
|
2764
|
+
w(T, '.agents/confirmations.jsonl', `${JSON.stringify({ ts: '2026-09-23T00:00:00Z', doc: 'workflow/plans/other.md', stage: 'approved', fingerprint: 'x', prev: 'draft', source: 'tty' })}\n`);
|
|
2765
|
+
r = run(T);
|
|
2766
|
+
check('检查14 台账无该文档 approved 行 → 仍 WARN(git 判据未被弱化)',
|
|
2767
|
+
r.status === 0 && /WARN 确认态缺失.*2026-09-23-confirm-ledger\.md/.test(outOf(r)), outOf(r));
|
|
2768
|
+
rmfix(T);
|
|
2769
|
+
}
|
|
2770
|
+
|
|
2771
|
+
// ---- 检查 4 全角逗号(2026-10-08 papercuts-cleanup-batch 修 4):路径引用后跟「,」不再吞入引用串 ----
|
|
2772
|
+
// 中文文档「`.agents/...`,以及…」写法曾把「,」并入引用 → 引用断档误报(p0-gate-noise-batch 实证 2 条)。
|
|
2773
|
+
{
|
|
2774
|
+
const T = mkfix();
|
|
2775
|
+
fs.mkdirSync(path.join(T, '.agents', 'commands'), { recursive: true });
|
|
2776
|
+
w(T, '.agents/commands/plan.md', '# 小命令\n');
|
|
2777
|
+
w(T, 'workflow/intents/2026-09-12-ref-comma.md', INTENT('ref comma', '状态: approved\n级别: L1\n日期: 2026-09-12\n模块: pipeline', '\n入口 `.agents/commands/plan.md`,以及后续步骤。\n'));
|
|
2778
|
+
w(T, 'workflow/plans/2026-09-12-ref-comma.md', PLAN('ref comma', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
2779
|
+
let r = run(T);
|
|
2780
|
+
check('检查4 路径后跟全角逗号 → 引用串不吞「,」,真实路径不误报断档',
|
|
2781
|
+
r.status === 0 && !outOf(r).includes('引用断档'), `exit=${r.status}\n${outOf(r)}`);
|
|
2782
|
+
w(T, 'workflow/intents/2026-09-12-ref-comma2.md', INTENT('ref comma2', '状态: approved\n级别: L1\n日期: 2026-09-12\n模块: pipeline', '\n入口 `.agents/commands/missing-broken.md`,后续。\n'));
|
|
2783
|
+
w(T, 'workflow/plans/2026-09-12-ref-comma2.md', PLAN('ref comma2', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
2784
|
+
r = run(T);
|
|
2785
|
+
// 负例用 ASCII 断档名:refRe 字符类本就只含 ASCII 路径字符(中文名路径不在扫描范围)
|
|
2786
|
+
check('检查4 全角逗号后的真断档 → 仍拦(判据不放松)',
|
|
2787
|
+
r.status === 0 && /引用断档.*missing-broken\.md/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
2788
|
+
rmfix(T);
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2706
2791
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2707
2792
|
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,182 @@
|
|
|
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 供测试 import 直测。
|
|
11
|
+
// 用法:node .agents/scripts/gen-workflow-dashboard.mjs [--dry-run]
|
|
12
|
+
// 测试:node templates/_agents/scripts/gen-workflow-dashboard.test.mjs
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const ROOT = process.env.CHECK_LOOP_ROOT || path.resolve(SCRIPT_DIR, '..', '..');
|
|
21
|
+
const agg = require('./agg-delegations.cjs');
|
|
22
|
+
|
|
23
|
+
// 关单时长控制带(默认带,数据说话后调):P50 ≤ 1 天 且 P90 ≤ 10 天为绿——本仓实测 P50=0/P90=7 在带内,
|
|
24
|
+
// 留 7 天 P90 余量覆盖跨周单;不带配置文件(YAGNI,真需要项目级调带时再外置)。
|
|
25
|
+
export const DURATION_BAND = { p50Max: 1, p90Max: 10 };
|
|
26
|
+
|
|
27
|
+
// closeDurations:done intents 关单天数(纯函数供测试)——doneMap: Map<rel, 最早 done 行 ISO ts>
|
|
28
|
+
export function closeDurations(intents, doneMap) {
|
|
29
|
+
const days = [];
|
|
30
|
+
let skipped = 0;
|
|
31
|
+
for (const it of intents) {
|
|
32
|
+
const ts = doneMap.get(it.rel);
|
|
33
|
+
const t = ts === undefined ? NaN : Date.parse(ts);
|
|
34
|
+
const d0 = Date.parse(`${it.date}T00:00:00Z`);
|
|
35
|
+
if (!Number.isFinite(t) || !Number.isFinite(d0)) { skipped++; continue; }
|
|
36
|
+
days.push({ rel: it.rel, days: Math.max(0, Math.floor((t - d0) / 86400000)) }); // floor=日历天差:同日关门 0.625 天仍记 0(round 会记 1)
|
|
37
|
+
}
|
|
38
|
+
days.sort((a, b) => a.days - b.days || a.rel.localeCompare(b.rel));
|
|
39
|
+
return { days, skipped };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// pickPct:百分位(升序数组;与 repo 内实测探针同口径 floor(len*p) 截断)
|
|
43
|
+
export function pickPct(sortedNums, p) {
|
|
44
|
+
if (!sortedNums.length) return null;
|
|
45
|
+
return sortedNums[Math.min(sortedNums.length - 1, Math.floor(sortedNums.length * p))];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// durationVerdict:时长带判定(三态:无样本 null / 带内 / 超带)
|
|
49
|
+
export function durationVerdict(p50, p90) {
|
|
50
|
+
if (p50 === null) return { ok: null, text: '⚪ 暂无关单样本(无带台账 done 行的 done intent)' };
|
|
51
|
+
const ok = p50 <= DURATION_BAND.p50Max && p90 <= DURATION_BAND.p90Max;
|
|
52
|
+
return { ok, text: ok ? '✅ 带内' : '❌ 超带' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// fmField:frontmatter 单行「键: 值」最小提取(check-loop fmGet 无 isMain 守卫不可 import——本件够用)
|
|
56
|
+
function fmField(text, key) {
|
|
57
|
+
const m = String(text).match(new RegExp(`^${key}: (.*)$`, 'm'));
|
|
58
|
+
return m ? m[1].trim() : '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// collectDoneIntents:done 态 intents(排除 _TEMPLATE)→ [{rel, date}]
|
|
62
|
+
function collectDoneIntents(root) {
|
|
63
|
+
const dir = path.join(root, 'workflow', 'intents');
|
|
64
|
+
const out = [];
|
|
65
|
+
if (!fs.existsSync(dir)) return out;
|
|
66
|
+
for (const f of fs.readdirSync(dir)) {
|
|
67
|
+
if (!f.endsWith('.md') || f.startsWith('_TEMPLATE')) continue;
|
|
68
|
+
const text = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
69
|
+
if (fmField(text, '状态') !== 'done') continue;
|
|
70
|
+
const date = fmField(text, '日期');
|
|
71
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) continue;
|
|
72
|
+
out.push({ rel: `workflow/intents/${f}`, date });
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// collectDoneMap:台账 earliest done 行(坏行容忍,doc 正斜杠根相对——沿检查 14/15 口径)
|
|
78
|
+
function collectDoneMap(root) {
|
|
79
|
+
const doneMap = new Map();
|
|
80
|
+
const ledgerPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
81
|
+
if (!fs.existsSync(ledgerPath)) return doneMap;
|
|
82
|
+
for (const line of fs.readFileSync(ledgerPath, 'utf8').split(/\r?\n/)) {
|
|
83
|
+
if (!line.trim()) continue;
|
|
84
|
+
try {
|
|
85
|
+
const e = JSON.parse(line);
|
|
86
|
+
if (e && e.stage === 'done' && typeof e.doc === 'string' && e.doc.startsWith('workflow/intents/')) {
|
|
87
|
+
const prev = doneMap.get(e.doc);
|
|
88
|
+
if (prev === undefined || e.ts < prev) doneMap.set(e.doc, e.ts);
|
|
89
|
+
}
|
|
90
|
+
} catch { /* 坏行容忍跳过 */ }
|
|
91
|
+
}
|
|
92
|
+
return doneMap;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// qualityRows:月度质量行(判据全部复用 agg——本函数只做分组与格式化)
|
|
96
|
+
export function qualityRows(root) {
|
|
97
|
+
const rows = agg.readLedger(root, { soft: true });
|
|
98
|
+
const byMonth = {};
|
|
99
|
+
for (const r of rows) (byMonth[r.date.slice(0, 7)] ||= []).push(r);
|
|
100
|
+
const months = Object.keys(byMonth).sort();
|
|
101
|
+
const verdictMap = new Map();
|
|
102
|
+
const monthOkMap = new Map();
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const ym of months) {
|
|
105
|
+
const m = agg.metrics(byMonth[ym]);
|
|
106
|
+
const { items, monthOk } = agg.gateMonth(m);
|
|
107
|
+
monthOkMap.set(ym, monthOk);
|
|
108
|
+
const [y, mo] = ym.split('-').map(Number);
|
|
109
|
+
const pd = new Date(Date.UTC(y, mo - 2, 1));
|
|
110
|
+
const prev = `${pd.getUTCFullYear()}-${String(pd.getUTCMonth() + 1).padStart(2, '0')}`;
|
|
111
|
+
const verdict = agg.expansionVerdict(monthOk, monthOkMap.get(prev), !months.includes(prev), items);
|
|
112
|
+
verdictMap.set(ym, verdict);
|
|
113
|
+
const pct = (x) => (Number.isFinite(x) ? `${Math.round(x * 100)}%` : '—');
|
|
114
|
+
const avg = Number.isFinite(m.avgRework) ? m.avgRework.toFixed(2) : '—';
|
|
115
|
+
out.push(`| ${ym} | ${m.total} | ${pct(m.passRate)} | ${avg} | ${pct(m.fallbackRate)} | ${verdict} |`);
|
|
116
|
+
}
|
|
117
|
+
return { rows: out, verdictMap, months };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const USAGE = '用法:node .agents/scripts/gen-workflow-dashboard.mjs [--dry-run]';
|
|
121
|
+
|
|
122
|
+
function main() {
|
|
123
|
+
const dry = process.argv.includes('--dry-run');
|
|
124
|
+
const intents = collectDoneIntents(ROOT);
|
|
125
|
+
const doneMap = collectDoneMap(ROOT);
|
|
126
|
+
const { days, skipped } = closeDurations(intents, doneMap);
|
|
127
|
+
const nums = days.map((d) => d.days);
|
|
128
|
+
const p50 = pickPct(nums, 0.5);
|
|
129
|
+
const p90 = pickPct(nums, 0.9);
|
|
130
|
+
const mx = nums.length ? nums[nums.length - 1] : null;
|
|
131
|
+
const dv = durationVerdict(p50, p90);
|
|
132
|
+
const q = qualityRows(ROOT);
|
|
133
|
+
const curYm = q.months[q.months.length - 1];
|
|
134
|
+
const qVerdict = curYm ? q.verdictMap.get(curYm) : '⚪ 台账暂无数据(delegations.md 无结果表行)';
|
|
135
|
+
|
|
136
|
+
const slowest = days.slice(-3).reverse().map((d) => `${d.rel.split('/').pop()}=${d.days}天`).join('、') || '—';
|
|
137
|
+
const stat = (x) => (x === null ? '—' : `${x} 天`);
|
|
138
|
+
const lines = [
|
|
139
|
+
'# workflow 仪表盘(生成物,勿手改)',
|
|
140
|
+
'',
|
|
141
|
+
`> 生成器 \`node .agents/scripts/gen-workflow-dashboard.mjs\`;生成于 ${new Date().toISOString()}`,
|
|
142
|
+
'> 观测面非门禁:红灯提示看,不拦提交;想看就跑一行命令(与 INDEX.md「状态变更后重跑」同模式)。',
|
|
143
|
+
'',
|
|
144
|
+
'## 红绿灯',
|
|
145
|
+
'',
|
|
146
|
+
`- ${qVerdict}|质量门(门槛权威 = workflow/delegations.md §并发扩容门槛:一次通过率 ≥90%、月度返工 0、无主兜底、样本 ≥5)${curYm ? `——当月 ${curYm}` : ''}`,
|
|
147
|
+
`- ${dv.text}|关单时长带(P50 ≤ ${DURATION_BAND.p50Max} 天 且 P90 ≤ ${DURATION_BAND.p90Max} 天):P50=${stat(p50)} / P90=${stat(p90)} / max=${stat(mx)}(样本 ${nums.length},无台账行跳过 ${skipped})`,
|
|
148
|
+
'',
|
|
149
|
+
'## 关单时长(done intents:立项日 → 台账最早 done 行)',
|
|
150
|
+
'',
|
|
151
|
+
'| 指标 | 值 |',
|
|
152
|
+
'|------|----|',
|
|
153
|
+
`| P50 | ${stat(p50)} |`,
|
|
154
|
+
`| P90 | ${stat(p90)} |`,
|
|
155
|
+
`| max | ${stat(mx)} |`,
|
|
156
|
+
`| 样本数 | ${nums.length} |`,
|
|
157
|
+
`| 无台账行跳过(存量单) | ${skipped} |`,
|
|
158
|
+
'',
|
|
159
|
+
`最慢 3 单:${slowest}`,
|
|
160
|
+
'',
|
|
161
|
+
'## 质量 / 返工(月度,判据复用 agg-delegations)',
|
|
162
|
+
'',
|
|
163
|
+
'| 月份 | 有效任务 | 一次通过率 | 平均返工 | 主兜底占比 | 扩容门判定 |',
|
|
164
|
+
'|------|----------|------------|----------|------------|------------|',
|
|
165
|
+
...(q.rows.length ? q.rows : ['| — | — | — | — | — | 台账暂无数据 |']),
|
|
166
|
+
'',
|
|
167
|
+
];
|
|
168
|
+
const out = lines.join('\n');
|
|
169
|
+
if (dry) {
|
|
170
|
+
console.log(out);
|
|
171
|
+
console.log(`(--dry-run:未写盘。${USAGE})`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const target = path.join(ROOT, 'workflow', 'DASHBOARD.md');
|
|
175
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
176
|
+
fs.writeFileSync(target, out, 'utf8');
|
|
177
|
+
console.log(`✅ 已生成 ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'}`);
|
|
178
|
+
console.log(` 红绿灯:${qVerdict.split('|')[0].trim()} 质量门 | ${dv.text} 关单时长带(P50=${stat(p50)} / P90=${stat(p90)},样本 ${nums.length})`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('gen-workflow-dashboard.mjs');
|
|
182
|
+
if (isMain) main();
|
|
@@ -0,0 +1,126 @@
|
|
|
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 } =
|
|
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
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
126
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -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
|