agentic-flow-kit 1.7.0 → 1.7.15
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 +40 -6
- package/modules/gates/README.md +1 -0
- package/modules/gates/test-protect/README.md +23 -0
- package/modules/gates/test-protect/check-test-protect.mjs +64 -0
- package/modules/gates/test-protect/local-pre-commit.line +1 -0
- package/modules/hosts/README.md +57 -0
- package/modules/hosts/claude/commands/wf-build.md +5 -24
- package/modules/hosts/claude/commands/wf-deploy.md +1 -0
- package/modules/hosts/claude/commands/wf-design.md +2 -2
- package/modules/hosts/claude/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/claude/commands/wf-plan.md +1 -1
- package/modules/hosts/claude/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/claude/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/claude/commands/wf-test.md +5 -4
- package/modules/hosts/codex/commands/wf-build.md +5 -24
- package/modules/hosts/codex/commands/wf-deploy.md +1 -0
- package/modules/hosts/codex/commands/wf-design.md +2 -2
- package/modules/hosts/codex/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/codex/commands/wf-plan.md +1 -1
- package/modules/hosts/codex/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/codex/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/codex/commands/wf-test.md +5 -4
- package/modules/hosts/cursor/commands/wf-build.md +5 -24
- package/modules/hosts/cursor/commands/wf-deploy.md +1 -0
- package/modules/hosts/cursor/commands/wf-design.md +2 -2
- package/modules/hosts/cursor/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/cursor/commands/wf-plan.md +1 -1
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/cursor/commands/wf-test.md +5 -4
- package/modules/hosts/opencode/commands/wf-build.md +5 -24
- package/modules/hosts/opencode/commands/wf-deploy.md +1 -0
- package/modules/hosts/opencode/commands/wf-design.md +2 -2
- package/modules/hosts/opencode/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/opencode/commands/wf-plan.md +1 -1
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/opencode/commands/wf-test.md +5 -4
- package/modules/hosts/trae/commands/wf-build.md +5 -24
- package/modules/hosts/trae/commands/wf-deploy.md +1 -0
- package/modules/hosts/trae/commands/wf-design.md +2 -2
- package/modules/hosts/trae/commands/wf-gate-checklist.md +1 -3
- package/modules/hosts/trae/commands/wf-plan.md +1 -1
- package/modules/hosts/trae/commands/wf-source-sync-check.md +1 -3
- package/modules/hosts/trae/commands/wf-sync-hosts.md +1 -3
- package/modules/hosts/trae/commands/wf-test.md +5 -4
- package/package.json +2 -2
- package/src/add-gate.mjs +25 -1
- package/src/add-gate.test.mjs +103 -0
- package/src/cli.mjs +17 -2
- package/src/doctor.mjs +99 -11
- package/src/gates.test.mjs +17 -0
- package/src/init.mjs +7 -1
- package/src/init.test.mjs +19 -0
- package/src/instance-ref.test.mjs +62 -0
- package/src/jscpd-drift.test.mjs +144 -0
- package/src/render.mjs +5 -0
- package/src/rule-budget.test.mjs +103 -0
- package/src/sync-hosts.mjs +4 -2
- package/src/trae-hooks.test.mjs +15 -3
- package/src/validate.mjs +72 -0
- package/src/validate.test.mjs +95 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/commands/build.md +5 -24
- package/templates/_agents/commands/deploy.md +1 -0
- package/templates/_agents/commands/design.md +2 -2
- package/templates/_agents/commands/gate-checklist.md +1 -3
- package/templates/_agents/commands/plan.md +1 -1
- package/templates/_agents/commands/source-sync-check.md +1 -3
- package/templates/_agents/commands/sync-hosts.md +1 -3
- package/templates/_agents/commands/test.md +5 -4
- package/templates/_agents/control-surfaces.txt +53 -0
- package/templates/_agents/scripts/agg-delegations.cjs +51 -6
- package/templates/_agents/scripts/agg-delegations.test.mjs +79 -1
- package/templates/_agents/scripts/check-engine-integrity.mjs +288 -0
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +227 -0
- package/templates/_agents/scripts/check-lane-surface.mjs +7 -2
- package/templates/_agents/scripts/check-ledger.test.mjs +2 -0
- package/templates/_agents/scripts/check-loop.mjs +206 -4
- package/templates/_agents/scripts/check-loop.test.mjs +312 -2
- package/templates/_agents/scripts/confirm-doc.mjs +14 -1
- package/templates/_agents/scripts/confirm-doc.test.mjs +59 -0
- package/templates/_agents/scripts/doctor.test.mjs +251 -1
- package/templates/_agents/scripts/draft-sediment.mjs +178 -0
- package/templates/_agents/scripts/draft-sediment.test.mjs +272 -0
- package/templates/_agents/scripts/gate-checklist.mjs +10 -2
- package/templates/_agents/scripts/gate-checklist.test.mjs +7 -1
- package/templates/_agents/scripts/gen-wiki-board.mjs +61 -6
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +74 -1
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +2 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +11 -5
- package/templates/_agents/scripts/kb-search.mjs +87 -12
- package/templates/_agents/scripts/kb-search.test.mjs +83 -0
- package/templates/_agents/scripts/pipeline-run.mjs +1 -1
- package/templates/_agents/scripts/solidify-task.mjs +1 -1
- package/templates/_agents/scripts/source-sync-check.test.mjs +9 -4
- package/templates/_agents/scripts/stage-gates.mjs +48 -1
- package/templates/_agents/scripts/stage-gates.test.mjs +105 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +34 -0
- package/templates/_agents/scripts/verify-wiki-consistency.test.mjs +44 -2
- package/templates/_agents/scripts/verify.mjs +1 -1
- package/templates/_agents/scripts-test-exempt.txt +3 -1
- package/templates/_agents/skills/wiki/SKILL.md +5 -3
- package/templates/wiki/INDEX.md +44 -1
- package/templates/wiki//347/237/245/350/257/206/346/262/211/346/267/200/346/200/273/350/247/210.html +22 -1
- package/templates/workflow/delegations.md +5 -4
- package/templates/workflow/incidents/_TEMPLATE.md +2 -0
- package/templates/workflow/intents/_TEMPLATE.md +3 -0
- package/templates/workflow/plans/_TEMPLATE.md +4 -0
- package/templates/workflow/specs/_TEMPLATE.md +23 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// draft-sediment.mjs 测试(B2,2026-10-10)——纯函数直测 + fixture 子进程跑真 CLI
|
|
3
|
+
// 纯函数(extractSections/topicOf/renderDraft/setSedimentField)import 直测;CLI 场景
|
|
4
|
+
// 以「换 cwd」方式在临时仓跑真脚本(脚本按相对路径 workflow/ + wiki/ 读写,无需注入口)。
|
|
5
|
+
// 覆盖:抽节/主题取法/草稿渲染/沉淀字段回写的纯逻辑 + dry-run 不写盘 / 实跑落草稿并回写
|
|
6
|
+
// 并重跑 gen-wiki-board / 状态守卫拦非 done|closed / 无高价值节拒抽 / 路径越界拒收
|
|
7
|
+
// 用法:node .agents/scripts/draft-sediment.test.mjs(仓库任意目录执行均可)
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import os from 'node:os';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { spawnSync } from 'node:child_process';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { extractSections, topicOf, renderDraft, setSedimentField } from './draft-sediment.mjs';
|
|
14
|
+
|
|
15
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const SED = path.join(SCRIPT_DIR, 'draft-sediment.mjs');
|
|
17
|
+
const TODAY = new Date().toISOString().slice(0, 10); // 与脚本内取法一致(草稿目录日期段)
|
|
18
|
+
const USAGE = '用法:node .agents/scripts/draft-sediment.test.mjs';
|
|
19
|
+
|
|
20
|
+
let pass = 0;
|
|
21
|
+
let fail = 0;
|
|
22
|
+
const check = (desc, cond, detail = '') => {
|
|
23
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
24
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? `\n${detail}` : ''}`); }
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// ---- 纯函数:extractSections ----
|
|
28
|
+
{
|
|
29
|
+
const text = [
|
|
30
|
+
'# INTENT — 权限收敛',
|
|
31
|
+
'',
|
|
32
|
+
'## 影响面',
|
|
33
|
+
'',
|
|
34
|
+
'- 模块:pipeline',
|
|
35
|
+
'- 数据库:无',
|
|
36
|
+
'',
|
|
37
|
+
'## 触达红线与边界',
|
|
38
|
+
'',
|
|
39
|
+
'> 这是引用块,应被剔除',
|
|
40
|
+
'<!-- 注释也应剔除 -->',
|
|
41
|
+
'<占位单行也应剔除>',
|
|
42
|
+
'- [x] 规则变更(证据:abc123)',
|
|
43
|
+
'',
|
|
44
|
+
'## 目标(不在抽取词表内)',
|
|
45
|
+
'',
|
|
46
|
+
'- 不应被抽出来',
|
|
47
|
+
'',
|
|
48
|
+
'## 影响面(空节)',
|
|
49
|
+
'',
|
|
50
|
+
'<待补>',
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
const got = extractSections(text, ['影响面', '触达红线']);
|
|
54
|
+
const titles = got.map((s) => s.title);
|
|
55
|
+
check('1a 抽取指定节(影响面)', titles.includes('影响面'), JSON.stringify(titles));
|
|
56
|
+
check('1b 节标题前缀匹配(「触达红线与边界」命中「触达红线」)', titles.includes('触达红线与边界'), JSON.stringify(titles));
|
|
57
|
+
check('1c 词表外节不抽(目标节缺席)', !titles.some((t) => t.startsWith('目标')), JSON.stringify(titles));
|
|
58
|
+
const yx = got.find((s) => s.title === '影响面');
|
|
59
|
+
check('1d 正文保留(模块行入库)', yx && yx.body.includes('- 模块:pipeline'), yx && yx.body);
|
|
60
|
+
const hx = got.find((s) => s.title === '触达红线与边界');
|
|
61
|
+
check('1e 占位行剔除(引用块/注释/单行尖括号均不进 body)',
|
|
62
|
+
hx && !hx.body.includes('引用块') && !hx.body.includes('注释也应') && !hx.body.includes('占位单行')
|
|
63
|
+
&& hx.body.includes('规则变更(证据:abc123)'), hx && hx.body);
|
|
64
|
+
check('1f 仅占位的空节被丢弃(「影响面(空节)」不入结果)',
|
|
65
|
+
!titles.includes('影响面(空节)'), JSON.stringify(titles));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---- 纯函数:topicOf(标题行优先于文件名)----
|
|
69
|
+
{
|
|
70
|
+
check('2a 标题行优先(# INTENT — 权限收敛 胜过文件名尾段 perm)',
|
|
71
|
+
topicOf('workflow/intents/2026-10-10-perm.md', '# INTENT — 权限收敛\n') === '权限收敛', '');
|
|
72
|
+
check('2b 无标题行退文件名尾段(去日期前缀)',
|
|
73
|
+
topicOf('workflow/intents/2026-10-10-perm.md', '# 无标题文档\n') === 'perm', '');
|
|
74
|
+
check('2c 无标题无日期退文件基名',
|
|
75
|
+
topicOf('wiki/随便/a.md', '# 无标题文档\n') === 'a', '');
|
|
76
|
+
check('2d INCIDENT 标题行同样识别',
|
|
77
|
+
topicOf('workflow/incidents/2026-09-12-x.md', '# INCIDENT — 越权写操作\n') === '越权写操作', '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- 纯函数:renderDraft ----
|
|
81
|
+
{
|
|
82
|
+
const out = renderDraft({ topic: '权限收敛', date: '2026-10-10', source: 'workflow/intents/2026-10-10-perm.md',
|
|
83
|
+
sections: [{ title: '影响面', body: '- 模块:pipeline' }], kind: 'intent' });
|
|
84
|
+
check('3a frontmatter 含来源与草稿状态', out.startsWith('---\n') && out.includes('来源: workflow/intents/2026-10-10-perm.md')
|
|
85
|
+
&& out.includes('状态: 草稿(待人工归类)'), out.split('\n').slice(0, 5).join('\n'));
|
|
86
|
+
check('3b intent 渲染为「需求沉淀草稿」', out.includes('# 权限收敛(需求沉淀草稿)'), out.split('\n')[4]);
|
|
87
|
+
check('3c incident 渲染为「事故复盘草稿」',
|
|
88
|
+
renderDraft({ topic: 't', date: '2026-10-10', source: 's', sections: [], kind: 'incident' })
|
|
89
|
+
.includes('# t(事故复盘草稿)'), '');
|
|
90
|
+
check('3d 抽取节原样落到正文 ## 节下', out.includes('## 影响面\n\n- 模块:pipeline'), out);
|
|
91
|
+
check('3e 草稿头含中间态提示(引导人工归类)', out.includes('中间态'), out);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---- 纯函数:setSedimentField ----
|
|
95
|
+
{
|
|
96
|
+
const fm = (body) => `---\n${body}\n---\n# INTENT — t\n`;
|
|
97
|
+
const a = setSedimentField(fm('状态: done\n级别: L1'), 'wiki/drafts-archive/2026-10-10-t/t.md');
|
|
98
|
+
check('4a 无沉淀字段 → 追加且 changed=true',
|
|
99
|
+
a.changed && a.text.includes('沉淀: wiki/drafts-archive/2026-10-10-t/t.md')
|
|
100
|
+
&& a.text.includes('# INTENT — t\n'), a.text);
|
|
101
|
+
const b = setSedimentField(fm('状态: done\n沉淀: 无'), 'wiki/drafts-archive/2026-10-10-t/t.md');
|
|
102
|
+
check('4b 「沉淀: 无」显式豁免位 → 覆盖(豁免是占位,可写)',
|
|
103
|
+
b.changed && b.text.includes('沉淀: wiki/drafts-archive/2026-10-10-t/t.md'), b.text);
|
|
104
|
+
const c = setSedimentField(fm('状态: done\n沉淀: wiki/规范/x.md'), 'wiki/drafts-archive/2026-10-10-t/t.md');
|
|
105
|
+
check('4c 已有人工登记 → 不覆盖 changed=false(防误抹)',
|
|
106
|
+
!c.changed && c.text.includes('沉淀: wiki/规范/x.md'), c.text);
|
|
107
|
+
const d = setSedimentField('# 无 frontmatter 的文档\n', 'wiki/drafts-archive/x.md');
|
|
108
|
+
check('4d 无 frontmatter → 不动 changed=false', !d.changed, d.text);
|
|
109
|
+
const e = setSedimentField(fm('状态: done\n沉淀: '), 'wiki/drafts-archive/y.md');
|
|
110
|
+
check('4e 空值沉淀位 → 覆盖(空同未填)',
|
|
111
|
+
e.changed && e.text.includes('沉淀: wiki/drafts-archive/y.md'), e.text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- fixture:临时仓(wiki 根带 INDEX/看板锚点,供 gen-wiki-board 重跑;workflow 两目录)----
|
|
115
|
+
const mkfx = () => {
|
|
116
|
+
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'draft-sed-test-'));
|
|
117
|
+
fs.mkdirSync(path.join(fx, 'wiki', '主题A'), { recursive: true });
|
|
118
|
+
fs.mkdirSync(path.join(fx, 'wiki', 'drafts-archive'), { recursive: true });
|
|
119
|
+
fs.writeFileSync(path.join(fx, 'wiki', '主题A', 'a.md'), '# a\n', 'utf8');
|
|
120
|
+
fs.writeFileSync(path.join(fx, 'wiki', 'INDEX.md'), [
|
|
121
|
+
'# wiki 索引(fixture)', '',
|
|
122
|
+
'| 主题 | 文件数 | 用途 |', '|---|---|---|',
|
|
123
|
+
'| 主题A | 1 | fixture 主题 |',
|
|
124
|
+
'| drafts-archive | — | 原文档备份(按 日期-主题 分目录)', '',
|
|
125
|
+
'合计:**1 份**知识文档,**1 个主题**', '',
|
|
126
|
+
'---', '', '## 文件 → 主题 → 归属目录 映射表', '',
|
|
127
|
+
'### 主题A(1)', '',
|
|
128
|
+
'| 文件名 | 主题 | 归属目录 |', '|---|---|---|',
|
|
129
|
+
'| a.md | 主题A | wiki/主题A/ |', '', '---', '',
|
|
130
|
+
'## 命名规则', '', '- fixture', '',
|
|
131
|
+
].join('\n'), 'utf8');
|
|
132
|
+
fs.writeFileSync(path.join(fx, 'wiki', '知识沉淀总览.html'), [
|
|
133
|
+
'<!doctype html><html><body><script>', 'const DATA = {',
|
|
134
|
+
' "summary": { "total": 1, "topics": 1, "files": 1, "archive": 0 },',
|
|
135
|
+
' "topics": [ { "name": "主题A", "files": [',
|
|
136
|
+
' { "file": "a.md", "topic": "主题A", "dir": "./主题A/" } ]} ]',
|
|
137
|
+
'};', '</script></body></html>', '',
|
|
138
|
+
].join('\n'), 'utf8');
|
|
139
|
+
fs.mkdirSync(path.join(fx, 'workflow', 'intents'), { recursive: true });
|
|
140
|
+
fs.mkdirSync(path.join(fx, 'workflow', 'incidents'), { recursive: true });
|
|
141
|
+
return fx;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// 高价值正文(intent:影响面 + 触达红线均有实质内容)
|
|
145
|
+
const INTENT_DONE = [
|
|
146
|
+
'---', '状态: done', '级别: L1', '日期: 2026-10-10', '模块: pipeline', '---',
|
|
147
|
+
'# INTENT — 权限收敛', '',
|
|
148
|
+
'## 影响面', '', '- 模块:pipeline', '- 数据库:无', '',
|
|
149
|
+
'## 触达红线', '', '- [x] 规则 / 契约变更(证据:abc123)', '',
|
|
150
|
+
'## 验收标准(可测试)', '', '- [x] 全绿(证据:npm test)', '',
|
|
151
|
+
].join('\n');
|
|
152
|
+
|
|
153
|
+
// 高价值正文(incident:复盘三件套齐全)
|
|
154
|
+
const INC_CLOSED = [
|
|
155
|
+
'---', '状态: closed', '级别: L1', '日期: 2026-09-12', '---',
|
|
156
|
+
'# INCIDENT — 越权写操作', '',
|
|
157
|
+
'## 影响面', '', '- 只读接口被放行写操作。', '',
|
|
158
|
+
'## 根因', '', '- 鉴权中间件早于路由注册生效。', '',
|
|
159
|
+
'## 为什么之前没拦住', '', '- 测试只覆盖了 happy path。', '',
|
|
160
|
+
'## 复盘三件套', '', '- 修复:注册顺序对调(SHA abc123)。', '- 防复发:加契约测试。', '- 台账:已登记。', '',
|
|
161
|
+
].join('\n');
|
|
162
|
+
|
|
163
|
+
const runSed = (fx, targetArgs) => spawnSync(process.execPath, [SED, ...targetArgs], {
|
|
164
|
+
cwd: fx, encoding: 'utf8',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---- CLI 5:--dry-run 不写盘 ----
|
|
168
|
+
{
|
|
169
|
+
const fx = mkfx();
|
|
170
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'intents', '2026-10-10-perm.md'), INTENT_DONE, 'utf8');
|
|
171
|
+
const r = runSed(fx, ['workflow/intents/2026-10-10-perm.md', '--dry-run']);
|
|
172
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
173
|
+
check('5a --dry-run exit 0 且明示未写盘', r.status === 0 && out.includes('未写盘'), `exit=${r.status}\n${out.slice(0, 400)}`);
|
|
174
|
+
check('5b 未落草稿(drafts-archive 仍空)',
|
|
175
|
+
fs.readdirSync(path.join(fx, 'wiki', 'drafts-archive')).length === 0, '');
|
|
176
|
+
check('5c 源文件未回写(无沉淀字段)',
|
|
177
|
+
!fs.readFileSync(path.join(fx, 'workflow', 'intents', '2026-10-10-perm.md'), 'utf8').includes('沉淀:'), '');
|
|
178
|
+
check('5d 预览含来源与抽取节标题', out.includes('来源: workflow/intents/2026-10-10-perm.md')
|
|
179
|
+
&& out.includes('## 影响面') && out.includes('## 触达红线'), out);
|
|
180
|
+
check('5e 主题取标题行(权限收敛)而非文件名尾段(perm)',
|
|
181
|
+
out.includes('wiki/drafts-archive/' + TODAY + '-权限收敛'), out);
|
|
182
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- CLI 6:实跑 intent —— 落草稿 + 回写沉淀 + 重跑 gen-wiki-board ----
|
|
186
|
+
{
|
|
187
|
+
const fx = mkfx();
|
|
188
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'intents', '2026-10-10-perm.md'), INTENT_DONE, 'utf8');
|
|
189
|
+
const r = runSed(fx, ['workflow/intents/2026-10-10-perm.md']);
|
|
190
|
+
const out = (r.stdout || '') + (r.stderr || '');
|
|
191
|
+
const draftP = path.join(fx, 'wiki', 'drafts-archive', `${TODAY}-权限收敛`, '权限收敛.md');
|
|
192
|
+
check('6a 实跑 exit 0', r.status === 0, `exit=${r.status}\n${out.slice(0, 500)}`);
|
|
193
|
+
check('6b 草稿落到 drafts-archive/<日期-主题>/<主题>.md',
|
|
194
|
+
fs.existsSync(draftP), `期望存在:${draftP}`);
|
|
195
|
+
const draft = fs.existsSync(draftP) ? fs.readFileSync(draftP, 'utf8') : '';
|
|
196
|
+
check('6c 草稿带来源 frontmatter 指回源文件',
|
|
197
|
+
draft.startsWith('---\n') && draft.includes('来源: workflow/intents/2026-10-10-perm.md')
|
|
198
|
+
&& draft.includes('(需求沉淀草稿)'), draft.split('\n').slice(0, 6).join('\n'));
|
|
199
|
+
const src = fs.readFileSync(path.join(fx, 'workflow', 'intents', '2026-10-10-perm.md'), 'utf8');
|
|
200
|
+
check('6d 源文件回写沉淀字段(B1 闭环——断链 WARN 由此消除)',
|
|
201
|
+
src.includes(`沉淀: wiki/drafts-archive/${TODAY}-权限收敛/权限收敛.md`), src.split('\n').slice(0, 7).join('\n'));
|
|
202
|
+
const board = fs.readFileSync(path.join(fx, 'wiki', '知识沉淀总览.html'), 'utf8');
|
|
203
|
+
check('6e gen-wiki-board 已重跑(看板归档计数 0→1)',
|
|
204
|
+
/"archive": 1/.test(board) && out.includes('gen-wiki-board'), board.split('\n').slice(0, 6).join('\n'));
|
|
205
|
+
check('6f 沉淀回写幂等:再跑一次不覆盖已有登记(changed=false 分支)',
|
|
206
|
+
runSed(fx, ['workflow/intents/2026-10-10-perm.md']).status === 0
|
|
207
|
+
&& src.includes(`沉淀: wiki/drafts-archive/${TODAY}-权限收敛/权限收敛.md`), '');
|
|
208
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- CLI 7:实跑 incident —— 事故口径四节 ----
|
|
212
|
+
{
|
|
213
|
+
const fx = mkfx();
|
|
214
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'incidents', '2026-09-12-x.md'), INC_CLOSED, 'utf8');
|
|
215
|
+
const r = runSed(fx, ['workflow/incidents/2026-09-12-x.md']);
|
|
216
|
+
const draftP = path.join(fx, 'wiki', 'drafts-archive', `${TODAY}-越权写操作`, '越权写操作.md');
|
|
217
|
+
const draft = fs.existsSync(draftP) ? fs.readFileSync(draftP, 'utf8') : '';
|
|
218
|
+
check('7a incident 实跑 exit 0 且草稿落盘', r.status === 0 && fs.existsSync(draftP),
|
|
219
|
+
`exit=${r.status}\n${(r.stdout || '') + (r.stderr || '')}`.slice(0, 400));
|
|
220
|
+
check('7b 草稿为事故复盘口径(标题 + 四节齐备)',
|
|
221
|
+
draft.includes('(事故复盘草稿)') && ['影响面', '根因', '为什么之前没拦住', '复盘三件套']
|
|
222
|
+
.every((h) => draft.includes('## ' + h)), draft.slice(0, 300));
|
|
223
|
+
check('7c 源文件回写沉淀字段',
|
|
224
|
+
fs.readFileSync(path.join(fx, 'workflow', 'incidents', '2026-09-12-x.md'), 'utf8')
|
|
225
|
+
.includes(`沉淀: wiki/drafts-archive/${TODAY}-越权写操作/越权写操作.md`), '');
|
|
226
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---- CLI 8:守卫(状态 / 无高价值节 / 路径越界)----
|
|
230
|
+
{
|
|
231
|
+
const fx = mkfx();
|
|
232
|
+
// 8a 非 done intent
|
|
233
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'intents', 'a-draft.md'),
|
|
234
|
+
INTENT_DONE.replace('状态: done', '状态: draft'), 'utf8');
|
|
235
|
+
const r8a = runSed(fx, ['workflow/intents/a-draft.md']);
|
|
236
|
+
check('8a 非 done intent → exit 1 且明示状态要求',
|
|
237
|
+
r8a.status === 1 && (r8a.stderr || '').includes('须为 done'), `exit=${r8a.status}\n${r8a.stderr}`);
|
|
238
|
+
// 8b 非 closed incident
|
|
239
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'incidents', 'b-open.md'),
|
|
240
|
+
INC_CLOSED.replace('状态: closed', '状态: open'), 'utf8');
|
|
241
|
+
const r8b = runSed(fx, ['workflow/incidents/b-open.md']);
|
|
242
|
+
check('8b 非 closed incident → exit 1 且明示状态要求',
|
|
243
|
+
r8b.status === 1 && (r8b.stderr || '').includes('须为 closed'), `exit=${r8b.status}\n${r8b.stderr}`);
|
|
244
|
+
// 8c 高价值节全为占位 → 拒抽
|
|
245
|
+
fs.writeFileSync(path.join(fx, 'workflow', 'intents', 'c-empty.md'), [
|
|
246
|
+
'---', '状态: done', '---', '# INTENT — 空单', '',
|
|
247
|
+
'## 影响面', '', '<待补>', '', '## 触达红线', '', '<待补>', '',
|
|
248
|
+
].join('\n'), 'utf8');
|
|
249
|
+
const r8c = runSed(fx, ['workflow/intents/c-empty.md']);
|
|
250
|
+
check('8c 高价值节仅占位 → exit 1 且明示无沉淀价值',
|
|
251
|
+
r8c.status === 1 && (r8c.stderr || '').includes('未抽取到高价值节'), `exit=${r8c.status}\n${r8c.stderr}`);
|
|
252
|
+
check('8c 占位拒抽不落草稿', !fs.existsSync(path.join(fx, 'wiki', 'drafts-archive', `${TODAY}-空单`)), '');
|
|
253
|
+
// 8d 路径在 workflow/ 外
|
|
254
|
+
fs.writeFileSync(path.join(fx, 'outside.md'), INTENT_DONE, 'utf8');
|
|
255
|
+
const r8d = runSed(fx, ['outside.md']);
|
|
256
|
+
check('8d 源文件不在 workflow/intents|incidents 下 → exit 1',
|
|
257
|
+
r8d.status === 1 && (r8d.stderr || '').includes('workflow/intents'), `exit=${r8d.status}\n${r8d.stderr}`);
|
|
258
|
+
// 8e 不存在的文件
|
|
259
|
+
const r8e = runSed(fx, ['workflow/intents/不存在.md']);
|
|
260
|
+
check('8e 源文件不存在 → exit 1', r8e.status === 1, `exit=${r8e.status}\n${r8e.stderr}`);
|
|
261
|
+
// 8f 无参数
|
|
262
|
+
const r8f = runSed(fx, []);
|
|
263
|
+
check('8f 无源文件参数 → exit 1 且打印用法', r8f.status === 1, `exit=${r8f.status}`);
|
|
264
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
268
|
+
if (fail) {
|
|
269
|
+
console.log(`\n${USAGE}`);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
process.exit(0);
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// 测试:node templates/_agents/scripts/gate-checklist.test.mjs(fixture + 真实仓库 baseline)
|
|
20
20
|
import fs from 'node:fs';
|
|
21
21
|
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
22
23
|
|
|
23
24
|
function fail(msg) { console.error('❌ ' + msg); process.exit(1); }
|
|
24
25
|
|
|
@@ -175,10 +176,17 @@ if (isMain) {
|
|
|
175
176
|
const pkgRoot = process.argv[2] && !process.argv[2].startsWith('--') ? process.argv[2] : null;
|
|
176
177
|
const base = pkgRoot || process.cwd();
|
|
177
178
|
const doctorPath = path.join(base, 'src', 'doctor.mjs');
|
|
179
|
+
// check-loop 解析序:装副本 .agents/(装户径)→ 包源脚本同级(纯包源径,装户端内容清理后形态)
|
|
180
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
178
181
|
const clPathMjs = path.join(base, '.agents', 'scripts', 'check-loop.mjs');
|
|
179
|
-
const
|
|
182
|
+
const clCandidates = [
|
|
183
|
+
clPathMjs,
|
|
184
|
+
path.join(base, '.agents', 'scripts', 'check-loop.sh'),
|
|
185
|
+
path.join(here, 'check-loop.mjs'),
|
|
186
|
+
];
|
|
187
|
+
const clPath = clCandidates.find((p) => fs.existsSync(p));
|
|
180
188
|
if (!fs.existsSync(doctorPath)) fail('找不到 doctor.mjs:' + doctorPath);
|
|
181
|
-
if (!
|
|
189
|
+
if (!clPath) fail('找不到 check-loop(.mjs / .sh 均无):' + clPathMjs);
|
|
182
190
|
const doctorSrc = fs.readFileSync(doctorPath, 'utf8');
|
|
183
191
|
const checkLoopSrc = fs.readFileSync(clPath, 'utf8');
|
|
184
192
|
const result = gateChecklist({ doctorSrc, checkLoopSrc });
|
|
@@ -153,9 +153,15 @@ const y = 2;
|
|
|
153
153
|
|
|
154
154
|
const realDoctor = () => fs.readFileSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'), 'utf8');
|
|
155
155
|
// check-loop 解析面与 CLI 同口径:.mjs 优先(2026-09-26 迁移后主位),fallback .sh(旧装户)
|
|
156
|
+
// 装户端内容清理后(2026-10-09):纯包源仓无 .agents/ 装副本——check-loop.mjs 在包源 scripts/ 下,
|
|
157
|
+
// 即本测试同级;.agents/ 优先(装户径),包源径兜底(纯包源径),两者皆无 → hasReal 已经把守。
|
|
156
158
|
const realCl = () => {
|
|
157
159
|
const mjs = path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.mjs');
|
|
158
|
-
|
|
160
|
+
if (fs.existsSync(mjs)) return fs.readFileSync(mjs, 'utf8');
|
|
161
|
+
const sh = path.join(SRC_ROOT, '.agents', 'scripts', 'check-loop.sh');
|
|
162
|
+
if (fs.existsSync(sh)) return fs.readFileSync(sh, 'utf8');
|
|
163
|
+
const pkgMjs = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
164
|
+
return fs.readFileSync(pkgMjs, 'utf8');
|
|
159
165
|
};
|
|
160
166
|
const hasReal = fs.existsSync(path.join(SRC_ROOT, 'src', 'doctor.mjs'));
|
|
161
167
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// wiki 索引生成器:磁盘 + INDEX 速览表「用途」列 → 自动重生成 INDEX 速览计数/映射表 + 看板 DATA
|
|
2
2
|
// 2026-09-13 引入:消除三层手工双写(短板①)。人工只维护两样:磁盘文件本身 + 速览表用途列。
|
|
3
|
-
// 用法:node .agents/scripts/gen-wiki-board.mjs [--dry-run] [--help]
|
|
3
|
+
// 用法:node .agents/scripts/gen-wiki-board.mjs [--dry-run] [--check] [--help]
|
|
4
4
|
// (收录/删除/移动 wiki 文件后必跑,随后跑 verify-wiki-consistency.mjs)
|
|
5
|
+
// --check 不写盘;INDEX/看板与磁盘不一致时 exit 1 并打印差异(供 doctor 漂移告警)
|
|
5
6
|
// 测试:node .agents/scripts/gen-wiki-board.test.mjs(fixture 回归,须全绿)
|
|
6
7
|
// 生成范围(锚点内整段重写,锚点外字节原样保留):
|
|
7
8
|
// INDEX.md 速览表数据行 / 「合计」行数字 / 「## 文件 → 主题 → 归属目录 映射表」到「## 命名规则」之间
|
|
@@ -11,7 +12,7 @@ import fs from 'node:fs';
|
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { execSync } from 'node:child_process';
|
|
13
14
|
|
|
14
|
-
const USAGE = `用法:node .agents/scripts/gen-wiki-board.mjs [--dry-run] [--help]
|
|
15
|
+
const USAGE = `用法:node .agents/scripts/gen-wiki-board.mjs [--dry-run] [--check] [--help]
|
|
15
16
|
|
|
16
17
|
事实源:磁盘 wiki/<主题>/ 一层文件 + INDEX.md 速览表「用途」列(人工列)。重生成:
|
|
17
18
|
- wiki/INDEX.md:速览表数据行与文件计数、「合计」行数字、映射表节
|
|
@@ -21,6 +22,7 @@ const USAGE = `用法:node .agents/scripts/gen-wiki-board.mjs [--dry-run] [--h
|
|
|
21
22
|
|
|
22
23
|
选项:
|
|
23
24
|
--dry-run 只打印将生成内容的摘要与逐行差异,不写盘
|
|
25
|
+
--check 不写盘;INDEX/看板与磁盘不一致时 exit 1 并打印差异(供 doctor 漂移告警)
|
|
24
26
|
--help 打印本说明`;
|
|
25
27
|
|
|
26
28
|
const args = process.argv.slice(2);
|
|
@@ -28,12 +30,13 @@ if (args.includes('--help') || args.includes('-h')) {
|
|
|
28
30
|
console.log(USAGE);
|
|
29
31
|
process.exit(0);
|
|
30
32
|
}
|
|
31
|
-
const unknown = args.filter((a) => a !== '--dry-run');
|
|
33
|
+
const unknown = args.filter((a) => a !== '--dry-run' && a !== '--check');
|
|
32
34
|
if (unknown.length) {
|
|
33
35
|
console.error(`❌ 未知参数:${unknown.join(' ')}\n\n${USAGE}`);
|
|
34
36
|
process.exit(1);
|
|
35
37
|
}
|
|
36
38
|
const DRY = args.includes('--dry-run');
|
|
39
|
+
const CHECK = args.includes('--check');
|
|
37
40
|
|
|
38
41
|
const WIKI = 'wiki';
|
|
39
42
|
const INDEX_P = path.join(WIKI, 'INDEX.md');
|
|
@@ -87,6 +90,34 @@ for (const d of fs.readdirSync(WIKI)) {
|
|
|
87
90
|
diskTopics.set(d, entries);
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
// ---- 草稿超龄提醒(P1-2):drafts-archive/<YYYY-MM-DD-主题>/ 落盘超龄未归类 → stdout WARN ----
|
|
94
|
+
// advisory 档:只提醒不阻断(与 B1 沉淀追踪同档),不改归档统计口径与任何生成物,exit code 不受影响。
|
|
95
|
+
// 目录名不带日期前缀(人工建的目录)→ 静默跳过,不 WARN 也不报错。
|
|
96
|
+
const DRAFT_AGE_DAYS = 14; // 超龄档位(天):与 B1 沉淀追踪同档 advisory,只提醒不阻断
|
|
97
|
+
const TODAY = (() => {
|
|
98
|
+
// FLOW_KIT_TODAY=YYYY-MM-DD 仅测试/演练用:覆盖「今天」便于构造超龄/未超龄用例;未设置或格式不合法时用系统当前日期
|
|
99
|
+
const env = process.env.FLOW_KIT_TODAY;
|
|
100
|
+
if (env && /^\d{4}-\d{2}-\d{2}$/.test(env)) return env;
|
|
101
|
+
const now = new Date();
|
|
102
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
103
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
|
|
104
|
+
})();
|
|
105
|
+
const utcMs = (s) => { // 'YYYY-MM-DD' → 当日 UTC 零点毫秒(与 TODAY 同构换算,天数差恒为精确整数)
|
|
106
|
+
const [y, m, d] = s.split('-').map(Number);
|
|
107
|
+
return Date.UTC(y, m - 1, d);
|
|
108
|
+
};
|
|
109
|
+
// rel:相对 drafts-archive 的 '/' 分隔路径(如 '2026-09-01-主题/主题.md',与上方 walk 的 rel 拼法一致)
|
|
110
|
+
const warnIfDraftAged = (rel) => {
|
|
111
|
+
if (!rel.includes('/')) return; // 直接躺在 drafts-archive 根(无日期目录包裹)→ 静默跳过
|
|
112
|
+
const dirName = rel.split('/')[0];
|
|
113
|
+
const m = dirName.match(/^(\d{4}-\d{2}-\d{2})/);
|
|
114
|
+
if (!m) return; // 目录名不带日期前缀(人工建的目录)→ 静默跳过
|
|
115
|
+
const days = (utcMs(TODAY) - utcMs(m[1])) / 86400000;
|
|
116
|
+
if (!(days > DRAFT_AGE_DAYS)) return; // 未超龄(恰 14 天不提醒,超过才提醒);NaN 同样落此静默跳过
|
|
117
|
+
const topic = dirName.slice('YYYY-MM-DD-'.length) || dirName; // 目录名 = 日期-主题;无主题段则退回目录名
|
|
118
|
+
console.log(`[WARN 草稿超龄] drafts-archive/${rel} 落于 ${m[1]} 未归类——草稿是中间态,归类到 wiki/${topic}/ 后跑 gen-wiki-board + verify-wiki-consistency`);
|
|
119
|
+
};
|
|
120
|
+
|
|
90
121
|
const archiveCount = (() => {
|
|
91
122
|
let n = 0;
|
|
92
123
|
const walk = (dir, rel = '') => {
|
|
@@ -95,7 +126,10 @@ const archiveCount = (() => {
|
|
|
95
126
|
// 集合键带 drafts-archive/ 前缀(扫描面扩全 wiki/ 后),walk rel 须拼前缀比对(复核 P2-b)
|
|
96
127
|
const r = rel ? `drafts-archive/${rel}/${e}` : `drafts-archive/${e}`;
|
|
97
128
|
if (fs.statSync(p).isDirectory()) walk(p, rel ? `${rel}/${e}` : e);
|
|
98
|
-
else if (!gitIgnoredArchive.has(r))
|
|
129
|
+
else if (!gitIgnoredArchive.has(r)) {
|
|
130
|
+
n++;
|
|
131
|
+
warnIfDraftAged(rel ? `${rel}/${e}` : e); // 超龄提醒挂在计数控分支:口径一致(ignored 件不提醒),不影响计数
|
|
132
|
+
}
|
|
99
133
|
}
|
|
100
134
|
};
|
|
101
135
|
if (fs.existsSync(path.join(WIKI, 'drafts-archive'))) walk(path.join(WIKI, 'drafts-archive'));
|
|
@@ -185,7 +219,28 @@ if (boardOut === boardText && !/const DATA = \{[\s\S]*?\n\};/.test(boardText)) {
|
|
|
185
219
|
process.exit(1);
|
|
186
220
|
}
|
|
187
221
|
|
|
188
|
-
// ---- 写盘(dry-run
|
|
222
|
+
// ---- 写盘(dry-run 只预览差异;--check 只比对不写盘)----
|
|
223
|
+
// 行尾归一比较(与 gen-workflow-index.mjs 同款:fresh clone 检出 CRLF、生成段恒 LF——
|
|
224
|
+
// 直接字符串相等会在克隆环境假阳性「漂移」;仅内容差异才算漂移)
|
|
225
|
+
const normEol = (s) => String(s).replace(/\r\n/g, '\n');
|
|
226
|
+
const indexOk = normEol(out) === normEol(indexText);
|
|
227
|
+
const boardOk = normEol(boardOut) === normEol(boardText);
|
|
228
|
+
const summary = `文件 ${totalFiles} 份 / 主题 ${diskTopics.size} 个 / 归档 ${archiveCount} 份`;
|
|
229
|
+
if (CHECK) {
|
|
230
|
+
if (indexOk && boardOk) {
|
|
231
|
+
console.log(`✅ wiki/INDEX.md 与看板均与磁盘一致(${summary})`);
|
|
232
|
+
process.exit(0);
|
|
233
|
+
}
|
|
234
|
+
if (!indexOk) {
|
|
235
|
+
console.error(`❌ wiki/INDEX.md 与磁盘不一致(${summary})——跑 node .agents/scripts/gen-wiki-board.mjs 重新生成`);
|
|
236
|
+
previewDiff('wiki/INDEX.md', indexText, out);
|
|
237
|
+
}
|
|
238
|
+
if (!boardOk) {
|
|
239
|
+
console.error(`❌ wiki/知识沉淀总览.html 与磁盘不一致(${summary})——跑 node .agents/scripts/gen-wiki-board.mjs 重新生成`);
|
|
240
|
+
previewDiff('wiki/知识沉淀总览.html', boardText, boardOut);
|
|
241
|
+
}
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
189
244
|
if (DRY) {
|
|
190
245
|
console.log('🔎 --dry-run:未写盘,差异预览如下');
|
|
191
246
|
previewDiff('wiki/INDEX.md', indexText, out);
|
|
@@ -196,7 +251,7 @@ if (DRY) {
|
|
|
196
251
|
}
|
|
197
252
|
|
|
198
253
|
const tail = DRY ? '将重写' : '已重写';
|
|
199
|
-
console.log(`${DRY ? '🔎 [dry-run] 将生成' : '✅ 已生成'}
|
|
254
|
+
console.log(`${DRY ? '🔎 [dry-run] 将生成' : '✅ 已生成'}:${summary};速览表 ${overviewRows.length} 行、映射表 ${ordered.length} 节、看板 DATA ${tail}`);
|
|
200
255
|
console.log(' 人工仅维护:磁盘文件 + 速览表「用途」列(新主题为 <待补> 的请补描述后重跑本脚本)');
|
|
201
256
|
|
|
202
257
|
// dry-run 差异预览:首尾对齐后取中段变更块(逐行),各方向最多显示 10 行
|
|
@@ -85,7 +85,8 @@ const mkfix = (eol) => {
|
|
|
85
85
|
return root;
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
// extraEnv:注入被测脚本环境变量(场景 8/9 用 FLOW_KIT_TODAY 固定「今天」,默认继承父进程环境不变)
|
|
89
|
+
const runGen = (root, extraArgs = [], extraEnv = {}) => spawnSync(process.execPath, [GEN, ...extraArgs], { cwd: root, encoding: 'utf8', env: { ...process.env, ...extraEnv } });
|
|
89
90
|
|
|
90
91
|
// ---- 场景 1:CRLF INDEX ——「用途」列重生成后保持 + 行尾保持(缺陷回归)----
|
|
91
92
|
{
|
|
@@ -168,6 +169,78 @@ const runGen = (root) => spawnSync(process.execPath, [GEN], { cwd: root, encodin
|
|
|
168
169
|
fs.rmSync(root, { recursive: true, force: true });
|
|
169
170
|
}
|
|
170
171
|
|
|
172
|
+
// ---- 场景 7:--check 漂移判定(2026-10-10 wiki-index-drift:供 doctor §6 镜像 workflow INDEX 检查)----
|
|
173
|
+
// 不写盘;一致 exit 0,任一生成区漂移 exit 1 + 差异预览。行尾归一(CRLF clone 不假阳性)
|
|
174
|
+
{
|
|
175
|
+
const root = mkfix('\r\n');
|
|
176
|
+
// 先跑一次正规生成,把 INDEX/看板对齐到磁盘事实
|
|
177
|
+
const r0 = runGen(root);
|
|
178
|
+
check('场景 7:前置生成 exit 0', r0.status === 0, `exit=${r0.status}\n${r0.stdout}${r0.stderr}`);
|
|
179
|
+
// 对齐后 --check 须一致(CRLF INDEX 尤其不能因行尾假阳性)
|
|
180
|
+
const r1 = runGen(root, ['--check']);
|
|
181
|
+
check('场景 7a --check 对齐态 → exit 0 且提示一致',
|
|
182
|
+
r1.status === 0 && (r1.stdout || '').includes('一致'), `exit=${r1.status}\n${r1.stdout}${r1.stderr}`);
|
|
183
|
+
check('场景 7a --check 不写盘(INDEX 内容字节不变)',
|
|
184
|
+
fs.readFileSync(path.join(root, 'wiki', 'INDEX.md'), 'utf8').includes(USAGE_COL), '');
|
|
185
|
+
// 磁盘新增文件 → 生成区与磁盘不一致 → exit 1
|
|
186
|
+
fs.writeFileSync(path.join(root, 'wiki', '主题A', 'b.md'), '# b\n', 'utf8');
|
|
187
|
+
const r2 = runGen(root, ['--check']);
|
|
188
|
+
check('场景 7b --check 漂移(磁盘新增文件未重生成)→ exit 1',
|
|
189
|
+
r2.status === 1 && (r2.stderr || '').includes('不一致'), `exit=${r2.status}\n${r2.stdout}${r2.stderr}`);
|
|
190
|
+
check('场景 7b --check 不写盘(漂移时仍不覆盖磁盘文件)',
|
|
191
|
+
!fs.readFileSync(path.join(root, 'wiki', 'INDEX.md'), 'utf8').includes('| b.md | 主题A |'), '');
|
|
192
|
+
// 修复后 --check 恢复 exit 0
|
|
193
|
+
const r3 = runGen(root);
|
|
194
|
+
const r4 = runGen(root, ['--check']);
|
|
195
|
+
check('场景 7c 重生成后 --check 恢复 exit 0', r3.status === 0 && r4.status === 0, `exit3=${r3.status} exit4=${r4.status}\n${r4.stdout}${r4.stderr}`);
|
|
196
|
+
// 看板 DATA 被手改 → exit 1(与 INDEX 漂移同档)
|
|
197
|
+
const board = path.join(root, 'wiki', '知识沉淀总览.html');
|
|
198
|
+
fs.writeFileSync(board, fs.readFileSync(board, 'utf8').replace('"total": 2', '"total": 99'), 'utf8');
|
|
199
|
+
const r5 = runGen(root, ['--check']);
|
|
200
|
+
check('场景 7d --check 看板 DATA 漂移 → exit 1',
|
|
201
|
+
r5.status === 1 && (r5.stderr || '').includes('知识沉淀总览.html'), `exit=${r5.status}\n${r5.stdout}${r5.stderr}`);
|
|
202
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ---- 场景 8:草稿超龄 WARN(P1-2:drafts-archive/<YYYY-MM-DD-主题>/ 落盘超 14 天未归类 → stdout 提醒)----
|
|
206
|
+
// advisory 档只提醒不阻断:exit 0、INDEX/看板 DATA 不因 WARN 有任何变化;FLOW_KIT_TODAY 固定「今天」保证时间可控
|
|
207
|
+
{
|
|
208
|
+
const root = mkfix('\n');
|
|
209
|
+
const DRAFT_DIR = '2026-09-01-登录优化';
|
|
210
|
+
const DRAFT_DATE = '2026-09-01';
|
|
211
|
+
fs.mkdirSync(path.join(root, 'wiki', 'drafts-archive', DRAFT_DIR));
|
|
212
|
+
fs.writeFileSync(path.join(root, 'wiki', 'drafts-archive', DRAFT_DIR, '登录优化.md'), '# 登录优化草稿\n', 'utf8');
|
|
213
|
+
const board = path.join(root, 'wiki', '知识沉淀总览.html');
|
|
214
|
+
const warnLine = `[WARN 草稿超龄] drafts-archive/${DRAFT_DIR}/登录优化.md 落于 ${DRAFT_DATE} 未归类——草稿是中间态,归类到 wiki/登录优化/ 后跑 gen-wiki-board + verify-wiki-consistency`;
|
|
215
|
+
|
|
216
|
+
// a) 「今天」= 草稿日期 + 15 天(> 14 天档)→ WARN 整行点名该草稿
|
|
217
|
+
const ra = runGen(root, [], { FLOW_KIT_TODAY: '2026-09-16' });
|
|
218
|
+
check('场景 8a:超龄 15 天 → exit 0(advisory 不阻断)', ra.status === 0, `exit=${ra.status}\n${ra.stdout}${ra.stderr}`);
|
|
219
|
+
check('场景 8a:stdout 含超龄 WARN 且整行点名草稿', (ra.stdout || '').includes(warnLine), ra.stdout);
|
|
220
|
+
check('场景 8a:生成物不受影响(看板归档计数仍为文件数 1)', /"archive": 1/.test(fs.readFileSync(board, 'utf8')), '');
|
|
221
|
+
|
|
222
|
+
// b) 「今天」= 同日 / 恰 14 天 → 未超龄,无 WARN(超过才提醒)
|
|
223
|
+
const rb = runGen(root, [], { FLOW_KIT_TODAY: DRAFT_DATE });
|
|
224
|
+
check('场景 8b:同日 → 无超龄 WARN', rb.status === 0 && !(rb.stdout || '').includes('[WARN 草稿超龄]'), rb.stdout);
|
|
225
|
+
const rb14 = runGen(root, [], { FLOW_KIT_TODAY: '2026-09-15' });
|
|
226
|
+
check('场景 8b:恰 14 天 → 无超龄 WARN(边界)', rb14.status === 0 && !(rb14.stdout || '').includes('[WARN 草稿超龄]'), rb14.stdout);
|
|
227
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- 场景 9:目录名无日期前缀(人工建的目录)→ 静默跳过:不 WARN、不报错、生成物不受影响(P1-2)----
|
|
231
|
+
{
|
|
232
|
+
const root = mkfix('\n');
|
|
233
|
+
fs.mkdirSync(path.join(root, 'wiki', 'drafts-archive', '临时素材'));
|
|
234
|
+
fs.writeFileSync(path.join(root, 'wiki', 'drafts-archive', '临时素材', 'x.md'), '# x\n', 'utf8');
|
|
235
|
+
// 「今天」取远期(2027-01-01):若误把无日期目录判成超龄必触发 WARN,可当场暴露缺陷
|
|
236
|
+
const r = runGen(root, [], { FLOW_KIT_TODAY: '2027-01-01' });
|
|
237
|
+
const board = fs.readFileSync(path.join(root, 'wiki', '知识沉淀总览.html'), 'utf8');
|
|
238
|
+
check('场景 9:无日期前缀目录 → exit 0(不报错)', r.status === 0, `exit=${r.status}\n${r.stdout}${r.stderr}`);
|
|
239
|
+
check('场景 9:无超龄 WARN(静默跳过)', !(r.stdout || '').includes('[WARN 草稿超龄]'), r.stdout);
|
|
240
|
+
check('场景 9:看板输出不受影响(归档计数仍为文件数 1)', /"archive": 1/.test(board), '');
|
|
241
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
|
|
171
244
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
172
245
|
if (fail) {
|
|
173
246
|
console.log(`\n${USAGE}`);
|
|
@@ -119,7 +119,7 @@ export function qualityRows(root) {
|
|
|
119
119
|
const avg = Number.isFinite(m.avgRework) ? m.avgRework.toFixed(2) : '—';
|
|
120
120
|
// 门禁噪声返工单列(2026-10-08 selfmeasure-and-modularize):设计返工与噪声返工分列可诊断——
|
|
121
121
|
// 「平均返工」把两者平均在一起时看不出该改门禁还是改设计。
|
|
122
|
-
out.push(`| ${ym} | ${m.total} | ${pct(m.passRate)} | ${m.reworkSum} | ${m.reworkNoiseSum} | ${avg} | ${pct(m.fallbackRate)} | ${verdict} |`);
|
|
122
|
+
out.push(`| ${ym} | ${m.total} | ${pct(m.passRate)} | ${m.reworkSum} | ${m.reworkNoiseSum} | ${avg} | ${pct(m.fallbackRate)} | ${agg.fmtAccept(m)} | ${verdict} |`);
|
|
123
123
|
}
|
|
124
124
|
return { rows: out, verdictMap, months };
|
|
125
125
|
}
|
|
@@ -202,7 +202,7 @@ function main() {
|
|
|
202
202
|
'',
|
|
203
203
|
'## 质量 / 返工(月度,判据复用 agg-delegations)',
|
|
204
204
|
'',
|
|
205
|
-
'| 月份 | 有效任务 | 一次通过率 | 设计返工 | 门禁噪声返工 | 平均返工 | 主兜底占比 | 扩容门判定 |',
|
|
205
|
+
'| 月份 | 有效任务 | 一次通过率 | 设计返工 | 门禁噪声返工 | 平均返工 | 主兜底占比 | 产出接受率 | 扩容门判定 |',
|
|
206
206
|
'|------|----------|------------|---------|-------------|--------|------------|------------|',
|
|
207
207
|
...(q.rows.length ? q.rows : ['| — | — | — | — | — | — | — | 台账暂无数据 |']),
|
|
208
208
|
'',
|
|
@@ -53,8 +53,11 @@ const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
|
53
53
|
// 场景 1 即 exit 1(workflow-enums 单源文件不可读),而包源侧同字节跑出全绿。
|
|
54
54
|
// 改为**逐级上溯探测**:找到含 `.agents/workflow-enums.txt` 的那一级即用,找不到则回退标准位置让
|
|
55
55
|
// loadEnums fail-loud(保持「缺失即响亮」的既有纪律,不静默兜底)。
|
|
56
|
+
// 包源仓布局补丁(装户端内容清理后):纯包源仓无 `.agents/`,枚举单源在 `templates/_agents/` 下——
|
|
57
|
+
// 上一级目录名是 `_agents` 而非 `.agents`。探测同步认这两种布局,回退仍走 fail-loud。
|
|
56
58
|
const resolveEnumsPath = () => {
|
|
57
|
-
|
|
59
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
let dir = scriptDir;
|
|
58
61
|
for (let i = 0; i < 6; i++) {
|
|
59
62
|
const cand = path.join(dir, '.agents', 'workflow-enums.txt');
|
|
60
63
|
if (fs.existsSync(cand)) return cand;
|
|
@@ -62,7 +65,10 @@ const resolveEnumsPath = () => {
|
|
|
62
65
|
if (parent === dir) break; // 到根
|
|
63
66
|
dir = parent;
|
|
64
67
|
}
|
|
65
|
-
|
|
68
|
+
// 包源径:脚本在 templates/_agents/scripts/,枚举在 templates/_agents/workflow-enums.txt
|
|
69
|
+
const pkgSrc = path.join(scriptDir, '..', 'workflow-enums.txt');
|
|
70
|
+
if (fs.existsSync(pkgSrc)) return pkgSrc;
|
|
71
|
+
return path.join(scriptDir, '..', '..', '.agents', 'workflow-enums.txt');
|
|
66
72
|
};
|
|
67
73
|
const ENUMS = loadEnums(resolveEnumsPath());
|
|
68
74
|
const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
|
|
@@ -168,7 +174,7 @@ const idxText = fs.existsSync(path.join(WORKFLOW, 'INDEX.md')) ? fs.readFileSync
|
|
|
168
174
|
const idxBytes = Buffer.byteLength(idxText, 'utf8');
|
|
169
175
|
const idxActive = Number((idxText.match(/^## 活跃((\d+))/m) || [])[1] ?? NaN);
|
|
170
176
|
|
|
171
|
-
// ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics
|
|
177
|
+
// ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics 立法)----
|
|
172
178
|
// 只读台账一手字段(doc/stage/ts/source),坏行跳过(与检查 15/19 同容错);观测面不挂门禁、不新增文档断言(检查 16 零接触)。
|
|
173
179
|
const LEDGER_P = path.join('.agents', 'confirmations.jsonl');
|
|
174
180
|
function readLedgerRows() {
|
|
@@ -249,9 +255,9 @@ const TABLE_HEAD = [
|
|
|
249
255
|
'| 月份 | 文档(活跃/终态) | 文档字节 | 模块已填/总 | 常驻面字节(预算峰值) | INDEX(活跃行) |',
|
|
250
256
|
'|---|---|---|---|---|---|',
|
|
251
257
|
];
|
|
252
|
-
//
|
|
258
|
+
// 漏斗表头(第二表)
|
|
253
259
|
const FUNNEL_HEAD = [
|
|
254
|
-
'## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl
|
|
260
|
+
'## 闭环漏斗(机器口径,源:.agents/confirmations.jsonl)',
|
|
255
261
|
'',
|
|
256
262
|
'| 月份 | 收口 | 完整链 | 协议前 | 一次通过 | 返工件 | 中位周期(天) | 台账行数 |',
|
|
257
263
|
'|---|---|---|---|---|---|---|---|',
|