@tangle-network/agent-eval 0.133.0 → 0.133.2
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/dist/agent-profile-cell-OhuTee9n.js +335 -0
- package/dist/agent-profile-cell-OhuTee9n.js.map +1 -0
- package/dist/{analyze-runs-BjPn_fOS.js → analyze-runs-B-afTpCv.js} +6 -19
- package/dist/analyze-runs-B-afTpCv.js.map +1 -0
- package/dist/{analyze-runs-DZr7JW-m.d.ts → analyze-runs-BmX-h_yn.d.ts} +3 -3
- package/dist/{analyze-runs-DZr7JW-m.d.ts.map → analyze-runs-BmX-h_yn.d.ts.map} +1 -1
- package/dist/{baseline-HsBvw_dk.js → baseline-DcX5hQDv.js} +2 -70
- package/dist/{baseline-HsBvw_dk.js.map → baseline-DcX5hQDv.js.map} +1 -1
- package/dist/baseline-hG3K85h4.d.ts.map +1 -1
- package/dist/benchmarks/index.d.ts +1 -1
- package/dist/benchmarks/index.js +1 -1
- package/dist/{benchmarks-DncrVrSr.js → benchmarks-CJr1H1_a.js} +3 -3
- package/dist/{benchmarks-DncrVrSr.js.map → benchmarks-CJr1H1_a.js.map} +1 -1
- package/dist/builder-eval/index.js +1 -1
- package/dist/campaign/index.d.ts +3 -3
- package/dist/campaign/index.js +2 -2
- package/dist/{campaign-CyQMCCEU.js → campaign-BJjn1rhw.js} +19 -13
- package/dist/{campaign-CyQMCCEU.js.map → campaign-BJjn1rhw.js.map} +1 -1
- package/dist/{client-D4F9hdzR.d.ts → client-COvaLoQG.d.ts} +2 -2
- package/dist/{client-D4F9hdzR.d.ts.map → client-COvaLoQG.d.ts.map} +1 -1
- package/dist/contract/index.d.ts +3 -3
- package/dist/contract/index.js +5 -5
- package/dist/control.js +1 -1
- package/dist/{eval-campaign-CYkUME2T.js → eval-campaign-DXhpZghy.js} +4 -5
- package/dist/{eval-campaign-CYkUME2T.js.map → eval-campaign-DXhpZghy.js.map} +1 -1
- package/dist/hosted/index.d.ts +1 -1
- package/dist/index-3cdlURSk.d.ts.map +1 -1
- package/dist/{index-CiP2DRCj.d.ts → index-BREtv3ZZ.d.ts} +3 -3
- package/dist/{index-CiP2DRCj.d.ts.map → index-BREtv3ZZ.d.ts.map} +1 -1
- package/dist/{index-B_eUhU9K.d.ts → index-C7Wue8R6.d.ts} +28 -5
- package/dist/{index-B_eUhU9K.d.ts.map → index-C7Wue8R6.d.ts.map} +1 -1
- package/dist/{index-BAvgST_9.d.ts → index-nhIYz9hn.d.ts} +67 -9
- package/dist/index-nhIYz9hn.d.ts.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.js +25 -18
- package/dist/index.js.map +1 -1
- package/dist/ledger-core/index.d.ts +2 -2
- package/dist/ledger-core/index.js +2 -2
- package/dist/ledger-core-CPZfcrC2.js +620 -0
- package/dist/ledger-core-CPZfcrC2.js.map +1 -0
- package/dist/meta-eval/index.js +2 -2
- package/dist/{mint-D5_87M5L.js → mint-BvkwcYZU.js} +2 -2
- package/dist/{mint-D5_87M5L.js.map → mint-BvkwcYZU.js.map} +1 -1
- package/dist/openapi.json +1 -1
- package/dist/{opencode-sqlite-BGrHeDu3.js → opencode-sqlite-8r6WUfHc.js} +2 -3
- package/dist/opencode-sqlite-8r6WUfHc.js.map +1 -0
- package/dist/{paired-arms-D9D0wXj2.js → paired-arms-6XItKzd1.js} +2 -2
- package/dist/{paired-arms-D9D0wXj2.js.map → paired-arms-6XItKzd1.js.map} +1 -1
- package/dist/pipelines/index.js +2 -2
- package/dist/profile-cell.js +1 -242
- package/dist/{propose-review-control-Bqb7daEJ.js → propose-review-control-SQ-n9-We.js} +2 -2
- package/dist/{propose-review-control-Bqb7daEJ.js.map → propose-review-control-SQ-n9-We.js.map} +1 -1
- package/dist/{release-report-DfmKSIEE.d.ts → release-report-CjHWa8Ia.d.ts} +2 -2
- package/dist/{release-report-DfmKSIEE.d.ts.map → release-report-CjHWa8Ia.d.ts.map} +1 -1
- package/dist/{release-report-oWt9f2k-.js → release-report-wuilQkvK.js} +3 -3
- package/dist/{release-report-oWt9f2k-.js.map → release-report-wuilQkvK.js.map} +1 -1
- package/dist/{replay-BMR4TEYY.js → replay-CJfGLdx4.js} +3 -3
- package/dist/{replay-BMR4TEYY.js.map → replay-CJfGLdx4.js.map} +1 -1
- package/dist/reporting.d.ts +2 -2
- package/dist/reporting.js +4 -4
- package/dist/{researcher-DMimgHtN.d.ts → researcher-CbSKhK8z.d.ts} +2 -2
- package/dist/{researcher-DMimgHtN.d.ts.map → researcher-CbSKhK8z.d.ts.map} +1 -1
- package/dist/{reward-hacking-BEvjdUtD.js → reward-hacking-Dl2UBzej.js} +3 -3
- package/dist/{reward-hacking-BEvjdUtD.js.map → reward-hacking-Dl2UBzej.js.map} +1 -1
- package/dist/rl.d.ts +1 -1
- package/dist/rl.js +6 -6
- package/dist/rollout/index.js +3 -3
- package/dist/{rollout-VEo41J0N.js → rollout-CreDz__7.js} +3 -3
- package/dist/{rollout-VEo41J0N.js.map → rollout-CreDz__7.js.map} +1 -1
- package/dist/{rubric-predictive-validity-B3xmbmS1.js → rubric-predictive-validity-QG7ydk0s.js} +2 -2
- package/dist/{rubric-predictive-validity-B3xmbmS1.js.map → rubric-predictive-validity-QG7ydk0s.js.map} +1 -1
- package/dist/{run-record-CN8Zd21B.js → run-record-BIwU2wdV.js} +2 -2
- package/dist/{run-record-CN8Zd21B.js.map → run-record-BIwU2wdV.js.map} +1 -1
- package/dist/{skillopt-optimization-method-DgN7U9iR.js → skillopt-optimization-method-CF6a327Q.js} +8 -4
- package/dist/{skillopt-optimization-method-DgN7U9iR.js.map → skillopt-optimization-method-CF6a327Q.js.map} +1 -1
- package/dist/{skillopt-optimization-method-CAASpcS3.d.ts → skillopt-optimization-method-wHF5xsUv.d.ts} +2 -2
- package/dist/{skillopt-optimization-method-CAASpcS3.d.ts.map → skillopt-optimization-method-wHF5xsUv.d.ts.map} +1 -1
- package/dist/{statistics-CnnxdpOg.js → statistics-DWM_AyLe.js} +90 -76
- package/dist/statistics-DWM_AyLe.js.map +1 -0
- package/dist/statistics-DbvkkDPa.d.ts.map +1 -1
- package/dist/{summary-report-DnUcjVpV.d.ts → summary-report-CFnQgNfg.d.ts} +2 -2
- package/dist/{summary-report-DnUcjVpV.d.ts.map → summary-report-CFnQgNfg.d.ts.map} +1 -1
- package/dist/{summary-report-BNs5nmXI.js → summary-report-Ci17nIdU.js} +4 -4
- package/dist/{summary-report-BNs5nmXI.js.map → summary-report-Ci17nIdU.js.map} +1 -1
- package/dist/supervisor-run/index.js +1 -1
- package/dist/{supervisor-run-_lnTLM3z.js → supervisor-run-B7lUGoyZ.js} +2 -2
- package/dist/{supervisor-run-_lnTLM3z.js.map → supervisor-run-B7lUGoyZ.js.map} +1 -1
- package/dist/traces.js +1 -1
- package/package.json +1 -1
- package/dist/analyze-runs-BjPn_fOS.js.map +0 -1
- package/dist/index-BAvgST_9.d.ts.map +0 -1
- package/dist/ledger-core-eqaI3PCD.js +0 -388
- package/dist/ledger-core-eqaI3PCD.js.map +0 -1
- package/dist/opencode-sqlite-BGrHeDu3.js.map +0 -1
- package/dist/pre-registration-DakwTRXk.js +0 -96
- package/dist/pre-registration-DakwTRXk.js.map +0 -1
- package/dist/profile-cell.js.map +0 -1
- package/dist/statistics-CnnxdpOg.js.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { C as AtomicFileLockUnavailable, S as
|
|
2
|
-
export { type AtomicFileLock, type AtomicFileLockAcquisition, AtomicFileLockError, type AtomicFileLockOptions, type AtomicFileLockOwner, type AtomicFileLockUnavailable, FileLedgerJournal, type FileLockResult, type LedgerAppendResult, type LedgerChainFields, type LedgerEntryOf, type LedgerEventBase, type LedgerFileContext, type LedgerHash, type LedgerJournalCodec, type LedgerLineContext, type LedgerProjector, type LedgerReplayResult, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, tryAcquireAtomicFileLock, tryWithLedgerFileLock, withLedgerFileLock };
|
|
1
|
+
import { A as AtomicFileLockOptions, C as LedgerCanonicalizationError, D as AtomicFileLock, E as hashCanonical, M as AtomicFileLockUnavailable, N as probeAtomicFileLock, O as AtomicFileLockAcquisition, P as tryAcquireAtomicFileLock, S as LEDGER_HASH_PATTERN, T as canonicalString, _ as LedgerFileContext, a as LedgerChainFields, b as withLedgerFileLock, c as LedgerJournalCodec, d as LedgerReplayResult, f as LedgerAnchoredEntry, g as FileLockResult, h as verifyEntriesAgainstTrustedHead, i as LedgerAppendResult, j as AtomicFileLockOwner, k as AtomicFileLockError, l as LedgerLineContext, m as trustedHeadPathFor, n as FileLedgerJournalOptions, o as LedgerEntryOf, p as LedgerTrustedHead, r as LedgerAppendOptions, s as LedgerEventBase, t as FileLedgerJournal, u as LedgerProjector, v as appendLedgerLine, w as LedgerHash, x as writeLedgerFileAtomically, y as tryWithLedgerFileLock } from "../index-nhIYz9hn.js";
|
|
2
|
+
export { type AtomicFileLock, type AtomicFileLockAcquisition, AtomicFileLockError, type AtomicFileLockOptions, type AtomicFileLockOwner, type AtomicFileLockUnavailable, FileLedgerJournal, type FileLedgerJournalOptions, type FileLockResult, LEDGER_HASH_PATTERN, type LedgerAnchoredEntry, type LedgerAppendOptions, type LedgerAppendResult, LedgerCanonicalizationError, type LedgerChainFields, type LedgerEntryOf, type LedgerEventBase, type LedgerFileContext, type LedgerHash, type LedgerJournalCodec, type LedgerLineContext, type LedgerProjector, type LedgerReplayResult, type LedgerTrustedHead, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as tryWithLedgerFileLock, c as
|
|
2
|
-
export { AtomicFileLockError, FileLedgerJournal, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, tryAcquireAtomicFileLock, tryWithLedgerFileLock, withLedgerFileLock };
|
|
1
|
+
import { a as tryWithLedgerFileLock, c as LEDGER_HASH_PATTERN, d as hashCanonical, f as AtomicFileLockError, i as appendLedgerLine, l as LedgerCanonicalizationError, m as tryAcquireAtomicFileLock, n as trustedHeadPathFor, o as withLedgerFileLock, p as probeAtomicFileLock, r as verifyEntriesAgainstTrustedHead, s as writeLedgerFileAtomically, t as FileLedgerJournal, u as canonicalString } from "../ledger-core-CPZfcrC2.js";
|
|
2
|
+
export { AtomicFileLockError, FileLedgerJournal, LEDGER_HASH_PATTERN, LedgerCanonicalizationError, appendLedgerLine, canonicalString, hashCanonical, probeAtomicFileLock, trustedHeadPathFor, tryAcquireAtomicFileLock, tryWithLedgerFileLock, verifyEntriesAgainstTrustedHead, withLedgerFileLock, writeLedgerFileAtomically };
|
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import { s as ValidationError } from "./errors-8YnH8WlF.js";
|
|
2
|
+
import { t as Mutex } from "./concurrency-MUjT7VjM.js";
|
|
3
|
+
import { closeSync, constants, existsSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { hostname } from "node:os";
|
|
7
|
+
import { canonicalCandidateBytes, canonicalCandidateJson, sha256Bytes } from "@tangle-network/agent-interface";
|
|
8
|
+
//#region src/ledger-core/atomic-file-lock.ts
|
|
9
|
+
/** Crash-safe filesystem lock shared by campaign persistence and run exclusion. */
|
|
10
|
+
var AtomicFileLockError = class extends Error {
|
|
11
|
+
name = "AtomicFileLockError";
|
|
12
|
+
};
|
|
13
|
+
/** Report whether another lock path prevents acquisition without modifying it. */
|
|
14
|
+
function probeAtomicFileLock(options) {
|
|
15
|
+
if (existsSync(recoveryPath(options.lockPath))) return {
|
|
16
|
+
acquired: false,
|
|
17
|
+
reason: "recovery"
|
|
18
|
+
};
|
|
19
|
+
const state = ownerState(options.lockPath);
|
|
20
|
+
if (state.state === "held") return {
|
|
21
|
+
acquired: false,
|
|
22
|
+
reason: "held",
|
|
23
|
+
holder: state.owner
|
|
24
|
+
};
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Try to acquire a complete, uniquely-owned lock inode.
|
|
29
|
+
*
|
|
30
|
+
* A hard link publishes fully-written owner metadata atomically. Stale-owner
|
|
31
|
+
* removal is serialized so one reclaimer cannot delete a new owner's lock.
|
|
32
|
+
*/
|
|
33
|
+
function tryAcquireAtomicFileLock(options) {
|
|
34
|
+
const pid = options.pid ?? process.pid;
|
|
35
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) throw new AtomicFileLockError("atomic file lock pid must be a positive integer");
|
|
36
|
+
const owner = {
|
|
37
|
+
pid,
|
|
38
|
+
host: hostname(),
|
|
39
|
+
nonce: randomUUID()
|
|
40
|
+
};
|
|
41
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
42
|
+
if (existsSync(recoveryPath(options.lockPath))) return {
|
|
43
|
+
acquired: false,
|
|
44
|
+
reason: "recovery"
|
|
45
|
+
};
|
|
46
|
+
if (tryLinkOwner(options.lockPath, owner, `acquire.${attempt}`)) return acquired(options.lockPath, owner);
|
|
47
|
+
const holder = ownerState(options.lockPath);
|
|
48
|
+
if (holder.state === "missing") continue;
|
|
49
|
+
if (holder.state === "held") return {
|
|
50
|
+
acquired: false,
|
|
51
|
+
reason: "held",
|
|
52
|
+
holder: holder.owner
|
|
53
|
+
};
|
|
54
|
+
const reclaimPath = recoveryPath(options.lockPath);
|
|
55
|
+
if (!tryLinkOwner(reclaimPath, owner, `reclaim.${attempt}`)) return {
|
|
56
|
+
acquired: false,
|
|
57
|
+
reason: "recovery"
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const current = ownerState(options.lockPath);
|
|
61
|
+
if (current.state === "held") return {
|
|
62
|
+
acquired: false,
|
|
63
|
+
reason: "held",
|
|
64
|
+
holder: current.owner
|
|
65
|
+
};
|
|
66
|
+
if (current.state === "stale") {
|
|
67
|
+
const tombstone = `${options.lockPath}.stale.${owner.nonce}.${attempt}`;
|
|
68
|
+
try {
|
|
69
|
+
renameSync(options.lockPath, tombstone);
|
|
70
|
+
unlinkSync(tombstone);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (!isMissing(error)) throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (tryLinkOwner(options.lockPath, owner, `recovered.${attempt}`)) return acquired(options.lockPath, owner);
|
|
76
|
+
} finally {
|
|
77
|
+
releaseOwnedPath(reclaimPath, owner);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new AtomicFileLockError(`could not acquire atomic file lock ${options.lockPath}`);
|
|
81
|
+
}
|
|
82
|
+
function acquired(lockPath, owner) {
|
|
83
|
+
return {
|
|
84
|
+
acquired: true,
|
|
85
|
+
lock: {
|
|
86
|
+
owner,
|
|
87
|
+
release: () => releaseOwnedPath(lockPath, owner)
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function tryLinkOwner(lockPath, owner, suffix) {
|
|
92
|
+
const ownerPath = `${lockPath}.${owner.pid}.${owner.nonce}.${suffix}.owner`;
|
|
93
|
+
const descriptor = openSync(ownerPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 384);
|
|
94
|
+
try {
|
|
95
|
+
writeFileSync(descriptor, `${canonicalOwner(owner)}\n`, "utf8");
|
|
96
|
+
fsyncSync(descriptor);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
closeSync(descriptor);
|
|
99
|
+
unlinkIfExists(ownerPath);
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
closeSync(descriptor);
|
|
103
|
+
try {
|
|
104
|
+
linkSync(ownerPath, lockPath);
|
|
105
|
+
return true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (!isAlreadyExists(error)) throw error;
|
|
108
|
+
return false;
|
|
109
|
+
} finally {
|
|
110
|
+
unlinkIfExists(ownerPath);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function ownerState(lockPath) {
|
|
114
|
+
const owner = readOwner(lockPath);
|
|
115
|
+
if (!owner) return { state: "missing" };
|
|
116
|
+
if (owner.host !== hostname()) return {
|
|
117
|
+
state: "held",
|
|
118
|
+
owner
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
process.kill(owner.pid, 0);
|
|
122
|
+
return {
|
|
123
|
+
state: "held",
|
|
124
|
+
owner
|
|
125
|
+
};
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (isNoSuchProcess(error)) return {
|
|
128
|
+
state: "stale",
|
|
129
|
+
owner
|
|
130
|
+
};
|
|
131
|
+
return {
|
|
132
|
+
state: "held",
|
|
133
|
+
owner
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function readOwner(lockPath) {
|
|
138
|
+
let contents;
|
|
139
|
+
try {
|
|
140
|
+
contents = readFileSync(lockPath, "utf8").trim();
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isMissing(error)) return void 0;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
let value;
|
|
146
|
+
try {
|
|
147
|
+
value = JSON.parse(contents);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
throw invalidOwner(lockPath, error);
|
|
150
|
+
}
|
|
151
|
+
if (!isOwner(value)) throw invalidOwner(lockPath);
|
|
152
|
+
return value;
|
|
153
|
+
}
|
|
154
|
+
function isOwner(value) {
|
|
155
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
156
|
+
const record = value;
|
|
157
|
+
return Object.keys(record).length === 3 && Number.isSafeInteger(record.pid) && Number(record.pid) > 0 && typeof record.host === "string" && record.host.trim().length > 0 && typeof record.nonce === "string" && record.nonce.trim().length > 0;
|
|
158
|
+
}
|
|
159
|
+
function invalidOwner(lockPath, cause) {
|
|
160
|
+
const error = new AtomicFileLockError(`atomic file lock has an invalid owner (${lockPath}); refusing unsafe recovery`);
|
|
161
|
+
if (cause !== void 0) Object.defineProperty(error, "cause", { value: cause });
|
|
162
|
+
return error;
|
|
163
|
+
}
|
|
164
|
+
function releaseOwnedPath(lockPath, owner) {
|
|
165
|
+
const current = readOwner(lockPath);
|
|
166
|
+
if (!current) return;
|
|
167
|
+
if (canonicalOwner(current) !== canonicalOwner(owner)) throw new AtomicFileLockError(`atomic file lock owner changed before release (${lockPath})`);
|
|
168
|
+
try {
|
|
169
|
+
unlinkSync(lockPath);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (!isMissing(error)) throw error;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function canonicalOwner(owner) {
|
|
175
|
+
return JSON.stringify({
|
|
176
|
+
host: owner.host,
|
|
177
|
+
nonce: owner.nonce,
|
|
178
|
+
pid: owner.pid
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function recoveryPath(lockPath) {
|
|
182
|
+
return `${lockPath}.reclaim`;
|
|
183
|
+
}
|
|
184
|
+
function isAlreadyExists(error) {
|
|
185
|
+
return error instanceof Error && "code" in error && error.code === "EEXIST";
|
|
186
|
+
}
|
|
187
|
+
function isMissing(error) {
|
|
188
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
189
|
+
}
|
|
190
|
+
function isNoSuchProcess(error) {
|
|
191
|
+
return error instanceof Error && "code" in error && error.code === "ESRCH";
|
|
192
|
+
}
|
|
193
|
+
function unlinkIfExists(path) {
|
|
194
|
+
try {
|
|
195
|
+
unlinkSync(path);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (!isMissing(error)) throw error;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//#endregion
|
|
201
|
+
//#region src/ledger-core/canonical.ts
|
|
202
|
+
/**
|
|
203
|
+
* Canonical bytes and digests for durable journal rows.
|
|
204
|
+
*
|
|
205
|
+
* Serialization is RFC 8785 through `@tangle-network/agent-interface` — the
|
|
206
|
+
* stack's single identity scheme — so a value carries the same digest in every
|
|
207
|
+
* package that produces or verifies one.
|
|
208
|
+
*
|
|
209
|
+
* Values canonical JSON cannot represent faithfully are refused, never coerced.
|
|
210
|
+
* `NaN`, `Infinity`, `undefined`, class instances, cycles, and unpaired
|
|
211
|
+
* surrogates each have a lossy encoding that maps distinct records onto one
|
|
212
|
+
* digest, and in a hash chain the digest IS the record's identity: a coercion
|
|
213
|
+
* there is a collision, not a convenience.
|
|
214
|
+
*/
|
|
215
|
+
/** Shape of every ledger digest: `sha256:` followed by 64 lowercase hex chars. */
|
|
216
|
+
const LEDGER_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
217
|
+
/** A value has no faithful canonical-JSON form, so it cannot be stored or hashed. */
|
|
218
|
+
var LedgerCanonicalizationError = class extends ValidationError {};
|
|
219
|
+
/** Canonical JSON encoding (RFC 8785): object keys sorted by UTF-16 code unit,
|
|
220
|
+
* recursively. This is the byte form that is hashed and stored, so equal values
|
|
221
|
+
* always encode identically. */
|
|
222
|
+
function canonicalString(value) {
|
|
223
|
+
try {
|
|
224
|
+
return canonicalCandidateJson(value);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
throw canonicalizationError(value, error);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function hashCanonical(value) {
|
|
230
|
+
try {
|
|
231
|
+
return sha256Bytes(canonicalCandidateBytes(value));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
throw canonicalizationError(value, error);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/** Name the offending value and path so a refusal is actionable. The RFC 8785
|
|
237
|
+
* encoder above is the sole authority on acceptance; this walk only explains a
|
|
238
|
+
* rejection it already made. */
|
|
239
|
+
function canonicalizationError(value, cause) {
|
|
240
|
+
const defect = firstNonCanonicalDefect(value, "$", /* @__PURE__ */ new Set());
|
|
241
|
+
return new LedgerCanonicalizationError(`value has no canonical JSON form: ${defect !== "" ? defect : cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
242
|
+
}
|
|
243
|
+
function firstNonCanonicalDefect(value, path, ancestors) {
|
|
244
|
+
if (value === null) return "";
|
|
245
|
+
if (value === void 0) return `${path} is undefined`;
|
|
246
|
+
const type = typeof value;
|
|
247
|
+
if (type === "number") return Number.isFinite(value) ? "" : `${path} is ${String(value)}`;
|
|
248
|
+
if (type === "string") return isWellFormedUtf16(value) ? "" : `${path} contains an unpaired surrogate`;
|
|
249
|
+
if (type === "boolean") return "";
|
|
250
|
+
if (type !== "object") return `${path} is a ${type}`;
|
|
251
|
+
const object = value;
|
|
252
|
+
if (ancestors.has(object)) return `${path} closes a reference cycle`;
|
|
253
|
+
const prototype = Object.getPrototypeOf(object);
|
|
254
|
+
if (!Array.isArray(object) && prototype !== Object.prototype && prototype !== null) return `${path} is a ${object.constructor?.name ?? "non-plain"} instance, not plain JSON data`;
|
|
255
|
+
const nested = new Set(ancestors).add(object);
|
|
256
|
+
if (Array.isArray(object)) {
|
|
257
|
+
for (let index = 0; index < object.length; index += 1) {
|
|
258
|
+
const defect = firstNonCanonicalDefect(object[index], `${path}[${index}]`, nested);
|
|
259
|
+
if (defect !== "") return defect;
|
|
260
|
+
}
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
for (const [key, entry] of Object.entries(object)) {
|
|
264
|
+
if (!isWellFormedUtf16(key)) return `${path}.${key} has a key with an unpaired surrogate`;
|
|
265
|
+
const defect = firstNonCanonicalDefect(entry, `${path}.${key}`, nested);
|
|
266
|
+
if (defect !== "") return defect;
|
|
267
|
+
}
|
|
268
|
+
return "";
|
|
269
|
+
}
|
|
270
|
+
function isWellFormedUtf16(value) {
|
|
271
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
272
|
+
const code = value.charCodeAt(index);
|
|
273
|
+
if (code >= 55296 && code <= 56319) {
|
|
274
|
+
const next = value.charCodeAt(index + 1);
|
|
275
|
+
if (!(next >= 56320 && next <= 57343)) return false;
|
|
276
|
+
index += 1;
|
|
277
|
+
} else if (code >= 56320 && code <= 57343) return false;
|
|
278
|
+
}
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/ledger-core/journal-file.ts
|
|
283
|
+
/** Filesystem durability and cross-process exclusion for append-only journal files. */
|
|
284
|
+
/** Append one already-serialized line; the write is fsynced (file and directory)
|
|
285
|
+
* before returning so an acknowledged append survives a crash. */
|
|
286
|
+
function appendLedgerLine(path, line, context) {
|
|
287
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
288
|
+
const fd = openSync(path, constants.O_CREAT | constants.O_WRONLY | constants.O_APPEND, 384);
|
|
289
|
+
try {
|
|
290
|
+
writeAll(fd, Buffer.from(line, "utf8"), context);
|
|
291
|
+
fsyncSync(fd);
|
|
292
|
+
} finally {
|
|
293
|
+
closeSync(fd);
|
|
294
|
+
}
|
|
295
|
+
fsyncDirectory(dirname(path));
|
|
296
|
+
}
|
|
297
|
+
/** Replace a small sidecar file whole: write a temporary sibling, fsync it,
|
|
298
|
+
* then rename over the target. A reader therefore sees the previous contents
|
|
299
|
+
* or the new ones, never a partial file. */
|
|
300
|
+
function writeLedgerFileAtomically(path, contents, context) {
|
|
301
|
+
const directory = dirname(path);
|
|
302
|
+
mkdirSync(directory, { recursive: true });
|
|
303
|
+
const temporaryPath = `${path}.tmp`;
|
|
304
|
+
const fd = openSync(temporaryPath, constants.O_CREAT | constants.O_WRONLY | constants.O_TRUNC, 384);
|
|
305
|
+
try {
|
|
306
|
+
writeAll(fd, Buffer.from(contents, "utf8"), context);
|
|
307
|
+
fsyncSync(fd);
|
|
308
|
+
} finally {
|
|
309
|
+
closeSync(fd);
|
|
310
|
+
}
|
|
311
|
+
renameSync(temporaryPath, path);
|
|
312
|
+
fsyncDirectory(directory);
|
|
313
|
+
}
|
|
314
|
+
function withLedgerFileLock(path, context, run) {
|
|
315
|
+
const result = tryWithLedgerFileLock(path, context, run);
|
|
316
|
+
if (!result.acquired) throw context.integrityError(`${context.subject} lock is held (${path})`);
|
|
317
|
+
return result.value;
|
|
318
|
+
}
|
|
319
|
+
function tryWithLedgerFileLock(path, context, run) {
|
|
320
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
321
|
+
const lockPath = `${path}.lock`;
|
|
322
|
+
try {
|
|
323
|
+
const acquisition = tryAcquireAtomicFileLock({ lockPath });
|
|
324
|
+
if (!acquisition.acquired) return { acquired: false };
|
|
325
|
+
try {
|
|
326
|
+
return {
|
|
327
|
+
acquired: true,
|
|
328
|
+
value: run()
|
|
329
|
+
};
|
|
330
|
+
} finally {
|
|
331
|
+
acquisition.lock.release();
|
|
332
|
+
}
|
|
333
|
+
} catch (error) {
|
|
334
|
+
if (error instanceof AtomicFileLockError) throw context.integrityError(error.message, { cause: error });
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function writeAll(fd, bytes, context) {
|
|
339
|
+
let offset = 0;
|
|
340
|
+
while (offset < bytes.byteLength) {
|
|
341
|
+
const written = writeSync(fd, bytes, offset, bytes.byteLength - offset);
|
|
342
|
+
if (written <= 0) throw context.integrityError("filesystem wrote zero bytes");
|
|
343
|
+
offset += written;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
function fsyncDirectory(path) {
|
|
347
|
+
const fd = openSync(path, constants.O_RDONLY);
|
|
348
|
+
try {
|
|
349
|
+
fsyncSync(fd);
|
|
350
|
+
} finally {
|
|
351
|
+
closeSync(fd);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
//#endregion
|
|
355
|
+
//#region src/ledger-core/trusted-head.ts
|
|
356
|
+
/**
|
|
357
|
+
* Trusted head — the pin that makes deletion detectable.
|
|
358
|
+
*
|
|
359
|
+
* ## What the chain binds
|
|
360
|
+
*
|
|
361
|
+
* Every entry hashes the journal header, its sequence, the previous entry's
|
|
362
|
+
* hash, and its event, so editing, reordering, or removing an entry in the
|
|
363
|
+
* middle of a journal invalidates every hash after it. What a hash chain
|
|
364
|
+
* cannot bind is its own length. Dropping entries off the END leaves a shorter
|
|
365
|
+
* journal that is internally perfect, and rebuilding a journal from sequence 0
|
|
366
|
+
* with recomputed hashes produces a different, equally perfect chain. For an
|
|
367
|
+
* audit log of candidate registrations and promotion decisions that is the
|
|
368
|
+
* difference between a record of a search that happened and a record of one
|
|
369
|
+
* that did not.
|
|
370
|
+
*
|
|
371
|
+
* ## What the pin binds
|
|
372
|
+
*
|
|
373
|
+
* `<journal>.head` holds `{sequence, entryHash}` for one entry a writer
|
|
374
|
+
* actually appended, in a file separate from the journal it attests. Each
|
|
375
|
+
* entry hash commits to the entire prefix before it, so checking that the
|
|
376
|
+
* entry at exactly `sequence` still carries `entryHash` proves two things at
|
|
377
|
+
* once: the journal is at least `sequence + 1` entries long, and those entries
|
|
378
|
+
* are byte-for-byte the ones the writer saw. A journal that merely grew past
|
|
379
|
+
* the pin still verifies; a truncated, rolled-back, or wholesale-rewritten one
|
|
380
|
+
* cannot.
|
|
381
|
+
*
|
|
382
|
+
* ## What the pin does NOT defend against
|
|
383
|
+
*
|
|
384
|
+
* The pin is a second file, not a signature. A writer with access to BOTH the
|
|
385
|
+
* journal and its pin can rewrite both consistently, and the result verifies.
|
|
386
|
+
* Pinning raises the cost of undetected tampering from one file to two and
|
|
387
|
+
* nothing more; defending against a writer who holds both requires an anchor
|
|
388
|
+
* outside this store entirely — a signed head, or a digest published to an
|
|
389
|
+
* append-only service the tamperer does not control.
|
|
390
|
+
*
|
|
391
|
+
* Deleting the pin file downgrades a pinned journal to an unpinned one, which
|
|
392
|
+
* cannot be distinguished from a journal written before pinning existed. That
|
|
393
|
+
* is what `requireTrustedHead` is for: a journal opened under it refuses to
|
|
394
|
+
* read at all once a non-empty journal has no pin.
|
|
395
|
+
*/
|
|
396
|
+
/** Sibling pin file for a journal — deliberately NOT the journal, so rewriting
|
|
397
|
+
* the log is not enough to rewrite the trust recorded about it. */
|
|
398
|
+
function trustedHeadPathFor(journalPath) {
|
|
399
|
+
return `${journalPath}.head`;
|
|
400
|
+
}
|
|
401
|
+
/** The pin, or null when this journal has never been pinned. A pin that exists
|
|
402
|
+
* but does not parse is corruption or tamper of the trust record itself and
|
|
403
|
+
* fails loudly: reporting it as "no pin" would silently downgrade what the
|
|
404
|
+
* journal can prove. */
|
|
405
|
+
function readTrustedHeadFile(path, context) {
|
|
406
|
+
if (!existsSync(path)) return null;
|
|
407
|
+
const text = readFileSync(path, "utf8");
|
|
408
|
+
let raw;
|
|
409
|
+
try {
|
|
410
|
+
raw = JSON.parse(text);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
throw context.integrityError(`${context.subject} trusted head ${path} is not valid JSON`, { cause: error });
|
|
413
|
+
}
|
|
414
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw context.integrityError(`${context.subject} trusted head ${path} is not an object`);
|
|
415
|
+
const record = raw;
|
|
416
|
+
const keys = Object.keys(record).sort();
|
|
417
|
+
if (keys.length !== 2 || keys[0] !== "entryHash" || keys[1] !== "sequence") throw context.integrityError(`${context.subject} trusted head ${path} has keys [${keys.join(", ")}], expected [entryHash, sequence]`);
|
|
418
|
+
const { sequence, entryHash } = record;
|
|
419
|
+
if (typeof sequence !== "number" || !Number.isSafeInteger(sequence) || sequence < 0) throw context.integrityError(`${context.subject} trusted head ${path} sequence is not a non-negative integer`);
|
|
420
|
+
if (typeof entryHash !== "string" || !LEDGER_HASH_PATTERN.test(entryHash)) throw context.integrityError(`${context.subject} trusted head ${path} entryHash is not a sha256 digest`);
|
|
421
|
+
return {
|
|
422
|
+
sequence,
|
|
423
|
+
entryHash
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
/** Publish a pin by write-then-rename, so a crash leaves either the previous
|
|
427
|
+
* pin or the new one and never a torn file that would lock a journal out of
|
|
428
|
+
* its own trust record. */
|
|
429
|
+
function writeTrustedHeadFile(path, head, context) {
|
|
430
|
+
writeLedgerFileAtomically(path, `${canonicalString(head)}\n`, context);
|
|
431
|
+
}
|
|
432
|
+
/** The entry at exactly `head.sequence` must still carry `head.entryHash`.
|
|
433
|
+
* Entries must already have passed chain verification, which is what makes
|
|
434
|
+
* index and sequence interchangeable here. */
|
|
435
|
+
function verifyEntriesAgainstTrustedHead(entries, head, context, subject) {
|
|
436
|
+
const pinned = entries[head.sequence];
|
|
437
|
+
if (pinned === void 0) throw context.integrityError(`${subject} trusted head pins sequence ${head.sequence} (${head.entryHash}) but the journal has ${entries.length} entries — pinned history is missing (truncation, rollback, or rewrite)`);
|
|
438
|
+
if (pinned.sequence !== head.sequence) throw context.integrityError(`${subject} entry at index ${head.sequence} carries sequence ${pinned.sequence}; the chain was not verified before the trusted head`);
|
|
439
|
+
if (pinned.entryHash !== head.entryHash) throw context.integrityError(`${subject} entry at pinned sequence ${head.sequence} carries ${pinned.entryHash} but the trusted head recorded ${head.entryHash} — the journal was rewritten`);
|
|
440
|
+
}
|
|
441
|
+
//#endregion
|
|
442
|
+
//#region src/ledger-core/journal.ts
|
|
443
|
+
/**
|
|
444
|
+
* Generic durable append-only journal with a SHA-256 hash chain.
|
|
445
|
+
*
|
|
446
|
+
* The file format is canonical JSONL: every row is the canonical JSON encoding
|
|
447
|
+
* (RFC 8785, object keys sorted recursively) of one entry, and every entry
|
|
448
|
+
* hashes the previous entry's hash so the log is tamper-evident. Appends are
|
|
449
|
+
* serialized in-process per path and across processes via a lock file, fsynced
|
|
450
|
+
* before acknowledgement, and idempotent by `eventId`. A malformed,
|
|
451
|
+
* non-canonical, truncated, reordered, or conflicting log fails loudly; a bad
|
|
452
|
+
* row is never skipped.
|
|
453
|
+
*
|
|
454
|
+
* A hash chain binds each entry to its predecessor, which cannot prove that no
|
|
455
|
+
* entry was removed from the end: a valid shorter prefix is still a valid
|
|
456
|
+
* chain. `pinHead` records the appended entry in a sibling `<journal>.head`
|
|
457
|
+
* file and every read re-verifies the journal against a pin that is present,
|
|
458
|
+
* so truncation and wholesale rewrite are refused. `trusted-head.ts` carries
|
|
459
|
+
* the full threat model, including what a pin does not defend against.
|
|
460
|
+
*
|
|
461
|
+
* Domain vocabulary lives entirely in the consumer's codec: entry schema
|
|
462
|
+
* validation, the constant header fields stamped into every entry, the error
|
|
463
|
+
* taxonomy, and the state machine replayed over verified entries.
|
|
464
|
+
*/
|
|
465
|
+
const journalMutexes = /* @__PURE__ */ new Map();
|
|
466
|
+
function mutexFor(path) {
|
|
467
|
+
const existing = journalMutexes.get(path);
|
|
468
|
+
if (existing) return existing;
|
|
469
|
+
const mutex = new Mutex();
|
|
470
|
+
journalMutexes.set(path, mutex);
|
|
471
|
+
return mutex;
|
|
472
|
+
}
|
|
473
|
+
/** Durable filesystem journal. Construction performs no I/O; `append` and
|
|
474
|
+
* `replay` validate the complete existing file under the locks. */
|
|
475
|
+
var FileLedgerJournal = class {
|
|
476
|
+
path;
|
|
477
|
+
/** Sibling file holding this journal's trusted head. */
|
|
478
|
+
trustedHeadPath;
|
|
479
|
+
codec;
|
|
480
|
+
mutex;
|
|
481
|
+
requireTrustedHead;
|
|
482
|
+
constructor(path, codec, options = {}) {
|
|
483
|
+
this.path = resolve(path);
|
|
484
|
+
this.trustedHeadPath = trustedHeadPathFor(this.path);
|
|
485
|
+
this.codec = codec;
|
|
486
|
+
this.mutex = mutexFor(this.path);
|
|
487
|
+
this.requireTrustedHead = options.requireTrustedHead === true;
|
|
488
|
+
}
|
|
489
|
+
async replay() {
|
|
490
|
+
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => this.replayLocked()));
|
|
491
|
+
}
|
|
492
|
+
async append(event, options = {}) {
|
|
493
|
+
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => {
|
|
494
|
+
const before = this.replayLocked();
|
|
495
|
+
const existing = before.entries.find((entry) => entry.event.eventId === event.eventId);
|
|
496
|
+
if (existing) {
|
|
497
|
+
if (canonicalString(existing.event) !== canonicalString(event)) throw this.codec.conflictError(`eventId ${event.eventId} already exists with different content`);
|
|
498
|
+
return {
|
|
499
|
+
entry: existing,
|
|
500
|
+
appended: false,
|
|
501
|
+
projection: before.projection
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
const material = {
|
|
505
|
+
...this.codec.header,
|
|
506
|
+
sequence: before.entries.length,
|
|
507
|
+
previousHash: before.entries.at(-1)?.entryHash ?? null,
|
|
508
|
+
event
|
|
509
|
+
};
|
|
510
|
+
const entry = {
|
|
511
|
+
...material,
|
|
512
|
+
entryHash: hashCanonical(material)
|
|
513
|
+
};
|
|
514
|
+
const projection = this.project([...before.entries, entry]);
|
|
515
|
+
appendLedgerLine(this.path, `${canonicalString(entry)}\n`, this.codec);
|
|
516
|
+
if (options.pinHead === true) writeTrustedHeadFile(this.trustedHeadPath, {
|
|
517
|
+
sequence: entry.sequence,
|
|
518
|
+
entryHash: entry.entryHash
|
|
519
|
+
}, this.codec);
|
|
520
|
+
return {
|
|
521
|
+
entry,
|
|
522
|
+
appended: true,
|
|
523
|
+
projection
|
|
524
|
+
};
|
|
525
|
+
}));
|
|
526
|
+
}
|
|
527
|
+
/** The pinned head, or null when this journal has never been pinned. */
|
|
528
|
+
async trustedHead() {
|
|
529
|
+
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => readTrustedHeadFile(this.trustedHeadPath, this.codec)));
|
|
530
|
+
}
|
|
531
|
+
/** Pin the current verified head. The chain and any existing pin are checked
|
|
532
|
+
* first, so the pin only ever moves forward along a journal that still
|
|
533
|
+
* carries the history it already recorded. */
|
|
534
|
+
async pinTrustedHead() {
|
|
535
|
+
return this.mutex.runExclusive(() => withLedgerFileLock(this.path, this.codec, () => {
|
|
536
|
+
const entries = this.readEntries();
|
|
537
|
+
this.project(entries);
|
|
538
|
+
this.verifyTrustedHead(entries, false);
|
|
539
|
+
const last = entries.at(-1);
|
|
540
|
+
if (last === void 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} is empty; there is no head to pin`);
|
|
541
|
+
const head = {
|
|
542
|
+
sequence: last.sequence,
|
|
543
|
+
entryHash: last.entryHash
|
|
544
|
+
};
|
|
545
|
+
writeTrustedHeadFile(this.trustedHeadPath, head, this.codec);
|
|
546
|
+
return head;
|
|
547
|
+
}));
|
|
548
|
+
}
|
|
549
|
+
replayLocked() {
|
|
550
|
+
const entries = this.readEntries();
|
|
551
|
+
const projection = this.project(entries);
|
|
552
|
+
this.verifyTrustedHead(entries, this.requireTrustedHead);
|
|
553
|
+
return {
|
|
554
|
+
entries,
|
|
555
|
+
projection
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
verifyTrustedHead(entries, requirePin) {
|
|
559
|
+
const pinned = readTrustedHeadFile(this.trustedHeadPath, this.codec);
|
|
560
|
+
if (pinned === null) {
|
|
561
|
+
if (requirePin && entries.length > 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has ${entries.length} entries but no trusted head at ${this.trustedHeadPath} — without its pin the journal cannot prove nothing was deleted`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
verifyEntriesAgainstTrustedHead(entries, pinned, this.codec, `${this.codec.subject} ${this.path}`);
|
|
565
|
+
}
|
|
566
|
+
readEntries() {
|
|
567
|
+
if (!existsSync(this.path)) return [];
|
|
568
|
+
const text = readFileSync(this.path, "utf8");
|
|
569
|
+
if (text.length === 0) return [];
|
|
570
|
+
if (!text.endsWith("\n")) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has a truncated final record (missing newline)`);
|
|
571
|
+
const lines = text.slice(0, -1).split("\n");
|
|
572
|
+
const entries = [];
|
|
573
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
574
|
+
const line = lines[index];
|
|
575
|
+
if (line.length === 0) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has a blank row at line ${index + 1}`);
|
|
576
|
+
let raw;
|
|
577
|
+
try {
|
|
578
|
+
raw = JSON.parse(line);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
throw this.codec.integrityError(`${this.codec.subject} ${this.path} has invalid JSON at line ${index + 1}`, { cause: error });
|
|
581
|
+
}
|
|
582
|
+
const entry = this.codec.parseEntry(raw, {
|
|
583
|
+
path: this.path,
|
|
584
|
+
line: index + 1
|
|
585
|
+
});
|
|
586
|
+
let canonical;
|
|
587
|
+
try {
|
|
588
|
+
canonical = canonicalString(entry);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
throw this.codec.integrityError(`${this.codec.subject} ${this.path} has a row with no canonical JSON form at line ${index + 1}`, { cause: error });
|
|
591
|
+
}
|
|
592
|
+
if (line !== canonical) throw this.codec.integrityError(`${this.codec.subject} ${this.path} has non-canonical bytes at line ${index + 1}`);
|
|
593
|
+
entries.push(entry);
|
|
594
|
+
}
|
|
595
|
+
return entries;
|
|
596
|
+
}
|
|
597
|
+
project(entries) {
|
|
598
|
+
const projector = this.codec.createProjector();
|
|
599
|
+
const eventIds = /* @__PURE__ */ new Set();
|
|
600
|
+
let expectedPrevious = null;
|
|
601
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
602
|
+
const entry = entries[index];
|
|
603
|
+
this.codec.checkEntryHeader(entry, index);
|
|
604
|
+
if (entry.sequence !== index) throw this.codec.integrityError(`entry ${entry.event.eventId} has sequence ${entry.sequence}, expected ${index}`);
|
|
605
|
+
if (entry.previousHash !== expectedPrevious) throw this.codec.integrityError(`entry ${entry.event.eventId} does not extend the previous hash`);
|
|
606
|
+
const { entryHash: _entryHash, ...material } = entry;
|
|
607
|
+
const expectedHash = hashCanonical(material);
|
|
608
|
+
if (entry.entryHash !== expectedHash) throw this.codec.integrityError(`entry ${entry.event.eventId} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`);
|
|
609
|
+
expectedPrevious = entry.entryHash;
|
|
610
|
+
if (eventIds.has(entry.event.eventId)) throw this.codec.integrityError(`duplicate eventId ${entry.event.eventId} in durable ledger`);
|
|
611
|
+
eventIds.add(entry.event.eventId);
|
|
612
|
+
projector.apply(entry, index);
|
|
613
|
+
}
|
|
614
|
+
return projector.finish(entries);
|
|
615
|
+
}
|
|
616
|
+
};
|
|
617
|
+
//#endregion
|
|
618
|
+
export { tryWithLedgerFileLock as a, LEDGER_HASH_PATTERN as c, hashCanonical as d, AtomicFileLockError as f, appendLedgerLine as i, LedgerCanonicalizationError as l, tryAcquireAtomicFileLock as m, trustedHeadPathFor as n, withLedgerFileLock as o, probeAtomicFileLock as p, verifyEntriesAgainstTrustedHead as r, writeLedgerFileAtomically as s, FileLedgerJournal as t, canonicalString as u };
|
|
619
|
+
|
|
620
|
+
//# sourceMappingURL=ledger-core-CPZfcrC2.js.map
|