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
|
@@ -126,8 +126,10 @@ import { spawnSync } from 'node:child_process';
|
|
|
126
126
|
import { createHash } from 'node:crypto';
|
|
127
127
|
import { loadEnums } from './workflow-enums.mjs';
|
|
128
128
|
import { runCheck16 } from './check-metric-claims.mjs';
|
|
129
|
-
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, scopeMatches } from './policy.mjs';
|
|
129
|
+
import { auditEnabled, loadKitPolicy, hasFreshVerifyLine, ledgerChainHash, ledgerDocChainActive, scopeMatches } from './policy.mjs';
|
|
130
130
|
import { laneOfEntry } from './stage-gates.mjs';
|
|
131
|
+
import { readLedgerRows, readLedgerEntries, LEDGER_REL } from './ledger-loader.mjs';
|
|
132
|
+
import { prunedHashes as prunedHashesOf } from './ledger-prune.mjs';
|
|
131
133
|
// MARK_RE:incident 留痕形态的单源(第七轮复核 N3——此前 check-loop 内联复制一份同口径字面量,
|
|
132
134
|
// 两处靠注释与人工同步;改为复用 stage-gates.mjs 的导出,从结构上消除漂移可能)
|
|
133
135
|
import { MARK_RE, approvedTraceHit } from './stage-gates.mjs';
|
|
@@ -805,16 +807,10 @@ export function runCheckLoop(opts = {}) {
|
|
|
805
807
|
const doneTsOf = (() => {
|
|
806
808
|
const map = new Map();
|
|
807
809
|
try {
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
try {
|
|
813
|
-
const e = JSON.parse(line);
|
|
814
|
-
// append-only 取末次 done 跳转(同一 doc 多次 done 取最后一条 ts)
|
|
815
|
-
if (e && e.doc && ['done', 'closed'].includes(e.stage)) map.set(e.doc, e.ts || '');
|
|
816
|
-
} catch { /* 坏行跳过(同检查 15 口径) */ }
|
|
817
|
-
}
|
|
810
|
+
// 读法收敛 ledger-loader(2026-10-12 ledger-loader-p1):末次 done 跳转取自物理读序,
|
|
811
|
+
// loader 保持读序返回故「取末次」语义与逐处直读一致(分片按文件名序排在主台账之后=较新)
|
|
812
|
+
for (const e of readLedgerRows(ROOT)) {
|
|
813
|
+
if (e && e.doc && ['done', 'closed'].includes(e.stage)) map.set(e.doc, e.ts || '');
|
|
818
814
|
}
|
|
819
815
|
} catch { /* 台账不可读 → 无锚 → 全走全局布尔(fail-open,不误报 hard) */ }
|
|
820
816
|
return map;
|
|
@@ -1024,15 +1020,8 @@ export function runCheckLoop(opts = {}) {
|
|
|
1024
1020
|
// 台账 approved 文档集合:单次读取(坏行容忍跳过,与检查 15 同口径);缺失视为空集(git 判据仍守门)
|
|
1025
1021
|
const ledgerApproved = new Set();
|
|
1026
1022
|
try {
|
|
1027
|
-
const
|
|
1028
|
-
|
|
1029
|
-
for (const line of fs.readFileSync(lp, 'utf8').split(/\r?\n/)) {
|
|
1030
|
-
if (!line.trim()) continue;
|
|
1031
|
-
try {
|
|
1032
|
-
const e = JSON.parse(line);
|
|
1033
|
-
if (e && typeof e.doc === 'string' && e.stage === 'approved') ledgerApproved.add(e.doc);
|
|
1034
|
-
} catch { /* 坏行容忍跳过 */ }
|
|
1035
|
-
}
|
|
1023
|
+
for (const e of readLedgerRows(ROOT)) {
|
|
1024
|
+
if (typeof e.doc === 'string' && e.stage === 'approved') ledgerApproved.add(e.doc);
|
|
1036
1025
|
}
|
|
1037
1026
|
} catch { /* 台账不可读视为无 approved 行 */ }
|
|
1038
1027
|
for (const sub of ['specs', 'plans']) {
|
|
@@ -1071,18 +1060,26 @@ export function runCheckLoop(opts = {}) {
|
|
|
1071
1060
|
// 台账坏行容忍跳过(审计件,jsonl 追加式);frontmatter 存 16 位、台账存 64 位,按前 16 位配对。
|
|
1072
1061
|
{
|
|
1073
1062
|
const EFFECTIVE = kitPolicy.confirmDocsEffective;
|
|
1074
|
-
|
|
1063
|
+
// 取数走 ledger-loader 的**带来源行号视图**(2026-10-12 ledger-shard-write):主台账 + 分片
|
|
1064
|
+
// `.agents/confirmations.d/*.jsonl`。分片里的行同样受链校验——否则 `--shard` 写入就等于给台账开了
|
|
1065
|
+
// 一个防伪盲区(门禁看得见分片行、却不验它们的链)。
|
|
1066
|
+
// **此处曾刻意不走 loader**(单源化时留的例外,因当时要物理行号对齐);引入分片后该理由已不成立
|
|
1067
|
+
// ——单一行号跨来源无法定位,故改用带 rel 的定位。第二处例外仍是 check-ledger-invariant.mjs
|
|
1068
|
+
// (它审的是**盘面字节**的 append-only 前缀不变量,不是台账语义,见该文件头部)。
|
|
1075
1069
|
const ledger = [];
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1070
|
+
const ledgerRefs = []; // 与 ledger 同下标:{rel, no}——断裂报文的定位来源
|
|
1071
|
+
for (const e of readLedgerEntries(ROOT)) {
|
|
1072
|
+
if (e.row === null) continue; // 坏行跳过(容错口径同前;它占物理行号但不入 ledger 下标)
|
|
1073
|
+
ledger.push(e.row);
|
|
1074
|
+
ledgerRefs.push({ rel: e.rel, no: e.no });
|
|
1082
1075
|
}
|
|
1083
|
-
// 台账哈希链校验(2026-10-09 engine-quality-round2 D
|
|
1084
|
-
// policy.ledgerChainHash
|
|
1085
|
-
//
|
|
1076
|
+
// 台账哈希链校验(2026-10-09 engine-quality-round2 D;2026-10-12 ledger-doc-chain 改链域;
|
|
1077
|
+
// 写入侧=confirm-doc appendLedger,哈希单源 policy.ledgerChainHash——判定零复刻)。
|
|
1078
|
+
// **链域已从「文件物理序」改为「按 doc 的逻辑序」**(锚后生效,理由与不追溯口径见 POLICIES v8):
|
|
1079
|
+
// 原判据 ① prevHash = 前一带 hash 行(物理)的 hash;新判据 ① = **同行 doc** 最后一个带 hash 行的
|
|
1080
|
+
// hash。两者都要求 ② hash 可重算一致。无 hash 行仍按「链段重启」处理。
|
|
1081
|
+
// 并发 append / git 并集合并 / 分片归并这三种**正常协作**都会打断物理链——门禁若继续据此 hard 拦,
|
|
1082
|
+
// 必然被 --no-verify 绕过,治理失效;而 doc 链对就地改行 / 删行 / 插行三类篡改的检出能力不变。
|
|
1086
1083
|
// **坏行口径(复核 P2-1 勘误,此前注释误称「与写入侧一致」)**:坏行被本解析整体剔除、不重置游标
|
|
1087
1084
|
// ——坏行后写入的链行(写入侧读末行遇坏行 prevHash='')验链必失配出账。这是**预期 fail-loud**
|
|
1088
1085
|
// (坏行本身即台账完整性事故,应人工查验),非口径一致场景。
|
|
@@ -1093,19 +1090,36 @@ export function runCheckLoop(opts = {}) {
|
|
|
1093
1090
|
let prevHash = '';
|
|
1094
1091
|
let brokenAt = 0;
|
|
1095
1092
|
let brokenCount = 0;
|
|
1093
|
+
// 链域分界(2026-10-12 ledger-doc-chain):doc 链锚存在且合法时,**按行分域**验链——
|
|
1094
|
+
// 锚前行(ts < 锚)维持物理链口径(存量零变化);锚后行(ts ≥ 锚)按「同行 doc 的最后一个
|
|
1095
|
+
// 带 hash 行」接续,并同样要求 hash 可重算一致。锚缺键(v1-v7)→ 全表走物理链(= 旧行为)。
|
|
1096
|
+
// byDoc 对**两种域的行都更新**:锚后行需要接到锚前该 doc 的最后一行,跨域才能连续。
|
|
1097
|
+
const docChainOn = ledgerDocChainActive(kitPolicy);
|
|
1098
|
+
const docChainSince = docChainOn ? kitPolicy.ledgerDocChainSince : '';
|
|
1099
|
+
const byDoc = new Map();
|
|
1100
|
+
const prunedHashSet = prunedHashesOf(ROOT); // 特许修剪凭据(无 manifest 时为空集,零 IO 成本)
|
|
1096
1101
|
ledger.forEach((row, idx) => {
|
|
1097
1102
|
if (!row || typeof row.hash !== 'string') { prevHash = ''; return; } // 存量行/无链字段 → 链段重启
|
|
1098
1103
|
const { hash, prevHash: p, ...rest } = row;
|
|
1099
|
-
const
|
|
1104
|
+
const inDocChain = docChainOn && typeof row.ts === 'string' && row.ts >= docChainSince;
|
|
1105
|
+
const expected = inDocChain ? (byDoc.get(row.doc) ?? '') : prevHash;
|
|
1106
|
+
// 特许修剪链段重启(2026-10-12 ledger-prune-manifest):删掉某 doc 的链行后,其后继行仍带被删行
|
|
1107
|
+
// 的 prevHash,而该行已不在表内 → 必然失配。本行的 prevHash 若出现在 manifest 声明的被删哈希里,
|
|
1108
|
+
// 即视为**有凭据的合法重启点**而非篡改(凭据 = .agents/ledger-prune-manifest.json,入库可对质)。
|
|
1109
|
+
const prunedRestart = (p ?? '') !== expected && prunedHashSet.has(p ?? '');
|
|
1110
|
+
const ok = ((p ?? '') === expected || prunedRestart) && ledgerChainHash(rest, p ?? '') === hash;
|
|
1100
1111
|
if (!ok) {
|
|
1101
|
-
if (!brokenAt) brokenAt = idx + 1;
|
|
1112
|
+
if (!brokenAt) brokenAt = idx + 1; // 1-based 下标,供 ledgerRefs 定位
|
|
1102
1113
|
brokenCount++;
|
|
1103
1114
|
}
|
|
1104
1115
|
prevHash = hash;
|
|
1116
|
+
if (typeof row.doc === 'string') byDoc.set(row.doc, hash);
|
|
1105
1117
|
});
|
|
1106
1118
|
if (brokenAt) {
|
|
1107
|
-
|
|
1108
|
-
|
|
1119
|
+
// 定位用 ledgerRefs(来源 + 行号):跨来源后单一行号无法定位;坏行占位不计入 ledger 下标
|
|
1120
|
+
const at = ledgerRefs[brokenAt - 1] || { rel: LEDGER_REL, no: brokenAt };
|
|
1121
|
+
const domain = docChainOn ? '锚前物理链 / 锚后 doc 链' : '物理链';
|
|
1122
|
+
blockers.push(`- [台账链断裂] 自 ${at.rel}:${at.no} 起哈希链校验失败(${domain},共 ${brokenCount} 行不一致——append-only 台账出现就地改/删痕迹;prevHash 接续或内容重算不符。整文件重写不可机器防,对质走台账自身 git 历史)`);
|
|
1109
1123
|
}
|
|
1110
1124
|
}
|
|
1111
1125
|
// scope 子项(2026-10-09 审查 A1 干净版):受管台账行须带**机器派生**的改动面 scope{files,sha256}。
|
|
@@ -1335,13 +1349,7 @@ export function runCheckLoop(opts = {}) {
|
|
|
1335
1349
|
if (typeof since === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(since)) {
|
|
1336
1350
|
const ledgerRows = [];
|
|
1337
1351
|
{
|
|
1338
|
-
const
|
|
1339
|
-
if (fs.existsSync(lp)) {
|
|
1340
|
-
for (const line of linesOf(lp) || []) {
|
|
1341
|
-
if (!line.trim()) continue;
|
|
1342
|
-
try { ledgerRows.push(JSON.parse(line)); } catch { /* 坏行跳过(同检查 15 口径) */ }
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1352
|
+
for (const e of readLedgerRows(ROOT)) ledgerRows.push(e);
|
|
1345
1353
|
}
|
|
1346
1354
|
const hasRow = (rel) => ledgerRows.some((e) => e && e.doc === rel && (e.stage === 'approved' || e.stage === 'done'));
|
|
1347
1355
|
const docConfirmed = (rel, abs) => {
|
|
@@ -28,6 +28,10 @@ const check = (desc, cond, detail = '') => {
|
|
|
28
28
|
else { fail++; console.log(`FAIL ${desc}${detail ? '\n ' + detail : ''}`); }
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
+
// 夹具闭包工具(2026-10-12 fixture-closure-rootfix):本文件此前 5 处手写依赖枚举,2026-10-12 那批
|
|
32
|
+
// 台账改动漏拷 ledger-loader.mjs 就假绿过(看板漂移门恒绿)。改为按入口自动求传递闭包——新增依赖自动跟随。
|
|
33
|
+
const { copyScriptClosure, copyAgentsFiles } = await import('./fixture-scripts.mjs');
|
|
34
|
+
|
|
31
35
|
// mkfix:空 fixture 根(四目录 + 枚举单源内联 8 键——fixture 自包含,测消费逻辑而非当前词表值)
|
|
32
36
|
const ENUMS_FIXTURE = [
|
|
33
37
|
'doc.status.all=draft approved done superseded cancelled',
|
|
@@ -383,9 +387,8 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
383
387
|
{
|
|
384
388
|
const T = mkfix();
|
|
385
389
|
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
390
|
+
copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs']);
|
|
391
|
+
copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
|
|
389
392
|
w(T, 'workflow/intents/2026-09-22-mod-ok.md', INTENT('mod ok', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline', '\n## 验收标准(可测试)\n- [ ] 用例通过\n'));
|
|
390
393
|
w(T, 'workflow/plans/2026-09-22-mod-ok.md', PLAN('mod ok', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
391
394
|
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
@@ -412,9 +415,8 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
412
415
|
{
|
|
413
416
|
const T = mkfix();
|
|
414
417
|
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
fs.copyFileSync(path.join(SCRIPT_DIR, '..', 'workflow-modules.txt'), path.join(T, '.agents', 'workflow-modules.txt'));
|
|
418
|
+
copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs']);
|
|
419
|
+
copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
|
|
418
420
|
w(T, 'workflow/intents/2026-09-22-drift.md', INTENT('drift', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
419
421
|
w(T, 'workflow/plans/2026-09-22-drift.md', PLAN('drift', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
420
422
|
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
@@ -430,10 +432,10 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
430
432
|
const mkDashFix = () => {
|
|
431
433
|
const T = mkfix();
|
|
432
434
|
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
// 看板/索引生成器的台账读取依赖(ledger-loader 等)由闭包自动带入——此前手写枚举漏拷过,
|
|
436
|
+
// 表现是「import 失败 → 看板静默不生成 → 漂移门恒绿」,即假绿比红危险(2026-10-12 已修,见 fixture-scripts.mjs)
|
|
437
|
+
copyScriptClosure(path.join(T, '.agents', 'scripts'), ['gen-workflow-index.mjs', 'gen-workflow-dashboard.mjs']);
|
|
438
|
+
copyAgentsFiles(path.join(T, '.agents'), ['workflow-modules.txt']);
|
|
437
439
|
w(T, 'workflow/intents/2026-09-22-dash.md', INTENT('dash', '状态: approved\n级别: L1\n日期: 2026-09-22\n模块: pipeline'));
|
|
438
440
|
w(T, 'workflow/plans/2026-09-22-dash.md', PLAN('dash', '状态: approved\n级别: L1\n模块: pipeline'));
|
|
439
441
|
spawnSync(process.execPath, ['.agents/scripts/gen-workflow-index.mjs'], { cwd: T });
|
|
@@ -460,7 +462,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
460
462
|
const T = mkfix();
|
|
461
463
|
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
462
464
|
fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
|
|
463
|
-
|
|
465
|
+
copyAgentsFiles(path.join(T, '.agents'), ['rule-budgets.txt']);
|
|
464
466
|
// 超限样本自校准:从拷入的预算表读 AGENTS.md 上限(预算可上调,硬编码样本会随上调失效——2026-10-06-v114-backflow-batch)
|
|
465
467
|
const agLimit = Number(fs.readFileSync(path.join(T, '.agents', 'rule-budgets.txt'), 'utf8').split(/\r?\n/).find((l) => l.startsWith('AGENTS.md ')).trim().split(' ').pop());
|
|
466
468
|
w(T, 'AGENTS.md', '# AGENTS 小体积 fixture\n');
|
|
@@ -488,7 +490,7 @@ const mkConfirmedDoc = (root, rel, fmBody) => {
|
|
|
488
490
|
const T = mkfix();
|
|
489
491
|
fs.mkdirSync(path.join(T, '.agents', 'scripts'), { recursive: true });
|
|
490
492
|
fs.copyFileSync(path.join(SCRIPT_DIR, 'rule-budget.sh'), path.join(T, '.agents', 'scripts', 'rule-budget.sh'));
|
|
491
|
-
|
|
493
|
+
copyAgentsFiles(path.join(T, '.agents'), ['rule-budgets.txt']);
|
|
492
494
|
const agLimit = Number(fs.readFileSync(path.join(T, '.agents', 'rule-budgets.txt'), 'utf8').split(/\r?\n/).find((l) => l.startsWith('AGENTS.md ')).trim().split(' ').pop());
|
|
493
495
|
gitInit(T);
|
|
494
496
|
w(T, 'AGENTS.md', 'x'.repeat(agLimit + 512));
|
|
@@ -2919,8 +2921,11 @@ w(T, 'workflow/intents/2026-09-12-deep.md', INTENT('deep', '状态: draft\n级
|
|
|
2919
2921
|
writeLedger(T, [tampered, row2]);
|
|
2920
2922
|
let r = run(T);
|
|
2921
2923
|
let out = outOf(r);
|
|
2924
|
+
// 定位措辞随 2026-10-12 ledger-shard-write 由「第 N 行(物理)」改为 `rel:行号`——
|
|
2925
|
+
// 台账视图含主台账 + 分片后,单一行号跨来源无法定位,必须带来源。首断行号仍须精确指向第 1 行。
|
|
2922
2926
|
check('场景 21a 链断裂:中间行篡改 → hard 台账链断裂(指首断行号)',
|
|
2923
|
-
r.status !== 0 && out.includes('台账链断裂') &&
|
|
2927
|
+
r.status !== 0 && out.includes('台账链断裂') && /\.agents\/confirmations\.jsonl:1\b/.test(out),
|
|
2928
|
+
`exit=${r.status}\n${out.slice(-400)}`);
|
|
2924
2929
|
// 反证还原:篡改行恢复原 quote → 链复通(断言非恒绿)
|
|
2925
2930
|
writeLedger(T, [row1, row2]);
|
|
2926
2931
|
r = run(T);
|
|
@@ -17,14 +17,16 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
|
+
import { readLedgerLines } from './ledger-loader.mjs';
|
|
20
21
|
|
|
21
22
|
export function runCheck16(ctx) {
|
|
22
23
|
const { ROOT, ENUMS, docFiles, fmGet, inSet, isTracked, linesOf, readdirOrNull, warnings } = ctx;
|
|
23
24
|
const mcFile = path.join(ROOT, '.agents', 'metric-claims.txt');
|
|
24
25
|
if (fs.existsSync(mcFile)) {
|
|
25
26
|
const ledgerLines = () => {
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
// 读法收敛 ledger-loader(2026-10-12 ledger-loader-p1):本指标按**原始文本行**计数
|
|
28
|
+
// (total / 含 "batch" 字段的行数),故用 lines 视图而非 rows 视图。
|
|
29
|
+
const ls = readLedgerLines(ROOT);
|
|
28
30
|
return { total: ls.length, withBatch: ls.filter((l) => /"batch"/.test(l)).length };
|
|
29
31
|
};
|
|
30
32
|
// 内置取数器:全部本地确定性、零网络。**装户零配置即用**(不依赖任何装户文件)。
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// compact-ledgers.mjs — 台账分片归并器(2026-10-12 ledger-shard-write 配套)
|
|
3
|
+
// 用途:里程碑节点或发布前,把 `.agents/confirmations.d/*.jsonl` 的分片确认行并回主台账
|
|
4
|
+
// `.agents/confirmations.jsonl` 并清理分片。`--shard` 写入的分片若长期不归并,会让台账面无限碎片化,
|
|
5
|
+
// 且分片的历史不受主台账的前缀不变量约束(见 check-ledger-invariant 已知边界)——本脚本就是收敛动作。
|
|
6
|
+
//
|
|
7
|
+
// 保持 append-only 前缀不变量:主台账既有行**严格保序不动**,分片新行只追加到末尾——这正是
|
|
8
|
+
// check-ledger-invariant 的「旧 blob 是新 blob 的逐字节前缀」所要求的形态。绝不做重排/去重改写。
|
|
9
|
+
//
|
|
10
|
+
// 安全设计(三条,缺一条都可能损坏主台账):
|
|
11
|
+
// ① 默认 dry-run:只报告「将并入 N 行」,不动盘。必须显式 execute 才写。
|
|
12
|
+
// ② 原子覆写:同目录临时文件 + rename。中途崩溃 = 主台账原样、分片未删,可安全重跑。
|
|
13
|
+
// ③ 幂等:并集按整行文本去重(复用 git-merge-jsonl 的同款判据),重复执行结果不变。
|
|
14
|
+
// 任一分片读取失败 → 立即中止,主台账与全部分片保持原样。
|
|
15
|
+
//
|
|
16
|
+
// 用法:node .agents/scripts/compact-ledgers.mjs [--root <仓库根>] [--execute]
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { mergeJsonlLines } from './git-merge-jsonl.mjs';
|
|
20
|
+
import { LEDGER_REL, SHARD_DIR_REL } from './ledger-loader.mjs';
|
|
21
|
+
|
|
22
|
+
const linesOf = (t) => (t || '').replace(/\r\n/g, '\n').split('\n').filter((l) => l.trim().length > 0);
|
|
23
|
+
|
|
24
|
+
// compactLedgers(root, { execute }):纯函数形态,供测试直测。
|
|
25
|
+
// 返回 { ok, skipped?, reason?, added, before, after, shards, shardLines, error? }
|
|
26
|
+
export function compactLedgers(root = process.cwd(), { execute = false } = {}) {
|
|
27
|
+
const mainLedger = path.join(root, ...LEDGER_REL.split('/'));
|
|
28
|
+
const shardDir = path.join(root, ...SHARD_DIR_REL.split('/'));
|
|
29
|
+
|
|
30
|
+
if (!fs.existsSync(shardDir) || !fs.statSync(shardDir).isDirectory()) {
|
|
31
|
+
return { ok: true, skipped: true, reason: '无分片目录', added: 0, before: 0, after: 0, shards: 0, shardLines: 0 };
|
|
32
|
+
}
|
|
33
|
+
const shardFiles = fs.readdirSync(shardDir).filter((f) => f.endsWith('.jsonl')).sort();
|
|
34
|
+
if (shardFiles.length === 0) {
|
|
35
|
+
return { ok: true, skipped: true, reason: '分片目录无 .jsonl', added: 0, before: 0, after: 0, shards: 0, shardLines: 0 };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mainBefore = fs.existsSync(mainLedger) ? fs.readFileSync(mainLedger, 'utf8') : '';
|
|
39
|
+
const mainBeforeLines = linesOf(mainBefore);
|
|
40
|
+
|
|
41
|
+
let shardCombined = '';
|
|
42
|
+
let shardLineCount = 0;
|
|
43
|
+
for (const f of shardFiles) {
|
|
44
|
+
try {
|
|
45
|
+
const t = fs.readFileSync(path.join(shardDir, f), 'utf8');
|
|
46
|
+
shardLineCount += linesOf(t).length;
|
|
47
|
+
shardCombined += '\n' + t;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
// fail-soft 中止:主台账与全部分片保持原样(修好重跑即可——并集按行去重,重跑安全)
|
|
50
|
+
return { ok: false, error: `读取分片失败: ${f} (${err.message})`, added: 0, before: mainBeforeLines.length, after: mainBeforeLines.length, shards: shardFiles.length, shardLines: shardLineCount };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const merged = mergeJsonlLines(mainBefore, shardCombined);
|
|
55
|
+
const added = linesOf(merged).filter((l) => !mainBeforeLines.includes(l)).length;
|
|
56
|
+
const after = linesOf(merged).length;
|
|
57
|
+
|
|
58
|
+
if (!execute) {
|
|
59
|
+
return { ok: true, skipped: true, reason: 'dry-run', added, before: mainBeforeLines.length, after, shards: shardFiles.length, shardLines: shardLineCount };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const tmpMain = `${mainLedger}.tmp`;
|
|
63
|
+
try {
|
|
64
|
+
fs.writeFileSync(tmpMain, merged, 'utf8');
|
|
65
|
+
fs.renameSync(tmpMain, mainLedger);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
try { fs.unlinkSync(tmpMain); } catch { /* tmp 可能未创建或删除失败 */ }
|
|
68
|
+
return { ok: false, error: `主台账写入失败: ${err.message}`, added: 0, before: mainBeforeLines.length, after: mainBeforeLines.length, shards: shardFiles.length, shardLines: shardLineCount };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const f of shardFiles) {
|
|
72
|
+
try { fs.unlinkSync(path.join(shardDir, f)); } catch { /* 残留分片不影响正确性,下轮再清 */ }
|
|
73
|
+
}
|
|
74
|
+
try { if (fs.readdirSync(shardDir).length === 0) fs.rmdirSync(shardDir); } catch { /* 忽略 */ }
|
|
75
|
+
|
|
76
|
+
return { ok: true, added, before: mainBeforeLines.length, after, shards: shardFiles.length, shardLines: shardLineCount };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const isMain = process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('compact-ledgers.mjs');
|
|
80
|
+
if (isMain) {
|
|
81
|
+
const argv = process.argv.slice(2);
|
|
82
|
+
const i = argv.indexOf('--root');
|
|
83
|
+
const root = path.resolve(i >= 0 && argv[i + 1] ? argv[i + 1] : process.cwd());
|
|
84
|
+
const execute = argv.includes('--execute');
|
|
85
|
+
const r = compactLedgers(root, { execute });
|
|
86
|
+
|
|
87
|
+
if (!r.ok) { console.error(`[compact-ledgers] ${r.error}`); console.error('[compact-ledgers] 已中止:主台账与分片保持原样,修好重跑即可。'); process.exit(1); }
|
|
88
|
+
if (r.skipped) { console.log(`[compact-ledgers] ${r.reason},无需归并。`); process.exit(0); }
|
|
89
|
+
if (!execute) {
|
|
90
|
+
console.log(`[compact-ledgers] dry-run:将并入 ${r.added} 行(分片共 ${r.shardLines} 行 / ${r.shards} 个文件,其中 ${r.shardLines - r.added} 行主台账已有),主台账 ${r.before} → ${r.after} 行`);
|
|
91
|
+
console.log('[compact-ledgers] 加 --execute 执行(未加则不动盘)。');
|
|
92
|
+
process.exit(0);
|
|
93
|
+
}
|
|
94
|
+
console.log(`[compact-ledgers] ✅ 归并完成:并入 ${r.added} 行,主台账 ${r.before} → ${r.after} 行;${r.shards} 个分片已清理。`);
|
|
95
|
+
console.log('[compact-ledgers] 提醒:归并产生的台账变更请随下一笔提交入库——历史前缀不变量按提交判,不入库等于没生效。');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// compact-ledgers.test.mjs — 台账分片写入与 compact 归并 fixture 套件(2026-10-12 ledger-shard-write)
|
|
3
|
+
// 覆盖:分片写入定位 + 跨来源链衔接 / compact 默认 dry-run 不动盘 / 幂等 / 前缀不变式(既有行保序)/
|
|
4
|
+
// 读取失败中止 / 分片历史扫描与 compact 删除豁免 / 行未并入时仍拦丢行。
|
|
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 { compactLedgers } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'compact-ledgers.mjs')).href);
|
|
13
|
+
const { appendLedger } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'confirm-doc.mjs')).href);
|
|
14
|
+
const { ledgerChainHash } = await import(pathToFileURL(path.join(SCRIPT_DIR, 'policy.mjs')).href);
|
|
15
|
+
// 夹具闭包(2026-10-12 fixture-closure-rootfix):整闭包自动跟随,不再手写依赖枚举(漏拷 = 假绿)
|
|
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 DOC_A = 'workflow/intents/2026-10-01-a.md';
|
|
25
|
+
const DOC_B = 'workflow/intents/2026-10-01-b.md';
|
|
26
|
+
const baseEntry = (doc, stage = 'approved') => ({
|
|
27
|
+
ts: new Date().toISOString(), doc, stage, fingerprint: 'a'.repeat(64), source: 'tty',
|
|
28
|
+
});
|
|
29
|
+
// --shard 依赖 doc 链口径(policyVersion ≥ 8):分片与主台账是不同文件,只有 doc 链跨来源接续。
|
|
30
|
+
// 装 kit.json 的 policyVersion=8 来启用,与真实装户升级后的形态一致。
|
|
31
|
+
const enableDocChain = (root) => {
|
|
32
|
+
fs.mkdirSync(path.join(root, '.agents'), { recursive: true });
|
|
33
|
+
fs.writeFileSync(path.join(root, '.agents', 'kit.json'), JSON.stringify({ policyVersion: 8 }), 'utf8');
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============ 场景 1:--shard 写入定位到分片,且跨来源链接得上 ============
|
|
37
|
+
{
|
|
38
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-write-'));
|
|
39
|
+
enableDocChain(root);
|
|
40
|
+
// 先用主台账写一行(模拟该 doc 的历史行在主台账)
|
|
41
|
+
appendLedger(root, baseEntry(DOC_A));
|
|
42
|
+
const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
43
|
+
check('S1 默认写主台账', fs.existsSync(mainPath));
|
|
44
|
+
|
|
45
|
+
// 再对同一 doc 用 --shard 写一行:前驱必须接主台账那行,而不是从 '' 重启
|
|
46
|
+
appendLedger(root, baseEntry(DOC_A, 'done'), { shard: true });
|
|
47
|
+
const shardPath = path.join(root, '.agents', 'confirmations.d', '2026-10-01-a.jsonl');
|
|
48
|
+
check('S1 --shard 写入分片文件(按 doc 基名命名)', fs.existsSync(shardPath));
|
|
49
|
+
|
|
50
|
+
const mainRows = fs.readFileSync(mainPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
51
|
+
const shardRows = fs.readFileSync(shardPath, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
|
|
52
|
+
check('S1 分片行的 prevHash 接主台账同 doc 行(跨来源衔接,链不断)',
|
|
53
|
+
shardRows[0].prevHash === mainRows[0].hash, `${shardRows[0].prevHash} vs ${mainRows[0].hash}`);
|
|
54
|
+
// 行内重算仍一致(doc 链前提)
|
|
55
|
+
const { hash, prevHash, ...rest } = shardRows[0];
|
|
56
|
+
check('S1 分片行行内哈希可重算', ledgerChainHash(rest, prevHash) === hash);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ============ 场景 2:compact 默认 dry-run 不动盘 ============
|
|
60
|
+
{
|
|
61
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-dry-'));
|
|
62
|
+
appendLedger(root, baseEntry(DOC_A));
|
|
63
|
+
appendLedger(root, baseEntry(DOC_B), { shard: true });
|
|
64
|
+
const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
65
|
+
const before = fs.readFileSync(mainPath, 'utf8');
|
|
66
|
+
const shardPath = path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl');
|
|
67
|
+
|
|
68
|
+
const r = compactLedgers(root); // 默认 execute:false
|
|
69
|
+
check('S2 dry-run 标记 skipped 且给出并入行数', r.ok && r.skipped && r.reason === 'dry-run' && r.added === 1, JSON.stringify(r));
|
|
70
|
+
check('S2 dry-run 主台账字节不变', fs.readFileSync(mainPath, 'utf8') === before);
|
|
71
|
+
check('S2 dry-run 分片仍在', fs.existsSync(shardPath));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ============ 场景 3:--execute 归并 + 前缀不变式 ============
|
|
75
|
+
{
|
|
76
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-exec-'));
|
|
77
|
+
appendLedger(root, baseEntry(DOC_A));
|
|
78
|
+
const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
79
|
+
const before = fs.readFileSync(mainPath, 'utf8');
|
|
80
|
+
appendLedger(root, baseEntry(DOC_B), { shard: true });
|
|
81
|
+
|
|
82
|
+
const r = compactLedgers(root, { execute: true });
|
|
83
|
+
const after = fs.readFileSync(mainPath, 'utf8');
|
|
84
|
+
check('S3 execute 归并成功并计入 1 行', r.ok && !r.skipped && r.added === 1, JSON.stringify(r));
|
|
85
|
+
check('S3 既有行严格保序在前(append-only 前缀不变式)', after.startsWith(before), JSON.stringify(after.slice(0, 40)));
|
|
86
|
+
check('S3 分片已清理', !fs.existsSync(path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl')));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ============ 场景 4:幂等(重跑不重复追加)============
|
|
90
|
+
{
|
|
91
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-idem-'));
|
|
92
|
+
enableDocChain(root);
|
|
93
|
+
appendLedger(root, baseEntry(DOC_A));
|
|
94
|
+
appendLedger(root, baseEntry(DOC_B), { shard: true });
|
|
95
|
+
compactLedgers(root, { execute: true });
|
|
96
|
+
const mainPath = path.join(root, '.agents', 'confirmations.jsonl');
|
|
97
|
+
const after1 = fs.readFileSync(mainPath, 'utf8');
|
|
98
|
+
const mergedLine = after1.trim().split('\n').pop(); // 归并时实际追加的那一行(含 prevHash/hash)
|
|
99
|
+
|
|
100
|
+
// 把**同一行原文**再放进分片重跑(模拟重复归并同批内容)——行文本逐字相同才会被去重
|
|
101
|
+
fs.mkdirSync(path.join(root, '.agents', 'confirmations.d'), { recursive: true });
|
|
102
|
+
fs.writeFileSync(path.join(root, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'), mergedLine + '\n', 'utf8');
|
|
103
|
+
const r = compactLedgers(root, { execute: true });
|
|
104
|
+
const after2 = fs.readFileSync(mainPath, 'utf8');
|
|
105
|
+
check('S4 重复归并不重复追加(并集按整行去重)', r.ok && r.added === 0 && after1 === after2, `added=${r.added}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============ 场景 5:无分片 / 空分片 → 跳过且不报错 ============
|
|
109
|
+
{
|
|
110
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-none-'));
|
|
111
|
+
const r = compactLedgers(root);
|
|
112
|
+
check('S5 无分片目录 → 跳过且 ok', r.ok && r.skipped);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ============ 场景 6:分片历史扫描 + compact 删除豁免(真 git)============
|
|
116
|
+
{
|
|
117
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'shard-hist-'));
|
|
118
|
+
const git = (...args) => spawnSync('git', args, { cwd: d, encoding: 'utf8' });
|
|
119
|
+
git('init', '-q', '.'); git('config', 'user.email', 't@t.io'); git('config', 'user.name', 't');
|
|
120
|
+
git('config', 'core.autocrlf', 'false');
|
|
121
|
+
fs.mkdirSync(path.join(d, '.agents', 'scripts'), { recursive: true });
|
|
122
|
+
copyScriptClosure(path.join(d, '.agents', 'scripts'), ['check-ledger-invariant.mjs']);
|
|
123
|
+
const mkDoc = (rel) => {
|
|
124
|
+
const p = path.join(d, ...rel.split('/'));
|
|
125
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
126
|
+
fs.writeFileSync(p, '---\n状态: approved\n---\n# doc\n', 'utf8');
|
|
127
|
+
};
|
|
128
|
+
const rowObj = (doc) => JSON.stringify({ ts: '2026-10-01T00:00:00Z', doc, stage: 'approved', fingerprint: 'a'.repeat(64), source: 'tty' });
|
|
129
|
+
const runGate = () => spawnSync(process.execPath, ['.agents/scripts/check-ledger-invariant.mjs'], { cwd: d, encoding: 'utf8' });
|
|
130
|
+
|
|
131
|
+
mkDoc(DOC_A); mkDoc(DOC_B);
|
|
132
|
+
// 基线:主台账一行 + 分片一行(分片持有 B 的行)
|
|
133
|
+
fs.mkdirSync(path.join(d, '.agents', 'confirmations.d'), { recursive: true });
|
|
134
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowObj(DOC_A) + '\n', 'utf8');
|
|
135
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'), rowObj(DOC_B) + '\n', 'utf8');
|
|
136
|
+
git('add', '-A'); git('commit', '-qm', 'base with shard');
|
|
137
|
+
let r = runGate();
|
|
138
|
+
check('S6 分片入库且未删 → 历史扫描放行', r.status === 0, `exit=${r.status} ${r.stderr}`);
|
|
139
|
+
|
|
140
|
+
// compact 归并后删分片:删除豁免应认领
|
|
141
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.jsonl'), rowObj(DOC_A) + '\n' + rowObj(DOC_B) + '\n', 'utf8');
|
|
142
|
+
fs.rmSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-b.jsonl'));
|
|
143
|
+
git('add', '-A'); git('commit', '-qm', 'compact merged');
|
|
144
|
+
r = runGate();
|
|
145
|
+
check('S6 compact 归并删分片 → 删除豁免认领,exit 0', r.status === 0, `exit=${r.status} ${r.stderr}`);
|
|
146
|
+
check('S6 豁免记录点名 compact 归并形态', /compact 归并形态/.test(r.stderr), r.stderr.slice(0, 200));
|
|
147
|
+
|
|
148
|
+
// 未归并就删分片(丢行)→ 仍 hard 拦。用**从未进过主台账**的独立 doc C,
|
|
149
|
+
// 否则它的行已在主台账里(早先 compact 并过),删除豁免会正确地放行——那正是豁免该有的行为。
|
|
150
|
+
const DOC_C = 'workflow/intents/2026-10-01-c.md';
|
|
151
|
+
mkDoc(DOC_C);
|
|
152
|
+
fs.writeFileSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-c.jsonl'), rowObj(DOC_C) + '\n', 'utf8');
|
|
153
|
+
git('add', '-A'); git('commit', '-qm', 'add shard c');
|
|
154
|
+
fs.rmSync(path.join(d, '.agents', 'confirmations.d', '2026-10-01-c.jsonl'));
|
|
155
|
+
git('add', '-A'); git('commit', '-qm', 'drop shard c without merge');
|
|
156
|
+
r = runGate();
|
|
157
|
+
check('S6 未归并即删分片(丢行)→ exit 1 hard 拦', r.status === 1, `exit=${r.status}`);
|
|
158
|
+
check('S6 报文点名「分片删除丢行」', /分片删除丢行/.test(r.stderr), r.stderr.slice(0, 300));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n[compact-ledgers.test] PASS ${pass} / FAIL ${fail}`);
|
|
162
|
+
process.exit(fail ? 1 : 0);
|