agentic-flow-kit 1.7.15 → 1.7.16
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 +41 -3
- package/modules/hosts/claude/commands/wf-design.md +5 -4
- package/modules/hosts/claude/commands/wf-new-task.md +42 -21
- package/modules/hosts/claude/commands/wf-review.md +1 -1
- package/modules/hosts/claude/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/claude/commands/wf-test.md +1 -1
- package/modules/hosts/codex/commands/wf-design.md +5 -4
- package/modules/hosts/codex/commands/wf-new-task.md +42 -21
- package/modules/hosts/codex/commands/wf-review.md +1 -1
- package/modules/hosts/codex/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/codex/commands/wf-test.md +1 -1
- package/modules/hosts/cursor/commands/wf-design.md +5 -4
- package/modules/hosts/cursor/commands/wf-new-task.md +42 -21
- package/modules/hosts/cursor/commands/wf-review.md +1 -1
- package/modules/hosts/cursor/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/cursor/commands/wf-test.md +1 -1
- package/modules/hosts/opencode/commands/wf-design.md +5 -4
- package/modules/hosts/opencode/commands/wf-new-task.md +42 -21
- package/modules/hosts/opencode/commands/wf-review.md +1 -1
- package/modules/hosts/opencode/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/opencode/commands/wf-test.md +1 -1
- package/modules/hosts/trae/commands/wf-design.md +5 -4
- package/modules/hosts/trae/commands/wf-new-task.md +42 -21
- package/modules/hosts/trae/commands/wf-review.md +1 -1
- package/modules/hosts/trae/commands/wf-sync-hosts.md +7 -13
- package/modules/hosts/trae/commands/wf-test.md +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +43 -4
- package/src/doctor.mjs +37 -1
- package/src/executor.mjs +245 -0
- package/src/executor.test.mjs +292 -0
- package/src/init.mjs +7 -0
- package/src/profiles.mjs +3 -0
- package/src/render.mjs +3 -3
- package/src/sync-hosts.mjs +37 -43
- package/src/sync-hosts.test.mjs +67 -56
- package/templates/.gitattributes +8 -0
- package/templates/AGENTS.md +1 -1
- package/templates/_agents/commands/design.md +5 -4
- package/templates/_agents/commands/new-task.md +42 -21
- package/templates/_agents/commands/review.md +1 -1
- package/templates/_agents/commands/sync-hosts.md +7 -13
- package/templates/_agents/commands/test.md +1 -1
- package/templates/_agents/control-surfaces.txt +1 -1
- package/templates/_agents/engine-lock.json +246 -0
- package/templates/_agents/lane-surfaces.txt +1 -1
- package/templates/_agents/level-hints.txt +30 -0
- package/templates/_agents/scripts/agg-delegations.test.mjs +5 -3
- package/templates/_agents/scripts/check-engine-integrity.mjs +55 -126
- package/templates/_agents/scripts/check-engine-integrity.test.mjs +33 -45
- package/templates/_agents/scripts/check-lane-surface.mjs +72 -23
- package/templates/_agents/scripts/check-lane-surface.test.mjs +66 -0
- package/templates/_agents/scripts/check-ledger-invariant.mjs +112 -14
- package/templates/_agents/scripts/check-ledger-invariant.test.mjs +5 -1
- package/templates/_agents/scripts/check-loop-rev.test.mjs +6 -4
- package/templates/_agents/scripts/check-loop.mjs +49 -41
- package/templates/_agents/scripts/check-loop.test.mjs +18 -13
- package/templates/_agents/scripts/check-metric-claims.mjs +4 -2
- package/templates/_agents/scripts/compact-ledgers.mjs +97 -0
- package/templates/_agents/scripts/compact-ledgers.test.mjs +162 -0
- package/templates/_agents/scripts/confirm-doc.mjs +40 -11
- package/templates/_agents/scripts/doctor.test.mjs +44 -1
- package/templates/_agents/scripts/edit-face-check.test.mjs +5 -4
- package/templates/_agents/scripts/fill-intent.mjs +2 -2
- package/templates/_agents/scripts/fixture-scripts.mjs +153 -0
- package/templates/_agents/scripts/fixture-scripts.test.mjs +107 -0
- package/templates/_agents/scripts/gate-checklist.mjs +4 -9
- package/templates/_agents/scripts/gate-checklist.test.mjs +2 -11
- package/templates/_agents/scripts/gen-workflow-dashboard.mjs +8 -11
- package/templates/_agents/scripts/gen-workflow-metrics.mjs +9 -33
- package/templates/_agents/scripts/git-merge-jsonl.mjs +63 -0
- package/templates/_agents/scripts/git-merge-jsonl.test.mjs +125 -0
- package/templates/_agents/scripts/ledger-doc-chain.test.mjs +166 -0
- package/templates/_agents/scripts/ledger-loader.mjs +108 -0
- package/templates/_agents/scripts/ledger-loader.test.mjs +131 -0
- package/templates/_agents/scripts/ledger-prune.mjs +96 -0
- package/templates/_agents/scripts/ledger-prune.test.mjs +120 -0
- package/templates/_agents/scripts/ledger-ts-monotonic.test.mjs +135 -0
- package/templates/_agents/scripts/level-criteria.mjs +232 -0
- package/templates/_agents/scripts/level-criteria.test.mjs +279 -0
- package/templates/_agents/scripts/pipeline-run.mjs +4 -2
- package/templates/_agents/scripts/policy.mjs +47 -0
- package/templates/_agents/scripts/stage-gates.mjs +4 -9
- package/templates/_agents/scripts/wf-journal.test.mjs +1 -1
- package/templates/_agents/scripts/wiki-search.test.mjs +5 -6
- package/templates/_agents/scripts/workflows-check.mjs +1 -1
- package/templates/_agents/workflows/pipeline-closing.md +4 -5
- package/templates/_githooks/pre-commit +3 -34
- package/templates/_githooks/pre-push +3 -3
- package/templates/workflow/README.md +11 -0
- package/modules/hosts/claude/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/codex/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/cursor/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/opencode/commands/wf-source-sync-check.md +0 -89
- package/modules/hosts/trae/commands/wf-source-sync-check.md +0 -90
- package/templates/_agents/commands/source-sync-check.md +0 -103
- package/templates/_agents/scripts/check-ledger.mjs +0 -109
- package/templates/_agents/scripts/check-ledger.test.mjs +0 -199
- package/templates/_agents/scripts/source-sync-check.mjs +0 -156
- package/templates/_agents/scripts/source-sync-check.test.mjs +0 -286
- package/templates/_agents/workflows/source-sync-repair.md +0 -30
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-loader.test.mjs — 台账读取视图单源 fixture 套件(2026-10-12 ledger-loader-p1)
|
|
3
|
+
// 覆盖:主台账单源 / 分片并入与读序 / 坏行跳过 / 缺失静默 / IO 失败 fail-soft 出账 /
|
|
4
|
+
// lines 视图保行内原文 / 两侧视图与旧逐处直读的等价性(Step 1 行为不变的回归护栏)。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
|
|
10
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
// 兄弟脚本动态载入必须走 file:// URL——win32 绝对路径('E:\...')直接 import 会被 ESM loader
|
|
12
|
+
// 判 ERR_UNSUPPORTED_ESM_URL_SCHEME(protocol 'e:'),非 Windows 平台不受影响
|
|
13
|
+
const { ledgerSources, readLedgerLines, readLedgerRows, LEDGER_REL, SHARD_DIR_REL } =
|
|
14
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'ledger-loader.mjs')).href);
|
|
15
|
+
|
|
16
|
+
let pass = 0, fail = 0;
|
|
17
|
+
const check = (name, ok, detail = '') => {
|
|
18
|
+
if (ok) { pass++; console.log(` ok ${name}`); }
|
|
19
|
+
else { fail++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const mkRoot = () => fs.mkdtempSync(path.join(os.tmpdir(), 'ledger-loader-'));
|
|
23
|
+
const w = (root, rel, text) => {
|
|
24
|
+
const p = path.join(root, ...rel.split('/'));
|
|
25
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
26
|
+
fs.writeFileSync(p, text, 'utf8');
|
|
27
|
+
};
|
|
28
|
+
const row = (o) => JSON.stringify(o);
|
|
29
|
+
|
|
30
|
+
// ============ 场景 1:无台账(装户/新仓)→ 空视图,不抛 ============
|
|
31
|
+
{
|
|
32
|
+
const r = mkRoot();
|
|
33
|
+
check('S1 无台账时 sources 为空', ledgerSources(r).length === 0);
|
|
34
|
+
check('S1 无台账时 lines 为空', readLedgerLines(r).length === 0);
|
|
35
|
+
check('S1 无台账时 rows 为空', readLedgerRows(r).length === 0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============ 场景 2:仅主台账 → 与旧逐处直读等价 ============
|
|
39
|
+
{
|
|
40
|
+
const r = mkRoot();
|
|
41
|
+
const rows = [
|
|
42
|
+
row({ ts: '2026-10-01T00:00:00Z', doc: 'workflow/intents/a.md', stage: 'approved', fingerprint: 'a'.repeat(64) }),
|
|
43
|
+
row({ ts: '2026-10-02T00:00:00Z', doc: 'workflow/intents/a.md', stage: 'done', fingerprint: 'a'.repeat(64) }),
|
|
44
|
+
row({ ts: '2026-10-03T00:00:00Z', doc: 'workflow/specs/b.md', stage: 'approved', fingerprint: 'b'.repeat(64) }),
|
|
45
|
+
];
|
|
46
|
+
w(r, LEDGER_REL, rows.join('\n') + '\n');
|
|
47
|
+
const got = readLedgerRows(r);
|
|
48
|
+
check('S2 rows 数与行数一致', got.length === 3, `实得 ${got.length}`);
|
|
49
|
+
check('S2 读序 = 物理序(末次 done 在后)',
|
|
50
|
+
got[1].stage === 'done' && got[2].doc === 'workflow/specs/b.md');
|
|
51
|
+
// 等价性护栏:旧口径(过滤空行 + JSON.parse + 跳过 null/非对象)
|
|
52
|
+
const legacy = rows.map((l) => JSON.parse(l));
|
|
53
|
+
check('S2 与旧逐处直读逐行等价', JSON.stringify(got) === JSON.stringify(legacy));
|
|
54
|
+
check('S2 lines 视图保留行内原文', readLedgerLines(r).length === 3 && /"stage":"approved"/.test(readLedgerLines(r)[0]));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============ 场景 3:坏行跳过(与检查 15/19 同容错口径) ============
|
|
58
|
+
{
|
|
59
|
+
const r = mkRoot();
|
|
60
|
+
w(r, LEDGER_REL, [
|
|
61
|
+
row({ ts: '1', doc: 'workflow/intents/a.md', stage: 'approved' }),
|
|
62
|
+
'{不是 JSON',
|
|
63
|
+
'',
|
|
64
|
+
' ',
|
|
65
|
+
row({ ts: '2', doc: 'workflow/intents/b.md', stage: 'approved' }),
|
|
66
|
+
].join('\n') + '\n');
|
|
67
|
+
const got = readLedgerRows(r);
|
|
68
|
+
check('S3 坏行跳过、空行不计入', got.length === 2, `实得 ${got.length}`);
|
|
69
|
+
check('S3 坏行后仍继续读到后续行', got[1].doc === 'workflow/intents/b.md');
|
|
70
|
+
check('S3 lines 视图含坏行原文(计数口径要全量行)', readLedgerLines(r).length === 3, `实得 ${readLedgerLines(r).length}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============ 场景 4:分片并入——主台账先、分片按文件名序后(分片=较新) ============
|
|
74
|
+
{
|
|
75
|
+
const r = mkRoot();
|
|
76
|
+
w(r, LEDGER_REL, row({ ts: '2026-10-01T00:00:00Z', doc: 'workflow/intents/a.md', stage: 'approved' }) + '\n');
|
|
77
|
+
w(r, `${SHARD_DIR_REL}/b.jsonl`, row({ ts: '2026-10-05T00:00:00Z', doc: 'workflow/intents/b.md', stage: 'approved' }) + '\n');
|
|
78
|
+
w(r, `${SHARD_DIR_REL}/a.jsonl`, row({ ts: '2026-10-09T00:00:00Z', doc: 'workflow/intents/c.md', stage: 'approved' }) + '\n');
|
|
79
|
+
const srcs = ledgerSources(r).map((s) => s.rel);
|
|
80
|
+
check('S4 来源清单:主台账在最前', srcs[0] === LEDGER_REL, JSON.stringify(srcs));
|
|
81
|
+
check('S4 来源清单含分片(文件名序 a → b)', srcs.length === 3 && srcs[1] === `${SHARD_DIR_REL}/a.jsonl`, JSON.stringify(srcs));
|
|
82
|
+
const got = readLedgerRows(r);
|
|
83
|
+
// 读序 = 主台账 → 分片按**文件名**序:a.jsonl(内含 c.md)先于 b.jsonl
|
|
84
|
+
check('S4 读序:主台账行在前、分片行按文件名序在后',
|
|
85
|
+
got[0].doc === 'workflow/intents/a.md' && got[1].doc === 'workflow/intents/c.md' && got[2].doc === 'workflow/intents/b.md',
|
|
86
|
+
JSON.stringify(got.map((e) => e.doc)));
|
|
87
|
+
check('S4 分片行可见(末次取值口径:分片较新)',
|
|
88
|
+
[...got.reduce((m, e) => m.set(e.doc, e.ts), new Map()).values()].length === 3);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ============ 场景 5:分片目录不存在 / 非目录 → 静默零影响 ============
|
|
92
|
+
{
|
|
93
|
+
const r = mkRoot();
|
|
94
|
+
w(r, LEDGER_REL, row({ ts: '1', doc: 'workflow/intents/a.md', stage: 'approved' }) + '\n');
|
|
95
|
+
w(r, 'not-a-dir.txt', 'x');
|
|
96
|
+
check('S5 无分片目录时只返回主台账', ledgerSources(r).length === 1);
|
|
97
|
+
// 分片路径被同名普通文件占位 → 非目录,按不启用处理
|
|
98
|
+
w(r, SHARD_DIR_REL, '我是文件不是目录');
|
|
99
|
+
check('S5 分片路径非目录时静默跳过', ledgerSources(r).length === 1 && readLedgerRows(r).length === 1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ============ 场景 6:IO 失败 fail-soft —— 已读来源照常返回,缺的来源出账不抛 ============
|
|
103
|
+
{
|
|
104
|
+
const r = mkRoot();
|
|
105
|
+
w(r, LEDGER_REL, row({ ts: '1', doc: 'workflow/intents/a.md', stage: 'approved' }) + '\n');
|
|
106
|
+
// 造一个 existsSync 为真但读取失败的来源:目录形态的 .jsonl(读目录抛 EISDIR)
|
|
107
|
+
const shardDir = path.join(r, ...SHARD_DIR_REL.split('/'));
|
|
108
|
+
fs.mkdirSync(shardDir, { recursive: true });
|
|
109
|
+
fs.mkdirSync(path.join(shardDir, 'boom.jsonl'), { recursive: true }); // 目录而非文件 → readFileSync 抛
|
|
110
|
+
const origErr = console.error;
|
|
111
|
+
const errs = [];
|
|
112
|
+
console.error = (...a) => { errs.push(a.join(' ')); };
|
|
113
|
+
let got = null;
|
|
114
|
+
try { got = readLedgerRows(r); } finally { console.error = origErr; }
|
|
115
|
+
check('S6 读取失败不抛异常', got !== null);
|
|
116
|
+
check('S6 主台账行仍正常返回', got.length === 1 && got[0].doc === 'workflow/intents/a.md');
|
|
117
|
+
check('S6 失败来源显式出账(不静默丢行)',
|
|
118
|
+
errs.some((e) => e.includes('boom.jsonl')), JSON.stringify(errs));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ============ 场景 7:CRLF 台账 → 行视图不残留 \r(消费方免各自处理) ============
|
|
122
|
+
{
|
|
123
|
+
const r = mkRoot();
|
|
124
|
+
w(r, LEDGER_REL, row({ ts: '1', doc: 'workflow/intents/a.md', stage: 'approved' }) + '\r\n');
|
|
125
|
+
const lines = readLedgerLines(r);
|
|
126
|
+
check('S7 CRLF 行尾不残留 \\r', lines.length === 1 && !lines[0].endsWith('\r'));
|
|
127
|
+
check('S7 CRLF 下 rows 仍可解析', readLedgerRows(r).length === 1);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
console.log(`\n[ledger-loader.test] PASS ${pass} / FAIL ${fail}`);
|
|
131
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-prune.mjs — 台账特许修剪的判据单源(2026-10-12 ledger-prune-manifest)
|
|
3
|
+
// 为什么需要它:确认台账 append-only(README 硬规则 6),历史行禁删改。但现实中存在**合法**的删行场景——
|
|
4
|
+
// 仓库所有者明令删除某批 workflow 文档(归档、误建单清理、GDPR/合规下架)。此时台账里对应的确认行
|
|
5
|
+
// 也必须随之删除,否则行级校验④「doc 在当前树存在」会持续报红,退路只剩「手改台账」→ 直接踩
|
|
6
|
+
// `--no-verify` 的红线。
|
|
7
|
+
// 在本模块之前,flow-kit 对此**无任何合法出口**——这不是一个「缺个功能」,而是一条会把人逼向绕过的死路。
|
|
8
|
+
//
|
|
9
|
+
// 本模块提供的唯一出口(**特许**,不是豁免):逐行对账 + 凭据入库。
|
|
10
|
+
// manifest = .agents/ledger-prune-manifest.json,格式:
|
|
11
|
+
// { "ts": "<ISO>", "authority": "<授权来源:谁/哪次指令>", "reason": "<为什么删>",
|
|
12
|
+
// "removes": ["<被删行的 sha256>", ...], "docs": ["<家族名>", ...] }
|
|
13
|
+
// 判据(matchPrune,四条缺一不可):
|
|
14
|
+
// ① manifest 存在且 removes 非空;
|
|
15
|
+
// ② 每个哈希**恰命中一行**(哈希不存在或声明重复都判失败——防「一个哈希对多行」的糊弄);
|
|
16
|
+
// ③ 新内容 == 旧内容 − manifest 所列各行(**保序**)——存在 manifest 之外的任何增删改即失败;
|
|
17
|
+
// ④ 每个被删行的 doc ∈ manifest.docs 清单(空清单 = 不约束)。
|
|
18
|
+
// 凭据(被删行哈希)随 manifest 入 git,事后可对质;manifest 自身是 owned 面,人工审查面。
|
|
19
|
+
//
|
|
20
|
+
// **删行后该 doc 的哈希链会断**:被删行 R1(hash=H1)之后的后继行 R2 带 prevHash=H1,而 R1 已不在表内
|
|
21
|
+
// → 校验时 byDoc(doc) 为空、R2 的 H1 失配。本模块导出 prunedHashes() 供链校验识别这一**有凭据的链段重启**:
|
|
22
|
+
// 「本行的 prevHash 出现在 manifest.prunedHashes 中」即视为合法重启点,而非篡改。
|
|
23
|
+
// 诚实边界:manifest 是可写文件,**刻意伪造一份 manifest 即可让任意断链合法化**——这与「台账本地可写」
|
|
24
|
+
// 的既有信任边界同级(见 README 确认门诚实边界 / policy.ledgerChainHash)。本模块不宣称通道关闭,
|
|
25
|
+
// 它把「顺手手改台账」抬到「必须提交一份带授权与理由、可被 git 历史对质的凭据」——性质不同,可事后追责。
|
|
26
|
+
import fs from 'node:fs';
|
|
27
|
+
import path from 'node:path';
|
|
28
|
+
import { createHash } from 'node:crypto';
|
|
29
|
+
import { spawnSync } from 'node:child_process';
|
|
30
|
+
|
|
31
|
+
export const PRUNE_REL = '.agents/ledger-prune-manifest.json';
|
|
32
|
+
|
|
33
|
+
export const sha256line = (l) => createHash('sha256').update(l, 'utf8').digest('hex');
|
|
34
|
+
|
|
35
|
+
const linesOf = (text) => (text || '').replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim().length > 0);
|
|
36
|
+
|
|
37
|
+
// loadManifestFromRef:从某个 git 对象引用读 manifest(历史全扫用:该提交树上的 manifest)。
|
|
38
|
+
// 用 spawnSync + status 判定而非 execFileSync:后者失败时 stderr 直通父进程,历史扫描探针在旧提交
|
|
39
|
+
// 尚无 manifest 时会刷屏 fatal 噪声(实测教训)——探测失败必须静默归 null。
|
|
40
|
+
export function loadManifestFromRef(ref, cwd) {
|
|
41
|
+
try {
|
|
42
|
+
const r = spawnSync('git', ['show', ref], { cwd, encoding: 'utf8' });
|
|
43
|
+
if (r.status !== 0) return null;
|
|
44
|
+
const m = JSON.parse(r.stdout);
|
|
45
|
+
return (m && typeof m === 'object') ? m : null;
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// loadManifestFromDisk:读工作树上的 manifest(链校验用;缺文件 = 无修剪凭据,返回 null)。
|
|
52
|
+
export function loadManifestFromDisk(root) {
|
|
53
|
+
const p = path.join(root, ...PRUNE_REL.split('/'));
|
|
54
|
+
try {
|
|
55
|
+
if (!fs.existsSync(p)) return null;
|
|
56
|
+
const m = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
57
|
+
return (m && typeof m === 'object') ? m : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null; // 损坏 = 视同无凭据 → 链断裂照报(fail-closed 于校验面)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// matchPrune:逐行对账。返回 {removed, docs, hashes} 或 {err}。
|
|
64
|
+
export function matchPrune(headText, curText, manifest) {
|
|
65
|
+
if (!manifest || !Array.isArray(manifest.removes) || !manifest.removes.length) {
|
|
66
|
+
return { err: 'manifest 缺失或 removes 为空' };
|
|
67
|
+
}
|
|
68
|
+
const head = linesOf(headText);
|
|
69
|
+
const cur = linesOf(curText);
|
|
70
|
+
const want = new Set(manifest.removes);
|
|
71
|
+
const kept = head.filter((l) => !want.has(sha256line(l)));
|
|
72
|
+
const removedLines = head.filter((l) => want.has(sha256line(l)));
|
|
73
|
+
|
|
74
|
+
if (removedLines.length !== manifest.removes.length) {
|
|
75
|
+
return { err: `manifest 哈希命中 ${removedLines.length} 行 ≠ 声明 ${manifest.removes.length} 行(哈希不存在或声明重复)` };
|
|
76
|
+
}
|
|
77
|
+
if (kept.join('\n') !== cur.join('\n')) {
|
|
78
|
+
return { err: '新内容 ≠ 旧内容 − manifest 所列行(存在 manifest 之外的增删改)' };
|
|
79
|
+
}
|
|
80
|
+
const docs = new Set(manifest.docs || []);
|
|
81
|
+
for (const l of removedLines) {
|
|
82
|
+
let doc = '';
|
|
83
|
+
try { doc = JSON.parse(l).doc || ''; } catch { return { err: '被删行非 JSON' }; }
|
|
84
|
+
if (docs.size && !docs.has(path.basename(doc).replace(/\.md$/, ''))) {
|
|
85
|
+
return { err: `被删行 doc ${doc} 不在 manifest.docs 清单内` };
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { removed: removedLines.length, docs: [...docs], hashes: [...want] };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// prunedHashes:工作树 manifest 声明的被删行哈希集合(链校验的合法重启点判定用)。
|
|
92
|
+
export function prunedHashes(root) {
|
|
93
|
+
const m = loadManifestFromDisk(root);
|
|
94
|
+
if (!m || !Array.isArray(m.removes)) return new Set();
|
|
95
|
+
return new Set(m.removes.filter((h) => typeof h === 'string'));
|
|
96
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-prune.test.mjs — 台账特许修剪通道 fixture 套件(2026-10-12 ledger-prune-manifest)
|
|
3
|
+
// 覆盖:四条判据逐条(哈希命中数 / 内容对账 / docs 清单 / manifest 缺失)+ 链段重启凭据 +
|
|
4
|
+
// --staged 真 git 端到端(放行与拦截)+ 无 manifest 时 append-only 红线不变。
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
9
|
+
import { spawnSync } from 'node:child_process';
|
|
10
|
+
|
|
11
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const { matchPrune, sha256line, PRUNE_REL } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'ledger-prune.mjs')).href);
|
|
13
|
+
// 夹具闭包(2026-10-12 fixture-closure-rootfix):check-ledger-invariant 及其传递依赖(ledger-loader /
|
|
14
|
+
// policy 等)整闭包拷入。此前手写枚举漏拷 ledger-loader.mjs → 该脚本 import 失败 → 门禁静默不跑 →
|
|
15
|
+
// 假绿;现在闭包自动跟随,且拷完 assertFixtureScripts 会在建夹具当场炸出全量缺失清单。
|
|
16
|
+
const { copyScriptClosure } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'fixture-scripts.mjs')).href);
|
|
17
|
+
|
|
18
|
+
let pass = 0, fail = 0;
|
|
19
|
+
const check = (name, ok, detail = '') => {
|
|
20
|
+
if (ok) { pass++; console.log(` ok ${name}`); }
|
|
21
|
+
else { fail++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const L = (...xs) => xs.filter(Boolean).join('\n') + '\n';
|
|
25
|
+
const rowA = L('{"ts":"2026-10-01T00:00:00Z","doc":"workflow/intents/2026-10-01-a.md","stage":"approved","fingerprint":"' + 'a'.repeat(64) + '"}');
|
|
26
|
+
const rowB = L('{"ts":"2026-10-02T00:00:00Z","doc":"workflow/plans/2026-10-01-a.md","stage":"approved","fingerprint":"' + 'b'.repeat(64) + '"}');
|
|
27
|
+
const rowC = L('{"ts":"2026-10-03T00:00:00Z","doc":"workflow/intents/2026-10-01-z.md","stage":"approved","fingerprint":"' + 'c'.repeat(64) + '"}');
|
|
28
|
+
const HEAD = rowA + rowB + rowC;
|
|
29
|
+
|
|
30
|
+
// ============ 场景 1:四条判据逐条 ============
|
|
31
|
+
{
|
|
32
|
+
const mk = (over = {}) => ({
|
|
33
|
+
ts: '2026-10-12T00:00:00Z', authority: '仓库所有者', reason: '归档下架一批单',
|
|
34
|
+
removes: [sha256line(rowB.trim())], docs: ['2026-10-01-a'], ...over,
|
|
35
|
+
});
|
|
36
|
+
// ① manifest 缺失 / removes 空
|
|
37
|
+
check('S1① manifest 缺失 → err', !!(matchPrune(HEAD, rowA + rowC, null) || {}).err);
|
|
38
|
+
check('S1① removes 为空 → err', !!(matchPrune(HEAD, rowA + rowC, mk({ removes: [] })).err));
|
|
39
|
+
// ② 哈希不存在
|
|
40
|
+
check('S1② 哈希不存在 → err(命中数不符)',
|
|
41
|
+
!!(matchPrune(HEAD, rowA + rowC, mk({ removes: ['0'.repeat(64)] })).err));
|
|
42
|
+
// ② 哈希声明重复(两行内容相同 → 一个哈希命中 2 行)
|
|
43
|
+
const dup = L('{"ts":"1","doc":"workflow/intents/d.md","stage":"approved"}', '{"ts":"1","doc":"workflow/intents/d.md","stage":"approved"}');
|
|
44
|
+
check('S1② 重复行声明单哈希 → err(命中数 ≠ 声明数)',
|
|
45
|
+
!!(matchPrune(dup, '', mk({ removes: [sha256line(dup.split('\n')[0])] })).err));
|
|
46
|
+
// ③ 存在 manifest 之外的改动
|
|
47
|
+
check('S1③ 多删一行(manifest 外) → err',
|
|
48
|
+
!!(matchPrune(HEAD, rowA, mk()).err));
|
|
49
|
+
check('S1③ 改写保留行内容 → err',
|
|
50
|
+
!!(matchPrune(HEAD, rowA + rowC.replace('approved', 'done'), mk()).err));
|
|
51
|
+
check('S1③ 插入新行 → err',
|
|
52
|
+
!!(matchPrune(HEAD, rowA + rowC + rowB, mk()).err));
|
|
53
|
+
// ④ doc 不在清单
|
|
54
|
+
check('S1④ 被删行 doc 不在 manifest.docs → err',
|
|
55
|
+
!!(matchPrune(HEAD, rowA + rowC, mk({ docs: ['some-other'] })).err));
|
|
56
|
+
check('S1④ docs 为空数组 = 不约束 → 放行',
|
|
57
|
+
!(matchPrune(HEAD, rowA + rowC, mk({ docs: [] })).err));
|
|
58
|
+
// 全对
|
|
59
|
+
const ok = matchPrune(HEAD, rowA + rowC, mk());
|
|
60
|
+
check('S1 全部判据通过 → 放行且计数正确',
|
|
61
|
+
!ok.err && ok.removed === 1 && ok.docs[0] === '2026-10-01-a', JSON.stringify(ok));
|
|
62
|
+
check('S1 返回被删哈希供链校验复用', Array.isArray(ok.hashes) && ok.hashes[0] === sha256line(rowB.trim()));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ============ 场景 2:--staged 真 git 端到端 ============
|
|
66
|
+
{
|
|
67
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'prune-e2e-'));
|
|
68
|
+
const git = (args, cwd = d) => spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
69
|
+
git(['init', '-q', '.']);
|
|
70
|
+
git(['config', 'user.email', 't@t.io']); git(['config', 'user.name', 't']);
|
|
71
|
+
git(['config', 'core.autocrlf', 'false']);
|
|
72
|
+
const mkfix = () => {
|
|
73
|
+
fs.mkdirSync(path.join(d, '.agents', 'scripts'), { recursive: true });
|
|
74
|
+
copyScriptClosure(path.join(d, '.agents', 'scripts'), ['check-ledger-invariant.mjs']);
|
|
75
|
+
for (const p of ['workflow/intents/2026-10-01-a.md', 'workflow/plans/2026-10-01-a.md', 'workflow/intents/2026-10-01-z.md']) {
|
|
76
|
+
fs.mkdirSync(path.join(d, path.dirname(p)), { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(d, p), '---\n状态: approved\n---\n# doc\n', 'utf8');
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), HEAD, 'utf8');
|
|
80
|
+
git(['add', '-A']); git(['commit', '-qm', 'base']);
|
|
81
|
+
};
|
|
82
|
+
const runGate = () => spawnSync(process.execPath, ['.agents/scripts/check-ledger-invariant.mjs', '--staged'],
|
|
83
|
+
{ cwd: d, encoding: 'utf8' });
|
|
84
|
+
|
|
85
|
+
// ① 无 manifest 直接删行 → 仍 hard-block(append-only 红线不变)
|
|
86
|
+
mkfix();
|
|
87
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowA + rowC, 'utf8');
|
|
88
|
+
git(['add', '-A']);
|
|
89
|
+
let r = runGate();
|
|
90
|
+
check('S2① 无 manifest 删行 → exit 1(红线不变)', r.status === 1, `exit=${r.status} ${r.stderr}`);
|
|
91
|
+
check('S2① 报文含「特许修剪对账失败」', /台账重写/.test(r.stderr), r.stderr);
|
|
92
|
+
|
|
93
|
+
// ② 带 manifest 同笔暂存 → 放行
|
|
94
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowA + rowC, 'utf8');
|
|
95
|
+
fs.writeFileSync(path.join(d, PRUNE_REL), JSON.stringify({
|
|
96
|
+
ts: '2026-10-12T00:00:00Z', authority: '仓库所有者指令', reason: '下架 a 家族',
|
|
97
|
+
removes: [sha256line(rowB.trim())], docs: ['2026-10-01-a'],
|
|
98
|
+
}, null, 2), 'utf8');
|
|
99
|
+
git(['add', '-A']);
|
|
100
|
+
r = runGate();
|
|
101
|
+
check('S2② 带合法 manifest → exit 0 放行', r.status === 0, `exit=${r.status} ${r.stderr}`);
|
|
102
|
+
check('S2② 放行报文点名凭据与行数', /特许修剪通过/.test(r.stdout) && /移除 1 行/.test(r.stdout), r.stdout);
|
|
103
|
+
|
|
104
|
+
// ③ manifest 只写一半(另删一行未申报) → 仍拦
|
|
105
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowA, 'utf8');
|
|
106
|
+
git(['add', '-A']);
|
|
107
|
+
r = runGate();
|
|
108
|
+
check('S2③ manifest 外的额外删行 → exit 1', r.status === 1, `exit=${r.status}`);
|
|
109
|
+
check('S2③ 报文说明内容 ≠ 旧 − manifest', /旧内容 − manifest/.test(r.stderr), r.stderr);
|
|
110
|
+
|
|
111
|
+
// ④ 提交后跑历史全扫 → 认领修剪、不报前缀破坏
|
|
112
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowA + rowC, 'utf8');
|
|
113
|
+
git(['add', '-A']); git(['commit', '-qm', 'prune with manifest']);
|
|
114
|
+
r = spawnSync(process.execPath, ['.agents/scripts/check-ledger-invariant.mjs'], { cwd: d, encoding: 'utf8' });
|
|
115
|
+
check('S2④ 历史全扫认领修剪 → exit 0', r.status === 0, `exit=${r.status} ${r.stderr}`);
|
|
116
|
+
check('S2④ 全扫输出含修剪认领记录', /特许修剪 1 行/.test(r.stderr + r.stdout), (r.stderr + r.stdout).slice(0, 300));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log(`\n[ledger-prune.test] PASS ${pass} / FAIL ${fail}`);
|
|
120
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-ts-monotonic.test.mjs — 台账行级校验 ts 口径修正 + 分片覆盖 fixture 套件(2026-10-12)
|
|
3
|
+
// 命题:并集合并 / 分片归并会制造**跨 doc** 的物理序 ts 乱序,这是正常协作而非篡改;判据必须从
|
|
4
|
+
// 「全表非降序」改为「同 doc 内非降序」,同时不能因此丢掉真问题的检出(同 doc 内倒挂照 hard 拦)。
|
|
5
|
+
// 覆盖面:跨 doc 乱序放行 + 计数出账 / 同 doc 内倒挂仍拦 / 分片行纳入行级校验 / 豁免行号不漂移。
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
|
|
12
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
// 夹具闭包(2026-10-12 fixture-closure-rootfix):整闭包自动跟随,不再手写依赖枚举(漏拷 = 假绿)
|
|
14
|
+
const { copyScriptClosure } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'fixture-scripts.mjs')).href);
|
|
15
|
+
|
|
16
|
+
let pass = 0, fail = 0;
|
|
17
|
+
const check = (name, ok, detail = '') => {
|
|
18
|
+
if (ok) { pass++; console.log(` ok ${name}`); }
|
|
19
|
+
else { fail++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// 建真 git fixture(行级校验只在有提交历史时才会跑到;--staged 也需要 HEAD 基线)
|
|
23
|
+
function mkRepo() {
|
|
24
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'tsmono-'));
|
|
25
|
+
const git = (...args) => spawnSync('git', args, { cwd: d, encoding: 'utf8' });
|
|
26
|
+
git('init', '-q', '.');
|
|
27
|
+
git('config', 'user.email', 't@t.io'); git('config', 'user.name', 't');
|
|
28
|
+
git('config', 'core.autocrlf', 'false');
|
|
29
|
+
fs.mkdirSync(path.join(d, '.agents', 'scripts'), { recursive: true });
|
|
30
|
+
copyScriptClosure(path.join(d, '.agents', 'scripts'), ['check-ledger-invariant.mjs']);
|
|
31
|
+
return { d, git };
|
|
32
|
+
}
|
|
33
|
+
const docFile = (d, rel) => {
|
|
34
|
+
const p = path.join(d, ...rel.split('/'));
|
|
35
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
36
|
+
fs.writeFileSync(p, '---\n状态: approved\n---\n# doc\n', 'utf8');
|
|
37
|
+
};
|
|
38
|
+
const row = (ts, doc, extra = {}) => JSON.stringify({
|
|
39
|
+
ts, doc, stage: 'approved', fingerprint: 'a'.repeat(64), source: 'tty', ...extra,
|
|
40
|
+
});
|
|
41
|
+
const writeLedger = (d, lines) => {
|
|
42
|
+
fs.mkdirSync(path.join(d, '.agents'), { recursive: true });
|
|
43
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), lines.filter(Boolean).join('\n') + '\n', 'utf8');
|
|
44
|
+
};
|
|
45
|
+
const runGate = (d) => spawnSync(process.execPath, ['.agents/scripts/check-ledger-invariant.mjs'], { cwd: d, encoding: 'utf8' });
|
|
46
|
+
|
|
47
|
+
// ============ 场景 1:跨 doc 物理序 ts 乱序(并集合并形态)→ 放行 + 计数出账 ============
|
|
48
|
+
// a 的末行 ts 晚于 b 的首行 ts——正是「A 分支先提交、B 分支的行被并集追加到尾部」产生的形态。
|
|
49
|
+
{
|
|
50
|
+
const { d, git } = mkRepo();
|
|
51
|
+
docFile(d, 'workflow/intents/2026-10-01-a.md');
|
|
52
|
+
docFile(d, 'workflow/intents/2026-10-01-b.md');
|
|
53
|
+
writeLedger(d, [
|
|
54
|
+
row('2026-10-10T00:00:00Z', 'workflow/intents/2026-10-01-a.md'), // 较晚
|
|
55
|
+
row('2026-10-01T00:00:00Z', 'workflow/intents/2026-10-01-b.md'), // 较早,跨 doc 乱序
|
|
56
|
+
]);
|
|
57
|
+
git('add', '-A'); git('commit', '-qm', 'x');
|
|
58
|
+
const r = runGate(d);
|
|
59
|
+
check('S1 跨 doc ts 乱序 → exit 0 放行(正常协作形态)', r.status === 0, `exit=${r.status} ${r.stderr}`);
|
|
60
|
+
check('S1 乱序计数出账且标明不阻断',
|
|
61
|
+
/跨 doc 物理序 ts 乱序 1 处/.test(r.stdout) && /不阻断/.test(r.stdout), r.stdout);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============ 场景 2:同 doc 内 ts 倒挂 → 仍 hard 拦(真问题未被放松)============
|
|
65
|
+
{
|
|
66
|
+
const { d, git } = mkRepo();
|
|
67
|
+
docFile(d, 'workflow/intents/2026-10-01-a.md');
|
|
68
|
+
writeLedger(d, [
|
|
69
|
+
row('2026-10-10T00:00:00Z', 'workflow/intents/2026-10-01-a.md'),
|
|
70
|
+
row('2026-10-01T00:00:00Z', 'workflow/intents/2026-10-01-a.md'), // 同 doc 倒挂
|
|
71
|
+
]);
|
|
72
|
+
git('add', '-A'); git('commit', '-qm', 'x');
|
|
73
|
+
const r = runGate(d);
|
|
74
|
+
check('S2 同 doc 内 ts 倒挂 → exit 1 hard 拦', r.status === 1, `exit=${r.status}`);
|
|
75
|
+
check('S2 报文点名「追加时序倒挂」并带 doc',
|
|
76
|
+
/追加时序倒挂/.test(r.stderr) && /2026-10-01-a/.test(r.stderr), r.stderr);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============ 场景 3:分片行纳入行级校验(分片里的坏行不能逃逸)============
|
|
80
|
+
{
|
|
81
|
+
const { d, git } = mkRepo();
|
|
82
|
+
docFile(d, 'workflow/intents/2026-10-01-a.md');
|
|
83
|
+
writeLedger(d, [row('2026-10-01T00:00:00Z', 'workflow/intents/2026-10-01-a.md')]);
|
|
84
|
+
// 分片里放一行 fingerprint 非 64 位 hex 的坏行——只扫主台账会漏掉它
|
|
85
|
+
fs.mkdirSync(path.join(d, '.agents', 'confirmations.d'), { recursive: true });
|
|
86
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', 'x.jsonl'),
|
|
87
|
+
JSON.stringify({ ts: '2026-10-02T00:00:00Z', doc: 'workflow/intents/2026-10-01-a.md', stage: 'approved', fingerprint: 'bad', source: 'tty' }) + '\n', 'utf8');
|
|
88
|
+
git('add', '-A'); git('commit', '-qm', 'x');
|
|
89
|
+
const r = runGate(d);
|
|
90
|
+
check('S3 分片内坏行被行级校验抓到 → exit 1', r.status === 1, `exit=${r.status} ${r.stdout}`);
|
|
91
|
+
check('S3 报文指出 fingerprint 非 64 位 hex', /fingerprint 非 64 位 hex/.test(r.stderr), r.stderr);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============ 场景 4:分片内 doc 不存在 → 同样被抓 ============
|
|
95
|
+
{
|
|
96
|
+
const { d, git } = mkRepo();
|
|
97
|
+
docFile(d, 'workflow/intents/2026-10-01-a.md');
|
|
98
|
+
writeLedger(d, [row('2026-10-01T00:00:00Z', 'workflow/intents/2026-10-01-a.md')]);
|
|
99
|
+
fs.mkdirSync(path.join(d, '.agents', 'confirmations.d'), { recursive: true });
|
|
100
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', 'y.jsonl'),
|
|
101
|
+
row('2026-10-02T00:00:00Z', 'workflow/intents/2026-10-01-ghost.md') + '\n', 'utf8');
|
|
102
|
+
git('add', '-A'); git('commit', '-qm', 'x');
|
|
103
|
+
const r = runGate(d);
|
|
104
|
+
check('S4 分片内 doc 在当前树不存在 → exit 1', r.status === 1, `exit=${r.status}`);
|
|
105
|
+
check('S4 报文点名 ghost.md', /ghost\.md/.test(r.stderr), r.stderr);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============ 场景 5:豁免行号不因引入分片而漂移 ============
|
|
109
|
+
// 视图序 = 主台账在前、分片在后 → 主台账第 2 行的豁免登记在引入分片后仍指向同一行。
|
|
110
|
+
{
|
|
111
|
+
const { d, git } = mkRepo();
|
|
112
|
+
docFile(d, 'workflow/intents/2026-10-01-a.md');
|
|
113
|
+
docFile(d, 'workflow/intents/2026-10-01-b.md');
|
|
114
|
+
// 第 2 行是「doc 不存在」的坏行(须靠豁免放行)
|
|
115
|
+
writeLedger(d, [
|
|
116
|
+
row('2026-10-01T00:00:00Z', 'workflow/intents/2026-10-01-a.md'),
|
|
117
|
+
row('2026-10-02T00:00:00Z', 'workflow/intents/2026-10-01-ghost.md'),
|
|
118
|
+
]);
|
|
119
|
+
fs.writeFileSync(path.join(d, '.agents', 'ledger-line-exempt.json'),
|
|
120
|
+
JSON.stringify([{ line: 2, doc: 'workflow/intents/2026-10-01-ghost.md', reason: '文档已归档' }]), 'utf8');
|
|
121
|
+
git('add', '-A'); git('commit', '-qm', 'x');
|
|
122
|
+
const before = runGate(d);
|
|
123
|
+
check('S5 前:无分片时豁免生效 → exit 0', before.status === 0, `exit=${before.status} ${before.stderr}`);
|
|
124
|
+
|
|
125
|
+
// 追加一个分片(视图序追加在主台账之后),豁免行号应仍指向 ghost 那行
|
|
126
|
+
fs.mkdirSync(path.join(d, '.agents', 'confirmations.d'), { recursive: true });
|
|
127
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', 'z.jsonl'),
|
|
128
|
+
row('2026-10-03T00:00:00Z', 'workflow/intents/2026-10-01-b.md') + '\n', 'utf8');
|
|
129
|
+
git('add', '-A'); git('commit', '-qm', 'y');
|
|
130
|
+
const after = runGate(d);
|
|
131
|
+
check('S5 后:引入分片,豁免行号未漂移 → 仍 exit 0', after.status === 0, `exit=${after.status} ${after.stderr}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
console.log(`\n[ledger-ts-monotonic.test] PASS ${pass} / FAIL ${fail}`);
|
|
135
|
+
process.exit(fail ? 1 : 0);
|