agentic-flow-kit 1.3.0 → 1.5.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/agents/implementer.md +5 -6
- package/modules/hosts/claude/agents/independent-reviewer.md +6 -6
- package/modules/hosts/claude/agents/ui-verifier.md +5 -5
- package/modules/hosts/claude/commands/wf-build.md +21 -21
- package/modules/hosts/claude/commands/wf-deploy.md +2 -2
- package/modules/hosts/claude/commands/wf-design.md +8 -8
- package/modules/hosts/claude/commands/wf-gate-checklist.md +8 -8
- package/modules/hosts/claude/commands/wf-maintain.md +12 -12
- package/modules/hosts/claude/commands/wf-new-task.md +9 -9
- package/modules/hosts/claude/commands/wf-pipeline-run.md +3 -3
- package/modules/hosts/claude/commands/wf-plan.md +10 -11
- package/modules/hosts/claude/commands/wf-review.md +4 -4
- package/modules/hosts/claude/commands/wf-source-sync-check.md +10 -10
- package/modules/hosts/claude/commands/wf-sync-hosts.md +9 -9
- package/modules/hosts/claude/commands/wf-test.md +6 -7
- package/modules/hosts/codex/agents/implementer.md +5 -6
- package/modules/hosts/codex/agents/independent-reviewer.md +6 -6
- package/modules/hosts/codex/agents/ui-verifier.md +5 -5
- package/modules/hosts/codex/commands/wf-build.md +21 -21
- package/modules/hosts/codex/commands/wf-deploy.md +2 -2
- package/modules/hosts/codex/commands/wf-design.md +8 -8
- package/modules/hosts/codex/commands/wf-gate-checklist.md +8 -8
- package/modules/hosts/codex/commands/wf-maintain.md +12 -12
- package/modules/hosts/codex/commands/wf-new-task.md +9 -9
- package/modules/hosts/codex/commands/wf-pipeline-run.md +3 -3
- package/modules/hosts/codex/commands/wf-plan.md +10 -11
- package/modules/hosts/codex/commands/wf-review.md +4 -4
- package/modules/hosts/codex/commands/wf-source-sync-check.md +10 -10
- package/modules/hosts/codex/commands/wf-sync-hosts.md +9 -9
- package/modules/hosts/codex/commands/wf-test.md +6 -7
- package/modules/hosts/cursor/agents/implementer.md +5 -6
- package/modules/hosts/cursor/agents/independent-reviewer.md +6 -6
- package/modules/hosts/cursor/agents/ui-verifier.md +5 -5
- package/modules/hosts/cursor/commands/wf-build.md +21 -21
- package/modules/hosts/cursor/commands/wf-deploy.md +2 -2
- package/modules/hosts/cursor/commands/wf-design.md +8 -8
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +8 -8
- package/modules/hosts/cursor/commands/wf-maintain.md +12 -12
- package/modules/hosts/cursor/commands/wf-new-task.md +9 -9
- package/modules/hosts/cursor/commands/wf-pipeline-run.md +3 -3
- package/modules/hosts/cursor/commands/wf-plan.md +10 -11
- package/modules/hosts/cursor/commands/wf-review.md +4 -4
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +10 -10
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +9 -9
- package/modules/hosts/cursor/commands/wf-test.md +6 -7
- package/modules/hosts/omp/agents/implementer.md +5 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +6 -6
- package/modules/hosts/omp/agents/ui-verifier.md +5 -5
- package/modules/hosts/opencode/agents/implementer.md +5 -6
- package/modules/hosts/opencode/agents/independent-reviewer.md +6 -6
- package/modules/hosts/opencode/agents/ui-verifier.md +5 -5
- package/modules/hosts/opencode/commands/wf-build.md +21 -21
- package/modules/hosts/opencode/commands/wf-deploy.md +2 -2
- package/modules/hosts/opencode/commands/wf-design.md +8 -8
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +8 -8
- package/modules/hosts/opencode/commands/wf-maintain.md +12 -12
- package/modules/hosts/opencode/commands/wf-new-task.md +9 -9
- package/modules/hosts/opencode/commands/wf-pipeline-run.md +3 -3
- package/modules/hosts/opencode/commands/wf-plan.md +10 -11
- package/modules/hosts/opencode/commands/wf-review.md +4 -4
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +10 -10
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +9 -9
- package/modules/hosts/opencode/commands/wf-test.md +6 -7
- package/modules/hosts/trae/agents/implementer.md +5 -6
- package/modules/hosts/trae/agents/independent-reviewer.md +6 -6
- package/modules/hosts/trae/agents/ui-verifier.md +5 -5
- package/modules/hosts/trae/commands/wf-build.md +21 -21
- package/modules/hosts/trae/commands/wf-deploy.md +2 -2
- package/modules/hosts/trae/commands/wf-design.md +8 -8
- package/modules/hosts/trae/commands/wf-gate-checklist.md +8 -8
- package/modules/hosts/trae/commands/wf-maintain.md +12 -12
- package/modules/hosts/trae/commands/wf-new-task.md +9 -9
- package/modules/hosts/trae/commands/wf-pipeline-run.md +3 -3
- package/modules/hosts/trae/commands/wf-plan.md +10 -11
- package/modules/hosts/trae/commands/wf-review.md +4 -4
- package/modules/hosts/trae/commands/wf-source-sync-check.md +10 -10
- package/modules/hosts/trae/commands/wf-sync-hosts.md +9 -9
- package/modules/hosts/trae/commands/wf-test.md +6 -7
- package/modules/hosts/zcode/agents/implementer.md +5 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +6 -6
- package/modules/hosts/zcode/agents/ui-verifier.md +5 -5
- package/package.json +7 -2
- package/src/doctor.mjs +1 -1
- package/src/init.test.mjs +1 -1
- package/src/run-tests.mjs +15 -1
- package/src/sync-hosts.test.mjs +4 -4
- package/src/sync.test.mjs +1 -1
- package/templates/_agents/commands/build.md +21 -21
- package/templates/_agents/commands/deploy.md +2 -2
- package/templates/_agents/commands/design.md +8 -8
- package/templates/_agents/commands/gate-checklist.md +8 -8
- package/templates/_agents/commands/maintain.md +12 -12
- package/templates/_agents/commands/new-task.md +9 -9
- package/templates/_agents/commands/pipeline-run.md +3 -3
- package/templates/_agents/commands/plan.md +10 -11
- package/templates/_agents/commands/review.md +4 -4
- package/templates/_agents/commands/source-sync-check.md +10 -10
- package/templates/_agents/commands/sync-hosts.md +9 -9
- package/templates/_agents/commands/test.md +6 -7
- package/templates/_agents/roles/implementer.md +5 -6
- package/templates/_agents/roles/independent-reviewer.md +6 -6
- package/templates/_agents/roles/ui-verifier.md +5 -5
- package/templates/_agents/scripts/agg-delegations.cjs +41 -13
- package/templates/_agents/scripts/agg-delegations.test.mjs +78 -3
- 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-invariant.mjs +0 -1
- package/templates/_agents/scripts/check-loop.mjs +112 -117
- package/templates/_agents/scripts/check-loop.test.mjs +115 -1
- package/templates/_agents/scripts/commit-check-trigger.test.mjs +0 -1
- package/templates/_agents/scripts/confirm-doc.mjs +27 -7
- package/templates/_agents/scripts/confirm-doc.test.mjs +35 -0
- package/templates/_agents/scripts/ensure-board.mjs +2 -2
- package/templates/_agents/scripts/fill-intent.mjs +2 -2
- package/templates/_agents/scripts/fill-plan.mjs +1 -2
- package/templates/_agents/scripts/gate-seg.mjs +54 -0
- package/templates/_agents/scripts/gate-seg.test.mjs +112 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +1 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +76 -9
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +64 -3
- package/templates/_agents/scripts/gen-workflow-index.test.mjs +1 -1
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +1 -1
- package/templates/_agents/scripts/kb-search.mjs +1 -2
- package/templates/_agents/scripts/kb-search.test.mjs +1 -1
- package/templates/_agents/scripts/pipeline-run-docs.mjs +76 -0
- package/templates/_agents/scripts/pipeline-run-run.mjs +63 -0
- package/templates/_agents/scripts/pipeline-run.mjs +8 -128
- package/templates/_agents/scripts/policy.mjs +37 -3
- package/templates/_agents/scripts/source-sync-check.mjs +2 -2
- package/templates/_agents/scripts/source-sync-check.test.mjs +1 -1
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +0 -1
- package/templates/_agents/scripts/verify.mjs +27 -2
- package/templates/_agents/scripts/verify.test.mjs +53 -0
- package/templates/_agents/scripts/workflow-board-server.test.mjs +0 -1
- package/templates/_agents/scripts/workflow-enums.test.mjs +0 -1
- package/templates/_agents/scripts-test-exempt.txt +4 -1
|
@@ -736,5 +736,40 @@ const writeTrust = (root, cfg) => {
|
|
|
736
736
|
fs.rmSync(root, { recursive: true, force: true });
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
// ---- S40 测试绿凭证按 doc 绑定(2026-10-08 verify-doc-binding)----
|
|
740
|
+
// 核心防线三条:① v6 + 只有**他单**绿行 → advisory 出账(漏洞已堵);② v6 + 本单绿行 → 静默;
|
|
741
|
+
// ③ **v5 + 只有他单绿行 → 静默**(向后兼容反例——收紧只许对 v6+ 生效,存量装户零变化)。
|
|
742
|
+
// 文案锚点:既有 S38/S39 锚定「无测试绿凭证」六字连续 + 「verify.mjs」出现,本组不得破坏。
|
|
743
|
+
{
|
|
744
|
+
const mkVerifyRoot = (pv, lines) => {
|
|
745
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-docbind-'));
|
|
746
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
747
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
748
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
749
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: pv }), 'utf8');
|
|
750
|
+
fs.writeFileSync(path.join(root, '.agents', 'verifications.jsonl'), lines, 'utf8');
|
|
751
|
+
fs.writeFileSync(path.join(docP, '2026-10-08-m.md'),
|
|
752
|
+
'---\n状态: approved\n级别: L1\n确认指纹: cccc3333cccc3333\n---\n# I\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n', 'utf8');
|
|
753
|
+
return root;
|
|
754
|
+
};
|
|
755
|
+
const green = (doc) => `${JSON.stringify({ ts: new Date().toISOString(), exitCode: 0, suite: 'npm test', passed: 1, failed: 0, ...(doc ? { doc } : {}) })}\n`;
|
|
756
|
+
const MINE = 'workflow/intents/2026-10-08-m.md';
|
|
757
|
+
const OTHER = 'workflow/intents/2026-10-08-other.md';
|
|
758
|
+
const runM = (root) => spawnSync(process.execPath, [CLI, MINE, '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
759
|
+
|
|
760
|
+
const r40 = runM(mkVerifyRoot(6, green(OTHER)));
|
|
761
|
+
check('S40① v6 + 只有他单绿行 → advisory 出账(跨单背书漏洞已堵)+ 文案带 --doc',
|
|
762
|
+
r40.status === 0 && /无测试绿凭证/.test(r40.stderr) && /--doc/.test(r40.stderr), String(r40.stderr).slice(0, 200));
|
|
763
|
+
const r41 = runM(mkVerifyRoot(6, green(MINE)));
|
|
764
|
+
check('S40② v6 + 本单绿行 → 静默(绑定成立)',
|
|
765
|
+
r41.status === 0 && !/无测试绿凭证/.test(r41.stderr), String(r41.stderr).slice(0, 200));
|
|
766
|
+
const r42 = runM(mkVerifyRoot(5, green(OTHER)));
|
|
767
|
+
check('S40③ v5 + 只有他单绿行 → **静默**(兼容反例:收紧只对 v6+ 生效,存量装户零变化)',
|
|
768
|
+
r42.status === 0 && !/无测试绿凭证/.test(r42.stderr), String(r42.stderr).slice(0, 200));
|
|
769
|
+
const r43 = runM(mkVerifyRoot(6, green(null)));
|
|
770
|
+
check('S40④ v6 + 不带 doc 的绿行(CI 场景)→ 不给本单背书,advisory 出账',
|
|
771
|
+
r43.status === 0 && /无测试绿凭证/.test(r43.stderr), String(r43.stderr).slice(0, 200));
|
|
772
|
+
}
|
|
773
|
+
|
|
739
774
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
740
775
|
process.exit(fail ? 1 : 0);
|
|
@@ -105,7 +105,7 @@ function openBrowser(url) {
|
|
|
105
105
|
try {
|
|
106
106
|
if (process.platform === 'win32') spawn('cmd', ['/c', 'start', '', url], { detached: true, stdio: 'ignore', windowsHide: true }).unref();
|
|
107
107
|
else spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { detached: true, stdio: 'ignore' }).unref();
|
|
108
|
-
} catch { }
|
|
108
|
+
} catch { /* 浏览器拉起失败不阻断看板流程 */ }
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// ---- 主流程(返回退出码,自然排水退出——不用 process.exit:强退在 Windows 上会触发 libuv 断言)----
|
|
@@ -123,7 +123,7 @@ async function main() {
|
|
|
123
123
|
board = b;
|
|
124
124
|
const stale = staleCode(b.startedAt);
|
|
125
125
|
if (stale === true) {
|
|
126
|
-
try { process.kill(b.pid); } catch {
|
|
126
|
+
try { process.kill(b.pid); } catch { /* 进程可能已自行退出 */ }
|
|
127
127
|
await sleep(500);
|
|
128
128
|
action = 'restarted (stale code detected)';
|
|
129
129
|
} else if (stale === null) {
|
|
@@ -31,8 +31,8 @@ const SECTIONS = [
|
|
|
31
31
|
{ title: '目标', hint: '- <可验证的目标>' },
|
|
32
32
|
{ title: '非目标(L1 微改动无实质内容可删本节,不硬填)', hint: '- <明确不做的,防止范围蔓延>' },
|
|
33
33
|
{ title: '约束(L1 微改动无实质内容可删本节,不硬填)', hint: '- <复用什么、不许动什么>' },
|
|
34
|
-
{ title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更
|
|
35
|
-
{ title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 或 Quick-Plan 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 /
|
|
34
|
+
{ title: '影响面', hint: '- 模块:<词表之一,见 .agents/workflow-modules.txt>\n- 数据库:无 / <表名>(涉及 schema 变更 → L3;无 schema 的运行时 / 管线结构面亦为 L3,判据见 .agents/commands/new-task.md §级别判断)\n- 前端页面:<路由或组件,无则删本行>' },
|
|
35
|
+
{ title: '触达红线(对照 AGENTS.md,勾选仅标记触及范围;具体如何满足在同名 spec 或 Quick-Plan 中说明)', hint: '- [ ] <按本项目 AGENTS.md 红线逐行补;无则删本行>\n- [ ] 规则 / 契约变更(编码权威 / 共享契约 / 接口签名 / 既有参数语义 / 全局口径)→ 级别至少 L2\n- [ ] schema / 迁移 SQL / DI 链 / 认证与中间件管线 → 级别 L3\n\n> 触及任一红线即为 L2 / L3,必须立 ../specs/YYYY-MM-DD-<主题>.md 写明如何满足,方可起草 plan。' },
|
|
36
36
|
{ title: '验收标准(可测试)', hint: '- [ ] <逐条可测试;写不出可测试判据 = 还没想清楚>\n\n> **闭环对账**:关单在 test 阶段(不依赖 deploy)。intent 置 done 前逐条勾验,每条补证据——`- [x] <判据>(证据:<commit SHA / 测试用例名 / 冒烟脚本输出>)`。\n> done 状态仍有未勾项会被 check-loop 拦截(2026-09-12 起新建 intent 为 hard-block,存量 intent 仅 warning 提示);勾选但缺「证据:」为 hard-block。' },
|
|
37
37
|
];
|
|
38
38
|
|
|
@@ -9,7 +9,6 @@ import path from 'node:path';
|
|
|
9
9
|
import { ENUMS } from './workflow-enums.mjs';
|
|
10
10
|
import { draftGateFor, resolveWorkspace } from './stage-gates.mjs';
|
|
11
11
|
|
|
12
|
-
const TODAY = new Date().toISOString().slice(0, 10);
|
|
13
12
|
const LEVELS = ENUMS['level.all']; // 级别词表单源(.agents/workflow-enums.txt)
|
|
14
13
|
|
|
15
14
|
function parseArgs(argv) {
|
|
@@ -40,7 +39,7 @@ const FULL_SECTIONS = [
|
|
|
40
39
|
L1_SECTIONS[2],
|
|
41
40
|
];
|
|
42
41
|
|
|
43
|
-
export function renderPlan({ topic, level = 'L1'
|
|
42
|
+
export function renderPlan({ topic, level = 'L1' }) {
|
|
44
43
|
if (!LEVELS.includes(level)) throw new Error('level 不在枚举 ' + LEVELS.join('|') + ':' + level);
|
|
45
44
|
const sections = level === 'L1' ? L1_SECTIONS : FULL_SECTIONS;
|
|
46
45
|
const fm = '---\n状态: draft\n级别: ' + level + '\n模块: pipeline\n---\n';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// gate-seg.mjs — 门禁 ROI 段插桩共用件(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 为什么独立成件:check-loop.mjs 与 check-hygiene.mjs **互相不能反向 import**(前者已 import 后者,
|
|
3
|
+
// 后者再 import 前者即循环依赖)——而两者都要插桩。判据单源必须只有一处,故抽本件。
|
|
4
|
+
//
|
|
5
|
+
// 机制:每次 gateSeg() 调用先**结算上一段**(段内 warnings/blockers 增量条数 + 耗时),再记本段起点。
|
|
6
|
+
// 统计完全由数组长度差推出——**不改任何 warnings.push / blockers.push 调用点**,判定逻辑与文案零改动。
|
|
7
|
+
// 默认路径(无 --gate-stats)下 segs 只在内存里攒、不打印不写盘 → 输出契约逐字节不变(硬约束:
|
|
8
|
+
// pre-push / pre-commit 消费者 + doctor 的 WARN 行计数 + check-loop.test.mjs 223 断言全依赖它)。
|
|
9
|
+
//
|
|
10
|
+
// 耗时口径**刻意包含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 sh rule-budget.sh)——
|
|
11
|
+
// 门禁真实成本本就要含子进程;不做剔除,但看板与 spec 均显式声明该口径防误读。
|
|
12
|
+
export function gateSeg(collector, id, label) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const prev = collector.current;
|
|
15
|
+
if (prev) {
|
|
16
|
+
collector.segs.push({
|
|
17
|
+
id: prev.id,
|
|
18
|
+
label: prev.label,
|
|
19
|
+
warns: collector.warnings.length - prev.w,
|
|
20
|
+
blocks: collector.blockers.length - prev.b,
|
|
21
|
+
ms: now - prev.t,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
collector.current = { id, label, w: collector.warnings.length, b: collector.blockers.length, t: now };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// finishSegs:结算末段(末段之后没有下一次 gateSeg 调用,须显式收口,否则最后一段不进账)
|
|
28
|
+
export function finishSegs(collector) {
|
|
29
|
+
if (!collector.current) return collector.segs; // 已结算过(幂等——writeGateStats 与输出段各调一次不会重复入账)
|
|
30
|
+
gateSeg(collector, '__end__', '__end__'); // 本次调用结算上一段;记下的 current 置空防重复结算
|
|
31
|
+
collector.current = null;
|
|
32
|
+
return collector.segs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// makeCollector:建收集器(引用同一对数组,不复制数据)
|
|
36
|
+
export function makeCollector(warnings, blockers) {
|
|
37
|
+
return { warnings, blockers, segs: [], current: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// appendSegs:并入「在别处结算出的段」,供模块化门禁使用(2026-10-08 gate-roi-metrics 修复)。
|
|
41
|
+
// 背景:check-hygiene.mjs 产出的是**局部** warnings 数组(由 check-loop 事后 spread 进全局),
|
|
42
|
+
// 故它的增量在模块执行期间**不反映**到 check-loop 的数组上。若直接共用游标插桩,后果是确定的错账:
|
|
43
|
+
// 本模块 5 段恒记 0 条命中,而它的全部增量会被误记到模块之后的第一个段(检查 3)头上——
|
|
44
|
+
// 实测已抓到:某次 hygiene 产出 1 条警告被记成「检查 13 命中」。
|
|
45
|
+
// 语义:先结算本收集器当前段(模块前的检查,如检查 1)→ 按序并入外来段 → 游标清空,
|
|
46
|
+
// 下一个 gateSeg 从零起算,Δ 天然正确。
|
|
47
|
+
// 空 segs 直接返回且**不结算**当前段:没有东西可并入时不该改变原有结算时序。
|
|
48
|
+
export function appendSegs(collector, segs) {
|
|
49
|
+
if (!segs || !segs.length) return collector.segs;
|
|
50
|
+
finishSegs(collector);
|
|
51
|
+
collector.segs.push(...segs);
|
|
52
|
+
collector.current = null;
|
|
53
|
+
return collector.segs;
|
|
54
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// gate-seg.test.mjs — 门禁 ROI 段插桩共用件测试(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 被测:gateSeg 结算语义 / finishSegs 幂等 / makeCollector 引用同一对数组。
|
|
3
|
+
// 判据单源所在:check-loop.mjs 与 check-hygiene.mjs 都靠本件插桩,故它的正确性是全部门禁成本度量的地基——
|
|
4
|
+
// 段归属算错则整张 ROI 表失真而无人察觉。端到端回归网仍是 check-loop.test.mjs(223 断言)。
|
|
5
|
+
// 用法:node templates/_agents/scripts/gate-seg.test.mjs
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const { gateSeg, finishSegs, makeCollector, appendSegs } =
|
|
11
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'gate-seg.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
|
+
// ---- makeCollector:引用同一对数组(不复制)----
|
|
20
|
+
{
|
|
21
|
+
const w = [], b = [];
|
|
22
|
+
const c = makeCollector(w, b);
|
|
23
|
+
check('makeCollector:引用同一对数组(push 即被看见,无复制)', c.warnings === w && c.blockers === b);
|
|
24
|
+
check('makeCollector:初始 segs 为空、current 为空', Array.isArray(c.segs) && c.segs.length === 0 && c.current === null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- gateSeg:每次调用结算上一段,首调不结算 ----
|
|
28
|
+
{
|
|
29
|
+
const w = [], b = [];
|
|
30
|
+
const c = makeCollector(w, b);
|
|
31
|
+
gateSeg(c, '1', '检查一');
|
|
32
|
+
check('gateSeg:首次调用只记起点不结算(segs 仍空)', c.segs.length === 0 && c.current.id === '1');
|
|
33
|
+
w.push('a'); b.push('b'); b.push('c'); // 段一产出 1 warn + 2 blocks
|
|
34
|
+
gateSeg(c, '2', '检查二');
|
|
35
|
+
check('gateSeg:第二次调用结算上一段(warns=1 blocks=2,id/label 对)',
|
|
36
|
+
c.segs.length === 1 && c.segs[0].id === '1' && c.segs[0].label === '检查一'
|
|
37
|
+
&& c.segs[0].warns === 1 && c.segs[0].blocks === 2, JSON.stringify(c.segs));
|
|
38
|
+
check('gateSeg:结算后 current 切到新段', c.current.id === '2');
|
|
39
|
+
// 零命中段也要入账(死检查识别依赖「声明了但从未命中」——漏记即误判为未声明)
|
|
40
|
+
gateSeg(c, '3', '检查三');
|
|
41
|
+
check('gateSeg:零命中段照样入账(warns=0 blocks=0)',
|
|
42
|
+
c.segs.length === 2 && c.segs[1].id === '2' && c.segs[1].warns === 0 && c.segs[1].blocks === 0, JSON.stringify(c.segs));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- finishSegs:结算末段 + 幂等(writeGateStats 与输出段各调一次不得重复入账)----
|
|
46
|
+
{
|
|
47
|
+
const w = [], b = [];
|
|
48
|
+
const c = makeCollector(w, b);
|
|
49
|
+
gateSeg(c, '1', '检查一');
|
|
50
|
+
w.push('x');
|
|
51
|
+
const segs1 = finishSegs(c);
|
|
52
|
+
check('finishSegs:结算末段(末段后无下次调用否则该段丢失)',
|
|
53
|
+
segs1.length === 1 && segs1[0].id === '1' && segs1[0].warns === 1, JSON.stringify(segs1));
|
|
54
|
+
const segs2 = finishSegs(c);
|
|
55
|
+
check('finishSegs:幂等——二次调用不重复入账(防 __end__ 伪段污染表)',
|
|
56
|
+
segs2.length === 1 && !segs2.some((s) => s.id === '__end__'), JSON.stringify(segs2));
|
|
57
|
+
check('finishSegs:结算后 current 置空', c.current === null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- ms 单调不减(耗时度量地基)----
|
|
61
|
+
{
|
|
62
|
+
const c = makeCollector([], []);
|
|
63
|
+
gateSeg(c, '1', 'a');
|
|
64
|
+
const t = Date.now(); while (Date.now() - t < 5) { /* 占位:保证有可测间隔 */ }
|
|
65
|
+
gateSeg(c, '2', 'b');
|
|
66
|
+
check('耗时字段:ms 为非负整数且单调(门禁成本度量的地基)',
|
|
67
|
+
Number.isInteger(c.segs[0].ms) && c.segs[0].ms >= 0, JSON.stringify(c.segs[0]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- appendSegs:模块化门禁的段归属(本次实抓到错账的根因,钉死)----
|
|
71
|
+
// 症状:check-hygiene.mjs 的增量不反映到 check-loop 的数组上,直接共用游标会让 hygiene 各段恒记 0,
|
|
72
|
+
// 而它的全部增量被误记到模块之后的第一段(检查 3)。下面是该场景的最小复现与修复后行为。
|
|
73
|
+
{
|
|
74
|
+
const globalW = [], globalB = [];
|
|
75
|
+
const g = makeCollector(globalW, globalB);
|
|
76
|
+
gateSeg(g, '1', '检查1'); // 模块前的一段
|
|
77
|
+
globalW.push('w-check1'); // 检查 1 产出 1 条
|
|
78
|
+
|
|
79
|
+
// 模块(局部数组):检查 2 → 检查 11 各产出 1 条
|
|
80
|
+
const localW = [];
|
|
81
|
+
const l = makeCollector(localW, []);
|
|
82
|
+
gateSeg(l, '2', '检查2');
|
|
83
|
+
localW.push('w-check2');
|
|
84
|
+
gateSeg(l, '11', '检查11');
|
|
85
|
+
localW.push('w-check11');
|
|
86
|
+
|
|
87
|
+
appendSegs(g, finishSegs(l));
|
|
88
|
+
const order = g.segs.map((s) => s.id);
|
|
89
|
+
const byId = new Map(g.segs.map((s) => [s.id, s.warns]));
|
|
90
|
+
check('appendSegs:模块后的一段先结算(检查 1 不被外来段吞掉)',
|
|
91
|
+
order[0] === '1' && byId.get('1') === 1, JSON.stringify(g.segs));
|
|
92
|
+
check('appendSegs:外来段按序并入且各自归属正确(2 → 1 条、11 → 1 条,**不都记到后一段**)',
|
|
93
|
+
order.slice(1).join(',') === '2,11' && byId.get('2') === 1 && byId.get('11') === 1, JSON.stringify(g.segs));
|
|
94
|
+
check('appendSegs:并入后游标清空,下一段从零起算(不把外来增量算进模块之后的段)',
|
|
95
|
+
g.current === null, JSON.stringify(g.current));
|
|
96
|
+
gateSeg(g, '3', '检查3');
|
|
97
|
+
globalW.push(...['w-check3a', 'w-check3b']);
|
|
98
|
+
gateSeg(g, '4', '检查4');
|
|
99
|
+
check('appendSegs:模块之后的第一段只记自己的增量(检查 3 = 2 条,不含 hygiene 的 2 条)',
|
|
100
|
+
g.segs.find((s) => s.id === '3').warns === 2, JSON.stringify(g.segs));
|
|
101
|
+
|
|
102
|
+
// 空外来段:既不并入也不提前结算(不改变原有结算时序)
|
|
103
|
+
const w2 = [], b2 = [];
|
|
104
|
+
const g2 = makeCollector(w2, b2);
|
|
105
|
+
gateSeg(g2, '1', 'a');
|
|
106
|
+
appendSegs(g2, []);
|
|
107
|
+
check('appendSegs:空外来段 → 收集器原样不动(不结算、不并入,结算时序不变)',
|
|
108
|
+
g2.segs.length === 0 && g2.current !== null, JSON.stringify(g2));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
112
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -141,7 +141,7 @@ let out = indexText;
|
|
|
141
141
|
{
|
|
142
142
|
const lines = out.split(/\r?\n/);
|
|
143
143
|
const headIdx = lines.findIndex((l) => /^\| 主题 \|/.test(l));
|
|
144
|
-
if (headIdx < 0 || !/^[
|
|
144
|
+
if (headIdx < 0 || !/^[|:\-\s]+$/.test(lines[headIdx + 1] || '')) {
|
|
145
145
|
console.error('❌ INDEX 速览表表头未找到,未写盘');
|
|
146
146
|
process.exit(1);
|
|
147
147
|
}
|
|
@@ -7,8 +7,13 @@
|
|
|
7
7
|
// (多次 done 取首关;无台账行的存量单诚实跳过不虚构)。真实分布基准(2026-10-08 本仓 43 单):P50=0 / P90=7 / max=8。
|
|
8
8
|
// 复用:require('./agg-delegations.cjs') 纯函数(readLedger / metrics / gateMonth / expansionVerdict)——
|
|
9
9
|
// 质量判据零重复(N3 教训);readLedger 的 root 显式参数 + soft 模式为本批使能导出(原签名行为不变)。
|
|
10
|
-
// isMain 守卫 + 纯函数导出(board-kb-p1 先例):closeDurations / pickPct / durationVerdict 供测试 import 直测。
|
|
11
|
-
// 用法:node .agents/scripts/gen-workflow-dashboard.mjs [--dry-run]
|
|
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 教训)。
|
|
12
17
|
// 测试:node templates/_agents/scripts/gen-workflow-dashboard.test.mjs
|
|
13
18
|
import fs from 'node:fs';
|
|
14
19
|
import path from 'node:path';
|
|
@@ -112,15 +117,52 @@ export function qualityRows(root) {
|
|
|
112
117
|
verdictMap.set(ym, verdict);
|
|
113
118
|
const pct = (x) => (Number.isFinite(x) ? `${Math.round(x * 100)}%` : '—');
|
|
114
119
|
const avg = Number.isFinite(m.avgRework) ? m.avgRework.toFixed(2) : '—';
|
|
115
|
-
|
|
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} |`);
|
|
116
123
|
}
|
|
117
124
|
return { rows: out, verdictMap, months };
|
|
118
125
|
}
|
|
119
126
|
|
|
120
|
-
|
|
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
|
+
}
|
|
121
161
|
|
|
122
162
|
function main() {
|
|
123
|
-
const
|
|
163
|
+
const args = process.argv.slice(2);
|
|
164
|
+
const dry = args.includes('--dry-run');
|
|
165
|
+
const check = args.includes('--check');
|
|
124
166
|
const intents = collectDoneIntents(ROOT);
|
|
125
167
|
const doneMap = collectDoneMap(ROOT);
|
|
126
168
|
const { days, skipped } = closeDurations(intents, doneMap);
|
|
@@ -160,18 +202,43 @@ function main() {
|
|
|
160
202
|
'',
|
|
161
203
|
'## 质量 / 返工(月度,判据复用 agg-delegations)',
|
|
162
204
|
'',
|
|
163
|
-
'| 月份 | 有效任务 | 一次通过率 | 平均返工 | 主兜底占比 | 扩容门判定 |',
|
|
164
|
-
'
|
|
165
|
-
...(q.rows.length ? q.rows : ['| — | — | — | — | — | 台账暂无数据 |']),
|
|
205
|
+
'| 月份 | 有效任务 | 一次通过率 | 设计返工 | 门禁噪声返工 | 平均返工 | 主兜底占比 | 扩容门判定 |',
|
|
206
|
+
'|------|----------|------------|---------|-------------|--------|------------|------------|',
|
|
207
|
+
...(q.rows.length ? q.rows : ['| — | — | — | — | — | — | — | 台账暂无数据 |']),
|
|
208
|
+
'',
|
|
209
|
+
'## 门禁 ROI(门禁自身的度量)',
|
|
210
|
+
'',
|
|
211
|
+
...gateRoiLines(),
|
|
166
212
|
'',
|
|
167
213
|
];
|
|
168
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
|
+
}
|
|
169
237
|
if (dry) {
|
|
170
238
|
console.log(out);
|
|
171
239
|
console.log(`(--dry-run:未写盘。${USAGE})`);
|
|
172
240
|
return;
|
|
173
241
|
}
|
|
174
|
-
const target = path.join(ROOT, 'workflow', 'DASHBOARD.md');
|
|
175
242
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
176
243
|
fs.writeFileSync(target, out, 'utf8');
|
|
177
244
|
console.log(`✅ 已生成 ${path.relative(ROOT, target) || 'workflow/DASHBOARD.md'}`);
|
|
@@ -10,10 +10,8 @@ import { spawnSync } from 'node:child_process';
|
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
11
11
|
|
|
12
12
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
-
const { closeDurations, pickPct, durationVerdict, qualityRows, DURATION_BAND } =
|
|
13
|
+
const { closeDurations, pickPct, durationVerdict, qualityRows, DURATION_BAND, normalizeForCheck, gateRoiLines } =
|
|
14
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
15
|
import { createRequire } from 'node:module';
|
|
18
16
|
const require2 = createRequire(import.meta.url);
|
|
19
17
|
const aggCjs = require2(path.join(SCRIPT_DIR, 'agg-delegations.cjs'));
|
|
@@ -122,5 +120,68 @@ const INTENT = (fm) => `---\n${fm}\n---\n# INTENT — x\n`;
|
|
|
122
120
|
fs.rmSync(empty, { recursive: true, force: true });
|
|
123
121
|
}
|
|
124
122
|
|
|
123
|
+
// ---- ④ --check 漂移门(2026-10-08 selfmeasure-and-modularize)----
|
|
124
|
+
// 归一(CRLF + 「生成于」戳)不过则门禁恒红 = 不可消退噪声(audit-gate-hardening P3),故纯函数层先钉死。
|
|
125
|
+
{
|
|
126
|
+
const base = '# dash\n> 生成于 2026-10-07T13:56:00.376Z\n| P50 | 0 天 |\n';
|
|
127
|
+
check('--check 归一:仅「生成于」戳不同 → 归一后相等(否则门禁恒红)',
|
|
128
|
+
normalizeForCheck(base) === normalizeForCheck(base.replace('2026-10-07T13:56:00.376Z', '2026-10-08T00:00:00.000Z')));
|
|
129
|
+
check('--check 归一:CRLF 盘面 vs LF 生成段 → 相等(gen-workflow-index 2026-09-30 同坑先例)',
|
|
130
|
+
normalizeForCheck(base) === normalizeForCheck(base.replace(/\n/g, '\r\n')));
|
|
131
|
+
check('--check 归一:内容真漂移仍不等(归一不得吞真差异)',
|
|
132
|
+
normalizeForCheck(base) !== normalizeForCheck(base.replace('| P50 | 0 天 |', '| P50 | 7 天 |')));
|
|
133
|
+
|
|
134
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-check-'));
|
|
135
|
+
W(root, 'workflow/intents/2026-10-01-x.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
136
|
+
const gen = path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs');
|
|
137
|
+
const runCheck = () => spawnSync(process.execPath, [gen, '--check'], {
|
|
138
|
+
cwd: root, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: root },
|
|
139
|
+
});
|
|
140
|
+
const rMiss = runCheck();
|
|
141
|
+
check('--check 端到端:盘面缺失 → exit 1 + 提示生成', rMiss.status === 1 && /不存在或不可读/.test(rMiss.stderr || ''), `status=${rMiss.status}`);
|
|
142
|
+
spawnSync(process.execPath, [gen], { cwd: root, env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
143
|
+
const rOk = runCheck();
|
|
144
|
+
check('--check 端到端:刚生成 → exit 0', rOk.status === 0 && /一致/.test(rOk.stdout || ''), `status=${rOk.status} ${rOk.stdout || ''}${rOk.stderr || ''}`);
|
|
145
|
+
fs.appendFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), '\n<!-- drift -->\n');
|
|
146
|
+
const rDrift = runCheck();
|
|
147
|
+
check('--check 端到端:内容漂移 → exit 1 + 差异行号 + 修复命令',
|
|
148
|
+
rDrift.status === 1 && /首个差异在第 \d+ 行/.test(rDrift.stderr || '') && /gen-workflow-dashboard\.mjs 重新生成/.test(rDrift.stderr || ''), `status=${rDrift.status} ${rDrift.stderr || ''}`);
|
|
149
|
+
const noWrite = fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
150
|
+
check('--check 端到端:不写盘(漂移态下盘面保持原样)', noWrite.includes('<!-- drift -->'));
|
|
151
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---- ⑤ 门禁 ROI 节(2026-10-08 gate-roi-metrics)----
|
|
155
|
+
// 本节存在的核心断言不是「有数值」,而是「**不因运行数据而漂移」**:DASHBOARD 被检查 11 --check 覆盖,
|
|
156
|
+
// 采集数值每跑一次门禁就变一次——铺进去等于每次采集必报一条漂移 WARN(不可消除噪声)。
|
|
157
|
+
{
|
|
158
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'fk-dash-roi-'));
|
|
159
|
+
W(root, 'workflow/intents/2026-10-01-x.md', INTENT('状态: done\n级别: L1\n日期: 2026-10-01\n模块: pipeline'));
|
|
160
|
+
const gen = path.join(SCRIPT_DIR, 'gen-workflow-dashboard.mjs');
|
|
161
|
+
const run = (...args) => spawnSync(process.execPath, [gen, ...args], { cwd: root, encoding: 'utf8', env: { ...process.env, CHECK_LOOP_ROOT: root } });
|
|
162
|
+
const dashOf = () => fs.readFileSync(path.join(root, 'workflow', 'DASHBOARD.md'), 'utf8');
|
|
163
|
+
|
|
164
|
+
// 无采集态(先于造 cache 生成):节恒为静态文案,故与有无数据无关
|
|
165
|
+
const rl = gateRoiLines();
|
|
166
|
+
check('门禁ROI:节内容给出采集命令 + 看数值命令 + 无数据时 CLI 自带「未采集」提示(优雅降级不落空节)',
|
|
167
|
+
rl.join('\n').includes('check-loop.mjs --gate-stats') && rl.join('\n').includes('agg-gate-stats.mjs') && rl.join('\n').includes('未采集'), rl.join('\n'));
|
|
168
|
+
check('门禁ROI:节内容不含任何耗时/次数运行数值(防检查 11 每次采集报漂移的设计核心)',
|
|
169
|
+
!/\d+\s*(ms|s)\b/.test(rl.join('\n')) && !/运行\s*\d/.test(rl.join('\n')), rl.join('\n'));
|
|
170
|
+
run();
|
|
171
|
+
const dash0 = dashOf();
|
|
172
|
+
check('门禁ROI:生成物含该节 + 耗时口径显式声明「含子进程」防误读',
|
|
173
|
+
dash0.includes('## 门禁 ROI') && dash0.includes('含子进程时间'), dash0.slice(-800));
|
|
174
|
+
|
|
175
|
+
// 采集数据从无到有 → DASHBOARD 必须仍一致:这是本节设计的核心钉子
|
|
176
|
+
const cacheRel = path.join(root, '.agents', 'cache', 'gate-stats.jsonl');
|
|
177
|
+
fs.mkdirSync(path.dirname(cacheRel), { recursive: true });
|
|
178
|
+
fs.writeFileSync(cacheRel, JSON.stringify({ ts: '2026-10-08T00:00:00Z', segs: [{ id: '8', warns: 3, ms: 11809 }], totalMs: 11809 }) + '\n');
|
|
179
|
+
check('门禁ROI:采集数据「从无到有」后 --check 仍 exit 0', run('--check').status === 0, `status=${run('--check').status}`);
|
|
180
|
+
fs.appendFileSync(cacheRel, JSON.stringify({ ts: '2026-10-08T01:00:00Z', segs: [{ id: '8', warns: 9, ms: 99 }], totalMs: 99 }) + '\n');
|
|
181
|
+
check('门禁ROI:采集数据「再变一次」后 --check 仍 exit 0 且盘面字节恒等(零新增漂移源)',
|
|
182
|
+
run('--check').status === 0 && dashOf() === dash0, `status=${run('--check').status}`);
|
|
183
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
125
186
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
126
187
|
process.exit(fail ? 1 : 0);
|
|
@@ -124,7 +124,7 @@ const run = (root, args = []) => spawnSync(process.execPath, [GEN, ...args], { c
|
|
|
124
124
|
'状态: draft\n级别: L1\n日期: 2026-09-22\n模块: pipeline',
|
|
125
125
|
'workflow-doc-retrieval',
|
|
126
126
|
), 'utf8');
|
|
127
|
-
|
|
127
|
+
run(root);
|
|
128
128
|
const idx = fs.readFileSync(path.join(root, 'workflow', 'INDEX.md'), 'utf8');
|
|
129
129
|
check('场景 5:kebab-case H1 完整保留(未被截成末段)', idx.includes('| workflow-doc-retrieval |') && !idx.includes('| retrieval |'), idx.split('\n').filter((l) => l.includes('retrieval')).join('\n'));
|
|
130
130
|
check('场景 5:em-dash 前缀仍正常剥离', idx.includes('| 在跑的需求甲 |'), '');
|
|
@@ -48,7 +48,7 @@ const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
|
48
48
|
// **不能用固定上溯级数**(2026-09-28 修正):本脚本在两处运行、深度不同——
|
|
49
49
|
// 包源 `templates/_agents/scripts/` → 上溯 3 级到仓库根
|
|
50
50
|
// 装户 `.agents/scripts/` → 上溯 2 级到安装根
|
|
51
|
-
// 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install
|
|
51
|
+
// 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install>/../.agents/…`)。
|
|
52
52
|
// 实测暴露路径:CI 新增「shipped 套件」步骤后,`.agents/scripts/gen-workflow-metrics.test.mjs`
|
|
53
53
|
// 场景 1 即 exit 1(workflow-enums 单源文件不可读),而包源侧同字节跑出全绿。
|
|
54
54
|
// 改为**逐级上溯探测**:找到含 `.agents/workflow-enums.txt` 的那一级即用,找不到则回退标准位置让
|
|
@@ -39,7 +39,6 @@ const USAGE = `用法: node .agents/scripts/kb-search.mjs [选项] <词1> [词2
|
|
|
39
39
|
|
|
40
40
|
const args = process.argv.slice(2);
|
|
41
41
|
let scope = '';
|
|
42
|
-
let scopeExplicit = false;
|
|
43
42
|
let typeFilter = null;
|
|
44
43
|
let moduleFilter = '';
|
|
45
44
|
let statusFilter = 'all';
|
|
@@ -53,7 +52,7 @@ let rebuild = false;
|
|
|
53
52
|
const words = [];
|
|
54
53
|
for (let i = 0; i < args.length; i++) {
|
|
55
54
|
const a = args[i];
|
|
56
|
-
if (a === '--scope') { scope = args[++i] || '';
|
|
55
|
+
if (a === '--scope') { scope = args[++i] || ''; }
|
|
57
56
|
else if (a === '--type') typeFilter = (args[++i] || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
58
57
|
else if (a === '--module') moduleFilter = args[++i] || '';
|
|
59
58
|
else if (a === '--status') statusFilter = args[++i] || '';
|
|
@@ -109,7 +109,7 @@ const files = (out) => (out.match(/^📄 (workflow\/\S+)/gm) || []).map((l) => l
|
|
|
109
109
|
// ---- 场景 5:增量失效——warm 后改 1 个源文件,仅该文件重解析且输出与纯扫一致 ----
|
|
110
110
|
{
|
|
111
111
|
const root = mkfix();
|
|
112
|
-
|
|
112
|
+
run(root, ['编码模板']);
|
|
113
113
|
const planP = path.join(root, 'workflow', 'plans', '2026-09-20-ccc.md');
|
|
114
114
|
fs.appendFileSync(planP, '\n## 影响面\n- 新标记NEWWORD:编码模板修订\n', 'utf8');
|
|
115
115
|
const after = run(root, ['编码模板', 'NEWWORD']);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// pipeline-run-docs.mjs — 文档纯函数层(2026-10-08 engine-quality-batch,L2 自 pipeline-run.mjs 拆出)
|
|
2
|
+
// fmGet / placeholdersIn / sectionBody / validateDraftContent / closeoutComplete / summarizeDoc——
|
|
3
|
+
// 零 IO、零 process 依赖、单向被 pipeline-run.mjs(CLI 编排层)消费;判据注释随函数原样迁入。
|
|
4
|
+
// export 面 = 原主文件具名导出(placeholdersIn/validateDraftContent/closeoutComplete)+ 层内消费三函数;
|
|
5
|
+
// 主文件经 re-export 保持对外导出面不变(workflow-board-server 等消费方零改动)。
|
|
6
|
+
export function fmGet(text, key) {
|
|
7
|
+
const m = text.split(/\r?\n/).find((l) => l.startsWith(`${key}:`));
|
|
8
|
+
return m ? m.slice(key.length + 1).trim() : null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 占位符判定(复核 P1-3 收窄,2026-10-02):先剔除 code span(`…`)与引号串("…")——
|
|
12
|
+
// 正文合法的命令语法占位(如 `next --delegated "<原话>"`)几乎总在 code span 内,
|
|
13
|
+
// 模板实占位符(如 <为什么做;写明需求来源…>)从不包 code span;再匹配剩余裸尖括号。
|
|
14
|
+
export function placeholdersIn(text) {
|
|
15
|
+
const stripped = String(text)
|
|
16
|
+
.replace(/^```[\s\S]*?^```$/gm, '') // 复核 P2-B:跨行 code fence 整块剔除(围栏内命令示例占位不属模板占位符)
|
|
17
|
+
.replace(/`[^`\n]*`/g, '')
|
|
18
|
+
.replace(/"[^"\n]*"/g, '');
|
|
19
|
+
const hits = [];
|
|
20
|
+
for (const re of [/<[^>\n]{1,60}>/g, /YYYY-MM-DD 用户/g]) {
|
|
21
|
+
let m; while ((m = re.exec(stripped))) hits.push(m[0]);
|
|
22
|
+
}
|
|
23
|
+
return [...new Set(hits)].slice(0, 5);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function sectionBody(text, title) {
|
|
27
|
+
const lines = text.split(/\r?\n/);
|
|
28
|
+
const i = lines.findIndex((l) => l.trim() === `## ${title}` || l.trim() === `## ${title}(L1 微改动无实质内容可删本节,不硬填)` || l.trim().startsWith(`## ${title}`));
|
|
29
|
+
if (i < 0) return null;
|
|
30
|
+
const body = [];
|
|
31
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
32
|
+
if (/^##\s/.test(lines[j])) break;
|
|
33
|
+
body.push(lines[j]);
|
|
34
|
+
}
|
|
35
|
+
return body.join('\n').trim();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function validateDraftContent(text, kind, level) {
|
|
39
|
+
const problems = [];
|
|
40
|
+
if (!text) return ['文件不存在或为空'];
|
|
41
|
+
const state = fmGet(text, '状态');
|
|
42
|
+
if (!state) problems.push('frontmatter 缺「状态:」行');
|
|
43
|
+
if (kind !== 'incident' && !fmGet(text, '级别')) problems.push('frontmatter 缺「级别:」行');
|
|
44
|
+
const ph = placeholdersIn(text);
|
|
45
|
+
if (ph.length) problems.push(`模板占位符残留:${ph.join('、')}`);
|
|
46
|
+
const required = {
|
|
47
|
+
intent: level === 'L1' ? ['背景与问题', '目标', '验收标准'] : ['背景与问题', '历史教训/防复发', '目标', '非目标', '约束', '验收标准'],
|
|
48
|
+
spec: ['功能行为', '数据流', '系统改动', '约束遵守映射'],
|
|
49
|
+
plan: level === 'L1' ? ['改动方案', '约束与风险', '验证计划'] : ['改动方案', '任务拆解', '执行顺序', '验证计划'],
|
|
50
|
+
incident: ['时间线', '根因', '复盘三件套'],
|
|
51
|
+
}[kind] || [];
|
|
52
|
+
for (const sec of required) {
|
|
53
|
+
const b = sectionBody(text, sec);
|
|
54
|
+
if (b === null) { if (!(level === 'L1' && ['非目标', '约束', '任务拆解', '执行顺序'].includes(sec))) problems.push(`缺节:## ${sec}`); }
|
|
55
|
+
else if (!b) problems.push(`节为空:## ${sec}`);
|
|
56
|
+
}
|
|
57
|
+
return problems;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function closeoutComplete(text) {
|
|
61
|
+
const body = sectionBody(text, '验收标准');
|
|
62
|
+
if (body === null) return { ok: false, problems: ['缺「## 验收标准」节'] };
|
|
63
|
+
const lines = body.split(/\r?\n/);
|
|
64
|
+
const open = lines.filter((l) => /^\s*-\s\[\s\]\s/.test(l));
|
|
65
|
+
// 复核 P2-6:证据冒号后须直接跟内容(非空白、非收括号)——「(证据:)」空值不算过
|
|
66
|
+
const checkedNoEv = lines.filter((l) => /^\s*-\s\[x\]\s/.test(l) && !/证据[::][^)\s]/.test(l));
|
|
67
|
+
const problems = [];
|
|
68
|
+
if (open.length) problems.push(`未勾验收 ${open.length} 条`);
|
|
69
|
+
if (checkedNoEv.length) problems.push(`已勾缺证据 ${checkedNoEv.length} 条`);
|
|
70
|
+
return { ok: problems.length === 0, problems };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function summarizeDoc(text) {
|
|
74
|
+
const body = text.split(/\r?\n/).filter((l) => l.trim() && !l.startsWith('---') && !/^状态:|^级别:|^日期:|^模块:|^备注:|^risk_level:/.test(l));
|
|
75
|
+
return body.slice(0, 3).join(' / ').slice(0, 200);
|
|
76
|
+
}
|