agentic-flow-kit 1.2.2 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -4
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/fresh-init.test.mjs +10 -0
- package/src/init.mjs +1 -1
- package/src/profiles.mjs +12 -6
- package/src/sync.mjs +30 -26
- package/src/sync.test.mjs +82 -0
- package/templates/AGENTS.md +1 -0
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/rule-budgets.txt +3 -1
- package/templates/_agents/scripts/agg-delegations.cjs +56 -17
- package/templates/_agents/scripts/agg-delegations.test.mjs +76 -1
- package/templates/_agents/scripts/agg-gate-stats.mjs +113 -0
- package/templates/_agents/scripts/agg-gate-stats.test.mjs +95 -0
- package/templates/_agents/scripts/check-hygiene.mjs +167 -0
- package/templates/_agents/scripts/check-hygiene.test.mjs +182 -0
- package/templates/_agents/scripts/check-ledger.mjs +60 -16
- package/templates/_agents/scripts/check-ledger.test.mjs +70 -0
- package/templates/_agents/scripts/check-loop-unit.test.mjs +95 -0
- package/templates/_agents/scripts/check-loop.mjs +1119 -1097
- package/templates/_agents/scripts/check-loop.test.mjs +200 -1
- package/templates/_agents/scripts/confirm-doc.mjs +27 -6
- package/templates/_agents/scripts/confirm-doc.test.mjs +35 -0
- package/templates/_agents/scripts/gate-seg.mjs +54 -0
- package/templates/_agents/scripts/gate-seg.test.mjs +112 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +249 -0
- package/templates/_agents/scripts/gen-workflow-dashboard.test.mjs +189 -0
- package/templates/_agents/scripts/policy.mjs +31 -3
- package/templates/_agents/scripts/verify.mjs +27 -2
- package/templates/_agents/scripts/verify.test.mjs +53 -0
- package/templates/_github/workflows/kit-ci.yml +28 -0
|
@@ -17,7 +17,7 @@ import { entryConfirmed, MARK_RE, laneOfEntry, laneOfDoc } from './stage-gates.m
|
|
|
17
17
|
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
18
18
|
const CHECK_LOOP = path.join(SCRIPT_DIR, 'check-loop.mjs');
|
|
19
19
|
const GIT = process.platform === 'win32' ? 'git.exe' : 'git'; // Windows spawn 不补 .exe 扩展名(gitInit/gitCommitAll/场景 43/44/47 共用)
|
|
20
|
-
//
|
|
20
|
+
// 注意(2026-10-08 checkloop-importable 后):check-loop.mjs 已有 isMain 主守卫,可 import(runCheckLoop/四纯零件,直测见 check-loop-unit.test.mjs)——本套件保持端到端 spawn 作为行为回归网,
|
|
21
21
|
// 不可从其 import 函数直测(2026-10-04 review-fix-batch 实证;可 import 化记 papercuts 待 L2 重构)
|
|
22
22
|
|
|
23
23
|
let pass = 0;
|
|
@@ -423,6 +423,37 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
423
423
|
rmfix(T);
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
// ---- 场景 27b:DASHBOARD 漂移门(2026-10-08 selfmeasure-and-modularize 检查 11 扩覆盖面)----
|
|
427
|
+
// 三态:陈旧盘面 → WARN;盘面缺失(装户从未生成过看板)→ 不报(防常驻噪声);一致 → 不报。
|
|
428
|
+
{
|
|
429
|
+
const mkDashFix = () => {
|
|
430
|
+
const T = mkfix();
|
|
431
|
+
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
432
|
+
for (const f of ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs', 'agg-delegations.cjs', 'workflow-enums.mjs']) {
|
|
433
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, f), path.join(T, '.agents', 'scripts', f));
|
|
434
|
+
}
|
|
435
|
+
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
436
|
+
w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
437
|
+
w(T, 'workflow/plans/2026-09-22-dash.md', PLAN('dash', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
438
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
439
|
+
return T;
|
|
440
|
+
};
|
|
441
|
+
// ① DASHBOARD.md 从未生成(装户形态)→ 不得出 WARN
|
|
442
|
+
let T = mkDashFix();
|
|
443
|
+
let r = run(T);
|
|
444
|
+
check('观测面:装户未生成过 DASHBOARD.md → 不出仪表盘漂移 WARN(防常驻噪声)',
|
|
445
|
+
r.status === 0 && !outOf(r).includes('WARN 仪表盘漂移'), outOf(r));
|
|
446
|
+
rmfix(T);
|
|
447
|
+
// ② 生成后改事实 → 漂移 WARN,且 exit 0(advisory 不阻断)
|
|
448
|
+
T = mkDashFix();
|
|
449
|
+
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-dashboard.mjs'], { cwd: T });
|
|
450
|
+
w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: done\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
451
|
+
r = run(T);
|
|
452
|
+
check('观测面:DASHBOARD 漂移 → WARN 仪表盘漂移 且 exit 0',
|
|
453
|
+
r.status === 0 && outOf(r).includes('WARN 仪表盘漂移') && outOf(r).includes('gen-workflow-dashboard.mjs'), outOf(r));
|
|
454
|
+
rmfix(T);
|
|
455
|
+
}
|
|
456
|
+
|
|
426
457
|
// ---- 场景 28/29:常驻面预算——合规无告警;AGENTS.md 超限 → WARN 含一进一出 ----
|
|
427
458
|
{
|
|
428
459
|
const T = mkfix();
|
|
@@ -743,6 +774,24 @@ const evidenceFixture = (slug, declared, createDeclared) => {
|
|
|
743
774
|
check('git 基础设施异常:基础设施异常不冒充业务裁决(无证据无关误报)', !outOf(r).includes('证据无关'), outOf(r));
|
|
744
775
|
rmfix(T);
|
|
745
776
|
}
|
|
777
|
+
{
|
|
778
|
+
// 纯数字短 sha × 通道坏死(2026-10-07 v1.3.0 Release CI ubuntu 实证):fixture 短 sha 恰全数字
|
|
779
|
+
//(`--short` 7 位 ≈4%/run)时,rev-parse 实证分流在 CHECK_LOOP_GIT 整体坏死下不可能——
|
|
780
|
+
// 纯数字 text 豁免曾把 fail-closed 漏成 exit 0。守卫后:通道坏死(spawn 异常)→ forged 拦;
|
|
781
|
+
// 对照:通道健康时纯数字假 sha → rev-parse 正常回答不存在 → text 豁免语义保持(misfire 防线不回归)。
|
|
782
|
+
const T = mkfix();
|
|
783
|
+
gitInit(T);
|
|
784
|
+
w(T, 'workflow/plans/2026-09-26-pe7.md', PLAN('pe7', '状态: done\n级别: L1\n模块: material', '\n## 改动方案\n\n- src/app.mjs:实现功能\n'));
|
|
785
|
+
w(T, 'workflow/intents/2026-09-26-pe7.md', INTENT('pe7', '状态: done\n级别: L1\n日期: 2026-09-26', '\n## 验收标准(可测试)\n- [x] 演练穿越(证据:1234567)\n'));
|
|
786
|
+
gitCommitAll(T, 'docs(x): 纯数字证据通道坏死场景');
|
|
787
|
+
const rDead = run(T, { env: { CHECK_LOOP_GIT: 'definitely-not-git-xyz-2026' } });
|
|
788
|
+
check('git 基础设施异常:纯数字 sha × 通道坏死 → forged 拦不豁免(实证不可能即 fail-closed)',
|
|
789
|
+
rDead.status === 1 && outOf(rDead).includes('证据伪造'), `exit=${rDead.status}\n${outOf(rDead)}`);
|
|
790
|
+
const rAlive = run(T);
|
|
791
|
+
check('git 基础设施异常:纯数字假 sha × 通道健康 → text 豁免语义保持(misfire 防线不回归)',
|
|
792
|
+
rAlive.status === 0 && outOf(rAlive).includes('证据豁免 text'), `exit=${rAlive.status}\n${outOf(rAlive)}`);
|
|
793
|
+
rmfix(T);
|
|
794
|
+
}
|
|
746
795
|
{
|
|
747
796
|
const T = mkfix();
|
|
748
797
|
gitInit(T);
|
|
@@ -2703,5 +2752,155 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2703
2752
|
rmfix(T2);
|
|
2704
2753
|
}
|
|
2705
2754
|
|
|
2755
|
+
// ---- 检查 8 凭证按 doc 绑定(2026-10-08 verify-doc-binding):v6 收紧 / v5 不变 ----
|
|
2756
|
+
// 漏洞本体:凭证行不带 doc 字段 + 判定只看「全仓任意一份 24h 内绿行」→ 跑一次 verify 就能给全仓
|
|
2757
|
+
// 任意一单的「测试绿」声明背书。本组三条:① v6 + 只有他单绿行 → 仍出 WARN(漏洞已堵);
|
|
2758
|
+
// ② v6 + 本单绿行 → 豁免零 WARN;③ **v5 + 只有他单绿行 → 无 WARN**(兼容反例,收紧只对 v6+ 生效)。
|
|
2759
|
+
{
|
|
2760
|
+
const mkVeDoc = (pv, docField) => {
|
|
2761
|
+
const T = mkfix();
|
|
2762
|
+
gitInit(T);
|
|
2763
|
+
w(T, '.agents/kit.json', JSON.stringify({ policyVersion: pv }));
|
|
2764
|
+
const green = { ts: new Date().toISOString(), exitCode: 0, suite: 'npm test' };
|
|
2765
|
+
if (docField) green.doc = docField;
|
|
2766
|
+
w(T, '.agents/verifications.jsonl', `${JSON.stringify(green)}\n`);
|
|
2767
|
+
const iBody = '# INTENT — ve\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n';
|
|
2768
|
+
const pBody = '# PLAN — ve\n';
|
|
2769
|
+
const fpI = computeFingerprint(`---\n状态: approved\n级别: L1\n日期: 2026-10-07\n---\n${iBody}`);
|
|
2770
|
+
const fpP = computeFingerprint(`---\n状态: approved\n级别: L1\n---\n${pBody}`);
|
|
2771
|
+
w(T, 'workflow/intents/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n日期: 2026-10-07\n确认指纹: ${fpI.slice(0, 16)}\n---\n${iBody}`);
|
|
2772
|
+
w(T, 'workflow/plans/2026-10-07-ve.md', `---\n状态: done\n级别: L1\n确认指纹: ${fpP.slice(0, 16)}\n---\n${pBody}`);
|
|
2773
|
+
const now = new Date().toISOString();
|
|
2774
|
+
writeLedger(T, [
|
|
2775
|
+
{ ts: now, doc: 'workflow/intents/2026-10-07-ve.md', stage: 'done', fingerprint: fpI, prev: 'approved' },
|
|
2776
|
+
{ ts: now, doc: 'workflow/plans/2026-10-07-ve.md', stage: 'done', fingerprint: fpP, prev: 'approved' },
|
|
2777
|
+
]);
|
|
2778
|
+
gitCommitAll(T, 'feat: ve doc-binding fixture');
|
|
2779
|
+
return T;
|
|
2780
|
+
};
|
|
2781
|
+
const MINE = 'workflow/intents/2026-10-07-ve.md';
|
|
2782
|
+
const OTHER = 'workflow/intents/2026-10-07-someone-else.md';
|
|
2783
|
+
|
|
2784
|
+
const D1 = mkVeDoc(6, OTHER);
|
|
2785
|
+
let r = run(D1);
|
|
2786
|
+
check('检查8 凭证绑定:v6 + 只有**他单**绿行 → 仍出 WARN(跨单背书漏洞已堵)',
|
|
2787
|
+
r.status === 0 && outOf(r).includes('测试绿缺凭证') && !outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
|
|
2788
|
+
rmfix(D1);
|
|
2789
|
+
|
|
2790
|
+
const D2 = mkVeDoc(6, MINE);
|
|
2791
|
+
r = run(D2);
|
|
2792
|
+
check('检查8 凭证绑定:v6 + 本单绿行 → verify 豁免零 WARN(绑定成立)',
|
|
2793
|
+
r.status === 0 && outOf(r).includes('证据豁免 verify') && !outOf(r).includes('测试绿缺凭证'), `exit=${r.status}\n${outOf(r)}`);
|
|
2794
|
+
rmfix(D2);
|
|
2795
|
+
|
|
2796
|
+
const D3 = mkVeDoc(5, OTHER);
|
|
2797
|
+
r = run(D3);
|
|
2798
|
+
check('检查8 凭证绑定:v5 + 只有他单绿行 → **无 WARN**(兼容反例:收紧只对 v6+ 生效)',
|
|
2799
|
+
r.status === 0 && !outOf(r).includes('测试绿缺凭证') && outOf(r).includes('证据豁免 verify'), `exit=${r.status}\n${outOf(r)}`);
|
|
2800
|
+
rmfix(D3);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// ---- 检查 18 表头签名识别(2026-10-08 papercuts-cleanup-batch 修 2):节标题漂移不再静默停解析 ----
|
|
2804
|
+
// 修前按 `## 委派结果|自做任务结果` 精确节标题切段,装户把标题写成「委派结果表」即静默停解析该表
|
|
2805
|
+
// → L2/L3 done 回流无告警;修后按表头签名(与 agg-delegations.cjs splitTables 同口径)识别。
|
|
2806
|
+
{
|
|
2807
|
+
const T = mkfix();
|
|
2808
|
+
const gap = '2026-09-12-drift.md';
|
|
2809
|
+
w(T, `workflow/intents/${gap}`, INTENT('drift', '状态: done\n级别: L2\n日期: 2026-09-12', '\n## 验收标准(可测试)\n- [x] 用例通过(证据:fixture)\n'));
|
|
2810
|
+
w(T, `workflow/specs/${gap}`, SPEC('drift', '状态: approved\n级别: L2\n日期: 2026-09-12'));
|
|
2811
|
+
w(T, `workflow/plans/${gap}`, PLAN('drift', '状态: approved\n级别: L2\n日期: 2026-09-12'));
|
|
2812
|
+
const ledgerWarn = (r) => outOf(r).split('\n').some((l) => l.includes('委派台账') && l.includes(`intents/${gap}`));
|
|
2813
|
+
// 正例:节标题漂移(「委派结果表」≠精确节名)而表头不动 → 仍按表头签名解析,警告消失
|
|
2814
|
+
w(T, 'workflow/delegations.md', '## 委派结果表\n\n| 日期 | 被委派方 | 任务一句话 | 结果 | 备注 |\n|------|----------|------------|------|------|\n| 2026-09-12 | x | y | 一次通过 | 2026-09-12-drift |\n');
|
|
2815
|
+
let r = run(T);
|
|
2816
|
+
check('检查18 节标题漂移而表头不动 → 仍按表头签名解析,警告消失(修 2 核心场景)',
|
|
2817
|
+
r.status === 0 && !ledgerWarn(r), `exit=${r.status}\n${outOf(r)}`);
|
|
2818
|
+
// 负例:表头也漂移(新格式无签名列)→ 数据行不收集 → 警告仍在(识别不过宽)
|
|
2819
|
+
w(T, 'workflow/delegations.md', '## 委派结果表\n\n| 日期 | 承办 | 说明 |\n|------|------|------|\n| 2026-09-12 | x | 2026-09-12-drift |\n');
|
|
2820
|
+
r = run(T);
|
|
2821
|
+
check('检查18 表头也漂移(无签名列)→ 不收集 → 警告仍在(识别不过宽负例)',
|
|
2822
|
+
r.status === 0 && ledgerWarn(r), `exit=${r.status}\n${outOf(r)}`);
|
|
2823
|
+
rmfix(T);
|
|
2824
|
+
}
|
|
2825
|
+
|
|
2826
|
+
// ---- 检查 14 台账 OR(2026-10-08 papercuts-cleanup-batch 修 3):git 历史无 approved 行而台账有 → 不再误报 ----
|
|
2827
|
+
// 两跳确认同批提交时 git 历史不出现行首「状态: approved」(2026-09-28-adopter-derivers 实证)——
|
|
2828
|
+
// confirmations.jsonl 已证明两跳均走;判据改 OR 并集(git 判据保留,台账为第二通道)。
|
|
2829
|
+
{
|
|
2830
|
+
const T = mkfix();
|
|
2831
|
+
gitInit(T);
|
|
2832
|
+
const base = '2026-09-23-confirm-ledger';
|
|
2833
|
+
w(T, `workflow/incidents/${base}.md`, `---\n状态: closed\n级别: L1\n发现: 2026-09-23\n模块: material\n---\n# INCIDENT — confirm ledger\n\n${三件套(false)}`);
|
|
2834
|
+
w(T, `workflow/plans/${base}.md`, PLAN('confirm ledger', '状态: done\n级别: L1\n模块: material'));
|
|
2835
|
+
gitCommitAll(T, 'test'); // 两跳同批:git 历史无行首 approved(修前必 WARN)
|
|
2836
|
+
w(T, '.agents/confirmations.jsonl', `${JSON.stringify({ ts: '2026-09-23T00:00:00Z', doc: `workflow/plans/${base}.md`, stage: 'approved', fingerprint: 'x', prev: 'draft', source: 'tty' })}\n`);
|
|
2837
|
+
let r = run(T);
|
|
2838
|
+
// exit 1 属检查 15 指纹 hard 同场(台账有该 doc 行即受管,fixture 无真实指纹必拦——预期);
|
|
2839
|
+
// 本场景只断言检查 14 的 WARN 确认态缺失不再出账(台账 OR 兜住)
|
|
2840
|
+
check('检查14 git 历史无 approved 行而台账有 → 台账 OR 兜住不误报(修 3 核心场景)',
|
|
2841
|
+
!outOf(r).includes('WARN 确认态缺失'), `exit=${r.status}\n${outOf(r)}`);
|
|
2842
|
+
// 台账行 doc 不指向该文档 → 仍 WARN(OR 不弱化原判据)
|
|
2843
|
+
w(T, '.agents/confirmations.jsonl', `${JSON.stringify({ ts: '2026-09-23T00:00:00Z', doc: 'workflow/plans/other.md', stage: 'approved', fingerprint: 'x', prev: 'draft', source: 'tty' })}\n`);
|
|
2844
|
+
r = run(T);
|
|
2845
|
+
check('检查14 台账无该文档 approved 行 → 仍 WARN(git 判据未被弱化)',
|
|
2846
|
+
r.status === 0 && /WARN 确认态缺失.*2026-09-23-confirm-ledger\.md/.test(outOf(r)), outOf(r));
|
|
2847
|
+
rmfix(T);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// ---- 检查 4 全角逗号(2026-10-08 papercuts-cleanup-batch 修 4):路径引用后跟「,」不再吞入引用串 ----
|
|
2851
|
+
// 中文文档「`.agents/...`,以及…」写法曾把「,」并入引用 → 引用断档误报(p0-gate-noise-batch 实证 2 条)。
|
|
2852
|
+
{
|
|
2853
|
+
const T = mkfix();
|
|
2854
|
+
fs.mkdirSync(path.join(T, '.agents', 'commands'), { recursive: true });
|
|
2855
|
+
w(T, '.agents/commands/plan.md', '# 小命令\n');
|
|
2856
|
+
w(T, 'workflow/intents/2026-09-12-ref-comma.md', INTENT('ref comma', '状态: approved\n级别: L1\n日期: 2026-09-12\n模块: pipeline', '\n入口 `.agents/commands/plan.md`,以及后续步骤。\n'));
|
|
2857
|
+
w(T, 'workflow/plans/2026-09-12-ref-comma.md', PLAN('ref comma', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
2858
|
+
let r = run(T);
|
|
2859
|
+
check('检查4 路径后跟全角逗号 → 引用串不吞「,」,真实路径不误报断档',
|
|
2860
|
+
r.status === 0 && !outOf(r).includes('引用断档'), `exit=${r.status}\n${outOf(r)}`);
|
|
2861
|
+
w(T, 'workflow/intents/2026-09-12-ref-comma2.md', INTENT('ref comma2', '状态: approved\n级别: L1\n日期: 2026-09-12\n模块: pipeline', '\n入口 `.agents/commands/missing-broken.md`,后续。\n'));
|
|
2862
|
+
w(T, 'workflow/plans/2026-09-12-ref-comma2.md', PLAN('ref comma2', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
2863
|
+
r = run(T);
|
|
2864
|
+
// 负例用 ASCII 断档名:refRe 字符类本就只含 ASCII 路径字符(中文名路径不在扫描范围)
|
|
2865
|
+
check('检查4 全角逗号后的真断档 → 仍拦(判据不放松)',
|
|
2866
|
+
r.status === 0 && /引用断档.*missing-broken\.md/.test(outOf(r)), `exit=${r.status}\n${outOf(r)}`);
|
|
2867
|
+
rmfix(T);
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
// ---- 门禁 ROI 插桩(2026-10-08 gate-roi-metrics)----
|
|
2871
|
+
// 断言两件事:① 默认路径不落盘、stderr 无 gate-stats 行(输出契约零污染);
|
|
2872
|
+
// ② --gate-stats 落一行 jsonl 且段覆盖全部检查(判据不漏插)。
|
|
2873
|
+
// 既有 223 条断言零改动本身即最强钉子——任何 push 到 warnings 的插桩副作用都会把它们打红。
|
|
2874
|
+
{
|
|
2875
|
+
const T = mkfix();
|
|
2876
|
+
const statsRel = path.join('.agents', 'cache', 'gate-stats.jsonl');
|
|
2877
|
+
const plain = run(T);
|
|
2878
|
+
check('门禁ROI:默认路径不落 gate-stats.jsonl(统计默认静默)',
|
|
2879
|
+
!fs.existsSync(path.join(T, statsRel)), outOf(plain));
|
|
2880
|
+
check('门禁ROI:默认路径 stderr 无 [gate-stats] 摘要行(稳定输出契约零污染)',
|
|
2881
|
+
!outOf(plain).includes('[gate-stats]'), outOf(plain));
|
|
2882
|
+
const before = fs.existsSync(path.join(T, statsRel)) ? fs.readFileSync(path.join(T, statsRel), 'utf8') : '';
|
|
2883
|
+
|
|
2884
|
+
const st = run(T, { args: ['--gate-stats'] });
|
|
2885
|
+
check('门禁ROI:--gate-stats → 落盘 + stderr 摘要行', fs.existsSync(path.join(T, statsRel)) && outOf(st).includes('[gate-stats]'), outOf(st));
|
|
2886
|
+
const lines = fs.readFileSync(path.join(T, statsRel), 'utf8').split('\n').filter(Boolean);
|
|
2887
|
+
check('门禁ROI:--gate-stats 只 append 一行(每跑一次门禁 = 一条记录)',
|
|
2888
|
+
lines.length === 1 && !before, `lines=${lines.length} before=${before.length}`);
|
|
2889
|
+
let row = null;
|
|
2890
|
+
try { row = JSON.parse(lines[0]); } catch { /* 留给下一条断言报错 */ }
|
|
2891
|
+
const ids = row && Array.isArray(row.segs) ? row.segs.map((s) => String(s.id)) : [];
|
|
2892
|
+
const uniq = new Set(ids);
|
|
2893
|
+
check('门禁ROI:段 id 唯一且覆盖 19 个插桩点(14 主流程 + 5 hygiene 模块),无重复插桩',
|
|
2894
|
+
ids.length === 19 && uniq.size === 19, `ids=${ids.join(',')}`);
|
|
2895
|
+
const expected = ['1','2','3','4','5','6','8','9','10','11','12','13','14','15','16','17','18','19','20'];
|
|
2896
|
+
check('门禁ROI:段 id 集合 == 现有检查编号(不新增/不遗漏检查项编号)',
|
|
2897
|
+
expected.every((e) => uniq.has(e)) && uniq.size === expected.length, `got=${[...uniq].sort().join(',')}`);
|
|
2898
|
+
check('门禁ROI:每段都有 label + 非负 ms(成本度量的地基)',
|
|
2899
|
+
row && row.segs.every((s) => s.label && Number(s.ms) >= 0 && Number.isInteger(Number(s.ms))), JSON.stringify(row && row.segs.slice(0, 3)));
|
|
2900
|
+
check('门禁ROI:行顶层含 ts / root / totalMs(聚合器的窗口过滤依赖 ts)',
|
|
2901
|
+
row && typeof row.ts === 'string' && typeof row.root === 'string' && Number(row.totalMs) >= 0, JSON.stringify({ ts: row && row.ts, totalMs: row && row.totalMs }));
|
|
2902
|
+
rmfix(T);
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2706
2905
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
2707
2906
|
process.exit(fail ? 1 : 0);
|
|
@@ -42,18 +42,30 @@ import { fileURLToPath } from 'node:url';
|
|
|
42
42
|
import { confirmGateFor, doneGateFor, laneOfEntry, laneOfDoc } from './stage-gates.mjs';
|
|
43
43
|
import { loadKitPolicy, hasFreshVerifyLine } from './policy.mjs';
|
|
44
44
|
|
|
45
|
-
// hasFreshVerify(root):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
45
|
+
// hasFreshVerify(root, doc):读 <root>/.agents/verifications.jsonl 判 24h 窗口绿行(判定单源
|
|
46
46
|
// policy.hasFreshVerifyLine)。只读,台账缺失/不可读一律 false(=无凭证,advisory 层 fail-open)。
|
|
47
|
-
|
|
47
|
+
// doc 绑定(2026-10-08 verify-doc-binding):非空时只认 e.doc === doc 的绿行——不带 doc 的绿行
|
|
48
|
+
// (CI 场景)与**别的单**的绿行都不给本单背证。调用方负责「仅 v6+ 才传 doc」,本函数不做版本判断。
|
|
49
|
+
function hasFreshVerify(root, doc = null) {
|
|
48
50
|
try {
|
|
49
51
|
const p = path.join(root, '.agents', 'verifications.jsonl');
|
|
50
52
|
if (!fs.existsSync(p)) return false;
|
|
51
|
-
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/));
|
|
53
|
+
return hasFreshVerifyLine(fs.readFileSync(p, 'utf8').split(/\r?\n/), Date.now(), doc);
|
|
52
54
|
} catch {
|
|
53
55
|
return false;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// docRelOf:把用户传入的 doc 路径归一成「相对仓库根的 posix 相对路径」——与 verify.mjs --doc 的落账口径
|
|
60
|
+
// 必须逐字一致(一个转得动、另一个转不动 = 凭证永远匹配不上)。win32 反斜杠 / posix / 绝对路径三者同值。
|
|
61
|
+
function docRelOf(root, doc) {
|
|
62
|
+
try {
|
|
63
|
+
return path.relative(root, path.resolve(root, doc)).split(path.sep).join('/');
|
|
64
|
+
} catch {
|
|
65
|
+
return doc;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
// computeFingerprint(text):CRLF 归一 → 剔指纹行 → sha256 hex(64 位)
|
|
58
70
|
export function computeFingerprint(text) {
|
|
59
71
|
const norm = String(text).replace(/\r\n/g, '\n').split('\n')
|
|
@@ -319,10 +331,19 @@ if (isMain) {
|
|
|
319
331
|
// 测试绿凭证前置(2026-10-07 verify-evidence,advisory 不拦):→done 且文档含「验收标准」节且
|
|
320
332
|
// policy v5(verifySince)→ 24h 窗口内须有 verifications.jsonl 绿行。只读判定,任何读取异常降级
|
|
321
333
|
// 静默(fail-open——advisory 层不因台账缺失/坏行阻断关单);升 hard 走后续 policyVersion 演进。
|
|
334
|
+
// 2026-10-08 verify-doc-binding:v6+(verifyDocSince)时按本 doc 精确匹配绿行——他单的绿行不再
|
|
335
|
+
// 给本单背书;v1-v5 维持 v5 全局行为逐字不变。
|
|
336
|
+
// ⚠️ 文案锚点硬约束(既有 confirm-doc.test.mjs 断言锚定子串,零改动前提):
|
|
337
|
+
// 「无测试绿凭证」六字须**连续**,doc 与命令只能追加在其后——不得写成「无本单(x)的测试绿凭证」。
|
|
322
338
|
if (target === 'done' && typeof loadKitPolicy(root).verifySince === 'string'
|
|
323
|
-
&& /^##\s*[^#]*验收标准/m.test(text)
|
|
324
|
-
|
|
325
|
-
|
|
339
|
+
&& /^##\s*[^#]*验收标准/m.test(text)) {
|
|
340
|
+
const pol = loadKitPolicy(root);
|
|
341
|
+
const rel = docRelOf(root, doc);
|
|
342
|
+
const bound = typeof pol.verifyDocSince === 'string' ? rel : null;
|
|
343
|
+
if (!hasFreshVerify(root, bound)) {
|
|
344
|
+
console.error(`⚠️ ${doc} 置 done:24h 内无测试绿凭证(${bound ? `本单 ${rel} 须带 --doc 落账` : '凭证未按单绑定'})——关单勾验的「测试绿」声明缺机器事实`);
|
|
345
|
+
console.error(` 跑 node .agents/scripts/verify.mjs${bound ? ` --doc ${rel}` : ''} 后重新关单;本次不拦截(advisory,灰度第一档)`);
|
|
346
|
+
}
|
|
326
347
|
}
|
|
327
348
|
const fp = computeFingerprint(text);
|
|
328
349
|
// AI 自治放行分支(2026-09-30 ai-autonomy-trust):
|
|
@@ -736,5 +736,40 @@ const writeTrust = (root, cfg) => {
|
|
|
736
736
|
fs.rmSync(root, { recursive: true, force: true });
|
|
737
737
|
}
|
|
738
738
|
|
|
739
|
+
// ---- S40 测试绿凭证按 doc 绑定(2026-10-08 verify-doc-binding)----
|
|
740
|
+
// 核心防线三条:① v6 + 只有**他单**绿行 → advisory 出账(漏洞已堵);② v6 + 本单绿行 → 静默;
|
|
741
|
+
// ③ **v5 + 只有他单绿行 → 静默**(向后兼容反例——收紧只许对 v6+ 生效,存量装户零变化)。
|
|
742
|
+
// 文案锚点:既有 S38/S39 锚定「无测试绿凭证」六字连续 + 「verify.mjs」出现,本组不得破坏。
|
|
743
|
+
{
|
|
744
|
+
const mkVerifyRoot = (pv, lines) => {
|
|
745
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'confirm-docbind-'));
|
|
746
|
+
const docP = path.join(root, 'workflow', 'intents');
|
|
747
|
+
fs.mkdirSync(docP, { recursive: true });
|
|
748
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
749
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: pv }), 'utf8');
|
|
750
|
+
fs.writeFileSync(path.join(root, '.agents', 'verifications.jsonl'), lines, 'utf8');
|
|
751
|
+
fs.writeFileSync(path.join(docP, '2026-10-08-m.md'),
|
|
752
|
+
'---\n状态: approved\n级别: L1\n确认指纹: cccc3333cccc3333\n---\n# I\n\n## 验收标准(可测试)\n- [x] 用例通过(证据:测试全绿)\n', 'utf8');
|
|
753
|
+
return root;
|
|
754
|
+
};
|
|
755
|
+
const green = (doc) => `${JSON.stringify({ ts: new Date().toISOString(), exitCode: 0, suite: 'npm test', passed: 1, failed: 0, ...(doc ? { doc } : {}) })}\n`;
|
|
756
|
+
const MINE = 'workflow/intents/2026-10-08-m.md';
|
|
757
|
+
const OTHER = 'workflow/intents/2026-10-08-other.md';
|
|
758
|
+
const runM = (root) => spawnSync(process.execPath, [CLI, MINE, '--delegated', '可以'], { cwd: root, encoding: 'utf8' });
|
|
759
|
+
|
|
760
|
+
const r40 = runM(mkVerifyRoot(6, green(OTHER)));
|
|
761
|
+
check('S40① v6 + 只有他单绿行 → advisory 出账(跨单背书漏洞已堵)+ 文案带 --doc',
|
|
762
|
+
r40.status === 0 && /无测试绿凭证/.test(r40.stderr) && /--doc/.test(r40.stderr), String(r40.stderr).slice(0, 200));
|
|
763
|
+
const r41 = runM(mkVerifyRoot(6, green(MINE)));
|
|
764
|
+
check('S40② v6 + 本单绿行 → 静默(绑定成立)',
|
|
765
|
+
r41.status === 0 && !/无测试绿凭证/.test(r41.stderr), String(r41.stderr).slice(0, 200));
|
|
766
|
+
const r42 = runM(mkVerifyRoot(5, green(OTHER)));
|
|
767
|
+
check('S40③ v5 + 只有他单绿行 → **静默**(兼容反例:收紧只对 v6+ 生效,存量装户零变化)',
|
|
768
|
+
r42.status === 0 && !/无测试绿凭证/.test(r42.stderr), String(r42.stderr).slice(0, 200));
|
|
769
|
+
const r43 = runM(mkVerifyRoot(6, green(null)));
|
|
770
|
+
check('S40④ v6 + 不带 doc 的绿行(CI 场景)→ 不给本单背书,advisory 出账',
|
|
771
|
+
r43.status === 0 && /无测试绿凭证/.test(r43.stderr), String(r43.stderr).slice(0, 200));
|
|
772
|
+
}
|
|
773
|
+
|
|
739
774
|
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
740
775
|
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// gate-seg.mjs — 门禁 ROI 段插桩共用件(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 为什么独立成件:check-loop.mjs 与 check-hygiene.mjs **互相不能反向 import**(前者已 import 后者,
|
|
3
|
+
// 后者再 import 前者即循环依赖)——而两者都要插桩。判据单源必须只有一处,故抽本件。
|
|
4
|
+
//
|
|
5
|
+
// 机制:每次 gateSeg() 调用先**结算上一段**(段内 warnings/blockers 增量条数 + 耗时),再记本段起点。
|
|
6
|
+
// 统计完全由数组长度差推出——**不改任何 warnings.push / blockers.push 调用点**,判定逻辑与文案零改动。
|
|
7
|
+
// 默认路径(无 --gate-stats)下 segs 只在内存里攒、不打印不写盘 → 输出契约逐字节不变(硬约束:
|
|
8
|
+
// pre-push / pre-commit 消费者 + doctor 的 WARN 行计数 + check-loop.test.mjs 223 断言全依赖它)。
|
|
9
|
+
//
|
|
10
|
+
// 耗时口径**刻意包含子进程时间**(检查 11 调两个生成器 --check、检查 13 调 sh rule-budget.sh)——
|
|
11
|
+
// 门禁真实成本本就要含子进程;不做剔除,但看板与 spec 均显式声明该口径防误读。
|
|
12
|
+
export function gateSeg(collector, id, label) {
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const prev = collector.current;
|
|
15
|
+
if (prev) {
|
|
16
|
+
collector.segs.push({
|
|
17
|
+
id: prev.id,
|
|
18
|
+
label: prev.label,
|
|
19
|
+
warns: collector.warnings.length - prev.w,
|
|
20
|
+
blocks: collector.blockers.length - prev.b,
|
|
21
|
+
ms: now - prev.t,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
collector.current = { id, label, w: collector.warnings.length, b: collector.blockers.length, t: now };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// finishSegs:结算末段(末段之后没有下一次 gateSeg 调用,须显式收口,否则最后一段不进账)
|
|
28
|
+
export function finishSegs(collector) {
|
|
29
|
+
if (!collector.current) return collector.segs; // 已结算过(幂等——writeGateStats 与输出段各调一次不会重复入账)
|
|
30
|
+
gateSeg(collector, '__end__', '__end__'); // 本次调用结算上一段;记下的 current 置空防重复结算
|
|
31
|
+
collector.current = null;
|
|
32
|
+
return collector.segs;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// makeCollector:建收集器(引用同一对数组,不复制数据)
|
|
36
|
+
export function makeCollector(warnings, blockers) {
|
|
37
|
+
return { warnings, blockers, segs: [], current: null };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// appendSegs:并入「在别处结算出的段」,供模块化门禁使用(2026-10-08 gate-roi-metrics 修复)。
|
|
41
|
+
// 背景:check-hygiene.mjs 产出的是**局部** warnings 数组(由 check-loop 事后 spread 进全局),
|
|
42
|
+
// 故它的增量在模块执行期间**不反映**到 check-loop 的数组上。若直接共用游标插桩,后果是确定的错账:
|
|
43
|
+
// 本模块 5 段恒记 0 条命中,而它的全部增量会被误记到模块之后的第一个段(检查 3)头上——
|
|
44
|
+
// 实测已抓到:某次 hygiene 产出 1 条警告被记成「检查 13 命中」。
|
|
45
|
+
// 语义:先结算本收集器当前段(模块前的检查,如检查 1)→ 按序并入外来段 → 游标清空,
|
|
46
|
+
// 下一个 gateSeg 从零起算,Δ 天然正确。
|
|
47
|
+
// 空 segs 直接返回且**不结算**当前段:没有东西可并入时不该改变原有结算时序。
|
|
48
|
+
export function appendSegs(collector, segs) {
|
|
49
|
+
if (!segs || !segs.length) return collector.segs;
|
|
50
|
+
finishSegs(collector);
|
|
51
|
+
collector.segs.push(...segs);
|
|
52
|
+
collector.current = null;
|
|
53
|
+
return collector.segs;
|
|
54
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// gate-seg.test.mjs — 门禁 ROI 段插桩共用件测试(2026-10-08 gate-roi-metrics)
|
|
2
|
+
// 被测:gateSeg 结算语义 / finishSegs 幂等 / makeCollector 引用同一对数组。
|
|
3
|
+
// 判据单源所在:check-loop.mjs 与 check-hygiene.mjs 都靠本件插桩,故它的正确性是全部门禁成本度量的地基——
|
|
4
|
+
// 段归属算错则整张 ROI 表失真而无人察觉。端到端回归网仍是 check-loop.test.mjs(223 断言)。
|
|
5
|
+
// 用法:node templates/_agents/scripts/gate-seg.test.mjs
|
|
6
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const { gateSeg, finishSegs, makeCollector, appendSegs } =
|
|
11
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'gate-seg.mjs')).href);
|
|
12
|
+
|
|
13
|
+
let pass = 0, fail = 0;
|
|
14
|
+
const check = (name, cond, detail = '') => {
|
|
15
|
+
if (cond) { pass++; console.log(`PASS ${name}`); }
|
|
16
|
+
else { fail++; console.log(`FAIL ${name}${detail ? `——${detail}` : ''}`); }
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// ---- makeCollector:引用同一对数组(不复制)----
|
|
20
|
+
{
|
|
21
|
+
const w = [], b = [];
|
|
22
|
+
const c = makeCollector(w, b);
|
|
23
|
+
check('makeCollector:引用同一对数组(push 即被看见,无复制)', c.warnings === w && c.blockers === b);
|
|
24
|
+
check('makeCollector:初始 segs 为空、current 为空', Array.isArray(c.segs) && c.segs.length === 0 && c.current === null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---- gateSeg:每次调用结算上一段,首调不结算 ----
|
|
28
|
+
{
|
|
29
|
+
const w = [], b = [];
|
|
30
|
+
const c = makeCollector(w, b);
|
|
31
|
+
gateSeg(c, '1', '检查一');
|
|
32
|
+
check('gateSeg:首次调用只记起点不结算(segs 仍空)', c.segs.length === 0 && c.current.id === '1');
|
|
33
|
+
w.push('a'); b.push('b'); b.push('c'); // 段一产出 1 warn + 2 blocks
|
|
34
|
+
gateSeg(c, '2', '检查二');
|
|
35
|
+
check('gateSeg:第二次调用结算上一段(warns=1 blocks=2,id/label 对)',
|
|
36
|
+
c.segs.length === 1 && c.segs[0].id === '1' && c.segs[0].label === '检查一'
|
|
37
|
+
&& c.segs[0].warns === 1 && c.segs[0].blocks === 2, JSON.stringify(c.segs));
|
|
38
|
+
check('gateSeg:结算后 current 切到新段', c.current.id === '2');
|
|
39
|
+
// 零命中段也要入账(死检查识别依赖「声明了但从未命中」——漏记即误判为未声明)
|
|
40
|
+
gateSeg(c, '3', '检查三');
|
|
41
|
+
check('gateSeg:零命中段照样入账(warns=0 blocks=0)',
|
|
42
|
+
c.segs.length === 2 && c.segs[1].id === '2' && c.segs[1].warns === 0 && c.segs[1].blocks === 0, JSON.stringify(c.segs));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---- finishSegs:结算末段 + 幂等(writeGateStats 与输出段各调一次不得重复入账)----
|
|
46
|
+
{
|
|
47
|
+
const w = [], b = [];
|
|
48
|
+
const c = makeCollector(w, b);
|
|
49
|
+
gateSeg(c, '1', '检查一');
|
|
50
|
+
w.push('x');
|
|
51
|
+
const segs1 = finishSegs(c);
|
|
52
|
+
check('finishSegs:结算末段(末段后无下次调用否则该段丢失)',
|
|
53
|
+
segs1.length === 1 && segs1[0].id === '1' && segs1[0].warns === 1, JSON.stringify(segs1));
|
|
54
|
+
const segs2 = finishSegs(c);
|
|
55
|
+
check('finishSegs:幂等——二次调用不重复入账(防 __end__ 伪段污染表)',
|
|
56
|
+
segs2.length === 1 && !segs2.some((s) => s.id === '__end__'), JSON.stringify(segs2));
|
|
57
|
+
check('finishSegs:结算后 current 置空', c.current === null);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- ms 单调不减(耗时度量地基)----
|
|
61
|
+
{
|
|
62
|
+
const c = makeCollector([], []);
|
|
63
|
+
gateSeg(c, '1', 'a');
|
|
64
|
+
const t = Date.now(); while (Date.now() - t < 5) { /* 占位:保证有可测间隔 */ }
|
|
65
|
+
gateSeg(c, '2', 'b');
|
|
66
|
+
check('耗时字段:ms 为非负整数且单调(门禁成本度量的地基)',
|
|
67
|
+
Number.isInteger(c.segs[0].ms) && c.segs[0].ms >= 0, JSON.stringify(c.segs[0]));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- appendSegs:模块化门禁的段归属(本次实抓到错账的根因,钉死)----
|
|
71
|
+
// 症状:check-hygiene.mjs 的增量不反映到 check-loop 的数组上,直接共用游标会让 hygiene 各段恒记 0,
|
|
72
|
+
// 而它的全部增量被误记到模块之后的第一段(检查 3)。下面是该场景的最小复现与修复后行为。
|
|
73
|
+
{
|
|
74
|
+
const globalW = [], globalB = [];
|
|
75
|
+
const g = makeCollector(globalW, globalB);
|
|
76
|
+
gateSeg(g, '1', '检查1'); // 模块前的一段
|
|
77
|
+
globalW.push('w-check1'); // 检查 1 产出 1 条
|
|
78
|
+
|
|
79
|
+
// 模块(局部数组):检查 2 → 检查 11 各产出 1 条
|
|
80
|
+
const localW = [];
|
|
81
|
+
const l = makeCollector(localW, []);
|
|
82
|
+
gateSeg(l, '2', '检查2');
|
|
83
|
+
localW.push('w-check2');
|
|
84
|
+
gateSeg(l, '11', '检查11');
|
|
85
|
+
localW.push('w-check11');
|
|
86
|
+
|
|
87
|
+
appendSegs(g, finishSegs(l));
|
|
88
|
+
const order = g.segs.map((s) => s.id);
|
|
89
|
+
const byId = new Map(g.segs.map((s) => [s.id, s.warns]));
|
|
90
|
+
check('appendSegs:模块后的一段先结算(检查 1 不被外来段吞掉)',
|
|
91
|
+
order[0] === '1' && byId.get('1') === 1, JSON.stringify(g.segs));
|
|
92
|
+
check('appendSegs:外来段按序并入且各自归属正确(2 → 1 条、11 → 1 条,**不都记到后一段**)',
|
|
93
|
+
order.slice(1).join(',') === '2,11' && byId.get('2') === 1 && byId.get('11') === 1, JSON.stringify(g.segs));
|
|
94
|
+
check('appendSegs:并入后游标清空,下一段从零起算(不把外来增量算进模块之后的段)',
|
|
95
|
+
g.current === null, JSON.stringify(g.current));
|
|
96
|
+
gateSeg(g, '3', '检查3');
|
|
97
|
+
globalW.push(...['w-check3a', 'w-check3b']);
|
|
98
|
+
gateSeg(g, '4', '检查4');
|
|
99
|
+
check('appendSegs:模块之后的第一段只记自己的增量(检查 3 = 2 条,不含 hygiene 的 2 条)',
|
|
100
|
+
g.segs.find((s) => s.id === '3').warns === 2, JSON.stringify(g.segs));
|
|
101
|
+
|
|
102
|
+
// 空外来段:既不并入也不提前结算(不改变原有结算时序)
|
|
103
|
+
const w2 = [], b2 = [];
|
|
104
|
+
const g2 = makeCollector(w2, b2);
|
|
105
|
+
gateSeg(g2, '1', 'a');
|
|
106
|
+
appendSegs(g2, []);
|
|
107
|
+
check('appendSegs:空外来段 → 收集器原样不动(不结算、不并入,结算时序不变)',
|
|
108
|
+
g2.segs.length === 0 && g2.current !== null, JSON.stringify(g2));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`\n合计: PASS ${pass} / FAIL ${fail}`);
|
|
112
|
+
process.exit(fail ? 1 : 0);
|