agentic-flow-kit 0.4.0 → 0.6.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 +3 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
- package/modules/hosts/omp/agents/implementer.md +47 -6
- package/modules/hosts/omp/agents/independent-reviewer.md +44 -5
- package/modules/hosts/omp/agents/ui-verifier.md +43 -6
- package/modules/hosts/opencode/agents/implementer.md +47 -1
- package/modules/hosts/opencode/agents/independent-reviewer.md +44 -1
- package/modules/hosts/opencode/agents/ui-verifier.md +43 -1
- package/modules/hosts/opencode/commands/build.md +74 -7
- package/modules/hosts/opencode/commands/deploy.md +51 -6
- package/modules/hosts/opencode/commands/design.md +36 -6
- package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
- package/modules/hosts/opencode/commands/maintain.md +70 -6
- package/modules/hosts/opencode/commands/new-task.md +59 -6
- package/modules/hosts/opencode/commands/plan.md +35 -6
- package/modules/hosts/opencode/commands/review.md +41 -6
- package/modules/hosts/opencode/commands/source-sync-check.md +89 -0
- package/modules/hosts/opencode/commands/sync-hosts.md +66 -0
- package/modules/hosts/opencode/commands/test.md +58 -8
- package/modules/hosts/trae/agents/implementer.md +46 -2
- package/modules/hosts/trae/agents/independent-reviewer.md +44 -1
- package/modules/hosts/trae/agents/ui-verifier.md +41 -3
- package/modules/hosts/trae/commands/wf-build.md +74 -7
- package/modules/hosts/trae/commands/wf-deploy.md +51 -6
- package/modules/hosts/trae/commands/wf-design.md +36 -6
- package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
- package/modules/hosts/trae/commands/wf-maintain.md +70 -6
- package/modules/hosts/trae/commands/wf-new-task.md +59 -6
- package/modules/hosts/trae/commands/wf-plan.md +35 -6
- package/modules/hosts/trae/commands/wf-review.md +41 -6
- package/modules/hosts/trae/commands/wf-source-sync-check.md +90 -0
- package/modules/hosts/trae/commands/wf-sync-hosts.md +67 -0
- package/modules/hosts/trae/commands/wf-test.md +58 -7
- package/modules/hosts/trae/hooks/post-edit-check.cjs +3 -2
- package/modules/hosts/trae/hooks/pre-shell-check.cjs +40 -9
- package/modules/hosts/trae/hooks.json +1 -1
- package/modules/hosts/zcode/agents/implementer.md +47 -6
- package/modules/hosts/zcode/agents/independent-reviewer.md +44 -5
- package/modules/hosts/zcode/agents/ui-verifier.md +43 -5
- package/package.json +6 -3
- package/src/add-gate.mjs +5 -3
- package/src/cli.mjs +15 -2
- package/src/doctor.mjs +245 -15
- package/src/gate-dotnet-ca.test.mjs +124 -0
- package/src/init.mjs +79 -17
- package/src/init.test.mjs +84 -2
- package/src/pack.test.mjs +24 -0
- package/src/render.mjs +64 -4
- package/src/run-tests.mjs +7 -11
- package/src/sync-hosts.mjs +194 -0
- package/src/sync-hosts.test.mjs +170 -0
- package/src/sync.mjs +65 -20
- package/src/sync.test.mjs +52 -0
- package/src/trae-hooks.test.mjs +115 -0
- package/templates/AGENTS.md +2 -1
- package/templates/_agents/board/index.html +32 -3
- package/templates/_agents/commands/build.md +22 -1
- package/templates/_agents/commands/design.md +2 -0
- package/templates/_agents/commands/gate-checklist.md +91 -0
- package/templates/_agents/commands/maintain.md +2 -3
- package/templates/_agents/commands/plan.md +4 -1
- package/templates/_agents/commands/review.md +1 -0
- package/templates/_agents/commands/source-sync-check.md +103 -0
- package/templates/_agents/commands/sync-hosts.md +79 -0
- package/templates/_agents/commands/test.md +4 -2
- package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
- package/templates/_agents/notes/runtime-env.md +1 -1
- package/templates/_agents/roles/implementer.md +2 -0
- package/templates/_agents/roles/independent-reviewer.md +2 -0
- package/templates/_agents/roles/ui-verifier.md +2 -0
- package/templates/_agents/rule-budgets.txt +1 -1
- package/templates/_agents/scripts/agg-delegations.cjs +39 -30
- package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
- package/templates/_agents/scripts/check-loop.mjs +763 -0
- package/templates/_agents/scripts/check-loop.sh +5 -808
- package/templates/_agents/scripts/check-loop.test.mjs +1102 -0
- package/templates/_agents/scripts/confirm-doc.mjs +228 -0
- package/templates/_agents/scripts/confirm-doc.test.mjs +321 -0
- package/templates/_agents/scripts/doctor.test.mjs +371 -0
- package/templates/_agents/scripts/ensure-board.mjs +30 -3
- package/templates/_agents/scripts/fill-intent.mjs +59 -0
- package/templates/_agents/scripts/fill-intent.test.mjs +73 -0
- package/templates/_agents/scripts/fill-plan.mjs +60 -0
- package/templates/_agents/scripts/fill-plan.test.mjs +67 -0
- package/templates/_agents/scripts/fill-spec.mjs +52 -0
- package/templates/_agents/scripts/fill-spec.test.mjs +61 -0
- package/templates/_agents/scripts/gate-checklist.mjs +194 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +204 -0
- package/templates/_agents/scripts/gen-wiki-board.mjs +38 -24
- package/templates/_agents/scripts/gen-wiki-board.test.mjs +18 -0
- package/templates/_agents/scripts/gen-workflow-index.mjs +4 -2
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +12 -3
- package/templates/_agents/scripts/kb-search.mjs +25 -9
- package/templates/_agents/scripts/kb-search.test.mjs +38 -8
- package/templates/_agents/scripts/source-sync-check.mjs +145 -0
- package/templates/_agents/scripts/source-sync-check.test.mjs +244 -0
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
- package/templates/_agents/scripts/verify.mjs +8 -10
- package/templates/_agents/scripts/verify.test.mjs +13 -1
- package/templates/_agents/scripts/wf-journal.mjs +158 -0
- package/templates/_agents/scripts/wf-journal.test.mjs +150 -0
- package/templates/_agents/scripts/workflow-board-server.mjs +73 -17
- package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
- package/templates/_agents/scripts/workflow-enums.mjs +70 -0
- package/templates/_agents/scripts/workflow-enums.test.mjs +110 -0
- package/templates/_agents/scripts/workflows-check.mjs +260 -0
- package/templates/_agents/scripts/workflows-check.test.mjs +267 -0
- package/templates/_agents/workflow-enums.txt +19 -0
- package/templates/_agents/workflows/_TEMPLATE.md +75 -0
- package/templates/_agents/workflows/pipeline-closing.md +29 -0
- package/templates/_agents/workflows/source-sync-repair.md +30 -0
- package/templates/_agents/workflows/steps//347/244/272/344/276/213-/351/203/250/347/275/262/351/252/214/350/257/201.md +10 -0
- package/templates/_agents/workflows//347/244/272/344/276/213-/345/271/266/350/241/214/345/256/236/347/216/260/350/257/204/345/256/241.md +16 -0
- package/templates/_githooks/pre-commit +16 -1
- package/templates/workflow/README.md +1 -1
- package/templates/_agents/cache/kb-index.json +0 -1
- package/templates/_agents/scripts/check-loop.test.sh +0 -1042
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// check-loop.test.mjs — 闭环门禁 fixture 套件(2026-09-26 check-loop-node:自 check-loop.test.sh 全量移植)
|
|
3
|
+
// 方法:现场构造临时 workflow 根(含枚举单源内联件),spawn node check-loop.mjs(CHECK_LOOP_ROOT 注入,
|
|
4
|
+
// git 模式场景改 cwd 注入),断言 exit code 与输出关键词——场景集与关键词断言沿 sh 版逐条移植。
|
|
5
|
+
// git 场景用 git init 现场建仓;rule-budget --staged 两场景直调 sh(rule-budget.sh 仍为 sh——
|
|
6
|
+
// git 钩子环境保证;无 sh 环境打印 SKIP 不算失败)。
|
|
7
|
+
// 用法:node templates/_agents/scripts/check-loop.test.mjs(npm test 随跑)
|
|
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 { computeFingerprint } from './confirm-doc.mjs';
|
|
14
|
+
|
|
15
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
17
|
+
|
|
18
|
+
let pass = 0;
|
|
19
|
+
let fail = 0;
|
|
20
|
+
const check = (desc, cond, detail = '') => {
|
|
21
|
+
if (cond) { pass++; console.log(`PASS ${desc}`); }
|
|
22
|
+
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// mkfix:空 fixture 根(四目录 + 枚举单源内联 8 键——fixture 自包含,测消费逻辑而非当前词表值)
|
|
26
|
+
const ENUMS_FIXTURE = [
|
|
27
|
+
'doc.status.all=draft approved done superseded cancelled',
|
|
28
|
+
'doc.status.confirmed=approved done superseded cancelled',
|
|
29
|
+
'doc.status.active=draft approved',
|
|
30
|
+
'doc.status.terminal=done superseded cancelled',
|
|
31
|
+
'doc.status.abandoned=superseded cancelled',
|
|
32
|
+
'incident.status.all=open fixed closed',
|
|
33
|
+
'incident.status.active=open',
|
|
34
|
+
'level.all=L0 L1 L2 L3',
|
|
35
|
+
].join('\n') + '\n';
|
|
36
|
+
|
|
37
|
+
const mkfix = () => {
|
|
38
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'check-loop-test-'));
|
|
39
|
+
for (const s of ['intents', 'specs', 'plans', 'incidents']) {
|
|
40
|
+
fs.mkdirSync(path.join(d, 'workflow', s), { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(d, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE);
|
|
44
|
+
return d;
|
|
45
|
+
};
|
|
46
|
+
const w = (root, rel, content) => fs.writeFileSync(path.join(root, rel), content);
|
|
47
|
+
|
|
48
|
+
const INTENT = (name, fm, body = '') => `---\n${fm}\n---\n# INTENT — ${name}\n${body}`;
|
|
49
|
+
const PLAN = (name, fm, body = '') => `---\n${fm}\n---\n# PLAN — ${name}\n${body}`;
|
|
50
|
+
const SPEC = (name, fm, body = '') => `---\n${fm}\n---\n# SPEC — ${name}\n${body}`;
|
|
51
|
+
const 三件套 = (refs) => `## 复盘三件套(缺一不可)
|
|
52
|
+
|
|
53
|
+
1. 结构性修复
|
|
54
|
+
- 修复 commit:abc1234
|
|
55
|
+
- 影响环境:dev
|
|
56
|
+
- 是否需要新 intent:
|
|
57
|
+
${refs ? ' - 是 → ../intents/2026-09-12-ghost.md\n' : ' - 否 → 理由:实现 bug 单点修复\n'}
|
|
58
|
+
2. 防复发验证
|
|
59
|
+
- 回归清单条目在案
|
|
60
|
+
|
|
61
|
+
3. 规范条目
|
|
62
|
+
- AGENTS.md 某节
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
// run:默认 CHECK_LOOP_ROOT 注入(fixture 模式);{git:true} 时 cwd 注入(真 git 模式,测 tracked 过滤)
|
|
66
|
+
const run = (root, opts = {}) => spawnSync(process.execPath, [CHECK_LOOP], {
|
|
67
|
+
...(opts.git ? { cwd: root } : { cwd: ROOT_CWD, env: { ...process.env, CHECK_LOOP_ROOT: root } }),
|
|
68
|
+
encoding: 'utf8',
|
|
69
|
+
});
|
|
70
|
+
const ROOT_CWD = process.cwd();
|
|
71
|
+
const outOf = (r) => `${r.stdout || ''}${r.stderr || ''}`;
|
|
72
|
+
const expectHard = (desc, root, kw) => {
|
|
73
|
+
const r = run(root);
|
|
74
|
+
check(desc, r.status === 1 && outOf(r).includes(kw), `exit=${r.status}\n${outOf(r)}`);
|
|
75
|
+
};
|
|
76
|
+
const expectOk = (desc, root) => {
|
|
77
|
+
const r = run(root);
|
|
78
|
+
check(desc, r.status === 0, `exit=${r.status}\n${outOf(r)}`);
|
|
79
|
+
};
|
|
80
|
+
const gitInit = (root) => {
|
|
81
|
+
spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', ['init', '-q'], { cwd: root });
|
|
82
|
+
};
|
|
83
|
+
const gitCommitAll = (root, msg) => {
|
|
84
|
+
const G = process.platform === 'win32' ? 'git.exe' : 'git';
|
|
85
|
+
spawnSync(G, ['add', '-A'], { cwd: root });
|
|
86
|
+
spawnSync(G, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', msg], { cwd: root });
|
|
87
|
+
};
|
|
88
|
+
const rmfix = (root) => fs.rmSync(root, { recursive: true, force: true });
|
|
89
|
+
// writeLedger:fixture 台账辅助(检查 15 配对用;行 schema 同 confirm-doc.mjs appendLedger)
|
|
90
|
+
const writeLedger = (root, entries) => {
|
|
91
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
92
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.jsonl'),
|
|
93
|
+
entries.map((e) => JSON.stringify(e)).join('\n') + '\n');
|
|
94
|
+
};
|
|
95
|
+
// mkConfirmedDoc:落一份「带真实指纹 + 台账配对」的文档(复刻 confirm-doc 落态形态)
|
|
96
|
+
const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
97
|
+
const noFp = `---\n${fmBody}\n---\n# DOC\n`;
|
|
98
|
+
const fp = computeFingerprint(noFp);
|
|
99
|
+
fs.writeFileSync(path.join(root, rel), `---\n${fmBody}\n确认指纹: ${fp.slice(0, 16)}\n---\n# DOC\n`);
|
|
100
|
+
return { rel: rel.replace(/\\/g, '/'), fp };
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ---- 场景 1:全合法闭环(intent+plan,L1,done 全勾验带证据)→ exit 0 ----
|
|
104
|
+
{
|
|
105
|
+
const T = mkfix();
|
|
106
|
+
w(T, 'workflow/intents/2026-09-12-ok.md', INTENT('ok', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:dotnet test 全绿)\n'));
|
|
107
|
+
w(T, 'workflow/plans/2026-09-12-ok.md', PLAN('ok', '状态: done\n级别: L1'));
|
|
108
|
+
expectOk('全合法闭环(L1 done 勾验带证据) → exit 0', T);
|
|
109
|
+
rmfix(T);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ---- 场景 2:intent 缺 plan → hard ----
|
|
113
|
+
{
|
|
114
|
+
const T = mkfix();
|
|
115
|
+
w(T, 'workflow/intents/2026-09-12-a.md', INTENT('a', '状态: approved\n级别: L1\n日期: 2026-09-12'));
|
|
116
|
+
expectHard('intent 缺 plan → hard 配对断裂', T, '配对断裂');
|
|
117
|
+
rmfix(T);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---- 场景 3:L2 intent 缺 spec → hard ----
|
|
121
|
+
{
|
|
122
|
+
const T = mkfix();
|
|
123
|
+
w(T, 'workflow/intents/2026-09-12-b.md', INTENT('b', '状态: approved\n级别: L2\n日期: 2026-09-12'));
|
|
124
|
+
w(T, 'workflow/plans/2026-09-12-b.md', PLAN('b', '状态: approved\n级别: L2'));
|
|
125
|
+
expectHard('L2 intent 缺 spec → hard 配对断裂', T, 'intent 缺 spec');
|
|
126
|
+
rmfix(T);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---- 场景 4:L3 spec 缺确认三件 → hard ----
|
|
130
|
+
{
|
|
131
|
+
const T = mkfix();
|
|
132
|
+
w(T, 'workflow/intents/2026-09-12-c.md', INTENT('c', '状态: approved\n级别: L3\n日期: 2026-09-12'));
|
|
133
|
+
w(T, 'workflow/specs/2026-09-12-c.md', SPEC('c', '状态: approved\n级别: L3', '\n## 确认与复核\n'));
|
|
134
|
+
w(T, 'workflow/plans/2026-09-12-c.md', PLAN('c', '状态: approved\n级别: L3'));
|
|
135
|
+
expectHard('L3 spec 缺确认三件 → hard', T, 'L3 确认缺失');
|
|
136
|
+
rmfix(T);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- 场景 5:L3 spec 确认三件齐全 → exit 0 ----
|
|
140
|
+
{
|
|
141
|
+
const T = mkfix();
|
|
142
|
+
w(T, 'workflow/intents/2026-09-12-d.md', INTENT('d', '状态: approved\n级别: L3\n日期: 2026-09-12'));
|
|
143
|
+
w(T, 'workflow/specs/2026-09-12-d.md', SPEC('d', '状态: approved\n级别: L3\n确认结果: approved\n确认时间: 2026-09-12', '\n## 确认与复核\n- 独立复核:已由 independent-reviewer 新会话完成,结论 approved\n'));
|
|
144
|
+
w(T, 'workflow/plans/2026-09-12-d.md', PLAN('d', '状态: approved\n级别: L3'));
|
|
145
|
+
expectOk('L3 确认三件齐全(frontmatter 2 件+正文独立复核) → exit 0', T);
|
|
146
|
+
rmfix(T);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- 场景 6:新建 done intent 验收未勾验 → hard;带附注不影响判定(papercut #4 回归)----
|
|
150
|
+
// 换锚(2026-09-28 check8-git-anchor)后「新建」由 **git 首次加入日期**判定(文件名字段不再作锚),
|
|
151
|
+
// 故本场景须为真 git 仓——否则不可判定、走存量口径而不再 hard。**意图与断言强度均不变**。
|
|
152
|
+
{
|
|
153
|
+
const T = mkfix();
|
|
154
|
+
gitInit(T);
|
|
155
|
+
w(T, 'workflow/intents/2026-09-13-e.md', INTENT('e', '状态: done\n级别: L1\n日期: 2026-09-13\n备注: 回溯补档,原工作 2026-09-12 完成', '\n## 验收标准(可测试)\n- [ ] 用例通过\n'));
|
|
156
|
+
w(T, 'workflow/plans/2026-09-13-e.md', PLAN('e', '状态: done\n级别: L1'));
|
|
157
|
+
gitCommitAll(T, 'add');
|
|
158
|
+
expectHard('新建 done 未勾验(附注在备注键) → hard 验收未对账', T, '验收未对账');
|
|
159
|
+
rmfix(T);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---- 场景 7:存量(<2026-09-12) done 未勾验 → warning,exit 0 ----
|
|
163
|
+
{
|
|
164
|
+
const T = mkfix();
|
|
165
|
+
w(T, 'workflow/intents/2026-09-01-f.md', INTENT('f', '状态: done\n级别: L1\n日期: 2026-09-01', '\n## 验收标准(可测试)\n- [ ] 用例通过\n'));
|
|
166
|
+
w(T, 'workflow/plans/2026-09-01-f.md', PLAN('f', '状态: done\n级别: L1'));
|
|
167
|
+
expectOk('存量 done 未勾验 → warning 不阻断', T);
|
|
168
|
+
rmfix(T);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ---- 场景 8:incident 选「是」但 intent 不存在 → hard 回路断档 ----
|
|
172
|
+
{
|
|
173
|
+
const T = mkfix();
|
|
174
|
+
w(T, 'workflow/incidents/2026-09-12-g.md', `---\n状态: fixed\n级别: L2\n发现: 2026-09-12\n---\n# INCIDENT — g\n\n${三件套(true)}`);
|
|
175
|
+
w(T, 'workflow/plans/2026-09-12-g.md', PLAN('g', '状态: done\n级别: L2'));
|
|
176
|
+
w(T, 'workflow/specs/2026-09-12-g.md', SPEC('g', '状态: approved\n级别: L2'));
|
|
177
|
+
expectHard('incident 回路引用不存在 intent → hard 回路断档', T, '回路断档');
|
|
178
|
+
rmfix(T);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---- 场景 9:incident 流程 legacy → 豁免级别/配对检查,exit 0 ----
|
|
182
|
+
{
|
|
183
|
+
const T = mkfix();
|
|
184
|
+
w(T, 'workflow/incidents/2026-08-27-h.md', `---\n状态: closed\n流程: legacy\n发现: 2026-08-27\n---\n# INCIDENT — h(legacy 回填,无级别无配对)\n\n${三件套(false)}`);
|
|
185
|
+
expectOk('incident legacy 豁免配对 → exit 0', T);
|
|
186
|
+
rmfix(T);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ---- 场景 10:无 frontmatter → 状态键缺失 warning,exit 0(非 L3) ----
|
|
190
|
+
{
|
|
191
|
+
const T = mkfix();
|
|
192
|
+
w(T, 'workflow/intents/2026-09-12-i.md', '# INTENT — i(未迁移旧格式)\n\n状态:draft\n');
|
|
193
|
+
w(T, 'workflow/plans/2026-09-12-i.md', PLAN('i', '状态: approved\n级别: L1'));
|
|
194
|
+
const r = run(T);
|
|
195
|
+
check('无 frontmatter → warning 提示状态键缺失', r.status === 0 && /WARN 状态未确认.*i\.md/.test(outOf(r)), outOf(r));
|
|
196
|
+
rmfix(T);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---- 场景 11:frontmatter 状态值域外(自造值)→ warning 带当前值,exit 0 ----
|
|
200
|
+
{
|
|
201
|
+
const T = mkfix();
|
|
202
|
+
w(T, 'workflow/intents/2026-09-12-j.md', INTENT('j', '状态: 进行中\n级别: L1\n日期: 2026-09-12'));
|
|
203
|
+
w(T, 'workflow/plans/2026-09-12-j.md', PLAN('j', '状态: approved\n级别: L1'));
|
|
204
|
+
const r = run(T);
|
|
205
|
+
check('状态值域外 → warning 且带当前值提示', r.status === 0 && /WARN 状态未确认.*j\.md.*进行中/.test(outOf(r)), outOf(r));
|
|
206
|
+
rmfix(T);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---- 场景 12:无 frontmatter 的旧格式 L3 声明 → warning 引导补 frontmatter(不回退读正文,双源防回归)----
|
|
210
|
+
{
|
|
211
|
+
const T = mkfix();
|
|
212
|
+
for (const s of ['intents', 'plans', 'specs']) {
|
|
213
|
+
w(T, `workflow/${s}/2026-09-12-k.md`, `# ${s.toUpperCase().slice(0, -1)} — k\n\n级别:L3\n状态:approved\n`);
|
|
214
|
+
}
|
|
215
|
+
const r = run(T);
|
|
216
|
+
check('无 frontmatter 旧格式 → warning 引导补 frontmatter(不回退读正文)',
|
|
217
|
+
r.status === 0 && /WARN 状态未确认.*k\.md(frontmatter 状态键当前值:『缺失』)/.test(outOf(r)), outOf(r));
|
|
218
|
+
rmfix(T);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- 场景 13:git fixture——L1 intent 与迁移 SQL 同 commit → warning 疑似判低(检查 10)----
|
|
222
|
+
{
|
|
223
|
+
const T = mkfix();
|
|
224
|
+
gitInit(T);
|
|
225
|
+
w(T, 'workflow/intents/2026-09-12-m.md', INTENT('m', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 场景构造项(证据:fixture)\n'));
|
|
226
|
+
w(T, 'workflow/plans/2026-09-12-m.md', PLAN('m', '状态: done\n级别: L1'));
|
|
227
|
+
fs.mkdirSync(path.join(T, 'docs', 'scripts'), { recursive: true });
|
|
228
|
+
w(T, 'docs/scripts/01-add-table.sql', 'CREATE TABLE t(id INT);\n');
|
|
229
|
+
gitCommitAll(T, 'test');
|
|
230
|
+
const r = run(T);
|
|
231
|
+
check('L1 入口文档提交触及迁移 SQL → warning 疑似判低', r.status === 0 && /WARN 级别疑似判低.*m\.md/.test(outOf(r)), outOf(r));
|
|
232
|
+
rmfix(T);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---- 场景 14:git fixture——L3 intent 与迁移 SQL 同 commit → 级别一致不告警 ----
|
|
236
|
+
{
|
|
237
|
+
const T = mkfix();
|
|
238
|
+
gitInit(T);
|
|
239
|
+
w(T, 'workflow/intents/2026-09-12-n.md', INTENT('n', '状态: done\n级别: L3\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 场景构造项(证据:fixture)\n'));
|
|
240
|
+
w(T, 'workflow/specs/2026-09-12-n.md', SPEC('n', '状态: done\n级别: L3\n确认结果: approved\n确认时间: 2026-09-12', '\n## 确认与复核\n- 独立复核:已由 independent-reviewer 新会话完成,结论 approved\n'));
|
|
241
|
+
w(T, 'workflow/plans/2026-09-12-n.md', PLAN('n', '状态: done\n级别: L3'));
|
|
242
|
+
fs.mkdirSync(path.join(T, 'docs', 'scripts'), { recursive: true });
|
|
243
|
+
w(T, 'docs/scripts/01-add-table.sql', 'CREATE TABLE t(id INT);\n');
|
|
244
|
+
gitCommitAll(T, 'test');
|
|
245
|
+
const r = run(T);
|
|
246
|
+
check('L3 入口文档提交触及迁移 SQL → 级别一致不告警', r.status === 0 && !outOf(r).includes('级别疑似判低'), outOf(r));
|
|
247
|
+
rmfix(T);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---- 场景 15:ZCode Adapter 缺失不告警(本地配置不入 git),存在且断线仍告警 ----
|
|
251
|
+
{
|
|
252
|
+
const T = mkfix();
|
|
253
|
+
w(T, 'workflow/intents/2026-09-12-o.md', INTENT('o', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:全绿)\n'));
|
|
254
|
+
w(T, 'workflow/plans/2026-09-12-o.md', PLAN('o', '状态: done\n级别: L1'));
|
|
255
|
+
fs.mkdirSync(path.join(T, '.agents', 'roles'), { recursive: true });
|
|
256
|
+
fs.mkdirSync(path.join(T, '.zcode', 'agents'), { recursive: true });
|
|
257
|
+
for (const r of ['implementer', 'independent-reviewer', 'ui-verifier']) w(T, `.agents/roles/${r}.md`, `# role ${r}\n`);
|
|
258
|
+
w(T, '.zcode/agents/implementer.md', '---\nname: implementer\n---\n断线内容,无角色引用\n');
|
|
259
|
+
const r = run(T);
|
|
260
|
+
check('ZCode 缺失跳过/存在断线仍告警',
|
|
261
|
+
r.status === 0 && outOf(r).includes('ZCode implementer 未引用公共角色') && !outOf(r).includes('ZCode 缺'), outOf(r));
|
|
262
|
+
rmfix(T);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ---- 场景 16:skills 引用仓库内与用户级均不存在 → 仍报引用断档 ----
|
|
266
|
+
{
|
|
267
|
+
const T = mkfix();
|
|
268
|
+
w(T, 'workflow/README.md', '# WF README\n\n技能见 .agents/skills/fake-skill(仓库内与用户级均不存在)\n');
|
|
269
|
+
w(T, 'workflow/intents/2026-09-12-p.md', INTENT('p', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:全绿)\n'));
|
|
270
|
+
w(T, 'workflow/plans/2026-09-12-p.md', PLAN('p', '状态: done\n级别: L1'));
|
|
271
|
+
const r = run(T);
|
|
272
|
+
check('skills 引用仓库内与用户级均无 → 仍报引用断档', r.status === 0 && /引用断档.*fake-skill/.test(outOf(r)), outOf(r));
|
|
273
|
+
rmfix(T);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ---- 场景 17:存量 done 含「存量对账豁免」声明 → 出账,无验收对账 warning ----
|
|
277
|
+
{
|
|
278
|
+
const T = mkfix();
|
|
279
|
+
w(T, 'workflow/intents/2026-09-01-q.md', INTENT('q', '状态: done\n级别: L1\n日期: 2026-09-01', '\n## 验收标准(可测试)\n> **存量对账豁免(2026-09-12,用户拍板不追溯)**:下方未勾项均为第二轮 UI/浏览器实测项。\n- [ ] 用例通过\n'));
|
|
280
|
+
w(T, 'workflow/plans/2026-09-01-q.md', PLAN('q', '状态: done\n级别: L1'));
|
|
281
|
+
const r = run(T);
|
|
282
|
+
check('存量含豁免声明 → 出账不计未对账', r.status === 0 && !outOf(r).includes('验收对账存量'), outOf(r));
|
|
283
|
+
rmfix(T);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ---- 场景 18:新建 done intent 缺「## 验收标准」节 → hard(2026-09-16 补口回归)----
|
|
287
|
+
{
|
|
288
|
+
const T = mkfix();
|
|
289
|
+
gitInit(T);
|
|
290
|
+
w(T, 'workflow/intents/2026-09-13-r.md', INTENT('r', '状态: done\n级别: L1\n日期: 2026-09-13', '\n## 目标\n关单前未写验收标准节。\n'));
|
|
291
|
+
w(T, 'workflow/plans/2026-09-13-r.md', PLAN('r', '状态: done\n级别: L1'));
|
|
292
|
+
gitCommitAll(T, 'add');
|
|
293
|
+
expectHard('新建 done 缺验收标准节 → hard 验收未对账', T, '缺「## 验收标准」节');
|
|
294
|
+
rmfix(T);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---- 场景 19:新建 done 勾选项缩进写法 → hard(口径放宽后仍拦)----
|
|
298
|
+
{
|
|
299
|
+
const T = mkfix();
|
|
300
|
+
gitInit(T);
|
|
301
|
+
w(T, 'workflow/intents/2026-09-13-s.md', INTENT('s', '状态: done\n级别: L1\n日期: 2026-09-13', '\n## 验收标准(可测试)\n - [ ] 缩进未勾项(此前后静默通过)\n'));
|
|
302
|
+
w(T, 'workflow/plans/2026-09-13-s.md', PLAN('s', '状态: done\n级别: L1'));
|
|
303
|
+
gitCommitAll(T, 'add');
|
|
304
|
+
expectHard('新建 done 勾选项缩进 → hard 验收未对账', T, '验收未对账');
|
|
305
|
+
rmfix(T);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ---- 场景 20:incident 状态值域外 → warning 带当前值 ----
|
|
309
|
+
{
|
|
310
|
+
const T = mkfix();
|
|
311
|
+
w(T, 'workflow/incidents/2026-09-13-t.md', `---\n状态: 已修复\n级别: L1\n发现: 2026-09-13\n---\n# INCIDENT — t\n\n${三件套(false)}`);
|
|
312
|
+
w(T, 'workflow/plans/2026-09-13-t.md', PLAN('t', '状态: done\n级别: L1'));
|
|
313
|
+
const r = run(T);
|
|
314
|
+
check('incident 状态值域外 → warning 且带当前值', r.status === 0 && /WARN 状态非法.*t\.md.*已修复/.test(outOf(r)), outOf(r));
|
|
315
|
+
rmfix(T);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---- 场景 21:incident 缺级别 → warning 明示配对检查已跳过 ----
|
|
319
|
+
{
|
|
320
|
+
const T = mkfix();
|
|
321
|
+
w(T, 'workflow/incidents/2026-09-13-u.md', `---\n状态: open\n发现: 2026-09-13\n---\n# INCIDENT — u\n\n${三件套(false)}`);
|
|
322
|
+
const r = run(T);
|
|
323
|
+
check('incident 缺级别 → warning 明示配对已跳过', r.status === 0 && /WARN 级别缺失.*u\.md.*配对检查已跳过/.test(outOf(r)), outOf(r));
|
|
324
|
+
rmfix(T);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- 场景 22:存量 done 缺验收标准节 → 聚合 warning 不 hard ----
|
|
328
|
+
{
|
|
329
|
+
const T = mkfix();
|
|
330
|
+
w(T, 'workflow/intents/2026-09-01-v.md', INTENT('v', '状态: done\n级别: L1\n日期: 2026-09-01', '\n## 目标\n存量旧格式,缺验收标准节。\n'));
|
|
331
|
+
w(T, 'workflow/plans/2026-09-01-v.md', PLAN('v', '状态: done\n级别: L1'));
|
|
332
|
+
const r = run(T);
|
|
333
|
+
check('存量 done 缺节 → 聚合 warning 不 hard', r.status === 0 && outOf(r).includes('验收对账存量'), outOf(r));
|
|
334
|
+
rmfix(T);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ---- 场景 23/24:git 仓库模式——未跟踪并行半成品不拦 push;提交进 HEAD 后仍 hard ----
|
|
338
|
+
{
|
|
339
|
+
const T = mkfix();
|
|
340
|
+
gitInit(T);
|
|
341
|
+
w(T, 'workflow/intents/2026-09-12-ok2.md', INTENT('ok2', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 场景构造项(证据:fixture)\n'));
|
|
342
|
+
w(T, 'workflow/plans/2026-09-12-ok2.md', PLAN('ok2', '状态: done\n级别: L1'));
|
|
343
|
+
gitCommitAll(T, 'base');
|
|
344
|
+
w(T, 'workflow/intents/2026-09-18-parallel-wip.md', INTENT('并行会话半成品', '状态: draft\n级别: L1\n日期: 2026-09-18'));
|
|
345
|
+
let r = run(T, { git: true });
|
|
346
|
+
check('仓库模式:未跟踪 draft intent(无 plan)不进扫描,不拦 push', r.status === 0 && !outOf(r).includes('parallel-wip'), `exit=${r.status}\n${outOf(r)}`);
|
|
347
|
+
gitCommitAll(T, 'wip');
|
|
348
|
+
r = run(T, { git: true });
|
|
349
|
+
check('仓库模式:已提交的入口缺 plan 断档仍 hard-block', r.status === 1 && /缺 plan.*parallel-wip/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
350
|
+
rmfix(T);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- 场景 25:检索层——模块合法 + INDEX 一致 → exit 0 无新告警(拷真实生成器+读取器+词表测接线)----
|
|
354
|
+
{
|
|
355
|
+
const T = mkfix();
|
|
356
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
357
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'gen-workflow-index.mjs'), path.join(T, '.agents', 'scripts', 'gen-workflow-index.mjs'));
|
|
358
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'workflow-enums.mjs'), path.join(T, '.agents', 'scripts', 'workflow-enums.mjs'));
|
|
359
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
360
|
+
w(T, 'workflow/intents/2026-09-22-mod-ok.md', INTENT('mod ok', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline', '\n## 验收标准(可测试)\n- [ ] 用例通过\n'));
|
|
361
|
+
w(T, 'workflow/plans/2026-09-22-mod-ok.md', PLAN('mod ok', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
362
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
363
|
+
const r = run(T);
|
|
364
|
+
check('检索层:模块合法+INDEX 一致 → exit 0 无新告警',
|
|
365
|
+
r.status === 0 && !outOf(r).includes('WARN 模块元数据') && !outOf(r).includes('WARN 索引漂移'), outOf(r));
|
|
366
|
+
rmfix(T);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ---- 场景 26:模块枚举非法 + 2026-09-22 起新建缺字段 → WARN 不阻断 ----
|
|
370
|
+
{
|
|
371
|
+
const T = mkfix();
|
|
372
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
373
|
+
w(T, 'workflow/intents/2026-09-22-mod-bad.md', INTENT('mod bad', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: 不存在的模块'));
|
|
374
|
+
w(T, 'workflow/plans/2026-09-22-mod-bad.md', PLAN('mod bad', '状态: approved\n级别: L1'));
|
|
375
|
+
const r = run(T);
|
|
376
|
+
const o = outOf(r);
|
|
377
|
+
check('检索层:模块非法/缺字段 → WARN 且 exit 0',
|
|
378
|
+
r.status === 0 && o.includes('WARN 模块元数据') && o.includes('不在词表') && o.includes('缺「模块:」字段'), o);
|
|
379
|
+
rmfix(T);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---- 场景 27:INDEX 漂移 → WARN 索引漂移,exit 0(调生成器 --check 口径)----
|
|
383
|
+
{
|
|
384
|
+
const T = mkfix();
|
|
385
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
386
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'gen-workflow-index.mjs'), path.join(T, '.agents', 'scripts', 'gen-workflow-index.mjs'));
|
|
387
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'workflow-enums.mjs'), path.join(T, '.agents', 'scripts', 'workflow-enums.mjs'));
|
|
388
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
389
|
+
w(T, 'workflow/intents/2026-09-22-drift.md', INTENT('drift', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
390
|
+
w(T, 'workflow/plans/2026-09-22-drift.md', PLAN('drift', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
391
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
392
|
+
w(T, 'workflow/plans/2026-09-22-drift.md', PLAN('drift', '状态: done\n级别: L1\n模块: pipeline')); // 生成后改状态 → 漂移
|
|
393
|
+
const r = run(T);
|
|
394
|
+
check('检索层:INDEX 漂移 → WARN 且 exit 0', r.status === 0 && outOf(r).includes('WARN 索引漂移'), outOf(r));
|
|
395
|
+
rmfix(T);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// ---- 场景 28/29:常驻面预算——合规无告警;AGENTS.md 超限 → WARN 含一进一出 ----
|
|
399
|
+
{
|
|
400
|
+
const T = mkfix();
|
|
401
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
402
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
|
|
403
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'rule-budgets.txt'), path.join(T, '.agents', 'rule-budgets.txt'));
|
|
404
|
+
w(T, 'AGENTS.md', '# AGENTS 小体积 fixture\n');
|
|
405
|
+
fs.mkdirSync(path.join(T, '.agents', 'commands'), { recursive: true });
|
|
406
|
+
w(T, '.agents/commands/plan.md', '# 小命令\n');
|
|
407
|
+
w(T, 'workflow/intents/2026-09-22-budget-ok.md', INTENT('budget ok', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
408
|
+
w(T, 'workflow/plans/2026-09-22-budget-ok.md', PLAN('budget ok', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
409
|
+
let r = run(T);
|
|
410
|
+
check('常驻面预算:合规 → 无超限告警且 exit 0', r.status === 0 && !outOf(r).includes('WARN 常驻面超限'), outOf(r));
|
|
411
|
+
w(T, 'AGENTS.md', 'x'.repeat(8000));
|
|
412
|
+
w(T, 'workflow/intents/2026-09-22-budget-bad.md', INTENT('budget bad', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
413
|
+
w(T, 'workflow/plans/2026-09-22-budget-bad.md', PLAN('budget bad', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
414
|
+
r = run(T);
|
|
415
|
+
const o = outOf(r);
|
|
416
|
+
check('常驻面预算:超限 → WARN(含一进一出提示) 且 exit 0', r.status === 0 && o.includes('WARN 常驻面超限') && o.includes('一进一出'), o);
|
|
417
|
+
rmfix(T);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// ---- 场景 30:rule-budget --staged 硬拦路径(rule-budget.sh 仍为 sh——无 sh 环境打印 SKIP)----
|
|
421
|
+
{
|
|
422
|
+
const probe = spawnSync('sh', ['-c', 'true']);
|
|
423
|
+
if (probe.error) {
|
|
424
|
+
console.log('SKIP rule-budget --staged 两场景(环境无 sh——rule-budget.sh 属 git 钩子 sh 环境面)');
|
|
425
|
+
} else {
|
|
426
|
+
const T = mkfix();
|
|
427
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
428
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
|
|
429
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'rule-budgets.txt'), path.join(T, '.agents', 'rule-budgets.txt'));
|
|
430
|
+
gitInit(T);
|
|
431
|
+
w(T, 'AGENTS.md', 'x'.repeat(8000));
|
|
432
|
+
spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', ['add', 'AGENTS.md'], { cwd: T });
|
|
433
|
+
let r = spawnSync('sh', ['.agents/scripts/rule-budget.sh', '--staged'], { cwd: T, encoding: 'utf8' });
|
|
434
|
+
check('rule-budget --staged:暂存超限 → exit 1(硬拦路径)',
|
|
435
|
+
r.status === 1 && `${r.stdout}${r.stderr}`.includes('常驻面超限:AGENTS.md'), `${r.stdout}${r.stderr}`);
|
|
436
|
+
w(T, 'AGENTS.md', '# 小体积\n');
|
|
437
|
+
spawnSync(process.platform === 'win32' ? 'git.exe' : 'git', ['add', 'AGENTS.md'], { cwd: T });
|
|
438
|
+
r = spawnSync('sh', ['.agents/scripts/rule-budget.sh', '--staged'], { cwd: T, encoding: 'utf8' });
|
|
439
|
+
check('rule-budget --staged:合规 → exit 0(不误拦)', r.status === 0, `${r.stdout}${r.stderr}`);
|
|
440
|
+
rmfix(T);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ---- 场景 31/32/33:确认态留痕(检查 14)——无 approved 历史 WARN;先 approved 后 done 无 WARN;非 git 跳过 ----
|
|
445
|
+
{
|
|
446
|
+
const T = mkfix();
|
|
447
|
+
gitInit(T);
|
|
448
|
+
w(T, 'workflow/incidents/2026-09-23-confirm-gate.md', `---\n状态: closed\n级别: L1\n发现: 2026-09-23\n模块: material\n---\n# INCIDENT — confirm gate\n\n${三件套(false)}`);
|
|
449
|
+
w(T, 'workflow/plans/2026-09-23-confirm-gate.md', PLAN('confirm gate', '状态: done\n级别: L1\n模块: material'));
|
|
450
|
+
gitCommitAll(T, 'test');
|
|
451
|
+
let r = run(T);
|
|
452
|
+
check('新 done 无 approved 历史 → WARN 确认态缺失 且 exit 0',
|
|
453
|
+
r.status === 0 && /WARN 确认态缺失.*2026-09-23-confirm-gate\.md/.test(outOf(r)), outOf(r));
|
|
454
|
+
rmfix(T);
|
|
455
|
+
}
|
|
456
|
+
{
|
|
457
|
+
const T = mkfix();
|
|
458
|
+
gitInit(T);
|
|
459
|
+
w(T, 'workflow/incidents/2026-09-23-confirm-ok.md', `---\n状态: closed\n级别: L1\n发现: 2026-09-23\n模块: material\n---\n# INCIDENT — confirm ok\n\n${三件套(false)}`);
|
|
460
|
+
w(T, 'workflow/plans/2026-09-23-confirm-ok.md', PLAN('confirm ok', '状态: approved\n级别: L1\n模块: material'));
|
|
461
|
+
gitCommitAll(T, 'approved');
|
|
462
|
+
w(T, 'workflow/plans/2026-09-23-confirm-ok.md', PLAN('confirm ok', '状态: done\n级别: L1\n模块: material'));
|
|
463
|
+
gitCommitAll(T, 'done');
|
|
464
|
+
const r = run(T);
|
|
465
|
+
check('approved → done 留痕 → 无确认态缺失告警', r.status === 0 && !outOf(r).includes('WARN 确认态缺失'), outOf(r));
|
|
466
|
+
rmfix(T);
|
|
467
|
+
}
|
|
468
|
+
{
|
|
469
|
+
const T = mkfix();
|
|
470
|
+
w(T, 'workflow/incidents/2026-09-23-confirm-nogit.md', `---\n状态: closed\n级别: L1\n发现: 2026-09-23\n模块: material\n---\n# INCIDENT — confirm nogit\n\n${三件套(false)}`);
|
|
471
|
+
w(T, 'workflow/plans/2026-09-23-confirm-nogit.md', PLAN('confirm nogit', '状态: done\n级别: L1\n模块: material'));
|
|
472
|
+
const r = run(T);
|
|
473
|
+
check('非 git fixture → 检查项 14 跳过,不误报', r.status === 0 && !outOf(r).includes('WARN 确认态缺失'), outOf(r));
|
|
474
|
+
rmfix(T);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ---- 场景 34/35/36:枚举单源——缺文件 fail-loud;缺键 fail-loud;CRLF 容忍 ----
|
|
478
|
+
{
|
|
479
|
+
const T = mkfix();
|
|
480
|
+
fs.rmSync(path.join(T, '.agents', 'workflow-enums.txt'));
|
|
481
|
+
w(T, 'workflow/intents/2026-09-12-ok.md', INTENT('ok', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:全绿)\n'));
|
|
482
|
+
expectHard('枚举单源文件缺失 → exit 1 fail-loud(不静默退化)', T, '枚举单源');
|
|
483
|
+
rmfix(T);
|
|
484
|
+
}
|
|
485
|
+
{
|
|
486
|
+
const T = mkfix();
|
|
487
|
+
const f = path.join(T, '.agents', 'workflow-enums.txt');
|
|
488
|
+
fs.writeFileSync(f, fs.readFileSync(f, 'utf8').replace(/^doc\.status\.confirmed=.*\n/m, ''));
|
|
489
|
+
expectHard('枚举单源缺键 → exit 1 fail-loud', T, '缺键');
|
|
490
|
+
rmfix(T);
|
|
491
|
+
}
|
|
492
|
+
{
|
|
493
|
+
const T = mkfix();
|
|
494
|
+
fs.writeFileSync(path.join(T, '.agents', 'workflow-enums.txt'), ENUMS_FIXTURE.replace(/\n/g, '\r\n'));
|
|
495
|
+
w(T, 'workflow/intents/2026-09-12-ok.md', INTENT('ok', '状态: approved\n级别: L1\n日期: 2026-09-12'));
|
|
496
|
+
w(T, 'workflow/plans/2026-09-12-ok.md', PLAN('ok', '状态: approved\n级别: L1'));
|
|
497
|
+
const r = run(T);
|
|
498
|
+
check('枚举单源 CRLF 行尾 → approved/done 判定不受 \\r 影响', r.status === 0 && !outOf(r).includes('状态未确认'), outOf(r));
|
|
499
|
+
rmfix(T);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// ---- 场景 37/38/39/40:检查 15 确认指纹对账——无指纹拦 / 配对齐过 / 指纹不符拦 / 生效日前豁免 ----
|
|
503
|
+
{
|
|
504
|
+
const T = mkfix();
|
|
505
|
+
w(T, 'workflow/intents/2026-09-27-cg.md', INTENT('cg', '状态: approved\n级别: L1\n日期: 2026-09-27'));
|
|
506
|
+
w(T, 'workflow/plans/2026-09-27-cg.md', PLAN('cg', '状态: draft\n级别: L1'));
|
|
507
|
+
expectHard('检查15:生效日起新档 approved 无指纹无台账 → hard 确认未对账', T, '确认未对账');
|
|
508
|
+
rmfix(T);
|
|
509
|
+
}
|
|
510
|
+
{
|
|
511
|
+
const T = mkfix();
|
|
512
|
+
const i1 = mkConfirmedDoc(T, 'workflow/intents/2026-09-27-ok15.md', '状态: approved\n级别: L1\n日期: 2026-09-27');
|
|
513
|
+
const p1 = mkConfirmedDoc(T, 'workflow/plans/2026-09-27-ok15.md', '状态: approved\n级别: L1');
|
|
514
|
+
writeLedger(T, [
|
|
515
|
+
{ ts: 'T', doc: i1.rel, stage: 'approved', fingerprint: i1.fp, prev: 'draft' },
|
|
516
|
+
{ ts: 'T', doc: p1.rel, stage: 'approved', fingerprint: p1.fp, prev: 'draft' },
|
|
517
|
+
]);
|
|
518
|
+
expectOk('检查15:指纹+台账配对齐 → exit 0', T);
|
|
519
|
+
rmfix(T);
|
|
520
|
+
}
|
|
521
|
+
{
|
|
522
|
+
const T = mkfix();
|
|
523
|
+
const i1 = mkConfirmedDoc(T, 'workflow/intents/2026-09-27-bad15.md', '状态: approved\n级别: L1\n日期: 2026-09-27');
|
|
524
|
+
w(T, 'workflow/plans/2026-09-27-bad15.md', PLAN('bad15', '状态: draft\n级别: L1'));
|
|
525
|
+
writeLedger(T, [{ ts: 'T', doc: i1.rel, stage: 'approved', fingerprint: 'e'.repeat(64), prev: 'draft' }]); // 指纹不符
|
|
526
|
+
expectHard('检查15:指纹与台账不配对 → hard 确认未对账', T, '确认未对账');
|
|
527
|
+
rmfix(T);
|
|
528
|
+
}
|
|
529
|
+
{
|
|
530
|
+
const T = mkfix();
|
|
531
|
+
// 豁免理由(2026-09-28 锚改台账 ts 后):**台账无行** ⇒ 存量未受管,与文档自报日期无关。
|
|
532
|
+
// 本场景构造的正是「日期早 + 无台账」——换锚前后结论一致,但理由已从「日期早于生效日」变为「无台账行」。
|
|
533
|
+
w(T, 'workflow/intents/2026-09-26-old15.md', INTENT('old15', '状态: approved\n级别: L1\n日期: 2026-09-26'));
|
|
534
|
+
w(T, 'workflow/plans/2026-09-26-old15.md', PLAN('old15', '状态: draft\n级别: L1'));
|
|
535
|
+
expectOk('检查15:存量 approved 无台账行 → 豁免 exit 0', T);
|
|
536
|
+
rmfix(T);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ---- 场景 40:检查 8——证据写在 [x] 续行(仓库通写法「(证据:…)」另起一行)→ 不报缺证据 ----
|
|
540
|
+
{
|
|
541
|
+
const T = mkfix();
|
|
542
|
+
w(T, 'workflow/intents/2026-09-12-ev1.md', INTENT('ev1', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
543
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过\n(证据:commit fixture;套件全绿)\n- [x] 文档补齐\n(证据:AGENTS.md 第 1 行)\n'));
|
|
544
|
+
w(T, 'workflow/plans/2026-09-12-ev1.md', PLAN('ev1', '状态: done\n级别: L1'));
|
|
545
|
+
const r = run(T);
|
|
546
|
+
check('证据在续行(全角冒号)→ 不报验收缺证据',
|
|
547
|
+
r.status === 0 && !outOf(r).includes('验收缺证据'), `exit=${r.status}\n${outOf(r)}`);
|
|
548
|
+
rmfix(T);
|
|
549
|
+
}
|
|
550
|
+
// ---- 场景 41:检查 8——[x] 无证据且续行也无 → 报验收缺证据(回归:不可把豁免放大成漏检)----
|
|
551
|
+
// 换锚后须真 git 仓:否则不可判定 → 走存量口径,「验收缺证据」这条 warning 也不再产出。
|
|
552
|
+
{
|
|
553
|
+
const T = mkfix();
|
|
554
|
+
gitInit(T);
|
|
555
|
+
w(T, 'workflow/intents/2026-09-12-ev2.md', INTENT('ev2', '状态: done\n级别: L1\n日期: 2026-09-12',
|
|
556
|
+
'\n## 验收标准(可测试)\n- [x] 用例通过\n- [x] 文档补齐\n(说明:无证据行)\n'));
|
|
557
|
+
w(T, 'workflow/plans/2026-09-12-ev2.md', PLAN('ev2', '状态: done\n级别: L1'));
|
|
558
|
+
gitCommitAll(T, 'add');
|
|
559
|
+
const r = run(T);
|
|
560
|
+
check('[x] 续行仍无证据 → 报验收缺证据',
|
|
561
|
+
r.status === 0 && outOf(r).includes('验收缺证据'), `exit=${r.status}\n${outOf(r)}`);
|
|
562
|
+
rmfix(T);
|
|
563
|
+
}
|
|
564
|
+
// ---- 场景 42:检查 4——fill-{intent,spec,plan}.mjs 花括号展开,三路皆存在 → 不报引用断档 ----
|
|
565
|
+
{
|
|
566
|
+
const T = mkfix();
|
|
567
|
+
w(T, 'AGENTS.md', '# AGENTS\n\n起草先跑 `node .agents/scripts/fill-{intent,spec,plan}.mjs` 拿结构化草稿。\n');
|
|
568
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
569
|
+
for (const n of ['fill-intent', 'fill-spec', 'fill-plan']) w(T, `.agents/scripts/${n}.mjs`, '// stub\n');
|
|
570
|
+
w(T, 'workflow/intents/2026-09-12-br1.md', INTENT('br1', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
571
|
+
w(T, 'workflow/plans/2026-09-12-br1.md', PLAN('br1', '状态: done\n级别: L1'));
|
|
572
|
+
const r = run(T);
|
|
573
|
+
check('花括号展开三路存在 → 不报引用断档',
|
|
574
|
+
r.status === 0 && !outOf(r).includes('引用断档'), `exit=${r.status}\n${outOf(r)}`);
|
|
575
|
+
rmfix(T);
|
|
576
|
+
}
|
|
577
|
+
// ---- 场景 43:检查 4——花括号展开三路皆不存在 → 仍报引用断档(带完整展开式文案)----
|
|
578
|
+
{
|
|
579
|
+
const T = mkfix();
|
|
580
|
+
w(T, 'AGENTS.md', '# AGENTS\n\n起草先跑 `node .agents/scripts/fill-{alpha,beta,gamma}.mjs` 拿结构化草稿。\n');
|
|
581
|
+
w(T, 'workflow/intents/2026-09-12-br2.md', INTENT('br2', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
582
|
+
w(T, 'workflow/plans/2026-09-12-br2.md', PLAN('br2', '状态: done\n级别: L1'));
|
|
583
|
+
const r = run(T);
|
|
584
|
+
check('花括号展开三路皆无 → 仍报引用断档',
|
|
585
|
+
r.status === 0 && /引用断档.*fill-\{alpha,beta,gamma\}\.mjs/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
586
|
+
rmfix(T);
|
|
587
|
+
}
|
|
588
|
+
// ---- 场景 44:检查 2——<主题> 与 .md 同行 = 命名约定描述 → 不报模板未填 ----
|
|
589
|
+
{
|
|
590
|
+
const T = mkfix();
|
|
591
|
+
w(T, 'workflow/intents/2026-09-01-nm1.md', INTENT('nm1', '状态: done\n级别: L1\n日期: 2026-09-01',
|
|
592
|
+
'\n编排脚本(`.agents/workflows/<主题>.md`):frontmatter + stages 表。\n复制本模板为 YYYY-MM-DD-<主题>.md 后填写。\n'));
|
|
593
|
+
w(T, 'workflow/plans/2026-09-01-nm1.md', PLAN('nm1', '状态: done\n级别: L1'));
|
|
594
|
+
const r = run(T);
|
|
595
|
+
check('<主题>+.md 命名约定行 → 不报模板未填',
|
|
596
|
+
r.status === 0 && !outOf(r).includes('模板未填'), `exit=${r.status}\n${outOf(r)}`);
|
|
597
|
+
rmfix(T);
|
|
598
|
+
}
|
|
599
|
+
// ---- 场景 45:检查 2——真未填占位(标题 <主题>,无 .md)→ 仍报模板未填 ----
|
|
600
|
+
{
|
|
601
|
+
const T = mkfix();
|
|
602
|
+
w(T, 'workflow/intents/2026-09-01-nm2.md', INTENT('<主题>', '状态: done\n级别: L1\n日期: 2026-09-01', '\n正文无占位。\n'));
|
|
603
|
+
w(T, 'workflow/plans/2026-09-01-nm2.md', PLAN('nm2', '状态: done\n级别: L1'));
|
|
604
|
+
const r = run(T);
|
|
605
|
+
check('标题 <主题>(无 .md)→ 仍报模板未填',
|
|
606
|
+
r.status === 0 && outOf(r).includes('模板未填'), `exit=${r.status}\n${outOf(r)}`);
|
|
607
|
+
rmfix(T);
|
|
608
|
+
}
|
|
609
|
+
// ---- 场景 46:检查 6——薄适配正文含 .agents/roles/<role>.md 引用 → 不报 Adapter 断线 ----
|
|
610
|
+
{
|
|
611
|
+
const T = mkfix();
|
|
612
|
+
w(T, 'workflow/intents/2026-09-12-ad1.md', INTENT('ad1', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
613
|
+
w(T, 'workflow/plans/2026-09-12-ad1.md', PLAN('ad1', '状态: done\n级别: L1'));
|
|
614
|
+
fs.mkdirSync(path.join(T, '.agents', 'roles'), { recursive: true });
|
|
615
|
+
fs.mkdirSync(path.join(T, '.zcode', 'agents'), { recursive: true });
|
|
616
|
+
w(T, '.agents/roles/implementer.md', '# role implementer\n');
|
|
617
|
+
w(T, '.zcode/agents/implementer.md', '---\nname: implementer\n---\n> 契约见 `.agents/roles/implementer.md`。\n');
|
|
618
|
+
const r = run(T);
|
|
619
|
+
check('薄适配含角色引用 → 不报 Adapter 断线',
|
|
620
|
+
r.status === 0 && !outOf(r).includes('Adapter 断线'), `exit=${r.status}\n${outOf(r)}`);
|
|
621
|
+
rmfix(T);
|
|
622
|
+
}
|
|
623
|
+
// ---- 场景 47:检查 4——fill-*.mjs 星号通配,目录内有匹配文件 → 不报引用断档;无匹配 → 仍报 ----
|
|
624
|
+
{
|
|
625
|
+
const T = mkfix();
|
|
626
|
+
w(T, 'AGENTS.md', '# AGENTS\n\n起草先跑 `node .agents/scripts/fill-*.mjs`;孤儿引用 `.agents/scripts/ghost-*.mjs`。\n');
|
|
627
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
628
|
+
for (const n of ['fill-intent', 'fill-spec', 'fill-plan']) w(T, `.agents/scripts/${n}.mjs`, '// stub\n');
|
|
629
|
+
w(T, 'workflow/intents/2026-09-12-gb1.md', INTENT('gb1', '状态: done\n级别: L1\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
630
|
+
w(T, 'workflow/plans/2026-09-12-gb1.md', PLAN('gb1', '状态: done\n级别: L1'));
|
|
631
|
+
const r = run(T);
|
|
632
|
+
check('星号通配有匹配 → 不报;无匹配 → 仍报引用断档',
|
|
633
|
+
r.status === 0 && !/引用断档.*fill-\*/.test(outOf(r)) && /引用断档.*ghost-\*\.mjs/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
634
|
+
rmfix(T);
|
|
635
|
+
}
|
|
636
|
+
// ---- 场景 48:检查 15——委托代录台账行(source/quote 额外字段)照常配对放行(两形态口径,2026-09-27 confirm-gate-delegated)----
|
|
637
|
+
{
|
|
638
|
+
const T = mkfix();
|
|
639
|
+
const i1 = mkConfirmedDoc(T, 'workflow/intents/2026-09-27-del15.md', '状态: approved\n级别: L1\n日期: 2026-09-27');
|
|
640
|
+
const p1 = mkConfirmedDoc(T, 'workflow/plans/2026-09-27-del15.md', '状态: approved\n级别: L1');
|
|
641
|
+
writeLedger(T, [
|
|
642
|
+
{ ts: 'T', doc: i1.rel, stage: 'approved', fingerprint: i1.fp, prev: 'draft', source: 'chat-delegated', quote: '2选2' },
|
|
643
|
+
{ ts: 'T', doc: p1.rel, stage: 'approved', fingerprint: p1.fp, prev: 'draft', source: 'tty' },
|
|
644
|
+
]);
|
|
645
|
+
expectOk('检查15:委托代录台账行(source+quote)照常配对 → exit 0', T);
|
|
646
|
+
rmfix(T);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// ---- 场景 49-54:检查 15 done 内容绑定(2026-09-27 audit-gate-hardening;同日 p2-batch 改锚台账 ts + 保分隔符复原)----
|
|
650
|
+
// done 形态复刻真实时序:approved 内容(含 approved 指纹)→ done 指纹在其上计算 → 落 done 文档 + 台账 done 行
|
|
651
|
+
// opt.ts:台账 done 行 ts(默认 2026-09-28T02:00Z——生效锚后);opt.sep:状态行键值分隔符(默认单空格,
|
|
652
|
+
// ' ' 复刻两跳间手工改成双空格的非规范格式)
|
|
653
|
+
{
|
|
654
|
+
const mkDoneFixture = (T, date, tamper, opt = {}) => {
|
|
655
|
+
const sep = opt.sep || ' ';
|
|
656
|
+
const ts = opt.ts || '2026-09-28T02:00:00.000Z';
|
|
657
|
+
const body = `\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n`;
|
|
658
|
+
const pre = `---\n状态:${sep}approved\n级别: L1\n日期: ${date}\n确认指纹: ${'a'.repeat(16)}\n---\n# INTENT — bind\n${body}`;
|
|
659
|
+
const fpDone = computeFingerprint(pre);
|
|
660
|
+
// tamper 只污染 done 落盘文本(不进指纹底稿)——复刻「done 确认后篡改正文」
|
|
661
|
+
const doneText = `---\n状态:${sep}done\n级别: L1\n日期: ${date}\n确认指纹: ${fpDone.slice(0, 16)}\n---\n# INTENT — bind\n${body}${tamper || ''}`;
|
|
662
|
+
w(T, 'workflow/intents/' + date + '-bind.md', doneText);
|
|
663
|
+
w(T, 'workflow/plans/' + date + '-bind.md', PLAN('bind', '状态: draft\n级别: L1'));
|
|
664
|
+
return { rel: 'workflow/intents/' + date + '-bind.md', fp: fpDone, ts };
|
|
665
|
+
};
|
|
666
|
+
{
|
|
667
|
+
const T = mkfix();
|
|
668
|
+
const d1 = mkDoneFixture(T, '2026-09-28');
|
|
669
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }]);
|
|
670
|
+
expectOk('检查15:done 内容与台账一致(复原重算=台账指纹)→ exit 0', T);
|
|
671
|
+
rmfix(T);
|
|
672
|
+
}
|
|
673
|
+
{
|
|
674
|
+
const T = mkfix();
|
|
675
|
+
const d1 = mkDoneFixture(T, '2026-09-28', '确认后被篡改的正文行\n');
|
|
676
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }]);
|
|
677
|
+
expectHard('检查15:done 后正文被篡改 → hard 确认内容漂移', T, '确认内容漂移');
|
|
678
|
+
rmfix(T);
|
|
679
|
+
}
|
|
680
|
+
{
|
|
681
|
+
const T = mkfix();
|
|
682
|
+
// 生效锚 = 台账 ts:文档日期晚、但确认发生在锚前(旧关单顺序时代)→ 豁免绑定
|
|
683
|
+
const d1 = mkDoneFixture(T, '2026-09-28', '锚前确认的文档即便日后改了也不绑\n', { ts: '2026-09-27T10:00:00.000Z' });
|
|
684
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }]);
|
|
685
|
+
expectOk('检查15:台账 ts 早于生效锚(2026-09-28)→ 豁免内容绑定', T);
|
|
686
|
+
rmfix(T);
|
|
687
|
+
}
|
|
688
|
+
{
|
|
689
|
+
const T = mkfix();
|
|
690
|
+
// 锚后确认 + 文档日期早(旧日期文档晚关单)+ 篡改 → 绑定照拦(p2-batch 改锚的核心收益)
|
|
691
|
+
const d1 = mkDoneFixture(T, '2026-09-27', '确认后被篡改的正文行\n');
|
|
692
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }]);
|
|
693
|
+
expectHard('检查15:文档日期早但台账 ts 锚后 + 篡改 → hard 确认内容漂移', T, '确认内容漂移');
|
|
694
|
+
rmfix(T);
|
|
695
|
+
}
|
|
696
|
+
{
|
|
697
|
+
const T = mkfix();
|
|
698
|
+
const d1 = mkDoneFixture(T, '2026-09-28');
|
|
699
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp }]); // 无 prev(schema 演进前行)
|
|
700
|
+
const r = run(T);
|
|
701
|
+
check('检查15:台账 done 行缺 prev → 降级 WARN 绑定跳过(exit 0 不拦)',
|
|
702
|
+
r.status === 0 && outOf(r).includes('绑定降级'), `exit=${r.status}\n${outOf(r)}`);
|
|
703
|
+
rmfix(T);
|
|
704
|
+
}
|
|
705
|
+
{
|
|
706
|
+
const T = mkfix();
|
|
707
|
+
// 两跳间状态行被手工改成双空格(非规范分隔符)——保分隔符复原不误伤(p2-batch P2-1)
|
|
708
|
+
const d1 = mkDoneFixture(T, '2026-09-28', '', { sep: ' ' });
|
|
709
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }]);
|
|
710
|
+
expectOk('检查15:状态行双空格(非规范分隔符)绑定仍过——保分隔符复原不误伤', T);
|
|
711
|
+
rmfix(T);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ---- 场景 55-56:检查 4 引用扫描收窄活跃态(2026-09-27 audit-gate-hardening)----
|
|
716
|
+
{
|
|
717
|
+
const T = mkfix();
|
|
718
|
+
// 日期取生效日前(2026-09-26):superseded 现入 15 配对集(closing-coverage),无台账行 ⇒ 免配对
|
|
719
|
+
// (2026-09-28 锚改台账 ts 后,豁免理由 = 无台账行),场景焦点保持在「终态退出引用扫描」本身
|
|
720
|
+
w(T, 'workflow/intents/2026-09-26-refterm.md', INTENT('refterm', '状态: superseded\n级别: L1\n日期: 2026-09-26', '\n引用 .agents/scripts/ghost-gone.mjs\n'));
|
|
721
|
+
w(T, 'workflow/plans/2026-09-26-refterm.md', PLAN('refterm', '状态: superseded\n级别: L1'));
|
|
722
|
+
const r = run(T);
|
|
723
|
+
check('检查4:终态文档引用断链不再报(历史叙述退出扫描)',
|
|
724
|
+
r.status === 0 && !outOf(r).includes('引用断档'), `exit=${r.status}\n${outOf(r)}`);
|
|
725
|
+
rmfix(T);
|
|
726
|
+
}
|
|
727
|
+
{
|
|
728
|
+
const T = mkfix();
|
|
729
|
+
w(T, 'workflow/intents/2026-09-26-reflive.md', INTENT('reflive', '状态: approved\n级别: L1\n日期: 2026-09-26', '\n引用 .agents/scripts/ghost-gone.mjs\n'));
|
|
730
|
+
w(T, 'workflow/plans/2026-09-26-reflive.md', PLAN('reflive', '状态: draft\n级别: L1'));
|
|
731
|
+
const r = run(T);
|
|
732
|
+
check('检查4:活跃文档引用断链仍报(收窄只放终态)',
|
|
733
|
+
r.status === 0 && outOf(r).includes('引用断档') && outOf(r).includes('ghost-gone.mjs'), `exit=${r.status}\n${outOf(r)}`);
|
|
734
|
+
rmfix(T);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// ---- 场景 57-60:检查 15 incidents 确认门(2026-09-27 gate-coverage,配对自 2026-09-28 起)----
|
|
738
|
+
// fixed/closed 须指纹+台账配对;closed 与 done 同口径内容绑定(ts 锚 + 保分隔符);open 起草态不加门
|
|
739
|
+
{
|
|
740
|
+
const mkIncFixture = (T, st, tamper, opt = {}) => {
|
|
741
|
+
const ts = opt.ts || '2026-09-28T02:00:00.000Z';
|
|
742
|
+
const body = 三件套(false);
|
|
743
|
+
const pre = `---\n状态: fixed\n级别: L1\n发现: 2026-09-28\n确认指纹: ${'a'.repeat(16)}\n---\n# INCIDENT — inc\n${body}`;
|
|
744
|
+
const fpClosed = computeFingerprint(pre);
|
|
745
|
+
// tamper 只污染 closed 落盘文本(不进指纹底稿)——复刻「closed 确认后篡改正文」
|
|
746
|
+
const closedText = `---\n状态: ${st}\n级别: L1\n发现: 2026-09-28\n确认指纹: ${fpClosed.slice(0, 16)}\n---\n# INCIDENT — inc\n${body}${tamper || ''}`;
|
|
747
|
+
w(T, 'workflow/incidents/2026-09-28-inc.md', closedText);
|
|
748
|
+
w(T, 'workflow/plans/2026-09-28-inc.md', PLAN('inc', '状态: draft\n级别: L1'));
|
|
749
|
+
return { rel: 'workflow/incidents/2026-09-28-inc.md', fp: fpClosed, ts };
|
|
750
|
+
};
|
|
751
|
+
{
|
|
752
|
+
// incident closed(生效日起)无指纹无台账 → hard 确认未对账
|
|
753
|
+
const T = mkfix();
|
|
754
|
+
w(T, 'workflow/incidents/2026-09-28-inc15.md', `---\n状态: closed\n级别: L1\n发现: 2026-09-28\n---\n# INCIDENT — inc15\n${三件套(false)}`);
|
|
755
|
+
w(T, 'workflow/plans/2026-09-28-inc15.md', PLAN('inc15', '状态: draft\n级别: L1'));
|
|
756
|
+
expectHard('检查15:incident closed(生效日起)无指纹无台账 → hard 确认未对账', T, '确认未对账');
|
|
757
|
+
rmfix(T);
|
|
758
|
+
}
|
|
759
|
+
{
|
|
760
|
+
const T = mkfix();
|
|
761
|
+
const i1 = mkIncFixture(T, 'closed');
|
|
762
|
+
writeLedger(T, [{ ts: i1.ts, doc: i1.rel, stage: 'closed', fingerprint: i1.fp, prev: 'fixed', source: 'tty' }]);
|
|
763
|
+
expectOk('检查15:incident closed 配对+内容绑定一致 → exit 0', T);
|
|
764
|
+
rmfix(T);
|
|
765
|
+
}
|
|
766
|
+
{
|
|
767
|
+
const T = mkfix();
|
|
768
|
+
const i1 = mkIncFixture(T, 'closed', 'closed 确认后被篡改的正文行\n');
|
|
769
|
+
writeLedger(T, [{ ts: i1.ts, doc: i1.rel, stage: 'closed', fingerprint: i1.fp, prev: 'fixed', source: 'tty' }]);
|
|
770
|
+
expectHard('检查15:incident closed 后正文被篡改 → hard 确认内容漂移', T, '确认内容漂移');
|
|
771
|
+
rmfix(T);
|
|
772
|
+
}
|
|
773
|
+
{
|
|
774
|
+
// open 起草态不加门(无指纹无台账 → 不报确认未对账)
|
|
775
|
+
const T = mkfix();
|
|
776
|
+
w(T, 'workflow/incidents/2026-09-28-incopen.md', `---\n状态: open\n级别: L1\n发现: 2026-09-28\n---\n# INCIDENT — incopen\n${三件套(false)}`);
|
|
777
|
+
w(T, 'workflow/plans/2026-09-28-incopen.md', PLAN('incopen', '状态: draft\n级别: L1'));
|
|
778
|
+
const r = run(T);
|
|
779
|
+
check('检查15:incident open(起草态)不进确认门 → 不报确认未对账',
|
|
780
|
+
r.status === 0 && !outOf(r).includes('确认未对账'), `exit=${r.status}\n${outOf(r)}`);
|
|
781
|
+
rmfix(T);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// ---- 场景 61-63:检查 15 放弃态覆盖(2026-09-27 closing-coverage:superseded/cancelled 配对 + 四终态绑定)----
|
|
786
|
+
{
|
|
787
|
+
const mkAbandonFixture = (T, st, tamper) => {
|
|
788
|
+
const ts = '2026-09-28T02:00:00.000Z';
|
|
789
|
+
const body = '\n正文行\n';
|
|
790
|
+
const pre = `---\n状态: approved\n级别: L1\n日期: 2026-09-28\n确认指纹: ${'a'.repeat(16)}\n---\n# INTENT — ab\n${body}`;
|
|
791
|
+
const fp = computeFingerprint(pre);
|
|
792
|
+
const after = `---\n状态: ${st}\n级别: L1\n日期: 2026-09-28\n确认指纹: ${fp.slice(0, 16)}\n---\n# INTENT — ab\n${body}${tamper || ''}`;
|
|
793
|
+
w(T, 'workflow/intents/2026-09-28-ab.md', after);
|
|
794
|
+
w(T, 'workflow/plans/2026-09-28-ab.md', PLAN('ab', '状态: draft\n级别: L1'));
|
|
795
|
+
return { rel: 'workflow/intents/2026-09-28-ab.md', fp, ts };
|
|
796
|
+
};
|
|
797
|
+
{
|
|
798
|
+
// superseded(生效日起)无指纹无台账 → hard 确认未对账(手改出账的口子收死)
|
|
799
|
+
const T = mkfix();
|
|
800
|
+
w(T, 'workflow/intents/2026-09-28-ab1.md', `---\n状态: superseded\n级别: L1\n日期: 2026-09-28\n---\n# INTENT — ab1\n`);
|
|
801
|
+
w(T, 'workflow/plans/2026-09-28-ab1.md', PLAN('ab1', '状态: draft\n级别: L1'));
|
|
802
|
+
expectHard('检查15:superseded(生效日起)无指纹无台账 → hard 确认未对账', T, '确认未对账');
|
|
803
|
+
rmfix(T);
|
|
804
|
+
}
|
|
805
|
+
{
|
|
806
|
+
// superseded + 配对 + 终态绑定:篡改 → hard 确认内容漂移
|
|
807
|
+
const T = mkfix();
|
|
808
|
+
const a1 = mkAbandonFixture(T, 'superseded', 'superseded 确认后被篡改的正文行\n');
|
|
809
|
+
writeLedger(T, [{ ts: a1.ts, doc: a1.rel, stage: 'superseded', fingerprint: a1.fp, prev: 'approved', source: 'tty' }]);
|
|
810
|
+
expectHard('检查15:superseded 后正文被篡改 → hard 确认内容漂移(四终态绑定)', T, '确认内容漂移');
|
|
811
|
+
rmfix(T);
|
|
812
|
+
}
|
|
813
|
+
{
|
|
814
|
+
// cancelled + 配对 + 绑定一致 → 过
|
|
815
|
+
const T = mkfix();
|
|
816
|
+
const a1 = mkAbandonFixture(T, 'cancelled');
|
|
817
|
+
writeLedger(T, [{ ts: a1.ts, doc: a1.rel, stage: 'cancelled', fingerprint: a1.fp, prev: 'approved', source: 'chat-delegated', quote: '不做了' }]);
|
|
818
|
+
expectOk('检查15:cancelled 配对+绑定一致 → exit 0', T);
|
|
819
|
+
rmfix(T);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// ---- 场景 64-66:检查 15 并录批次审计(2026-09-28 batch-ledger-audit 改判据:读 batch/of 事实)----
|
|
824
|
+
// 判据演进:原「同 quote + 相邻 ts 差 < 2s」反推调用次数(已被实证假阳性+假阴性,整段退役);
|
|
825
|
+
// 现「台账行按 batch 分组、组内 of>1 且含 delegated 行 → warning」。本块场景随之改判定依据,
|
|
826
|
+
// **断言强度不放宽、场景不删**(同一意图换了机制):
|
|
827
|
+
// · 原「同 quote 双份 2s 内」→ 现「同 batch、of=2、delegated」→ 报(机制换、意图同:一次调用落两份)
|
|
828
|
+
// · 原「异 quote / >2s 不聚组」→ 现「不同 batch(或 of=1)→ 不报」;且**新增反向场景**:同 batch
|
|
829
|
+
// 但换 quote、或人为延迟跨 2s,仍须被拦——这正是旧判据的盲区(旧判据下会漏报)
|
|
830
|
+
// · 原「revert-draft 注记行不参与」→ 现由 VALID_STAGES 过滤承接(同意图)
|
|
831
|
+
{
|
|
832
|
+
const mk2LedgerDocs = (T) => {
|
|
833
|
+
// 两份全配对绑定的 done 文档(复用 mkDoneFixture 的构造口径)
|
|
834
|
+
const mkOne = (slug, date) => {
|
|
835
|
+
const body = `\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n`;
|
|
836
|
+
const pre = `---\n状态: approved\n级别: L1\n日期: ${date}\n确认指纹: ${'a'.repeat(16)}\n---\n# INTENT — ${slug}\n${body}`;
|
|
837
|
+
const fp = computeFingerprint(pre);
|
|
838
|
+
w(T, `workflow/intents/${date}-${slug}.md`, `---\n状态: done\n级别: L1\n日期: ${date}\n确认指纹: ${fp.slice(0, 16)}\n---\n# INTENT — ${slug}\n${body}`);
|
|
839
|
+
w(T, `workflow/plans/${date}-${slug}.md`, PLAN(slug, '状态: draft\n级别: L1'));
|
|
840
|
+
return { rel: `workflow/intents/${date}-${slug}.md`, fp };
|
|
841
|
+
};
|
|
842
|
+
return [mkOne('ab1', '2026-09-28'), mkOne('ab2', '2026-09-28')];
|
|
843
|
+
};
|
|
844
|
+
{
|
|
845
|
+
// 同一批(batch 同、of=2)delegated → warning 可见(exit 0 不阻断)——原「同 quote 双份」的新机制等价物
|
|
846
|
+
const T = mkfix();
|
|
847
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
848
|
+
writeLedger(T, [
|
|
849
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起', batch: 'b1', seq: 1, of: 2 },
|
|
850
|
+
{ ts: '2026-09-28T02:00:00.100Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起', batch: 'b1', seq: 2, of: 2 },
|
|
851
|
+
]);
|
|
852
|
+
const r = run(T);
|
|
853
|
+
check('检查15:同 batch 且 of=2(delegated)→ WARN 确认并录(exit 0 审计可见性)',
|
|
854
|
+
r.status === 0 && outOf(r).includes('确认并录') && outOf(r).includes('2 份'),
|
|
855
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
856
|
+
rmfix(T);
|
|
857
|
+
}
|
|
858
|
+
{
|
|
859
|
+
// 【核心回归·旧判据盲区】同一批但**换 quote**("甲"/"乙")且 ts 相差 5s:
|
|
860
|
+
// 旧判据(同 quote + 差 < 2s)→ 两个信号都不命中 → **零告警**(漏报真实并录)
|
|
861
|
+
// 新判据读 batch/of(与 quote、ts 无关)→ 仍须报。此场景在旧判据下必然变红。
|
|
862
|
+
const T = mkfix();
|
|
863
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
864
|
+
writeLedger(T, [
|
|
865
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '甲', batch: 'b2', seq: 1, of: 2 },
|
|
866
|
+
{ ts: '2026-09-28T02:00:05.000Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '乙', batch: 'b2', seq: 2, of: 2 },
|
|
867
|
+
]);
|
|
868
|
+
const r = run(T);
|
|
869
|
+
check('检查15:同 batch 但换 quote + 隔 5s → 仍报并录(旧判据盲区,本单核心回归)',
|
|
870
|
+
r.status === 0 && outOf(r).includes('确认并录') && outOf(r).includes('2 份'),
|
|
871
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
872
|
+
rmfix(T);
|
|
873
|
+
}
|
|
874
|
+
{
|
|
875
|
+
// of=1(逐件调用)复用**同一句** quote 且 ts 极近 → **不报**。
|
|
876
|
+
// 这是旧判据的假阳性面:用户两次都说「可以」+ 快速连跑,旧判据必然误报;新判据读 of=1 正确放过。
|
|
877
|
+
const T = mkfix();
|
|
878
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
879
|
+
writeLedger(T, [
|
|
880
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '可以', batch: 'b3', seq: 1, of: 1 },
|
|
881
|
+
{ ts: '2026-09-28T02:00:00.050Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '可以', batch: 'b4', seq: 1, of: 1 },
|
|
882
|
+
]);
|
|
883
|
+
const r = run(T);
|
|
884
|
+
check('检查15:两次逐件调用(of=1)复用同一句 quote 且 ts 极近 → 不报(修旧判据假阳性)',
|
|
885
|
+
r.status === 0 && !outOf(r).includes('确认并录'),
|
|
886
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
887
|
+
rmfix(T);
|
|
888
|
+
}
|
|
889
|
+
{
|
|
890
|
+
// TTY 形态 of>1:用户亲手逐份过目键入(天然逐件),不构成违规 → 不报
|
|
891
|
+
const T = mkfix();
|
|
892
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
893
|
+
writeLedger(T, [
|
|
894
|
+
{ ts: '2026-09-28T03:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty', batch: 't1', seq: 1, of: 2 },
|
|
895
|
+
{ ts: '2026-09-28T03:00:01.000Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'tty', batch: 't1', seq: 2, of: 2 },
|
|
896
|
+
]);
|
|
897
|
+
const r = run(T);
|
|
898
|
+
check('检查15:TTY 形态 of=2(亲手逐份过目)→ 不报并录',
|
|
899
|
+
r.status === 0 && !outOf(r).includes('确认并录'),
|
|
900
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
901
|
+
rmfix(T);
|
|
902
|
+
}
|
|
903
|
+
{
|
|
904
|
+
// 无 batch 字段的历史行(schema 演进前)→ 静默跳过,不报、不降级出账
|
|
905
|
+
// (沿 audit-gate-hardening P3:无判定依据的行不产出不可消除噪声)
|
|
906
|
+
const T = mkfix();
|
|
907
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
908
|
+
writeLedger(T, [
|
|
909
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起' },
|
|
910
|
+
{ ts: '2026-09-28T02:00:01.500Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起' },
|
|
911
|
+
]);
|
|
912
|
+
const r = run(T);
|
|
913
|
+
check('检查15:无 batch 的历史行 → 静默跳过(不报、不降级出账)',
|
|
914
|
+
r.status === 0 && !outOf(r).includes('确认并录'),
|
|
915
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
916
|
+
rmfix(T);
|
|
917
|
+
}
|
|
918
|
+
{
|
|
919
|
+
// 不同 batch 各自 of=1;revert-draft 注记行(stage 非法)不参与 → 一条都不报
|
|
920
|
+
const T = mkfix();
|
|
921
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
922
|
+
writeLedger(T, [
|
|
923
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: 'x', batch: 'c1', seq: 1, of: 1 },
|
|
924
|
+
{ ts: '2026-09-28T02:00:00.400Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: 'x', batch: 'c2', seq: 1, of: 1 },
|
|
925
|
+
{ ts: '2026-09-28T02:00:00.900Z', doc: 'workflow/plans/2026-09-28-ghost.md', stage: 'revert-draft', fingerprint: 'n/a', prev: 'approved', source: 'chat-delegated', quote: 'x', batch: 'c3', seq: 1, of: 3 },
|
|
926
|
+
]);
|
|
927
|
+
const r = run(T);
|
|
928
|
+
check('检查15:异 batch 各自 of=1 + revert-draft 注记行 → 不报并录(注记行由 VALID_STAGES 过滤)',
|
|
929
|
+
r.status === 0 && !outOf(r).includes('确认并录') && !outOf(r).includes('ghost'),
|
|
930
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
931
|
+
rmfix(T);
|
|
932
|
+
}
|
|
933
|
+
{
|
|
934
|
+
// 【复核 P2-3】组内 `of` 不一致(人为构造)时判据不得因**行序**而改变结论:
|
|
935
|
+
// 原实现取 rows[0].of —— 首行 of=1、次行 of=2 会漏报;现取组内 some(of>1),两个方向都报。
|
|
936
|
+
// 正常批次由 confirm-doc 写同一个 of=docs.length,组内恒一致;本用例专门钉住顺序无关性。
|
|
937
|
+
const T = mkfix();
|
|
938
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
939
|
+
writeLedger(T, [
|
|
940
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '甲', batch: 'd1', seq: 1, of: 1 },
|
|
941
|
+
{ ts: '2026-09-28T02:00:01.000Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '乙', batch: 'd1', seq: 2, of: 2 },
|
|
942
|
+
]);
|
|
943
|
+
const r = run(T);
|
|
944
|
+
check('检查15:组内 of 不一致(首行 of=1 / 次行 of=2)→ 仍报(判据顺序无关,复核 P2-3)',
|
|
945
|
+
r.status === 0 && outOf(r).includes('确认并录'),
|
|
946
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
947
|
+
rmfix(T);
|
|
948
|
+
}
|
|
949
|
+
{
|
|
950
|
+
// 反向:首行 of=2 / 次行 of=1 —— 与上条同批、仅行序相反,结论必须一致(都报)
|
|
951
|
+
const T = mkfix();
|
|
952
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
953
|
+
writeLedger(T, [
|
|
954
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '甲', batch: 'd2', seq: 1, of: 2 },
|
|
955
|
+
{ ts: '2026-09-28T02:00:01.000Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '乙', batch: 'd2', seq: 2, of: 1 },
|
|
956
|
+
]);
|
|
957
|
+
const r2 = run(T);
|
|
958
|
+
check('检查15:组内 of 不一致(首行 of=2 / 次行 of=1)→ 仍报(与上条对称,顺序无关)',
|
|
959
|
+
r2.status === 0 && outOf(r2).includes('确认并录'),
|
|
960
|
+
`exit=${r2.status}\n${outOf(r2)}`);
|
|
961
|
+
rmfix(T);
|
|
962
|
+
}
|
|
963
|
+
{
|
|
964
|
+
// 【复核 P2-3 收口】「可报组与 revert-draft 注记行并存 → 恰报 1 次、注记行不参与」
|
|
965
|
+
// 上一版场景把两行改成异 batch of=1 后,fixture 里已无可报组,只验证了「无可报组时零报」——
|
|
966
|
+
// 本用例恢复旧版被覆盖的组合(旧断言为 `length===1 && includes('2 份')`,此处以新机制等价重建)。
|
|
967
|
+
const T = mkfix();
|
|
968
|
+
const [d1, d2] = mk2LedgerDocs(T);
|
|
969
|
+
writeLedger(T, [
|
|
970
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起', batch: 'p1', seq: 1, of: 2 },
|
|
971
|
+
{ ts: '2026-09-28T02:00:00.400Z', doc: d2.rel, stage: 'done', fingerprint: d2.fp, prev: 'approved', source: 'chat-delegated', quote: '两份一起', batch: 'p1', seq: 2, of: 2 },
|
|
972
|
+
// 注记行:与上批**同 batch id 但 stage 非法**——若 VALID_STAGES 过滤失效,它会被并入该批
|
|
973
|
+
{ ts: '2026-09-28T02:00:00.900Z', doc: 'workflow/plans/2026-09-28-ghost.md', stage: 'revert-draft', fingerprint: 'n/a', prev: 'approved', source: 'chat-delegated', quote: '两份一起', batch: 'p1', seq: 3, of: 3 },
|
|
974
|
+
]);
|
|
975
|
+
const r = run(T);
|
|
976
|
+
check('检查15:可报组与 revert-draft 注记行并存 → 恰报 1 次且注记行不参与(复核 P2-4 补回)',
|
|
977
|
+
r.status === 0 && (outOf(r).match(/确认并录/g) || []).length === 1
|
|
978
|
+
&& outOf(r).includes('2 份') && !outOf(r).includes('ghost'),
|
|
979
|
+
`exit=${r.status}\n${outOf(r)}`);
|
|
980
|
+
rmfix(T);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
// ---- 场景 67-71:检查 15 受管准入两条件取或(2026-09-28 incident confirm-gate-effective-date-anchor)----
|
|
985
|
+
// 换锚前:受管准入仅「自报日期 ≥ 生效日」——新档把日期写早即整段跳过判定(确认门静默不开)。
|
|
986
|
+
// 换锚后:受管 = ①台账有合法跳转行 ∨ ②自报日期 ≥ 生效日;两者皆不满足才豁免(存量)。
|
|
987
|
+
// ①独立于自报日期成立 → 堵死「确认过却写早日期逃掉对账」;②保留 → 兜住「新档完全没跑 confirm-doc」。
|
|
988
|
+
// 本块自包含构造(不复用他块作用域内的 mkDoneFixture,避免跨块引用)。
|
|
989
|
+
{
|
|
990
|
+
const mkAnchorFixture = (T, date, tamper, opt = {}) => {
|
|
991
|
+
const ts = opt.ts || '2026-09-28T02:00:00.000Z';
|
|
992
|
+
const body = `\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n`;
|
|
993
|
+
const pre = `---\n状态: approved\n级别: L1\n日期: ${date}\n确认指纹: ${'a'.repeat(16)}\n---\n# INTENT — anchor\n${body}`;
|
|
994
|
+
const fp = computeFingerprint(pre);
|
|
995
|
+
// tamper 只污染 done 落盘文本(不进指纹底稿)——复刻「done 确认后篡改正文」
|
|
996
|
+
w(T, `workflow/intents/${date}-anchor.md`,
|
|
997
|
+
`---\n状态: done\n级别: L1\n日期: ${date}\n确认指纹: ${fp.slice(0, 16)}\n---\n# INTENT — anchor\n${body}${tamper || ''}`);
|
|
998
|
+
w(T, `workflow/plans/${date}-anchor.md`, PLAN('anchor', '状态: draft\n级别: L1'));
|
|
999
|
+
return { rel: `workflow/intents/${date}-anchor.md`, fp, ts };
|
|
1000
|
+
};
|
|
1001
|
+
|
|
1002
|
+
{
|
|
1003
|
+
// 核心回归(本 incident 的触发条件):自报日期早于生效日,但台账 ts 在生效日之后 → 仍须 hard 拦。
|
|
1004
|
+
// 换锚前此场景被 `d < eff` 跳过判定(静默放行);换锚后按台账事实纳入受管。
|
|
1005
|
+
const T = mkfix();
|
|
1006
|
+
const d1 = mkAnchorFixture(T, '2026-09-20'); // 自报日期 2026-09-20(早于任何生效日)
|
|
1007
|
+
writeLedger(T, [{ ts: d1.ts, doc: d1.rel, stage: 'done', fingerprint: 'e'.repeat(64), prev: 'approved', source: 'tty' }]); // 指纹不符
|
|
1008
|
+
expectHard('检查15:自报日期早但台账 ts 锚后 + 指纹不符 → hard 确认未对账(换锚前被静默跳过)', T, '确认未对账');
|
|
1009
|
+
rmfix(T);
|
|
1010
|
+
}
|
|
1011
|
+
{
|
|
1012
|
+
// 存量豁免须**两条件皆不满足**:日期早(2026-09-26 < 生效日)且台账无行 → 免配对。
|
|
1013
|
+
// 注意「日期新但无台账」**不**豁免(那是新档漏走确认门,见上一条核心回归的反面)——
|
|
1014
|
+
// 两条件取或的语义即:任一条命中即受管,故豁免档必须两条都不命中。
|
|
1015
|
+
const T = mkfix();
|
|
1016
|
+
w(T, 'workflow/intents/2026-09-26-noledge.md', INTENT('noledge', '状态: approved\n级别: L1\n日期: 2026-09-26'));
|
|
1017
|
+
w(T, 'workflow/plans/2026-09-26-noledge.md', PLAN('noledge', '状态: draft\n级别: L1'));
|
|
1018
|
+
expectOk('检查15:日期早于生效日且台账无行 → 存量豁免(两条件皆不满足)', T);
|
|
1019
|
+
rmfix(T);
|
|
1020
|
+
}
|
|
1021
|
+
{
|
|
1022
|
+
// 条件①的**独立判别力**(复核 P2-2 更正):日期早(条件②不命中,本会被日期条件豁免)
|
|
1023
|
+
// + 台账有行但**指纹不匹配** → 条件①独立成立 ⇒ 受管 ⇒ 配对失败 ⇒ hard 拦。
|
|
1024
|
+
// 断言必须用 expectHard 而非 expectOk:旧单条件下该 doc 因日期早被整段跳过(exit 0),
|
|
1025
|
+
// 用 expectOk 会「新旧门都通过」而无法证伪条件①被移除(上一版即此缺陷,属无效测试)。
|
|
1026
|
+
// 现版本在忠实变异(还原旧单条件)下会变红——与核心回归场景同向锁定条件①的独立成立。
|
|
1027
|
+
const T = mkfix();
|
|
1028
|
+
const d1 = mkAnchorFixture(T, '2026-09-20');
|
|
1029
|
+
writeLedger(T, [{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: 'e'.repeat(64), prev: 'approved', source: 'tty' }]); // 指纹不符
|
|
1030
|
+
expectHard('检查15:日期早但台账有行(指纹不匹配)→ 受管并 hard 拦(条件①独立于日期成立)', T, '确认未对账');
|
|
1031
|
+
rmfix(T);
|
|
1032
|
+
}
|
|
1033
|
+
{
|
|
1034
|
+
// 重确认:同一 doc+stage 台账多行 → 取末次 ts 判锚(append-only 末次生效;不能因首行早于锚而误豁免)
|
|
1035
|
+
const T = mkfix();
|
|
1036
|
+
const d1 = mkAnchorFixture(T, '2026-09-28', '确认后被篡改的正文行\n');
|
|
1037
|
+
writeLedger(T, [
|
|
1038
|
+
{ ts: '2026-09-27T10:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }, // 早期行(锚前)
|
|
1039
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'done', fingerprint: d1.fp, prev: 'approved', source: 'tty' }, // 末次行(锚后)
|
|
1040
|
+
]);
|
|
1041
|
+
expectHard('检查15:同 doc+stage 台账多行取末次 ts → 篡改仍 hard 确认内容漂移(重确认场景)', T, '确认内容漂移');
|
|
1042
|
+
rmfix(T);
|
|
1043
|
+
}
|
|
1044
|
+
{
|
|
1045
|
+
// 非合法跳转行(revert-open / revert-draft 类回退注记)**不构成本 incident 的核心命题**——
|
|
1046
|
+
// 为把该属性与日期条件隔离,此处取日期早于生效日(条件②不命中),使受管准入**只**取决于台账条件:
|
|
1047
|
+
// · 若注记行被判为「合法跳转行」→ 台账条件命中 → 受管 → 无指纹 → hard 拦(期望:不拦,故失败)
|
|
1048
|
+
// · 期望:注记行被 VALID_STAGES 过滤 → 台账条件不命中 → 两条件皆不满足 → 存量豁免
|
|
1049
|
+
// 注记行不是「确认确实发生过」的证据,不能被当成台账行把文档拖进受管面
|
|
1050
|
+
// (否则回退操作反而制造 hard 拦——本 incident 自己就吃过这个亏,见 incident 备注「落态注记」)。
|
|
1051
|
+
const T = mkfix();
|
|
1052
|
+
const d1 = mkAnchorFixture(T, '2026-09-26');
|
|
1053
|
+
writeLedger(T, [
|
|
1054
|
+
{ ts: '2026-09-28T02:00:00.000Z', doc: d1.rel, stage: 'revert-open', fingerprint: 'n/a', prev: 'approved', source: 'chat-delegated', quote: '回退注记' },
|
|
1055
|
+
]);
|
|
1056
|
+
expectOk('检查15:仅 revert-open 注记行(非合法跳转 stage)→ 不构成台账条件,日期早 → 存量豁免 exit 0', T);
|
|
1057
|
+
rmfix(T);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// ---- 场景 72-74:检查 8 生效日锚改 git 首次加入日期(2026-09-28 check8-git-anchor)----
|
|
1062
|
+
// 换锚前锚取**文件名前 10 字符**——项目命名规范本身即 YYYY-MM-DD-<主题>,作者日常手写该前缀,
|
|
1063
|
+
// 「写早」无需任何额外动作、顺手即发生 → 一条 hard 门可被平凡绕过(有未勾验项却 exit 0)。
|
|
1064
|
+
// 换锚后锚取 git 首次加入日期(不可手填)→ 该通道关闭。
|
|
1065
|
+
{
|
|
1066
|
+
{
|
|
1067
|
+
// 【核心回归】文件名日期写早(2026-09-01 < accCutoff 2026-09-12),但**实际是新建档**
|
|
1068
|
+
// (git 首次加入在生效日之后)→ 仍须 hard 拦。换锚前此场景被 `filedate >= accCutoff` 跳过(静默放行)。
|
|
1069
|
+
const T = mkfix();
|
|
1070
|
+
gitInit(T);
|
|
1071
|
+
w(T, 'workflow/intents/2026-09-01-early.md', INTENT('early', '状态: done\n级别: L1\n日期: 2026-09-01', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1072
|
+
w(T, 'workflow/plans/2026-09-01-early.md', PLAN('early', '状态: done\n级别: L1'));
|
|
1073
|
+
gitCommitAll(T, 'add'); // 加入时间 = 今天(≥ accCutoff)→ git 事实:新建档
|
|
1074
|
+
expectHard('检查8:文件名日期写早但 git 首次加入够新 + 未勾验 → hard 拦(换锚前被静默跳过)', T, '验收未对账');
|
|
1075
|
+
rmfix(T);
|
|
1076
|
+
}
|
|
1077
|
+
{
|
|
1078
|
+
// 【退化语义】非 git 仓(fixture 常态)→ 不可判定 → 走存量口径,**不误报 hard**。
|
|
1079
|
+
// 钉住「换锚不引入新的误拦」:不可判定时宁可放过(advisory 层),不放 hard。
|
|
1080
|
+
const T = mkfix();
|
|
1081
|
+
w(T, 'workflow/intents/2026-09-15-nogit.md', INTENT('nogit', '状态: done\n级别: L1\n日期: 2026-09-15', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1082
|
+
w(T, 'workflow/plans/2026-09-15-nogit.md', PLAN('nogit', '状态: done\n级别: L1'));
|
|
1083
|
+
const r = run(T);
|
|
1084
|
+
check('检查8:非 git 仓 → 不可判定走存量口径(不误报 hard)',
|
|
1085
|
+
r.status === 0 && !outOf(r).includes('验收未对账'), `exit=${r.status}\n${outOf(r)}`);
|
|
1086
|
+
rmfix(T);
|
|
1087
|
+
}
|
|
1088
|
+
{
|
|
1089
|
+
// 【对照】同一内容、仅把**文件名**改为够新(09-15)→ 两锚下都应 hard 拦。
|
|
1090
|
+
// 与核心回归并置,说明「拦不拦由 git 事实决定,不由文件名字段决定」。
|
|
1091
|
+
const T = mkfix();
|
|
1092
|
+
gitInit(T);
|
|
1093
|
+
w(T, 'workflow/intents/2026-09-15-newer.md', INTENT('newer', '状态: done\n级别: L1\n日期: 2026-09-15', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1094
|
+
w(T, 'workflow/plans/2026-09-15-newer.md', PLAN('newer', '状态: done\n级别: L1'));
|
|
1095
|
+
gitCommitAll(T, 'add');
|
|
1096
|
+
expectHard('检查8:文件名日期够新 + git 加入够新 + 未勾验 → hard 拦(对照)', T, '验收未对账');
|
|
1097
|
+
rmfix(T);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
1102
|
+
process.exit(fail ? 1 : 0);
|