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
|
@@ -13,6 +13,7 @@ import fs from 'node:fs';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
import { loadEnums } from './workflow-enums.mjs';
|
|
16
|
+
import { readLedgerRows as loadLedgerRows } from './ledger-loader.mjs';
|
|
16
17
|
|
|
17
18
|
const USAGE = `用法:node .agents/scripts/gen-workflow-metrics.mjs [--month YYYY-MM] [--dry-run] [--help]
|
|
18
19
|
|
|
@@ -43,34 +44,10 @@ const WORKFLOW = 'workflow';
|
|
|
43
44
|
const METRICS_P = path.join(WORKFLOW, 'metrics.md');
|
|
44
45
|
const BUDGETS = path.join('.agents', 'rule-budgets.txt');
|
|
45
46
|
const DOC_TYPES = ['intents', 'specs', 'plans', 'incidents'];
|
|
46
|
-
// 活跃口径单源(
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
// 装户 `.agents/scripts/` → 上溯 2 级到安装根
|
|
51
|
-
// 首版硬编码 3 级,故**在每个装户安装里都解析不到枚举文件**(报错路径 `<install>/../.agents/…`)。
|
|
52
|
-
// 实测暴露路径:CI 新增「shipped 套件」步骤后,`.agents/scripts/gen-workflow-metrics.test.mjs`
|
|
53
|
-
// 场景 1 即 exit 1(workflow-enums 单源文件不可读),而包源侧同字节跑出全绿。
|
|
54
|
-
// 改为**逐级上溯探测**:找到含 `.agents/workflow-enums.txt` 的那一级即用,找不到则回退标准位置让
|
|
55
|
-
// loadEnums fail-loud(保持「缺失即响亮」的既有纪律,不静默兜底)。
|
|
56
|
-
// 包源仓布局补丁(装户端内容清理后):纯包源仓无 `.agents/`,枚举单源在 `templates/_agents/` 下——
|
|
57
|
-
// 上一级目录名是 `_agents` 而非 `.agents`。探测同步认这两种布局,回退仍走 fail-loud。
|
|
58
|
-
const resolveEnumsPath = () => {
|
|
59
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
60
|
-
let dir = scriptDir;
|
|
61
|
-
for (let i = 0; i < 6; i++) {
|
|
62
|
-
const cand = path.join(dir, '.agents', 'workflow-enums.txt');
|
|
63
|
-
if (fs.existsSync(cand)) return cand;
|
|
64
|
-
const parent = path.dirname(dir);
|
|
65
|
-
if (parent === dir) break; // 到根
|
|
66
|
-
dir = parent;
|
|
67
|
-
}
|
|
68
|
-
// 包源径:脚本在 templates/_agents/scripts/,枚举在 templates/_agents/workflow-enums.txt
|
|
69
|
-
const pkgSrc = path.join(scriptDir, '..', 'workflow-enums.txt');
|
|
70
|
-
if (fs.existsSync(pkgSrc)) return pkgSrc;
|
|
71
|
-
return path.join(scriptDir, '..', '..', '.agents', 'workflow-enums.txt');
|
|
72
|
-
};
|
|
73
|
-
const ENUMS = loadEnums(resolveEnumsPath());
|
|
47
|
+
// 活跃口径单源(workflow-enums.txt)。与脚本同层的上一级:
|
|
48
|
+
// 包源 templates/_agents/workflow-enums.txt,装户副本 .agents/workflow-enums.txt。
|
|
49
|
+
// 拷贝后相对位置不变,不按仓库根猜测另一套目录。
|
|
50
|
+
const ENUMS = loadEnums(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'workflow-enums.txt'));
|
|
74
51
|
const ACTIVE_STATUS = [...ENUMS['doc.status.active'], ...ENUMS['incident.status.active']];
|
|
75
52
|
const BEGIN = '<!-- GENERATED:BEGIN — gen-workflow-metrics.mjs 整段重写,手工说明写在本行之前 -->';
|
|
76
53
|
const END = '<!-- GENERATED:END -->';
|
|
@@ -176,12 +153,11 @@ const idxActive = Number((idxText.match(/^## 活跃((\d+))/m) || [])[1] ?? N
|
|
|
176
153
|
|
|
177
154
|
// ---- 闭环漏斗(2026-10-02 ledger-funnel-metrics 立法)----
|
|
178
155
|
// 只读台账一手字段(doc/stage/ts/source),坏行跳过(与检查 15/19 同容错);观测面不挂门禁、不新增文档断言(检查 16 零接触)。
|
|
179
|
-
|
|
156
|
+
// 台账读取收敛 ledger-loader 单源(2026-10-12 ledger-loader-p1)——口径不变(只读一手字段 doc/stage/ts/source、
|
|
157
|
+
// 坏行跳过,与检查 15/19 同容错)。**本步刻意保留「全量行不去重」**:去重会改漏斗 ledgerRows 计数口径,
|
|
158
|
+
// 属口径变更,须随分片功能同批生效,不得混入单源化本步。
|
|
180
159
|
function readLedgerRows() {
|
|
181
|
-
|
|
182
|
-
return fs.readFileSync(LEDGER_P, 'utf8').split(/\r?\n/)
|
|
183
|
-
.map((l) => { try { return JSON.parse(l); } catch { return null; } })
|
|
184
|
-
.filter((e) => e && typeof e === 'object');
|
|
160
|
+
return loadLedgerRows();
|
|
185
161
|
}
|
|
186
162
|
|
|
187
163
|
function funnelFromLedger(entries, month) {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// git-merge-jsonl.mjs — Git 自定义合并驱动:JSONL 追加式台账并集去重(2026-10-12 ledger-merge-driver)
|
|
3
|
+
// 解决什么:确认台账 `.agents/confirmations.jsonl` 是「尾部追加 + 多人/多 worktree 并行」形态。
|
|
4
|
+
// 两个分支各自追加行 → 同一文件末尾分叉 → git 默认三方合并把它当文本冲突,人工解且极易解错
|
|
5
|
+
// (常见错解:整取一侧,**静默丢掉另一侧的确认行**——门禁事后无从发现,人不可信面直接扩大)。
|
|
6
|
+
// 本驱动把合并降级为确定性的**并集去重**:两侧行全保留,无冲突可解。
|
|
7
|
+
//
|
|
8
|
+
// 接线(两步,缺任一步则退化为文本冲突——不静默丢数据,但要人工解):
|
|
9
|
+
// ① git config merge.jsonl-union.driver "node .agents/scripts/git-merge-jsonl.mjs %O %A %B"
|
|
10
|
+
// ② 目标仓 .gitattributes 含:.agents/*.jsonl merge=jsonl-union
|
|
11
|
+
// flow-kit init 会自动做 ①;②由 templates/.gitattributes 下发(**owned**——存量装户 sync 永不覆盖,
|
|
12
|
+
// 需自行补一行,见 README 台账并发口径)。
|
|
13
|
+
//
|
|
14
|
+
// 调用协议(由 .gitattributes 关联 merge=jsonl-union 触发):
|
|
15
|
+
// %O 祖先版本路径 | %A 当前分支版本(合并结果必须覆写回此文件)| %B 对方分支版本
|
|
16
|
+
//
|
|
17
|
+
// **为什么 union 去重不会破坏台账哈希链**(ledgerChainHash 的前提,勿破坏):
|
|
18
|
+
// 去重按**整行文本**精确比对,不解析、不改写行内任何字段 → 合并后每行文本与合并前逐字相同
|
|
19
|
+
// → JSON.parse 保持键序 → ledgerChainHash(row) 重算结果不变。哈希链的**行内自洽**因此恒成立。
|
|
20
|
+
// (跨合并边界的**链首尾衔接**是另一回事,由链域改造承接,见 check-loop 检查 15。)
|
|
21
|
+
import fs from 'node:fs';
|
|
22
|
+
|
|
23
|
+
// mergeJsonlLines:并集去重。保持 current(%A)既有行的原始顺序,其余 other(%B)新行追加末尾。
|
|
24
|
+
// 全部行按整行文本精确去重;CRLF 归一为 LF(台账判据与 check-ledger-invariant 的 blob 口径对齐)。
|
|
25
|
+
export function mergeJsonlLines(currentContent, otherContent) {
|
|
26
|
+
const norm = (str) => (str || '').replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim().length > 0);
|
|
27
|
+
const currentLines = norm(currentContent);
|
|
28
|
+
const otherLines = norm(otherContent);
|
|
29
|
+
|
|
30
|
+
const seen = new Set(currentLines);
|
|
31
|
+
const result = [...currentLines];
|
|
32
|
+
for (const line of otherLines) {
|
|
33
|
+
if (!seen.has(line)) { seen.add(line); result.push(line); }
|
|
34
|
+
}
|
|
35
|
+
return result.length > 0 ? result.join('\n') + '\n' : '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// CLI 执行入口(isMain 守卫沿本仓惯例:被 import 时零副作用,供 compact-ledgers 复用本函数)
|
|
39
|
+
const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('git-merge-jsonl.mjs');
|
|
40
|
+
if (isMain) {
|
|
41
|
+
// %O(祖先版本)本驱动用不到——并集去重只需 %A ∪ %B;保留位置占位以对齐 git 的 %O %A %B 调用协议
|
|
42
|
+
const [, , _ancestorPath, currentPath, otherPath] = process.argv;
|
|
43
|
+
|
|
44
|
+
if (!currentPath || !otherPath) {
|
|
45
|
+
console.error('用法:node git-merge-jsonl.mjs %O %A %B(由 .gitattributes 的 merge=jsonl-union 触发,不供手工调用)');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 原子覆写:直接 writeFileSync 会先截断原文件,进程中途崩溃将留下半写文件——git 合并中途失败
|
|
50
|
+
// 正是最需要「要么旧要么新」的场合。改为同目录临时文件 + rename。
|
|
51
|
+
const tmpPath = `${currentPath}.tmp`;
|
|
52
|
+
try {
|
|
53
|
+
const currentContent = fs.existsSync(currentPath) ? fs.readFileSync(currentPath, 'utf8') : '';
|
|
54
|
+
const otherContent = fs.existsSync(otherPath) ? fs.readFileSync(otherPath, 'utf8') : '';
|
|
55
|
+
fs.writeFileSync(tmpPath, mergeJsonlLines(currentContent, otherContent), 'utf8');
|
|
56
|
+
fs.renameSync(tmpPath, currentPath);
|
|
57
|
+
process.exit(0);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
try { fs.unlinkSync(tmpPath); } catch { /* tmp 可能尚未创建 */ }
|
|
60
|
+
console.error(`[git-merge-jsonl] 合并失败: ${err.message}——已保留原文件,git 将报为冲突(人工解,勿整取一侧丢行)`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// git-merge-jsonl.test.mjs — 台账并集合并驱动 fixture 套件(2026-10-12 ledger-merge-driver)
|
|
3
|
+
// 覆盖:并集去重 / %A 行序保持 / %B 新行追加 / 空侧 / CRLF 归一 / 幂等 / CLI 原子覆写与失败码,
|
|
4
|
+
// 以及**行内哈希重算不变**(union 不破坏台账哈希链的前提,见驱动头部注释)。
|
|
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 SCRIPT = path.join(SCRIPT_DIR, 'git-merge-jsonl.mjs');
|
|
13
|
+
const { mergeJsonlLines } = await import(pathToFileURL(SCRIPT).href);
|
|
14
|
+
const { ledgerChainHash } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'policy.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 L = (...xs) => xs.filter(Boolean).join('\n') + '\n';
|
|
23
|
+
const r1 = L('{"ts":"1","doc":"a","stage":"approved"}', '{"ts":"2","doc":"a","stage":"done"}');
|
|
24
|
+
const r2 = L('{"ts":"3","doc":"b","stage":"approved"}');
|
|
25
|
+
const shared = L('{"ts":"9","doc":"c","stage":"approved"}');
|
|
26
|
+
|
|
27
|
+
// ============ 场景 1:%A 独有 → 全保留,%A 顺序不变 ============
|
|
28
|
+
{
|
|
29
|
+
const out = mergeJsonlLines(r1, '');
|
|
30
|
+
check('S1 %B 空时结果 == %A(字节级)', out === r1, JSON.stringify(out));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ============ 场景 2:%B 有 %A 没有的新行 → 追加末尾 ============
|
|
34
|
+
{
|
|
35
|
+
const out = mergeJsonlLines(r1, r2);
|
|
36
|
+
check('S2 %B 新行追加到末尾', out === r1 + r2, JSON.stringify(out));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============ 场景 3:共有行只留一份(去重) ============
|
|
40
|
+
{
|
|
41
|
+
const out = mergeJsonlLines(r1 + shared, shared + r2);
|
|
42
|
+
const lines = out.trim().split('\n');
|
|
43
|
+
const cnt = lines.filter((l) => l === shared.trim()).length;
|
|
44
|
+
check('S3 共有行只保留一份', cnt === 1, `实得 ${cnt}`);
|
|
45
|
+
check('S3 %A 既有行顺序保持在前', lines[0].includes('"ts":"1"') && lines[1].includes('"ts":"2"'), JSON.stringify(lines));
|
|
46
|
+
check('S3 行数 = %A 行数 + %B 新增行数', lines.length === 4, `实得 ${lines.length}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============ 场景 4:双空 → 空串 ============
|
|
50
|
+
{
|
|
51
|
+
check('S4 双空返回空串', mergeJsonlLines('', '') === '');
|
|
52
|
+
check('S4 null 视为空', mergeJsonlLines(null, null) === '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ============ 场景 5:CRLF 归一为 LF(与 check-ledger-invariant 的 blob 口径对齐) ============
|
|
56
|
+
{
|
|
57
|
+
const out = mergeJsonlLines('{"ts":"1"}\r\n', '{"ts":"2"}\r\n');
|
|
58
|
+
check('S5 CRLF 归一为 LF', !out.includes('\r') && out === L('{"ts":"1"}', '{"ts":"2"}'), JSON.stringify(out));
|
|
59
|
+
check('S5 空行被剔除', mergeJsonlLines('{"ts":"1"}\n\n\n', '') === L('{"ts":"1"}'));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ============ 场景 6:幂等——同输入重复合并结果不变(compact 重跑安全的前提) ============
|
|
63
|
+
{
|
|
64
|
+
const once = mergeJsonlLines(r1, r2);
|
|
65
|
+
check('S6 重复合并幂等', mergeJsonlLines(once, r2) === once && mergeJsonlLines(r1, once) === once);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============ 场景 7【关键互证】:union 保留行文本 → 行内哈希重算不变 ============
|
|
69
|
+
// 背景:台账哈希链 ledgerChainHash(row) = sha256(prevHash + JSON.stringify(row)),依赖 JSON.stringify
|
|
70
|
+
// 的键序 = 写入序。union 按整行文本去重、不解析不改写 → 行文本逐字不变 → 键序不变 → 重算不变。
|
|
71
|
+
// 这条一旦被破坏(例如将来有人改成「解析后重排键再序列化」),台账链会整片失效且极难排查。
|
|
72
|
+
{
|
|
73
|
+
const mkRow = (prevHash, fields) => {
|
|
74
|
+
const row = { ...fields, prevHash };
|
|
75
|
+
return { ...row, hash: ledgerChainHash(fields, prevHash) };
|
|
76
|
+
};
|
|
77
|
+
const chained = [
|
|
78
|
+
JSON.stringify(mkRow('', { ts: '2026-10-01T00:00:00Z', doc: 'workflow/intents/a.md', stage: 'approved', fingerprint: 'a'.repeat(64), scope: { files: ['x.md'], sha256: 's' } })),
|
|
79
|
+
JSON.stringify(mkRow('PREVHASH', { ts: '2026-10-02T00:00:00Z', doc: 'workflow/intents/a.md', stage: 'done', fingerprint: 'a'.repeat(64) })),
|
|
80
|
+
].join('\n') + '\n';
|
|
81
|
+
const mine = chained;
|
|
82
|
+
const theirs = L('{"ts":"2026-10-03T00:00:00Z","doc":"workflow/intents/b.md","stage":"approved","fingerprint":"b","extra":1}');
|
|
83
|
+
const merged = mergeJsonlLines(mine, theirs);
|
|
84
|
+
const rows = merged.trim().split('\n').map((l) => JSON.parse(l));
|
|
85
|
+
const selfOk = rows.every((r) => {
|
|
86
|
+
if (typeof r.hash !== 'string') return true; // 无链字段行(存量/注记)不参与
|
|
87
|
+
const { hash, prevHash, ...rest } = r;
|
|
88
|
+
return ledgerChainHash(rest, prevHash ?? '') === hash;
|
|
89
|
+
});
|
|
90
|
+
check('S7 合并后每行行内哈希仍可重算一致', selfOk, JSON.stringify(rows));
|
|
91
|
+
check('S7 合并后行文本与合并前逐字相同(去重不改写字段)',
|
|
92
|
+
merged.trim().split('\n')[0] === mine.trim().split('\n')[0]);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ============ 场景 8:CLI 真跑 —— 覆写 %A、exit 0、无残留 .tmp ============
|
|
96
|
+
{
|
|
97
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-merge-'));
|
|
98
|
+
const A = path.join(d, 'A'), B = path.join(d, 'B'), O = path.join(d, 'O');
|
|
99
|
+
fs.writeFileSync(A, r1, 'utf8');
|
|
100
|
+
fs.writeFileSync(B, r1 + r2, 'utf8');
|
|
101
|
+
fs.writeFileSync(O, L(), 'utf8');
|
|
102
|
+
const r = spawnSync(process.execPath, [SCRIPT, O, A, B], { encoding: 'utf8' });
|
|
103
|
+
check('S8 CLI exit 0', r.status === 0, `exit=${r.status} ${r.stderr || ''}`);
|
|
104
|
+
check('S8 CLI 结果写入 %A', fs.readFileSync(A, 'utf8') === r1 + r2, JSON.stringify(fs.readFileSync(A, 'utf8')));
|
|
105
|
+
check('S8 无残留 .tmp', !fs.existsSync(`${A}.tmp`));
|
|
106
|
+
// 缺参数 → 用法提示 + exit 1(driver 由 git 触发,参数缺失属接线错误,应响亮)
|
|
107
|
+
const bad = spawnSync(process.execPath, [SCRIPT, O], { encoding: 'utf8' });
|
|
108
|
+
check('S8 缺参数 exit 1 且出用法', bad.status === 1 && bad.stderr.includes('用法'), `exit=${bad.status}`);
|
|
109
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============ 场景 9:%A 不存在(对侧新增文件)→ 以 %B 内容落盘 ============
|
|
113
|
+
{
|
|
114
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'jsonl-merge2-'));
|
|
115
|
+
const A = path.join(d, 'A'), B = path.join(d, 'B'), O = path.join(d, 'O');
|
|
116
|
+
fs.writeFileSync(B, r2, 'utf8');
|
|
117
|
+
fs.writeFileSync(O, L(), 'utf8');
|
|
118
|
+
const r = spawnSync(process.execPath, [SCRIPT, O, A, B], { encoding: 'utf8' });
|
|
119
|
+
check('S9 %A 缺失时以 %B 落盘且 exit 0', r.status === 0 && fs.readFileSync(A, 'utf8') === r2,
|
|
120
|
+
`exit=${r.status} ${r.stderr || ''}`);
|
|
121
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log(`\n[git-merge-jsonl.test] PASS ${pass} / FAIL ${fail}`);
|
|
125
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-doc-chain.test.mjs — 台账哈希链「链域改造」fixture 套件(2026-10-12 ledger-doc-chain)
|
|
3
|
+
// 核心命题:把链域从「文件物理序」改成「按 doc 的逻辑序」,在**不削弱篡改检出**的前提下让三种
|
|
4
|
+
// 正常协作(并发 append / git 并集合并 / 分片归并)不再撞 hard 门。
|
|
5
|
+
// 覆盖面:锚缺键=旧行为 / 锚前行维持物理链 / 锚后行按 doc 链 / 跨域衔接 / 三类篡改仍被抓 /
|
|
6
|
+
// 写入侧与校验侧口径一致(双方单源函数同源)。
|
|
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
|
+
const { ledgerChainHash, ledgerDocChainActive, ledgerDocChainPrev } =
|
|
12
|
+
await import(pathToFileURL(path.join(SCRIPT_DIR, 'policy.mjs')).href);
|
|
13
|
+
|
|
14
|
+
let pass = 0, fail = 0;
|
|
15
|
+
const check = (name, ok, detail = '') => {
|
|
16
|
+
if (ok) { pass++; console.log(` ok ${name}`); }
|
|
17
|
+
else { fail++; console.error(`FAIL ${name}${detail ? ' — ' + detail : ''}`); }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const ANCHOR = '2026-10-12T07:20:00Z';
|
|
21
|
+
const OLD = '2026-10-01T00:00:00Z'; // 锚前(旧物理链口径)
|
|
22
|
+
const NEW = '2026-10-13T00:00:00Z'; // 锚后(新 doc 链口径)
|
|
23
|
+
|
|
24
|
+
// mkRow:造一条链行(prevHash 由调用方给)
|
|
25
|
+
const mkRow = (ts, doc, stage, prevHash) => {
|
|
26
|
+
const fields = { ts, doc, stage, fingerprint: 'f'.repeat(64), source: 'tty' };
|
|
27
|
+
return { ...fields, prevHash, hash: ledgerChainHash(fields, prevHash) };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---- 校验侧判据的可执行镜像(必须与 check-loop.mjs 检查 15 同判据;此处用于单测,生产判据在 check-loop)----
|
|
31
|
+
function verifyChain(rows, policy) {
|
|
32
|
+
const on = ledgerDocChainActive(policy);
|
|
33
|
+
const since = on ? policy.ledgerDocChainSince : '';
|
|
34
|
+
let prevHash = '', brokenAt = 0, brokenCount = 0;
|
|
35
|
+
const byDoc = new Map();
|
|
36
|
+
rows.forEach((row, idx) => {
|
|
37
|
+
if (!row || typeof row.hash !== 'string') { prevHash = ''; return; }
|
|
38
|
+
const { hash, prevHash: p, ...rest } = row;
|
|
39
|
+
const inDoc = on && typeof row.ts === 'string' && row.ts >= since;
|
|
40
|
+
const expected = inDoc ? (byDoc.get(row.doc) ?? '') : prevHash;
|
|
41
|
+
const ok = (p ?? '') === expected && ledgerChainHash(rest, p ?? '') === hash;
|
|
42
|
+
if (!ok) { if (!brokenAt) brokenAt = idx + 1; brokenCount++; }
|
|
43
|
+
prevHash = hash;
|
|
44
|
+
if (typeof row.doc === 'string') byDoc.set(row.doc, hash);
|
|
45
|
+
});
|
|
46
|
+
return { brokenAt, brokenCount };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ================= 单元:锚开关 =================
|
|
50
|
+
{
|
|
51
|
+
check('U1 缺键(v1-v7)→ 新口径不生效', ledgerDocChainActive({}) === false);
|
|
52
|
+
check('U2 非法锚值 → 不生效(fail-safe 回旧口径)', ledgerDocChainActive({ ledgerDocChainSince: '2026-10-12' }) === false);
|
|
53
|
+
check('U3 合法 ISO 时刻锚 → 生效', ledgerDocChainActive({ ledgerDocChainSince: ANCHOR }) === true);
|
|
54
|
+
check('U4 v8 锚值合法', ledgerDocChainActive({ ledgerDocChainSince: ANCHOR }) === true);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ================= 单元:前驱取法 =================
|
|
58
|
+
{
|
|
59
|
+
const rA1 = mkRow(OLD, 'a.md', 'approved', '');
|
|
60
|
+
const rA2 = mkRow(OLD, 'b.md', 'approved', rA1.hash);
|
|
61
|
+
const rA3 = mkRow(OLD, 'a.md', 'done', rA2.hash);
|
|
62
|
+
const rows = [rA1, rA2, rA3];
|
|
63
|
+
check('U5 doc 链前驱取同行末行(非物理末行)', ledgerDocChainPrev(rows, 'a.md') === rA3.hash);
|
|
64
|
+
check('U6 另一 doc 的前驱独立', ledgerDocChainPrev(rows, 'b.md') === rA2.hash);
|
|
65
|
+
check('U7 该 doc 无链行 → 链段重启返回空', ledgerDocChainPrev(rows, 'zz.md') === '');
|
|
66
|
+
check('U8 空表 → 返回空', ledgerDocChainPrev([], 'a.md') === '' && ledgerDocChainPrev(null, 'a.md') === '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ================= 关键:缺锚 = 旧行为(存量零变化)=====================
|
|
70
|
+
{
|
|
71
|
+
const rA1 = mkRow(OLD, 'a.md', 'approved', '');
|
|
72
|
+
const rA2 = mkRow(OLD, 'b.md', 'approved', rA1.hash);
|
|
73
|
+
const rA3 = mkRow(OLD, 'a.md', 'done', rA2.hash);
|
|
74
|
+
// 物理链下这三行是自洽的(旧口径判定)
|
|
75
|
+
check('K1 缺锚:物理链自洽行不报断(= 存量零新增告警)',
|
|
76
|
+
verifyChain([rA1, rA2, rA3], {}).brokenCount === 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ================= 关键:锚前行维持物理链 =================
|
|
80
|
+
{
|
|
81
|
+
const pol = { ledgerDocChainSince: ANCHOR };
|
|
82
|
+
const rA1 = mkRow(OLD, 'a.md', 'approved', '');
|
|
83
|
+
const rA2 = mkRow(OLD, 'b.md', 'approved', rA1.hash); // 物理接续(doc 链口径下这行会断)
|
|
84
|
+
const rA3 = mkRow(OLD, 'a.md', 'done', rA2.hash);
|
|
85
|
+
check('K2 锚前:物理链接续的三行不误报(存量不受改造影响)',
|
|
86
|
+
verifyChain([rA1, rA2, rA3], pol).brokenCount === 0, JSON.stringify(verifyChain([rA1, rA2, rA3], pol)));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ================= 关键:锚后 doc 链 + 跨域衔接 =================
|
|
90
|
+
{
|
|
91
|
+
const pol = { ledgerDocChainSince: ANCHOR };
|
|
92
|
+
// 锚前:物理链两行(a 起点、b 接物理)
|
|
93
|
+
const rA1 = mkRow(OLD, 'a.md', 'approved', '');
|
|
94
|
+
const rB1 = mkRow(OLD, 'b.md', 'approved', rA1.hash);
|
|
95
|
+
// 锚后:doc 链接续(a 的新行接 rA1,b 的新行接 rB1——**不是**物理末行 rA1)
|
|
96
|
+
const rA2 = mkRow(NEW, 'a.md', 'done', rA1.hash);
|
|
97
|
+
const rB2 = mkRow(NEW, 'b.md', 'done', rB1.hash);
|
|
98
|
+
check('K3 锚后:doc 链接续自洽(跨域衔接成功)',
|
|
99
|
+
verifyChain([rA1, rB1, rA2, rB2], pol).brokenCount === 0, JSON.stringify(verifyChain([rA1, rB1, rA2, rB2], pol)));
|
|
100
|
+
check('K4 锚后 b 的前驱是 b 自己上一行(不是物理末行)',
|
|
101
|
+
rB2.prevHash === rB1.hash && rB2.prevHash !== rA2.hash);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ================= 关键:三种正常协作不再误报 =================
|
|
105
|
+
// 注意夹具语义:doc 链下某 doc 的**首个链行 prevHash 恒为 ''**(该 doc 链段重启),第二行才接前一行。
|
|
106
|
+
// 用物理前驱填夹具会造出「本就不该自洽」的样本——那是夹具错,不是判据错(K5/K6/K7 首版即栽在这里)。
|
|
107
|
+
{
|
|
108
|
+
const pol = { ledgerDocChainSince: ANCHOR };
|
|
109
|
+
const rA1 = mkRow(NEW, 'a.md', 'approved', '');
|
|
110
|
+
const rA2 = mkRow(NEW, 'a.md', 'done', rA1.hash);
|
|
111
|
+
const rB1 = mkRow(NEW, 'b.md', 'approved', '');
|
|
112
|
+
const rB2 = mkRow(NEW, 'b.md', 'done', rB1.hash);
|
|
113
|
+
// 协作①:两分支各自追加后并集合并 —— 行序可能被打乱(b 行被挪到 a 行之间)
|
|
114
|
+
check('K5 协作①并集合并(跨 doc 行序与物理链不同)→ doc 链仍自洽',
|
|
115
|
+
verifyChain([rA1, rB1, rA2, rB2], pol).brokenCount === 0, JSON.stringify(verifyChain([rA1, rB1, rA2, rB2], pol)));
|
|
116
|
+
// 协作②:分片归并把别的 doc 的行插进本 doc 链的中段
|
|
117
|
+
const rC1 = mkRow(NEW, 'c.md', 'approved', '');
|
|
118
|
+
check('K6 协作②分片归并插行 → doc 链仍自洽',
|
|
119
|
+
verifyChain([rA1, rC1, rA2, rB1, rB2], pol).brokenCount === 0, JSON.stringify(verifyChain([rA1, rC1, rA2, rB1, rB2], pol)));
|
|
120
|
+
// 基线:供下方篡改用例复用
|
|
121
|
+
const base = [rA1, rB1, rA2, rB2];
|
|
122
|
+
check('K7 基线:四行自洽', verifyChain(base, pol).brokenCount === 0, JSON.stringify(verifyChain(base, pol)));
|
|
123
|
+
|
|
124
|
+
// 篡改①:就地改中间行内容(不动其 hash)→ 重算必失配
|
|
125
|
+
const tampered1 = base.map((r) => (r === rB1 ? { ...r, stage: 'cancelled' } : r));
|
|
126
|
+
check('K8 篡改①就地改行 → 抓到', verifyChain(tampered1, pol).brokenCount > 0);
|
|
127
|
+
|
|
128
|
+
// 篡改②:删该 doc 的**中间行**(a 的首行)→ 其后继行 prevHash 失配。
|
|
129
|
+
// 诚实边界:**删某 doc 的末行在两种链域下都抓不到**(无后继可比),这是链式防伪的固有上限,
|
|
130
|
+
// 物理链同样如此;兜底仍是 check-ledger-invariant 的 append-only 历史全扫。
|
|
131
|
+
const deleted = base.filter((r) => r !== rA1);
|
|
132
|
+
check('K9 篡改②删中间行 → 抓到', verifyChain(deleted, pol).brokenCount > 0);
|
|
133
|
+
|
|
134
|
+
// 篡改③:插一行(自洽重算但前驱为空)→ 后继行前驱失配
|
|
135
|
+
const fFields = { ts: NEW, doc: 'a.md', stage: 'reverted', fingerprint: 'x'.repeat(64), source: 'tty' };
|
|
136
|
+
const forged = { ...fFields, prevHash: '', hash: ledgerChainHash(fFields, '') };
|
|
137
|
+
const inserted = [rA1, forged, rA2, rB1, rB2];
|
|
138
|
+
check('K10 篡改③插行(自洽重算但前驱为空)→ 抓到', verifyChain(inserted, pol).brokenCount > 0);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ================= 关键:写入侧与校验侧口径一致 =================
|
|
142
|
+
{
|
|
143
|
+
// 写入侧取前驱(ledgerDocChainPrev)与校验侧期望(byDoc 末值)必须是同一个值——否则新行一落账即断
|
|
144
|
+
const rA1 = mkRow(OLD, 'a.md', 'approved', '');
|
|
145
|
+
const rB1 = mkRow(OLD, 'b.md', 'approved', rA1.hash);
|
|
146
|
+
const rows = [rA1, rB1];
|
|
147
|
+
const writeSidePrev = ledgerDocChainPrev(rows, 'a.md');
|
|
148
|
+
// 校验侧:走一遍同样两行后,byDoc.get('a.md') 应等于写入侧取到的值
|
|
149
|
+
const byDoc = new Map();
|
|
150
|
+
for (const row of rows) byDoc.set(row.doc, row.hash);
|
|
151
|
+
check('K11 写入侧取前驱 == 校验侧期望(两侧单源一致)',
|
|
152
|
+
writeSidePrev === rA1.hash && byDoc.get('a.md') === writeSidePrev);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ================= 边界:无 ts 的链行 fail-safe 回旧口径 =================
|
|
156
|
+
{
|
|
157
|
+
const pol = { ledgerDocChainSince: ANCHOR };
|
|
158
|
+
const noTs = { doc: 'a.md', stage: 'approved', fingerprint: 'z'.repeat(64), prevHash: '', hash: '' };
|
|
159
|
+
const fields = { doc: 'a.md', stage: 'approved', fingerprint: 'z'.repeat(64) };
|
|
160
|
+
noTs.hash = ledgerChainHash(fields, '');
|
|
161
|
+
check('K12 无 ts 的链行按物理链判定(不因缺 ts 误入新域)',
|
|
162
|
+
verifyChain([noTs], pol).brokenCount === 0);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
console.log(`\n[ledger-doc-chain.test] PASS ${pass} / FAIL ${fail}`);
|
|
166
|
+
process.exit(fail ? 1 : 0);
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// ledger-loader.mjs — 确认台账读取视图单源(2026-10-12 ledger-loader-p1)
|
|
3
|
+
// 为什么有这个文件:确认台账 `.agents/confirmations.jsonl` 此前有 6 处生产代码各自
|
|
4
|
+
// `readFileSync` + `JSON.parse` 复刻一遍(stage-gates / check-loop ×4 / check-metric-claims /
|
|
5
|
+
// gen-workflow-dashboard / gen-workflow-metrics),口径与容错各写各的。后续引入分片台账
|
|
6
|
+
// (多人 / 多 worktree 并发,见 README 台账并发口径)时,逐处加分支必然漏改——漏一处即
|
|
7
|
+
// 「门禁看不见分片行」=半盲,且不可见比全红更危险。本文件把**读**收成单源。
|
|
8
|
+
//
|
|
9
|
+
// 本步(Step 1)范围:纯重构,**行为逐字不变**——分片目录不存在时,视图与逐处直读等价。
|
|
10
|
+
// 故意**不做去重**:去重会改 gen-workflow-metrics 的漏斗计数口径(当前计全部行),
|
|
11
|
+
// 属口径变更,须随分片功能同批生效,不得混入本步。
|
|
12
|
+
//
|
|
13
|
+
// 两种视图(消费方需求不同,勿合并):
|
|
14
|
+
// readLedgerLines —— 原始文本行,给「按行计数 / 文本特征匹配」用(检查 16 确认负担)。
|
|
15
|
+
// readLedgerRows —— 解析后对象数组,坏行跳过,顺序 = 物理读序(主台账先、分片按文件名序)。
|
|
16
|
+
//
|
|
17
|
+
// **不归本文件管的两处(刻意保留直读,改动须先改这里)**:
|
|
18
|
+
// ① check-loop.mjs 检查 15 哈希链校验——需**物理行号对齐**与**物理顺序**,
|
|
19
|
+
// 任何合并/去重视图都会毁掉它(物理行号 ↔ 数组下标换算依赖逐行原序)。
|
|
20
|
+
// ② check-ledger-invariant.mjs——校验的是**物理文件本身**的 append-only 前缀不变量,
|
|
21
|
+
// 它审的是盘面字节,不是台账语义。
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
export const LEDGER_REL = '.agents/confirmations.jsonl';
|
|
26
|
+
export const SHARD_DIR_REL = '.agents/confirmations.d';
|
|
27
|
+
|
|
28
|
+
// ledgerSources:台账来源清单,顺序即读序——主台账(存量基线)先,分片按文件名序后。
|
|
29
|
+
// 分片视为较新(后读覆盖),与「append-only 台账末次生效」同向。
|
|
30
|
+
// 缺失即跳过(装户无台账 / 未启用分片 = 零影响,向上兼容)。
|
|
31
|
+
export function ledgerSources(root = process.cwd()) {
|
|
32
|
+
const out = [];
|
|
33
|
+
const mainAbs = path.join(root, ...LEDGER_REL.split('/'));
|
|
34
|
+
if (fs.existsSync(mainAbs)) out.push({ rel: LEDGER_REL, abs: mainAbs });
|
|
35
|
+
|
|
36
|
+
const shardAbs = path.join(root, ...SHARD_DIR_REL.split('/'));
|
|
37
|
+
if (fs.existsSync(shardAbs) && fs.statSync(shardAbs).isDirectory()) {
|
|
38
|
+
let files = [];
|
|
39
|
+
try {
|
|
40
|
+
files = fs.readdirSync(shardAbs).filter((f) => f.endsWith('.jsonl')).sort();
|
|
41
|
+
} catch (err) {
|
|
42
|
+
// 目录读不出来仍要把已收集的来源交出去——本文件是 hard 门(检查 15)的数据源,
|
|
43
|
+
// IO 抖动不得伪装成「确认未对账」(fail-closed 会误伤真台账)
|
|
44
|
+
console.error(`[ledger-loader] 分片目录读取失败: ${SHARD_DIR_REL} (${err.message})——台账视图可能缺该来源`);
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
out.push({ rel: `${SHARD_DIR_REL}/${f}`, abs: path.join(shardAbs, f) });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// readLedgerLines:全部来源的非空原始行(保留行内原文,顺序 = 物理读序)。
|
|
55
|
+
export function readLedgerLines(root = process.cwd()) {
|
|
56
|
+
const lines = [];
|
|
57
|
+
for (const src of ledgerSources(root)) {
|
|
58
|
+
let text = '';
|
|
59
|
+
try {
|
|
60
|
+
text = fs.readFileSync(src.abs, 'utf8');
|
|
61
|
+
} catch (err) {
|
|
62
|
+
// 出账后继续:已读到的来源照常返回,缺的那个来源显式告警(不静默丢)
|
|
63
|
+
console.error(`[ledger-loader] 读取失败: ${src.rel} (${err.message})——台账视图可能缺该来源`);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
for (const line of text.split(/\r?\n/)) {
|
|
67
|
+
if (line.trim()) lines.push(line);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// readLedgerRows:全部来源解析后的行(坏行跳过,与检查 15 / 19 同容错口径)。
|
|
74
|
+
export function readLedgerRows(root = process.cwd()) {
|
|
75
|
+
const out = [];
|
|
76
|
+
for (const line of readLedgerLines(root)) {
|
|
77
|
+
try {
|
|
78
|
+
const e = JSON.parse(line);
|
|
79
|
+
if (e && typeof e === 'object') out.push(e);
|
|
80
|
+
} catch { /* 坏行跳过 */ }
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// readLedgerEntries:**带来源与行号**的视图,读序 = 主台账在前、分片按文件名序在后。
|
|
86
|
+
// 返回 [{ rel, no, row }],坏行 row=null(保留占位,否则物理行号会漂移)。
|
|
87
|
+
// 消费方:check-loop 检查 15(链断裂要报「哪个文件的第几行」,跨来源后「物理行号」单一定位已不够,
|
|
88
|
+
// 必须带 rel 才能定位)。此前检查 15 只读主台账故无需本视图——引入分片后改走这里。
|
|
89
|
+
export function readLedgerEntries(root = process.cwd()) {
|
|
90
|
+
const out = [];
|
|
91
|
+
for (const src of ledgerSources(root)) {
|
|
92
|
+
let text = '';
|
|
93
|
+
try {
|
|
94
|
+
text = fs.readFileSync(src.abs, 'utf8');
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error(`[ledger-loader] 读取失败: ${src.rel} (${err.message})——台账视图可能缺该来源`);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const lines = text.split(/\r?\n/);
|
|
100
|
+
for (let i = 0; i < lines.length; i++) {
|
|
101
|
+
if (!lines[i].trim()) continue;
|
|
102
|
+
let row = null;
|
|
103
|
+
try { row = JSON.parse(lines[i]); } catch { /* 坏行占位 */ }
|
|
104
|
+
out.push({ rel: src.rel, no: i + 1, row: (row && typeof row === 'object') ? row : null });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|