agentic-flow-kit 0.5.0 → 0.7.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 +2 -0
- package/modules/gates/dotnet-ca/check-architecture.sh +23 -5
- package/modules/hosts/omp/agents/implementer.md +2 -0
- package/modules/hosts/omp/agents/independent-reviewer.md +2 -0
- package/modules/hosts/omp/agents/ui-verifier.md +2 -0
- package/modules/hosts/opencode/agents/implementer.md +2 -0
- package/modules/hosts/opencode/agents/independent-reviewer.md +2 -0
- package/modules/hosts/opencode/agents/ui-verifier.md +2 -0
- package/modules/hosts/opencode/commands/build.md +20 -1
- package/modules/hosts/opencode/commands/design.md +2 -0
- package/modules/hosts/opencode/commands/gate-checklist.md +77 -0
- package/modules/hosts/opencode/commands/maintain.md +2 -3
- package/modules/hosts/opencode/commands/plan.md +4 -1
- package/modules/hosts/opencode/commands/review.md +1 -0
- 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 +4 -2
- package/modules/hosts/trae/agents/implementer.md +2 -0
- package/modules/hosts/trae/agents/independent-reviewer.md +2 -0
- package/modules/hosts/trae/agents/ui-verifier.md +2 -0
- package/modules/hosts/trae/commands/wf-build.md +20 -1
- package/modules/hosts/trae/commands/wf-design.md +2 -0
- package/modules/hosts/trae/commands/wf-gate-checklist.md +78 -0
- package/modules/hosts/trae/commands/wf-maintain.md +2 -3
- package/modules/hosts/trae/commands/wf-plan.md +4 -1
- package/modules/hosts/trae/commands/wf-review.md +1 -0
- 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 +4 -2
- 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 +2 -0
- package/modules/hosts/zcode/agents/independent-reviewer.md +2 -0
- package/modules/hosts/zcode/agents/ui-verifier.md +2 -0
- package/package.json +2 -2
- package/src/add-gate.mjs +5 -3
- package/src/doctor.mjs +121 -18
- package/src/gate-dotnet-ca.test.mjs +124 -0
- package/src/init.mjs +79 -17
- package/src/init.test.mjs +60 -1
- package/src/profiles.mjs +19 -2
- package/src/render.mjs +64 -4
- package/src/run-tests.mjs +3 -0
- package/src/sync-hosts.mjs +3 -2
- package/src/sync.mjs +64 -20
- package/src/sync.test.mjs +42 -0
- package/src/trae-hooks.test.mjs +115 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/board/index.html +32 -3
- package/templates/_agents/commands/build.md +5 -4
- package/templates/_agents/commands/design.md +1 -0
- package/templates/_agents/commands/maintain.md +1 -2
- package/templates/_agents/commands/plan.md +2 -0
- package/templates/_agents/commands/test.md +2 -1
- package/templates/_agents/hooks/check-pairing-incremental.sh +20 -7
- package/templates/_agents/metric-claims.txt +81 -0
- 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/scripts/agg-delegations.cjs +39 -30
- package/templates/_agents/scripts/agg-delegations.test.mjs +32 -0
- package/templates/_agents/scripts/check-loop.mjs +538 -16
- package/templates/_agents/scripts/check-loop.test.mjs +1116 -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 +81 -3
- package/templates/_agents/scripts/ensure-board.mjs +29 -2
- package/templates/_agents/scripts/gate-checklist.mjs +3 -0
- package/templates/_agents/scripts/gate-checklist.test.mjs +3 -3
- 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-metrics.mjs +31 -3
- package/templates/_agents/scripts/kb-search.mjs +22 -8
- package/templates/_agents/scripts/kb-search.test.mjs +38 -8
- package/templates/_agents/scripts/source-sync-check.mjs +30 -5
- package/templates/_agents/scripts/source-sync-check.test.mjs +133 -8
- package/templates/_agents/scripts/verify-wiki-consistency.mjs +46 -23
- package/templates/_agents/scripts/workflow-board-server.mjs +64 -9
- package/templates/_agents/scripts/workflow-board-server.test.mjs +136 -0
- package/templates/_agents/scripts/workflow-enums.mjs +4 -2
- package/templates/_agents/scripts/workflows-check.test.mjs +14 -3
- package/templates/_agents/workflow-enums.txt +3 -3
- package/templates/_agents/workflows/_TEMPLATE.md +1 -0
- package/templates/_githooks/pre-commit +16 -1
|
@@ -10,6 +10,7 @@ import os from 'node:os';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { spawnSync } from 'node:child_process';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { computeFingerprint } from './confirm-doc.mjs';
|
|
13
14
|
|
|
14
15
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
@@ -85,6 +86,19 @@ const gitCommitAll = (root, msg) => {
|
|
|
85
86
|
spawnSync(G, ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', msg], { cwd: root });
|
|
86
87
|
};
|
|
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
|
+
};
|
|
88
102
|
|
|
89
103
|
// ---- 场景 1:全合法闭环(intent+plan,L1,done 全勾验带证据)→ exit 0 ----
|
|
90
104
|
{
|
|
@@ -133,10 +147,14 @@ const rmfix = (root) => fs.rmSync(root, { recursive: true, force: true });
|
|
|
133
147
|
}
|
|
134
148
|
|
|
135
149
|
// ---- 场景 6:新建 done intent 验收未勾验 → hard;带附注不影响判定(papercut #4 回归)----
|
|
150
|
+
// 换锚(2026-09-28 check8-git-anchor)后「新建」由 **git 首次加入日期**判定(文件名字段不再作锚),
|
|
151
|
+
// 故本场景须为真 git 仓——否则不可判定、走存量口径而不再 hard。**意图与断言强度均不变**。
|
|
136
152
|
{
|
|
137
153
|
const T = mkfix();
|
|
154
|
+
gitInit(T);
|
|
138
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'));
|
|
139
156
|
w(T, 'workflow/plans/2026-09-13-e.md', PLAN('e', '状态: done\n级别: L1'));
|
|
157
|
+
gitCommitAll(T, 'add');
|
|
140
158
|
expectHard('新建 done 未勾验(附注在备注键) → hard 验收未对账', T, '验收未对账');
|
|
141
159
|
rmfix(T);
|
|
142
160
|
}
|
|
@@ -268,8 +286,10 @@ const rmfix = (root) => fs.rmSync(root, { recursive: true, force: true });
|
|
|
268
286
|
// ---- 场景 18:新建 done intent 缺「## 验收标准」节 → hard(2026-09-16 补口回归)----
|
|
269
287
|
{
|
|
270
288
|
const T = mkfix();
|
|
289
|
+
gitInit(T);
|
|
271
290
|
w(T, 'workflow/intents/2026-09-13-r.md', INTENT('r', '状态: done\n级别: L1\n日期: 2026-09-13', '\n## 目标\n关单前未写验收标准节。\n'));
|
|
272
291
|
w(T, 'workflow/plans/2026-09-13-r.md', PLAN('r', '状态: done\n级别: L1'));
|
|
292
|
+
gitCommitAll(T, 'add');
|
|
273
293
|
expectHard('新建 done 缺验收标准节 → hard 验收未对账', T, '缺「## 验收标准」节');
|
|
274
294
|
rmfix(T);
|
|
275
295
|
}
|
|
@@ -277,8 +297,10 @@ const rmfix = (root) => fs.rmSync(root, { recursive: true, force: true });
|
|
|
277
297
|
// ---- 场景 19:新建 done 勾选项缩进写法 → hard(口径放宽后仍拦)----
|
|
278
298
|
{
|
|
279
299
|
const T = mkfix();
|
|
300
|
+
gitInit(T);
|
|
280
301
|
w(T, 'workflow/intents/2026-09-13-s.md', INTENT('s', '状态: done\n级别: L1\n日期: 2026-09-13', '\n## 验收标准(可测试)\n - [ ] 缩进未勾项(此前后静默通过)\n'));
|
|
281
302
|
w(T, 'workflow/plans/2026-09-13-s.md', PLAN('s', '状态: done\n级别: L1'));
|
|
303
|
+
gitCommitAll(T, 'add');
|
|
282
304
|
expectHard('新建 done 勾选项缩进 → hard 验收未对账', T, '验收未对账');
|
|
283
305
|
rmfix(T);
|
|
284
306
|
}
|
|
@@ -477,5 +499,1099 @@ const rmfix = (root) => fs.rmSync(root, { recursive: true, force: true });
|
|
|
477
499
|
rmfix(T);
|
|
478
500
|
}
|
|
479
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
|
+
// **诚实边界(复核 P1 更正)**:该锚取 `%aI` = author date(作者自报时间戳),`git commit --date=<过去>`
|
|
1066
|
+
// 仍可伪造 → **不宣称「通道关闭」**,仅把伪造成本从「改文件名」抬到「主动伪造时间戳」。
|
|
1067
|
+
{
|
|
1068
|
+
{
|
|
1069
|
+
// 【核心回归】文件名日期写早(2026-09-01 < accCutoff 2026-09-12),但**实际是新建档**
|
|
1070
|
+
// (git 首次加入在生效日之后)→ 仍须 hard 拦。换锚前此场景被 `filedate >= accCutoff` 跳过(静默放行)。
|
|
1071
|
+
const T = mkfix();
|
|
1072
|
+
gitInit(T);
|
|
1073
|
+
w(T, 'workflow/intents/2026-09-01-early.md', INTENT('early', '状态: done\n级别: L1\n日期: 2026-09-01', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1074
|
+
w(T, 'workflow/plans/2026-09-01-early.md', PLAN('early', '状态: done\n级别: L1'));
|
|
1075
|
+
gitCommitAll(T, 'add'); // 加入时间 = 今天(≥ accCutoff)→ git 事实:新建档
|
|
1076
|
+
expectHard('检查8:文件名日期写早但 git 首次加入够新 + 未勾验 → hard 拦(换锚前被静默跳过)', T, '验收未对账');
|
|
1077
|
+
rmfix(T);
|
|
1078
|
+
}
|
|
1079
|
+
{
|
|
1080
|
+
// 【退化语义】非 git 仓(fixture 常态)→ 不可判定 → 走存量口径,**不误报 hard**。
|
|
1081
|
+
// 钉住「换锚不引入新的误拦」:不可判定时宁可放过(advisory 层),不放 hard。
|
|
1082
|
+
const T = mkfix();
|
|
1083
|
+
w(T, 'workflow/intents/2026-09-15-nogit.md', INTENT('nogit', '状态: done\n级别: L1\n日期: 2026-09-15', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1084
|
+
w(T, 'workflow/plans/2026-09-15-nogit.md', PLAN('nogit', '状态: done\n级别: L1'));
|
|
1085
|
+
const r = run(T);
|
|
1086
|
+
check('检查8:非 git 仓 → 不可判定走存量口径(不误报 hard)',
|
|
1087
|
+
r.status === 0 && !outOf(r).includes('验收未对账'), `exit=${r.status}\n${outOf(r)}`);
|
|
1088
|
+
rmfix(T);
|
|
1089
|
+
}
|
|
1090
|
+
{
|
|
1091
|
+
// 【对照】同一内容、仅把**文件名**改为够新(09-15)→ 两锚下都应 hard 拦。
|
|
1092
|
+
// 与核心回归并置,说明「拦不拦由 git 事实决定,不由文件名字段决定」。
|
|
1093
|
+
const T = mkfix();
|
|
1094
|
+
gitInit(T);
|
|
1095
|
+
w(T, 'workflow/intents/2026-09-15-newer.md', INTENT('newer', '状态: done\n级别: L1\n日期: 2026-09-15', '\n## 验收标准\n- [ ] 未勾项\n'));
|
|
1096
|
+
w(T, 'workflow/plans/2026-09-15-newer.md', PLAN('newer', '状态: done\n级别: L1'));
|
|
1097
|
+
gitCommitAll(T, 'add');
|
|
1098
|
+
expectHard('检查8:文件名日期够新 + git 加入够新 + 未勾验 → hard 拦(对照)', T, '验收未对账');
|
|
1099
|
+
rmfix(T);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// ---- 场景 75-79:检查 16 量化断言指标签名对账(2026-09-28 claim-exceeds-fix)----
|
|
1104
|
+
// 设计要点:本检查**只查显式签名**(`{{小写.点分}}`),不全文扫数字——理由是本仓「N 行/N 条/N 份」
|
|
1105
|
+
// 类表述多为历史叙述(描述某次提交当时的规模),全文扫描会产生不可消退的假阳性(P3 红线)。
|
|
1106
|
+
// 故本组用例的核心断言是**假阳性恒 0**:无签名时零告警;装户模板占位符(全大写)不误报。
|
|
1107
|
+
{
|
|
1108
|
+
const MC = 'ledger.lines = ledger.lines\ndocs.count.plans = docs.count.plans\n';
|
|
1109
|
+
const seedLedger = (T, lines) => w(T, '.agents/confirmations.jsonl', lines.join('\n') + '\n');
|
|
1110
|
+
{
|
|
1111
|
+
// 【核心回归】活跃文档留签名未回填 → warning 且提示实时值
|
|
1112
|
+
const T = mkfix();
|
|
1113
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1114
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}', '{"doc":"c","stage":"done"}']);
|
|
1115
|
+
w(T, 'workflow/plans/p1.md', PLAN('p1', '状态: draft\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1116
|
+
const r = run(T);
|
|
1117
|
+
const o = outOf(r);
|
|
1118
|
+
check('检查16:活跃文档留签名未回填 → WARN 且给出实时值 3(advisory 不阻断)',
|
|
1119
|
+
r.status === 0 && o.includes('指标待回填') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1120
|
+
rmfix(T);
|
|
1121
|
+
}
|
|
1122
|
+
{
|
|
1123
|
+
// 【假阳性恒 0 · 核心】无签名 → 零告警(历史叙述里的裸数字不得被扫)
|
|
1124
|
+
const T = mkfix();
|
|
1125
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1126
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1127
|
+
w(T, 'workflow/plans/p2.md', PLAN('p2', '状态: draft\n级别: L1', '\n台账共 47 行,其中 13 条并录(历史叙述,非签名)\n'));
|
|
1128
|
+
const r = run(T);
|
|
1129
|
+
check('检查16:无签名 → 零告警(裸数字是历史叙述,不得产生假阳性)',
|
|
1130
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1131
|
+
rmfix(T);
|
|
1132
|
+
}
|
|
1133
|
+
{
|
|
1134
|
+
// 【假阳性恒 0 · 关键边界】装户模板占位符(全大写 SCREAMING_CASE)不误报
|
|
1135
|
+
// ——不收窄形态会对本仓 plans 内 11 处合法模板占位符全部误报(实测量级)。
|
|
1136
|
+
// 注:占位符**运行时拼装**而非字面写入——本仓的占位符残留门禁(doctor §5)会把字面
|
|
1137
|
+
// 全大写 `{{X}}` 判为残留并改写包源副本,导致本用例在包源侧退化为测「<填写> 不误报」
|
|
1138
|
+
// (仍绿但不再证明 SCREAMING_CASE 被排除)。拼装后两侧语义一致、断言强度不退。
|
|
1139
|
+
const SCREAMING = (n) => '{{' + n + '}}';
|
|
1140
|
+
const T = mkfix();
|
|
1141
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1142
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1143
|
+
w(T, 'workflow/plans/p3.md', PLAN('p3', '状态: draft\n级别: L1',
|
|
1144
|
+
'\n构建 = ' + SCREAMING('BUILD_CMD') + ';端口 ' + SCREAMING('BOARD_PORT') + ';项目 ' + SCREAMING('PROJECT_NAME') + '\n'));
|
|
1145
|
+
const r = run(T);
|
|
1146
|
+
check('检查16:装户模板占位符(全大写 SCREAMING_CASE)→ 不误报(形态收窄)',
|
|
1147
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1148
|
+
rmfix(T);
|
|
1149
|
+
}
|
|
1150
|
+
{
|
|
1151
|
+
// 【fail-loud】引用未登记指标 → 明示(未登记=无从对账,不静默放过)
|
|
1152
|
+
const T = mkfix();
|
|
1153
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1154
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1155
|
+
w(T, 'workflow/plans/p4.md', PLAN('p4', '状态: draft\n级别: L1', '\n引用 {{nope.metric}}\n'));
|
|
1156
|
+
const r = run(T);
|
|
1157
|
+
check('检查16:引用未登记指标 → fail-loud 出账(不静默跳过)',
|
|
1158
|
+
outOf(r).includes('指标未登记'), `exit=${r.status}\n${outOf(r)}`);
|
|
1159
|
+
rmfix(T);
|
|
1160
|
+
}
|
|
1161
|
+
{
|
|
1162
|
+
// 【转义】讲语法 / 举例的行加反斜杠 → 不报(否则「文档教怎么用签名」被误判为「忘了回填」)
|
|
1163
|
+
// 该需求由本检查在自己的 spec 上抓到(实现后立刻 5 处误报)。
|
|
1164
|
+
const T = mkfix();
|
|
1165
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1166
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1167
|
+
w(T, 'workflow/plans/p8.md', PLAN('p8', '状态: draft\n级别: L1',
|
|
1168
|
+
'\n写法示例如 `\\{{ledger.lines}}`(本行是讲语法,非断言)\n'));
|
|
1169
|
+
const r = run(T);
|
|
1170
|
+
check('检查16:转义 \\{{...}} 的行为示意 → 不报(区分「讲语法」与「真断言」)',
|
|
1171
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1172
|
+
rmfix(T);
|
|
1173
|
+
}
|
|
1174
|
+
{
|
|
1175
|
+
// 【守卫】check-loop 自身可加载(防语法错致整条门禁静默失效)
|
|
1176
|
+
// 实证教训:本次实现期把 `continue` 误用在 forEach 回调内 → 模块 SyntaxError →
|
|
1177
|
+
// check-loop 崩溃退出,而「指标告警 0 条」看起来像通过。**假绿比红更危险**,故钉死。
|
|
1178
|
+
const r = run(mkfix());
|
|
1179
|
+
check('检查16:check-loop 可正常加载运行(防语法错致门禁静默失效——假绿比红危险)',
|
|
1180
|
+
r.status === 0 && !/SyntaxError|Illegal continue/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
1181
|
+
}
|
|
1182
|
+
{
|
|
1183
|
+
// 【fail-loud】登记了指标但无取数器 → 明示「登记了却没查」(防假阴性:登记表写了却不实现)
|
|
1184
|
+
const T = mkfix();
|
|
1185
|
+
w(T, '.agents/metric-claims.txt', MC + 'ghost.metric = ghost.metric\n');
|
|
1186
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1187
|
+
w(T, 'workflow/plans/p5.md', PLAN('p5', '状态: draft\n级别: L1'));
|
|
1188
|
+
const r = run(T);
|
|
1189
|
+
check('检查16:登记无取数器 → fail-loud(登记了却没实现=其实没查)',
|
|
1190
|
+
outOf(r).includes('无对应取数器'), `exit=${r.status}\n${outOf(r)}`);
|
|
1191
|
+
rmfix(T);
|
|
1192
|
+
}
|
|
1193
|
+
{
|
|
1194
|
+
// 【退化语义】登记表不存在 → 静默跳过(未启用该检查的装户不应被噪声打扰)
|
|
1195
|
+
const T = mkfix();
|
|
1196
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1197
|
+
w(T, 'workflow/plans/p6.md', PLAN('p6', '状态: draft\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1198
|
+
const r = run(T);
|
|
1199
|
+
check('检查16:登记表缺失 → 静默跳过(不误报,未启用装户零噪声)',
|
|
1200
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1201
|
+
rmfix(T);
|
|
1202
|
+
}
|
|
1203
|
+
{
|
|
1204
|
+
// 【活跃态尺度】终态文档不扫(沿检查 4 现有尺度:历史数字是历史叙述)
|
|
1205
|
+
const T = mkfix();
|
|
1206
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1207
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1208
|
+
w(T, 'workflow/plans/p7.md', PLAN('p7', '状态: done\n级别: L1', '\n台账共 {{ledger.lines}} 行\n'));
|
|
1209
|
+
const r = run(T);
|
|
1210
|
+
check('检查16:终态文档不扫(历史叙述豁免,沿检查 4 尺度)',
|
|
1211
|
+
r.status === 0 && !outOf(r).includes('指标待回填'), `exit=${r.status}\n${outOf(r)}`);
|
|
1212
|
+
rmfix(T);
|
|
1213
|
+
}
|
|
1214
|
+
{
|
|
1215
|
+
// 【取数正确性】docs.count.plans 应计活跃+终态全部 plans(排除 _TEMPLATE)
|
|
1216
|
+
// 注:取数器与 docFiles() 同口径——须**以数字开头**(命名规范强制 `YYYY-MM-DD-` 前缀),
|
|
1217
|
+
// 故 fixture 文件名必须合规(首版用 a.md/b.md 使取数为 0,暴露的是用例不合规而非判据错)。
|
|
1218
|
+
const T = mkfix();
|
|
1219
|
+
w(T, '.agents/metric-claims.txt', 'docs.count.plans = docs.count.plans\n');
|
|
1220
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1221
|
+
w(T, 'workflow/plans/2026-09-01-a.md', PLAN('a', '状态: draft\n级别: L1'));
|
|
1222
|
+
w(T, 'workflow/plans/2026-09-02-b.md', PLAN('b', '状态: done\n级别: L1'));
|
|
1223
|
+
w(T, 'workflow/plans/_TEMPLATE.md', PLAN('t', '状态: draft\n级别: L1'));
|
|
1224
|
+
w(T, 'workflow/plans/2026-09-03-c.md', PLAN('c', '状态: draft\n级别: L1', '\n共 {{docs.count.plans}} 份\n'));
|
|
1225
|
+
const r = run(T);
|
|
1226
|
+
check('检查16:取数正确——docs.count.plans 计 3 份(排除 _TEMPLATE,含终态)',
|
|
1227
|
+
outOf(r).includes('实时值 = 3'), `exit=${r.status}\n${outOf(r)}`);
|
|
1228
|
+
rmfix(T);
|
|
1229
|
+
}
|
|
1230
|
+
{
|
|
1231
|
+
// 【复核 P1 回归】转义按「逐个出现」而非整行——同一行的真签名不得被转义位藏住
|
|
1232
|
+
// 复核实证:首版「行内任一 `\{{` 即整行跳过」可被这样绕过:
|
|
1233
|
+
// `… \{{a.b}} 示意… {{ledger.lines}} 未回填` → 整行静默放过(0 告警)。
|
|
1234
|
+
const T = mkfix();
|
|
1235
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1236
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1237
|
+
w(T, 'workflow/plans/p9.md', PLAN('p9', '状态: draft\n级别: L1',
|
|
1238
|
+
'\n讲语法 \\{{a.b}} 示意,但本行也有真断言 {{ledger.lines}} 未回填\n'));
|
|
1239
|
+
const r = run(T);
|
|
1240
|
+
check('检查16:同行「转义示意 + 真未回填」→ 真签名仍报(转义按逐个出现,不按整行)',
|
|
1241
|
+
outOf(r).includes('指标待回填'), `exit=${r.status}\n${outOf(r)}`);
|
|
1242
|
+
rmfix(T);
|
|
1243
|
+
}
|
|
1244
|
+
{
|
|
1245
|
+
// 【复核 P2 回归】形态不符的签名不得静默漏过(首版只认严格小写点分 → 笔误零告警)
|
|
1246
|
+
const T = mkfix();
|
|
1247
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1248
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1249
|
+
w(T, 'workflow/plans/p10.md', PLAN('p10', '状态: draft\n级别: L1', '\n引用 {{Ledger.Lines}} 笔误\n'));
|
|
1250
|
+
const r = run(T);
|
|
1251
|
+
check('检查16:形态不符(大写点分笔误)→ 出账「指标形态」(不静默漏过)',
|
|
1252
|
+
outOf(r).includes('指标形态'), `exit=${r.status}\n${outOf(r)}`);
|
|
1253
|
+
rmfix(T);
|
|
1254
|
+
}
|
|
1255
|
+
{
|
|
1256
|
+
// 【复核 P2 回归】形态检查不得误伤装户模板占位符(全大写 SCREAMING_CASE 仍豁免)
|
|
1257
|
+
const T = mkfix();
|
|
1258
|
+
w(T, '.agents/metric-claims.txt', MC);
|
|
1259
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1260
|
+
const S = (n) => '{{' + n + '}}';
|
|
1261
|
+
w(T, 'workflow/plans/p11.md', PLAN('p11', '状态: draft\n级别: L1',
|
|
1262
|
+
'\n模板 ' + S('BUILD_CMD') + ' 与 ' + S('BOARD_PORT') + '\n'));
|
|
1263
|
+
const r = run(T);
|
|
1264
|
+
check('检查16:形态检查不误伤装户模板占位符(全大写仍豁免)',
|
|
1265
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1266
|
+
rmfix(T);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// ---- 场景 80-90:检查 16 装户侧 derivers 动态载入(2026-09-28 adopter-derivers)----
|
|
1271
|
+
// 本组核心是**三条假绿防线**:装户模块坏掉时必须响亮出账,绝不静默降级成「只用内置指标」。
|
|
1272
|
+
// 对照:模块**缺失**是正常态(零告警)——两组行为必须可区分,否则装户无法判断自己错在哪。
|
|
1273
|
+
{
|
|
1274
|
+
const MC2 = 'ledger.lines = ledger.lines\nmy.custom = my.custom\n';
|
|
1275
|
+
const MCB = 'ledger.lines = ledger.lines\ndocs.count.plans = docs.count.plans\n'; // 仅内置指标
|
|
1276
|
+
const seedLedger = (T, lines) => w(T, '.agents/confirmations.jsonl', lines.join('\n') + '\n');
|
|
1277
|
+
const activePlan = (T, name, body) => {
|
|
1278
|
+
// 同时写同名 intent:否则「配对断裂」hard-block 会干扰本组断言(本组只测取数器分层)
|
|
1279
|
+
w(T, `workflow/intents/2026-09-01-${name}.md`, INTENT(name, '状态: draft\n级别: L1'));
|
|
1280
|
+
w(T, `workflow/plans/2026-09-01-${name}.md`, PLAN(name, '状态: draft\n级别: L1', body));
|
|
1281
|
+
};
|
|
1282
|
+
{
|
|
1283
|
+
// 【正向】装户模块存在 + 自定义指标 → 取数正确
|
|
1284
|
+
const T = mkfix();
|
|
1285
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1286
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1287
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => 7 } };\n");
|
|
1288
|
+
activePlan(T, 'ok', '\n值 {{my.custom}}\n');
|
|
1289
|
+
const r = run(T);
|
|
1290
|
+
check('检查16装户:模块存在 + 自定义指标 → 实时值 = 7(取数正确)',
|
|
1291
|
+
outOf(r).includes('my.custom') && outOf(r).includes('实时值 = 7'), `exit=${r.status}\n${outOf(r)}`);
|
|
1292
|
+
rmfix(T);
|
|
1293
|
+
}
|
|
1294
|
+
{
|
|
1295
|
+
// 【假绿防线 ①】模块语法错 → 响亮出账 + check-loop 仍正常退出(不崩)
|
|
1296
|
+
const T = mkfix();
|
|
1297
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1298
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1299
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => { \n"); // 故意不闭合
|
|
1300
|
+
activePlan(T, 'syn', '\n值 {{my.custom}}\n');
|
|
1301
|
+
const r = run(T);
|
|
1302
|
+
const o = outOf(r);
|
|
1303
|
+
// 断言必须用**唯一的 warning 前缀**匹配——「装户取数器载入失败」这串字也出现在
|
|
1304
|
+
// 「无对应取数器」那条的提示语里(「见上方「…载入失败」条目」),只用它匹配会**在真实条目
|
|
1305
|
+
// 消失时仍然通过**(变异自验实测:静默吞掉 emit 行后该断言不翻红)。故用 `- [WARN 装户取数器载入失败]`。
|
|
1306
|
+
check('检查16装户:模块语法错 → 响亮出账「装户取数器载入失败」(独立成条,明示文件)',
|
|
1307
|
+
o.includes('- [WARN 装户取数器载入失败]') && o.includes('metric-derivers.cjs') && !o.includes('未登记该指标'),
|
|
1308
|
+
`exit=${r.status}\n${o}`);
|
|
1309
|
+
check('检查16装户:模块语法错 → 引擎自身不因装户代码崩溃(无裸 SyntaxError 崩栈)',
|
|
1310
|
+
r.status === 0 && !/Illegal|at ModuleLoader|at compileSourceTextModule/.test(o),
|
|
1311
|
+
`exit=${r.status}\n${o}`);
|
|
1312
|
+
rmfix(T);
|
|
1313
|
+
}
|
|
1314
|
+
{
|
|
1315
|
+
// 【假绿防线 ②】取数器抛异常 → 响亮出账(明示错误)
|
|
1316
|
+
const T = mkfix();
|
|
1317
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1318
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1319
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => { throw new Error('boom-xyz'); } } };\n");
|
|
1320
|
+
activePlan(T, 'thr', '\n值 {{my.custom}}\n');
|
|
1321
|
+
const r = run(T);
|
|
1322
|
+
check('检查16装户:取数器抛异常 → 响亮出账且明示错误',
|
|
1323
|
+
outOf(r).includes('指标取数失败') && outOf(r).includes('boom-xyz'), `exit=${r.status}\n${outOf(r)}`);
|
|
1324
|
+
rmfix(T);
|
|
1325
|
+
}
|
|
1326
|
+
{
|
|
1327
|
+
// 【假绿防线 ③】返回非有限数字(字符串 / NaN / Promise)→ 拦下并明示类型
|
|
1328
|
+
const T = mkfix();
|
|
1329
|
+
w(T, '.agents/metric-claims.txt', 'my.str = my.str\nmy.nan = my.nan\nmy.prom = my.prom\n');
|
|
1330
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1331
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1332
|
+
"module.exports = { derivers: {",
|
|
1333
|
+
" 'my.str': () => 'not-a-number',",
|
|
1334
|
+
" 'my.nan': () => NaN,",
|
|
1335
|
+
" 'my.prom': () => Promise.resolve(1),",
|
|
1336
|
+
"} };",
|
|
1337
|
+
'',
|
|
1338
|
+
].join('\n'));
|
|
1339
|
+
activePlan(T, 'types', '\n{{my.str}} {{my.nan}} {{my.prom}}\n');
|
|
1340
|
+
const r = run(T);
|
|
1341
|
+
const o = outOf(r);
|
|
1342
|
+
check('检查16装户:返回字符串 → 取数失败(明示类型 string)',
|
|
1343
|
+
o.includes('不是有限数字') && o.includes('string'), `exit=${r.status}\n${o}`);
|
|
1344
|
+
check('检查16装户:返回 NaN → 取数失败', o.includes('number(NaN)'), `exit=${r.status}\n${o}`);
|
|
1345
|
+
check('检查16装户:返回 Promise(误写 async)→ 取数失败并提示须同步',
|
|
1346
|
+
o.includes('Promise') && o.includes('同步'), `exit=${r.status}\n${o}`);
|
|
1347
|
+
rmfix(T);
|
|
1348
|
+
}
|
|
1349
|
+
{
|
|
1350
|
+
// 【区分性】模块缺失 → 零告警(与「模块坏」必须行为可区分)
|
|
1351
|
+
const T = mkfix();
|
|
1352
|
+
w(T, '.agents/metric-claims.txt', MCB);
|
|
1353
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1354
|
+
activePlan(T, 'none', '');
|
|
1355
|
+
const r = run(T);
|
|
1356
|
+
check('检查16装户:模块缺失 → 零告警(正常态,与「模块坏」可区分)',
|
|
1357
|
+
r.status === 0 && !outOf(r).includes('指标'), `exit=${r.status}\n${outOf(r)}`);
|
|
1358
|
+
rmfix(T);
|
|
1359
|
+
}
|
|
1360
|
+
{
|
|
1361
|
+
// 【优先级】装户定义内置同名 → 内置生效 + advisory 明示被忽略
|
|
1362
|
+
const T = mkfix();
|
|
1363
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\n');
|
|
1364
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}']);
|
|
1365
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'ledger.lines': () => 999 } };\n");
|
|
1366
|
+
activePlan(T, 'prio', '\n台账 {{ledger.lines}}\n');
|
|
1367
|
+
const r = run(T);
|
|
1368
|
+
const o = outOf(r);
|
|
1369
|
+
check('检查16装户:内置同名 → 内置值生效(2,非装户的 999)',
|
|
1370
|
+
o.includes('实时值 = 2'), `exit=${r.status}\n${o}`);
|
|
1371
|
+
check('检查16装户:内置同名 → advisory 明示装户定义被忽略',
|
|
1372
|
+
o.includes('被忽略'), `exit=${r.status}\n${o}`);
|
|
1373
|
+
rmfix(T);
|
|
1374
|
+
}
|
|
1375
|
+
{
|
|
1376
|
+
// 【未导出】模块存在但无 derivers 导出 → fail-loud
|
|
1377
|
+
const T = mkfix();
|
|
1378
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1379
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1380
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = {};\n");
|
|
1381
|
+
activePlan(T, 'noexp', '\n{{my.custom}}\n');
|
|
1382
|
+
const r = run(T);
|
|
1383
|
+
check('检查16装户:模块无 derivers 导出 → 响亮出账「载入失败」(独立成条,不静默)',
|
|
1384
|
+
outOf(r).includes('- [WARN 装户取数器载入失败]') && outOf(r).includes('未导出'), `exit=${r.status}\n${outOf(r)}`);
|
|
1385
|
+
rmfix(T);
|
|
1386
|
+
}
|
|
1387
|
+
{
|
|
1388
|
+
// 【旧路径迁移提示】残留首版 .mjs 文件时,须明示「已改用 .cjs」——
|
|
1389
|
+
// 否则装户对着「无取数器」查不出所以然(跨版本实测:require(esm) 在 Node 18/22 不可用,
|
|
1390
|
+
// 故首版 .mjs 契约在 CI 目标版本上根本不工作,必须给出可操作的迁移指引)
|
|
1391
|
+
const T = mkfix();
|
|
1392
|
+
w(T, '.agents/metric-claims.txt', MC2);
|
|
1393
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1394
|
+
w(T, '.agents/metric-derivers.mjs', "export const derivers = { 'my.custom': () => 7 };\n"); // 旧路径
|
|
1395
|
+
activePlan(T, 'legacy', '\n{{my.custom}}\n');
|
|
1396
|
+
const r = run(T);
|
|
1397
|
+
const o = outOf(r);
|
|
1398
|
+
check('检查16装户:残留旧 .mjs → 明示「已改用 .cjs」并给出 CJS 写法',
|
|
1399
|
+
o.includes('装户取数器载入失败') && o.includes('metric-derivers.mjs') && o.includes('.cjs') && o.includes('module.exports'),
|
|
1400
|
+
`exit=${r.status}\n${o}`);
|
|
1401
|
+
rmfix(T);
|
|
1402
|
+
}
|
|
1403
|
+
{
|
|
1404
|
+
// 【未登记指标仍走原路】装户定义了函数但没在 registry 登记 → 报「未登记」
|
|
1405
|
+
const T = mkfix();
|
|
1406
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\n');
|
|
1407
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1408
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'my.custom': () => 7 } };\n");
|
|
1409
|
+
activePlan(T, 'unreg', '\n{{my.custom}}\n');
|
|
1410
|
+
const r = run(T);
|
|
1411
|
+
check('检查16装户:函数有但 registry 未登记 → 报「未登记」(两处都要)',
|
|
1412
|
+
outOf(r).includes('指标未登记'), `exit=${r.status}\n${outOf(r)}`);
|
|
1413
|
+
rmfix(T);
|
|
1414
|
+
}
|
|
1415
|
+
{
|
|
1416
|
+
// 【ctx 能力】glob / read 取数正确
|
|
1417
|
+
const T = mkfix();
|
|
1418
|
+
w(T, '.agents/metric-claims.txt', 'my.sql = my.sql\nmy.read = my.read\n');
|
|
1419
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1420
|
+
fs.mkdirSync(path.join(T, 'backend', 'migrations'), { recursive: true }); // w() 不建父目录
|
|
1421
|
+
w(T, 'backend/migrations/001.sql', 'x');
|
|
1422
|
+
w(T, 'backend/migrations/002.sql', 'x');
|
|
1423
|
+
w(T, 'backend/other.txt', 'x');
|
|
1424
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1425
|
+
"module.exports = { derivers: {",
|
|
1426
|
+
" 'my.sql': (ctx) => ctx.glob('backend/migrations/*.sql').length,",
|
|
1427
|
+
" 'my.read': (ctx) => ctx.read('backend/migrations/001.sql').length,",
|
|
1428
|
+
"} };",
|
|
1429
|
+
'',
|
|
1430
|
+
].join('\n'));
|
|
1431
|
+
activePlan(T, 'ctx', '\n{{my.sql}} {{my.read}}\n');
|
|
1432
|
+
const r = run(T);
|
|
1433
|
+
const o = outOf(r);
|
|
1434
|
+
check('检查16装户:ctx.glob 计数正确(2 个 sql)', o.includes('my.sql') && o.includes('实时值 = 2'), `exit=${r.status}\n${o}`);
|
|
1435
|
+
check('检查16装户:ctx.read 读取正确(内容 1 字符)', o.includes('my.read') && o.includes('实时值 = 1'), `exit=${r.status}\n${o}`);
|
|
1436
|
+
rmfix(T);
|
|
1437
|
+
}
|
|
1438
|
+
{
|
|
1439
|
+
// 【存量零差异】无装户模块时,内置指标行为与改前一致
|
|
1440
|
+
const T = mkfix();
|
|
1441
|
+
w(T, '.agents/metric-claims.txt', MCB);
|
|
1442
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}', '{"doc":"b","stage":"done"}']);
|
|
1443
|
+
activePlan(T, 'base', '\n台账 {{ledger.lines}};plans {{docs.count.plans}}\n');
|
|
1444
|
+
const r = run(T);
|
|
1445
|
+
check('检查16装户:无模块时内置指标照常取数(零差异)',
|
|
1446
|
+
outOf(r).includes('实时值 = 2') && outOf(r).includes('实时值 = 1'), `exit=${r.status}\n${outOf(r)}`);
|
|
1447
|
+
rmfix(T);
|
|
1448
|
+
}
|
|
1449
|
+
{
|
|
1450
|
+
// 【ctx.glob 语义】自查修正回归:`**/` 须含零层、可跨段;`*` 不跨 `/`
|
|
1451
|
+
// (首版实现用「先转义后 replace 星号」,实测 `src/**/*.ts` 匹配不到 `src/a/b/c.ts`)
|
|
1452
|
+
const T = mkfix();
|
|
1453
|
+
w(T, '.agents/metric-claims.txt', 'g.deep = g.deep\ng.flat = g.flat\ng.skip = g.skip\n');
|
|
1454
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1455
|
+
fs.mkdirSync(path.join(T, 'src', 'a', 'b'), { recursive: true });
|
|
1456
|
+
fs.mkdirSync(path.join(T, 'src', 'nested'), { recursive: true });
|
|
1457
|
+
w(T, 'src/a/b/c.ts', 'x');
|
|
1458
|
+
w(T, 'src/top.ts', 'x');
|
|
1459
|
+
w(T, 'src/nested/deep.ts', 'x');
|
|
1460
|
+
fs.mkdirSync(path.join(T, 'node_modules', 'pkg'), { recursive: true });
|
|
1461
|
+
w(T, 'node_modules/pkg/dep.ts', 'x');
|
|
1462
|
+
w(T, '.agents/metric-derivers.cjs', [
|
|
1463
|
+
"module.exports = { derivers: {",
|
|
1464
|
+
" 'g.deep': (ctx) => ctx.glob('src/**/*.ts').length,",
|
|
1465
|
+
" 'g.flat': (ctx) => ctx.glob('src/*.ts').length,",
|
|
1466
|
+
" 'g.skip': (ctx) => ctx.glob('**/*.ts').length,",
|
|
1467
|
+
"} };",
|
|
1468
|
+
'',
|
|
1469
|
+
].join('\n'));
|
|
1470
|
+
activePlan(T, 'glob', '\n{{g.deep}} {{g.flat}} {{g.skip}}\n');
|
|
1471
|
+
const r = run(T);
|
|
1472
|
+
const o = outOf(r);
|
|
1473
|
+
check('检查16装户:ctx.glob 的 `**/` 含零层且跨段(src/**/*.ts 计 3)',
|
|
1474
|
+
o.includes('g.deep') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1475
|
+
check('检查16装户:ctx.glob 的 `*` 不跨 /(src/*.ts 计 1)',
|
|
1476
|
+
o.includes('g.flat') && o.includes('实时值 = 1'), `exit=${r.status}\n${o}`);
|
|
1477
|
+
check('检查16装户:ctx.glob 剪枝 node_modules(**/*.ts 计 3,非 4)',
|
|
1478
|
+
o.includes('g.skip') && o.includes('实时值 = 3'), `exit=${r.status}\n${o}`);
|
|
1479
|
+
rmfix(T);
|
|
1480
|
+
}
|
|
1481
|
+
{
|
|
1482
|
+
// 【ctx.glob 字面转义】点号须为字面(首版漏转义会误匹配)
|
|
1483
|
+
const T = mkfix();
|
|
1484
|
+
w(T, '.agents/metric-claims.txt', 'g.lit = g.lit\n');
|
|
1485
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1486
|
+
w(T, 'a.b.sql', 'x');
|
|
1487
|
+
w(T, 'axb.sql', 'x');
|
|
1488
|
+
w(T, '.agents/metric-derivers.cjs',
|
|
1489
|
+
"module.exports = { derivers: { 'g.lit': (ctx) => ctx.glob('a.b.sql').length } };\n");
|
|
1490
|
+
activePlan(T, 'lit', '\n{{g.lit}}\n');
|
|
1491
|
+
const r = run(T);
|
|
1492
|
+
check('检查16装户:ctx.glob 点号按字面(a.b.sql 计 1,不含 axb.sql)',
|
|
1493
|
+
outOf(r).includes('实时值 = 1'), `exit=${r.status}\n${outOf(r)}`);
|
|
1494
|
+
rmfix(T);
|
|
1495
|
+
}
|
|
1496
|
+
{
|
|
1497
|
+
// 【复核 P2 回归】模块坏但**没有任何装户指标**时,不得平白出账
|
|
1498
|
+
// (首版无条件报「载入失败」——半成品模块会给清白的装户增加不可消除的噪声)
|
|
1499
|
+
const T = mkfix();
|
|
1500
|
+
w(T, '.agents/metric-claims.txt', MCB); // 只有内置指标
|
|
1501
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1502
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'x.y': () => { \n"); // 坏模块
|
|
1503
|
+
activePlan(T, 'noise', '');
|
|
1504
|
+
const r = run(T);
|
|
1505
|
+
check('检查16装户:坏模块 + 无装户指标 → 不出账(无判定依据不产噪声)',
|
|
1506
|
+
r.status === 0 && !outOf(r).includes('装户取数器载入失败'), `exit=${r.status}\n${outOf(r)}`);
|
|
1507
|
+
rmfix(T);
|
|
1508
|
+
}
|
|
1509
|
+
{
|
|
1510
|
+
// 【复核 P2 回归】内置同名 advisory 按**指标**去重(首版按 registry 行重复出账)
|
|
1511
|
+
const T = mkfix();
|
|
1512
|
+
w(T, '.agents/metric-claims.txt', 'ledger.lines = ledger.lines\nledger.lines = ledger.lines\nledger.lines = ledger.lines\n');
|
|
1513
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1514
|
+
w(T, '.agents/metric-derivers.cjs', "module.exports = { derivers: { 'ledger.lines': () => 999 } };\n");
|
|
1515
|
+
activePlan(T, 'dedup', '\n台账 {{ledger.lines}}\n');
|
|
1516
|
+
const r = run(T);
|
|
1517
|
+
const n = (outOf(r).match(/被忽略/g) || []).length;
|
|
1518
|
+
check('检查16装户:内置同名 advisory 按指标去重(3 行登记 → 恰 1 条)', n === 1, `实际 ${n} 条\n${outOf(r)}`);
|
|
1519
|
+
rmfix(T);
|
|
1520
|
+
}
|
|
1521
|
+
{
|
|
1522
|
+
// 【复核 P2 回归】软链接成环 → 不得无界递归(复核实测首版 depth 128、同文件重复 64 份)
|
|
1523
|
+
const T = mkfix();
|
|
1524
|
+
w(T, '.agents/metric-claims.txt', 'g.loop = g.loop\n');
|
|
1525
|
+
seedLedger(T, ['{"doc":"a","stage":"done"}']);
|
|
1526
|
+
fs.mkdirSync(path.join(T, 'loopdir', 'inner'), { recursive: true });
|
|
1527
|
+
w(T, 'loopdir/inner/f.txt', 'x');
|
|
1528
|
+
// 用 **dir 型 symlink**(POSIX 语义的目录软链接),而非 junction(Windows 特有、不具代表性)。
|
|
1529
|
+
// 用户 2026-09-28 追问「这里用到 sh 了吗」促使追验:防环走 fs.realpathSync(Node 原生跨平台),
|
|
1530
|
+
// **与 sh 无关**——故本断言在两种链接语义下都应成立。
|
|
1531
|
+
let linked = false;
|
|
1532
|
+
try {
|
|
1533
|
+
fs.symlinkSync(path.join(T, 'loopdir'), path.join(T, 'loopdir', 'inner', 'back'), 'dir');
|
|
1534
|
+
linked = true;
|
|
1535
|
+
} catch { /* 无建链接权限(未开开发者模式)→ 跳过该断言 */ }
|
|
1536
|
+
w(T, '.agents/metric-derivers.cjs',
|
|
1537
|
+
"module.exports = { derivers: { 'g.loop': (ctx) => ctx.glob('loopdir/**/*.txt').length } };\n");
|
|
1538
|
+
activePlan(T, 'loop', '\n{{g.loop}}\n');
|
|
1539
|
+
const r = run(T);
|
|
1540
|
+
const o16 = outOf(r);
|
|
1541
|
+
check(linked
|
|
1542
|
+
// 防环生效 ⇒ 计数为 1(只有 inner/f.txt);无防环会沿环重复到 64
|
|
1543
|
+
? '检查16装户:dir 型 symlink 成环(POSIX 语义)→ 防环生效,计数恰 1'
|
|
1544
|
+
: '检查16装户:软链接成环用例跳过(本机无建链接权限)',
|
|
1545
|
+
linked ? o16.includes('实时值 = 1') : true, `exit=${r.status}\n${o16}`);
|
|
1546
|
+
rmfix(T);
|
|
1547
|
+
}
|
|
1548
|
+
{
|
|
1549
|
+
// 【归属契约】装户取数模块必须归 owned——否则落回 managed,重演本单要修的缺陷本身
|
|
1550
|
+
// (sync 永久报「本地已改」+ doctor WARN + check-loop 因供应链防线静默停摆)
|
|
1551
|
+
//
|
|
1552
|
+
// **本用例不得依赖包源 `src/`**(复核 P1 更正):首版写 `await import('../../../src/profiles.mjs')`,
|
|
1553
|
+
// 该路径在**本仓**解析到 `<repo>/src/`(存在),但在**装户安装**里 `.agents/scripts/` 只往上三层
|
|
1554
|
+
// 即 `<install>/../src/`——不存在 → `ERR_MODULE_NOT_FOUND` → **整个套件崩掉**。而本测试文件是
|
|
1555
|
+
// managed/shipped,故每个装户拿到的测试套件都是坏的;`npm test` 又只跑 `templates/` 副本
|
|
1556
|
+
// (见 src/run-tests.mjs),所以 CI 看不见。修法:优先动态 import,失败则**降级为读源码断言**
|
|
1557
|
+
// (isOwned 的判定是纯字面量列表,读源即可判,且包源/装副本都在同一路径下)。
|
|
1558
|
+
let isOwned = null;
|
|
1559
|
+
try {
|
|
1560
|
+
({ isOwned } = await import('../../../src/profiles.mjs'));
|
|
1561
|
+
} catch { /* 装户环境无包源 src/ → 走下面的降级断言 */ }
|
|
1562
|
+
if (typeof isOwned === 'function') {
|
|
1563
|
+
check('检查16装户:isOwned(.agents/metric-derivers.cjs) 为真(与登记表同归 owned)',
|
|
1564
|
+
isOwned('.agents/metric-derivers.cjs') === true);
|
|
1565
|
+
check('检查16装户:isOwned(.agents/metric-claims.txt) 仍为真(未被本单破坏)',
|
|
1566
|
+
isOwned('.agents/metric-claims.txt') === true);
|
|
1567
|
+
check('检查16装户:check-loop.mjs 仍归 managed(引擎不被误划入 owned)',
|
|
1568
|
+
isOwned('.agents/scripts/check-loop.mjs') === false);
|
|
1569
|
+
} else {
|
|
1570
|
+
// **装户环境降级断言**(复核 P1 更正):装户没有包源 `src/profiles.mjs`,故改验**可观测事实**——
|
|
1571
|
+
// 若 `metric-derivers.cjs` 已存在于盘上,它必须**不**记在 kit.json 的 managed 侧(在 managed 即为错,
|
|
1572
|
+
// 那正是本单要修的缺陷形态)。未安装该模块时跳过(无判定依据的行不产出噪声)。
|
|
1573
|
+
// 路径:本文件在 `<root>/.agents/scripts/`,故 kit.json 在 `../kit.json`(= `<root>/.agents/kit.json`)。
|
|
1574
|
+
let verdict = null;
|
|
1575
|
+
let detail = '';
|
|
1576
|
+
try {
|
|
1577
|
+
const kit = JSON.parse(fs.readFileSync(new URL('../kit.json', import.meta.url), 'utf8'));
|
|
1578
|
+
const rel = '.agents/metric-derivers.cjs';
|
|
1579
|
+
const inManaged = (kit.managed || []).some((f) => f.rel === rel);
|
|
1580
|
+
const inOwned = (kit.owned || []).some((f) => f.rel === rel);
|
|
1581
|
+
verdict = !inManaged;
|
|
1582
|
+
detail = `managed=${inManaged} owned=${inOwned}`;
|
|
1583
|
+
} catch (e) {
|
|
1584
|
+
verdict = null; detail = String(e.message).slice(0, 60);
|
|
1585
|
+
}
|
|
1586
|
+
if (verdict === null) {
|
|
1587
|
+
check('检查16装户:装户环境无法判定归属(无 kit.json)→ 跳过(不产噪声)', true);
|
|
1588
|
+
} else {
|
|
1589
|
+
check('检查16装户:装户环境——metric-derivers.cjs 不在 managed 侧(不在即正确)',
|
|
1590
|
+
verdict === true, detail);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
480
1596
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
481
1597
|
process.exit(fail ? 1 : 0);
|