@verax-ai/proxy 0.1.1 → 0.1.3
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/dist/approvals.d.ts +30 -1
- package/dist/approvals.js +64 -3
- package/dist/atomic-write.d.ts +1 -0
- package/dist/atomic-write.js +65 -0
- package/dist/in-flight-log.d.ts +12 -0
- package/dist/in-flight-log.js +65 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +3 -2
- package/dist/inputs.d.ts +7 -1
- package/dist/inputs.js +35 -18
- package/dist/jsonl-tail.d.ts +26 -0
- package/dist/jsonl-tail.js +218 -0
- package/dist/ledger-manifest.d.ts +88 -0
- package/dist/ledger-manifest.js +225 -0
- package/dist/ledger.d.ts +88 -9
- package/dist/ledger.js +541 -86
- package/dist/policy.js +9 -1
- package/dist/proxy.js +266 -166
- package/dist/serial-queue.d.ts +5 -0
- package/dist/serial-queue.js +9 -0
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export declare const LEGACY_PIECE_ID = "legacy";
|
|
2
|
+
export declare const DEFAULT_PIECE_MAX_ROWS = 50000;
|
|
3
|
+
export declare const DEFAULT_PIECE_MAX_BYTES: number;
|
|
4
|
+
export declare const MANIFEST_NAME = "ledger-manifest.json";
|
|
5
|
+
export declare const INDEX_NAME = "index.jsonl";
|
|
6
|
+
export type LedgerPieceRow = {
|
|
7
|
+
id: string;
|
|
8
|
+
decisions: string;
|
|
9
|
+
effects: string;
|
|
10
|
+
inputs: string;
|
|
11
|
+
n: number;
|
|
12
|
+
effectN: number;
|
|
13
|
+
firstMs: number | null;
|
|
14
|
+
lastMs: number | null;
|
|
15
|
+
lastHash: string | null;
|
|
16
|
+
closed: boolean;
|
|
17
|
+
closedAtMs?: number;
|
|
18
|
+
};
|
|
19
|
+
export type LedgerManifest = {
|
|
20
|
+
version: 1;
|
|
21
|
+
pieces: LedgerPieceRow[];
|
|
22
|
+
countedAtMs: number[];
|
|
23
|
+
};
|
|
24
|
+
export type LedgerIndexLine = {
|
|
25
|
+
ref: string;
|
|
26
|
+
tenantRef: string;
|
|
27
|
+
piece: string;
|
|
28
|
+
ts: number;
|
|
29
|
+
kind: string;
|
|
30
|
+
requestHash: string;
|
|
31
|
+
resolves: string | null;
|
|
32
|
+
hasEffect: boolean;
|
|
33
|
+
reasonCode: string;
|
|
34
|
+
subject: string;
|
|
35
|
+
policyHash: string;
|
|
36
|
+
};
|
|
37
|
+
export type PieceFiles = {
|
|
38
|
+
id: string;
|
|
39
|
+
decisions: string;
|
|
40
|
+
effects: string;
|
|
41
|
+
inputs: string;
|
|
42
|
+
copyDecisions: string;
|
|
43
|
+
copyEffects: string;
|
|
44
|
+
closed: boolean;
|
|
45
|
+
firstMs: number | null;
|
|
46
|
+
lastMs: number | null;
|
|
47
|
+
n: number;
|
|
48
|
+
effectN: number;
|
|
49
|
+
};
|
|
50
|
+
export declare function manifestPath(dir: string): string;
|
|
51
|
+
export declare function indexPath(dir: string): string;
|
|
52
|
+
export declare function legacyPieceRow(): LedgerPieceRow;
|
|
53
|
+
export declare function newPieceRow(id: string): LedgerPieceRow;
|
|
54
|
+
export declare function copyRel(pieceId: string, name: "decisions.jsonl" | "effects.jsonl"): string;
|
|
55
|
+
export declare function nextPieceId(existingIds: string[], atMs: number): string;
|
|
56
|
+
/**
|
|
57
|
+
* Null only when there is no manifest (the flat legacy layout). A manifest
|
|
58
|
+
* that is there but cannot be read or parsed throws: opening as a single
|
|
59
|
+
* legacy piece would chain new rows onto a closed file and hide every piece
|
|
60
|
+
* the manifest named.
|
|
61
|
+
*/
|
|
62
|
+
export declare function readLedgerManifest(dir: string): LedgerManifest | null;
|
|
63
|
+
export declare function writeLedgerManifest(dir: string, manifest: LedgerManifest): void;
|
|
64
|
+
export declare function listPieceFiles(dir: string): PieceFiles[];
|
|
65
|
+
export declare function pieceOverlaps(piece: {
|
|
66
|
+
firstMs: number | null;
|
|
67
|
+
lastMs: number | null;
|
|
68
|
+
n: number;
|
|
69
|
+
closed: boolean;
|
|
70
|
+
}, fromMs: number, toMs: number): boolean;
|
|
71
|
+
export declare function parseIndexText(text: string): LedgerIndexLine[];
|
|
72
|
+
export declare function readLedgerIndex(dir: string): LedgerIndexLine[];
|
|
73
|
+
/**
|
|
74
|
+
* Decisions the index knows, by distinct ref. An effect adds a second line
|
|
75
|
+
* for its decision's ref, so a line total would overstate what the index
|
|
76
|
+
* holds and let a short index pass as complete.
|
|
77
|
+
*/
|
|
78
|
+
export declare function decisionIndexCount(lines: readonly LedgerIndexLine[]): number;
|
|
79
|
+
/**
|
|
80
|
+
* What the on-disk index names, or null when the file is missing. Doctor
|
|
81
|
+
* compares this to the pieces; a short index is rebuilt on the next open
|
|
82
|
+
* but a running body does not see the missing refs until then.
|
|
83
|
+
*/
|
|
84
|
+
export declare function indexCoverage(dir: string): {
|
|
85
|
+
refs: number;
|
|
86
|
+
pieces: Set<string>;
|
|
87
|
+
} | null;
|
|
88
|
+
export declare function manifestExists(dir: string): boolean;
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { writeFileAtomic } from "./atomic-write.js";
|
|
4
|
+
export const LEGACY_PIECE_ID = "legacy";
|
|
5
|
+
export const DEFAULT_PIECE_MAX_ROWS = 50_000;
|
|
6
|
+
export const DEFAULT_PIECE_MAX_BYTES = 200 * 1024 * 1024;
|
|
7
|
+
export const MANIFEST_NAME = "ledger-manifest.json";
|
|
8
|
+
export const INDEX_NAME = "index.jsonl";
|
|
9
|
+
export function manifestPath(dir) {
|
|
10
|
+
return join(dir, MANIFEST_NAME);
|
|
11
|
+
}
|
|
12
|
+
export function indexPath(dir) {
|
|
13
|
+
return join(dir, INDEX_NAME);
|
|
14
|
+
}
|
|
15
|
+
export function legacyPieceRow() {
|
|
16
|
+
return {
|
|
17
|
+
id: LEGACY_PIECE_ID,
|
|
18
|
+
decisions: "decisions.jsonl",
|
|
19
|
+
effects: "effects.jsonl",
|
|
20
|
+
inputs: "inputs.jsonl",
|
|
21
|
+
n: 0,
|
|
22
|
+
effectN: 0,
|
|
23
|
+
firstMs: null,
|
|
24
|
+
lastMs: null,
|
|
25
|
+
lastHash: null,
|
|
26
|
+
closed: false,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function newPieceRow(id) {
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
decisions: `pieces/${id}/decisions.jsonl`,
|
|
33
|
+
effects: `pieces/${id}/effects.jsonl`,
|
|
34
|
+
inputs: `pieces/${id}/inputs.jsonl`,
|
|
35
|
+
n: 0,
|
|
36
|
+
effectN: 0,
|
|
37
|
+
firstMs: null,
|
|
38
|
+
lastMs: null,
|
|
39
|
+
lastHash: null,
|
|
40
|
+
closed: false,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function copyRel(pieceId, name) {
|
|
44
|
+
if (pieceId === LEGACY_PIECE_ID)
|
|
45
|
+
return `evidence-copy/${name}`;
|
|
46
|
+
return `evidence-copy/pieces/${pieceId}/${name}`;
|
|
47
|
+
}
|
|
48
|
+
export function nextPieceId(existingIds, atMs) {
|
|
49
|
+
const d = new Date(atMs);
|
|
50
|
+
const ym = `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
51
|
+
const letters = "abcdefghijklmnopqrstuvwxyz";
|
|
52
|
+
for (let i = 0; i < letters.length; i += 1) {
|
|
53
|
+
const id = `${ym}-${letters[i]}`;
|
|
54
|
+
if (!existingIds.includes(id))
|
|
55
|
+
return id;
|
|
56
|
+
}
|
|
57
|
+
let n = letters.length;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const id = `${ym}-z${n}`;
|
|
60
|
+
if (!existingIds.includes(id))
|
|
61
|
+
return id;
|
|
62
|
+
n += 1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Null only when there is no manifest (the flat legacy layout). A manifest
|
|
67
|
+
* that is there but cannot be read or parsed throws: opening as a single
|
|
68
|
+
* legacy piece would chain new rows onto a closed file and hide every piece
|
|
69
|
+
* the manifest named.
|
|
70
|
+
*/
|
|
71
|
+
export function readLedgerManifest(dir) {
|
|
72
|
+
let text;
|
|
73
|
+
try {
|
|
74
|
+
text = readFileSync(manifestPath(dir), "utf8");
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
if (err.code === "ENOENT")
|
|
78
|
+
return null;
|
|
79
|
+
throw new Error("ledger-manifest-unreadable: " + err.message);
|
|
80
|
+
}
|
|
81
|
+
let raw;
|
|
82
|
+
try {
|
|
83
|
+
raw = JSON.parse(text);
|
|
84
|
+
}
|
|
85
|
+
catch (err) {
|
|
86
|
+
throw new Error("ledger-manifest-corrupt: " + err.message);
|
|
87
|
+
}
|
|
88
|
+
if (!raw || raw.version !== 1 || !Array.isArray(raw.pieces) || raw.pieces.length === 0) {
|
|
89
|
+
throw new Error("ledger-manifest-invalid: version 1 with at least one piece expected");
|
|
90
|
+
}
|
|
91
|
+
const pieces = raw.pieces.map(asPieceRow);
|
|
92
|
+
if (pieces.some((p) => p === null))
|
|
93
|
+
throw new Error("ledger-manifest-invalid: piece row missing a field");
|
|
94
|
+
return {
|
|
95
|
+
version: 1,
|
|
96
|
+
pieces: pieces,
|
|
97
|
+
countedAtMs: Array.isArray(raw.countedAtMs)
|
|
98
|
+
? raw.countedAtMs.filter((n) => typeof n === "number")
|
|
99
|
+
: [],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function asPieceRow(raw) {
|
|
103
|
+
if (!raw || typeof raw !== "object")
|
|
104
|
+
return null;
|
|
105
|
+
const r = raw;
|
|
106
|
+
if (typeof r.id !== "string" || typeof r.decisions !== "string")
|
|
107
|
+
return null;
|
|
108
|
+
if (typeof r.effects !== "string" || typeof r.inputs !== "string")
|
|
109
|
+
return null;
|
|
110
|
+
if (typeof r.n !== "number" || typeof r.effectN !== "number")
|
|
111
|
+
return null;
|
|
112
|
+
if (typeof r.closed !== "boolean")
|
|
113
|
+
return null;
|
|
114
|
+
return {
|
|
115
|
+
id: r.id,
|
|
116
|
+
decisions: r.decisions,
|
|
117
|
+
effects: r.effects,
|
|
118
|
+
inputs: r.inputs,
|
|
119
|
+
n: r.n,
|
|
120
|
+
effectN: r.effectN,
|
|
121
|
+
firstMs: typeof r.firstMs === "number" ? r.firstMs : null,
|
|
122
|
+
lastMs: typeof r.lastMs === "number" ? r.lastMs : null,
|
|
123
|
+
lastHash: typeof r.lastHash === "string" ? r.lastHash : null,
|
|
124
|
+
closed: r.closed,
|
|
125
|
+
...(typeof r.closedAtMs === "number" ? { closedAtMs: r.closedAtMs } : {}),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
export function writeLedgerManifest(dir, manifest) {
|
|
129
|
+
writeFileAtomic(manifestPath(dir), `${JSON.stringify(manifest)}\n`);
|
|
130
|
+
}
|
|
131
|
+
export function listPieceFiles(dir) {
|
|
132
|
+
const manifest = readLedgerManifest(dir);
|
|
133
|
+
const pieces = manifest?.pieces ?? [legacyPieceRow()];
|
|
134
|
+
return pieces.map((p) => ({
|
|
135
|
+
id: p.id,
|
|
136
|
+
decisions: join(dir, p.decisions),
|
|
137
|
+
effects: join(dir, p.effects),
|
|
138
|
+
inputs: join(dir, p.inputs),
|
|
139
|
+
copyDecisions: join(dir, copyRel(p.id, "decisions.jsonl")),
|
|
140
|
+
copyEffects: join(dir, copyRel(p.id, "effects.jsonl")),
|
|
141
|
+
closed: p.closed,
|
|
142
|
+
firstMs: p.firstMs,
|
|
143
|
+
lastMs: p.lastMs,
|
|
144
|
+
n: p.n,
|
|
145
|
+
effectN: p.effectN,
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
export function pieceOverlaps(piece, fromMs, toMs) {
|
|
149
|
+
if (piece.firstMs === null || piece.lastMs === null)
|
|
150
|
+
return piece.n > 0 || !piece.closed;
|
|
151
|
+
return piece.lastMs >= fromMs && piece.firstMs < toMs;
|
|
152
|
+
}
|
|
153
|
+
export function parseIndexText(text) {
|
|
154
|
+
const rows = [];
|
|
155
|
+
for (const line of text.split("\n")) {
|
|
156
|
+
if (line === "")
|
|
157
|
+
continue;
|
|
158
|
+
// The index is appended without fsync, so a crash can leave its last
|
|
159
|
+
// line half written. Such a line is skipped; the ref count below then
|
|
160
|
+
// falls short of the ledger's and the index is rebuilt from the pieces.
|
|
161
|
+
let raw;
|
|
162
|
+
try {
|
|
163
|
+
raw = JSON.parse(line);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (typeof raw.ref !== "string" || typeof raw.piece !== "string")
|
|
169
|
+
continue;
|
|
170
|
+
rows.push({
|
|
171
|
+
ref: raw.ref,
|
|
172
|
+
tenantRef: typeof raw.tenantRef === "string" ? raw.tenantRef : "",
|
|
173
|
+
piece: raw.piece,
|
|
174
|
+
ts: typeof raw.ts === "number" ? raw.ts : 0,
|
|
175
|
+
kind: typeof raw.kind === "string" ? raw.kind : "",
|
|
176
|
+
requestHash: typeof raw.requestHash === "string" ? raw.requestHash : "",
|
|
177
|
+
resolves: typeof raw.resolves === "string" ? raw.resolves : null,
|
|
178
|
+
hasEffect: raw.hasEffect === true,
|
|
179
|
+
reasonCode: typeof raw.reasonCode === "string" ? raw.reasonCode : "",
|
|
180
|
+
subject: typeof raw.subject === "string" ? raw.subject : "",
|
|
181
|
+
policyHash: typeof raw.policyHash === "string" ? raw.policyHash : "",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
export function readLedgerIndex(dir) {
|
|
187
|
+
try {
|
|
188
|
+
return parseIndexText(readFileSync(indexPath(dir), "utf8"));
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
if (err.code === "ENOENT")
|
|
192
|
+
return [];
|
|
193
|
+
throw err;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Decisions the index knows, by distinct ref. An effect adds a second line
|
|
198
|
+
* for its decision's ref, so a line total would overstate what the index
|
|
199
|
+
* holds and let a short index pass as complete.
|
|
200
|
+
*/
|
|
201
|
+
export function decisionIndexCount(lines) {
|
|
202
|
+
const refs = new Set();
|
|
203
|
+
for (const l of lines) {
|
|
204
|
+
if (l.kind === "allow" || l.kind === "deny" || l.kind === "defer")
|
|
205
|
+
refs.add(l.ref);
|
|
206
|
+
}
|
|
207
|
+
return refs.size;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* What the on-disk index names, or null when the file is missing. Doctor
|
|
211
|
+
* compares this to the pieces; a short index is rebuilt on the next open
|
|
212
|
+
* but a running body does not see the missing refs until then.
|
|
213
|
+
*/
|
|
214
|
+
export function indexCoverage(dir) {
|
|
215
|
+
if (!existsSync(indexPath(dir)))
|
|
216
|
+
return null;
|
|
217
|
+
const lines = readLedgerIndex(dir);
|
|
218
|
+
const pieces = new Set();
|
|
219
|
+
for (const line of lines)
|
|
220
|
+
pieces.add(line.piece);
|
|
221
|
+
return { refs: decisionIndexCount(lines), pieces };
|
|
222
|
+
}
|
|
223
|
+
export function manifestExists(dir) {
|
|
224
|
+
return existsSync(manifestPath(dir));
|
|
225
|
+
}
|
package/dist/ledger.d.ts
CHANGED
|
@@ -7,9 +7,24 @@ export declare const ledgerFs: {
|
|
|
7
7
|
open: typeof open;
|
|
8
8
|
};
|
|
9
9
|
import { type EffectRow, type SignedEffectExtract } from "@cedulon/effect-extract";
|
|
10
|
-
import
|
|
10
|
+
import { type TailVisit } from "./jsonl-tail.ts";
|
|
11
|
+
import type { DecisionInputs, EffectSigner, ExtractWindow, Ledger, LedgerEffect, WitnessClass } from "./types.ts";
|
|
11
12
|
import type { DecisionKind, SignedDecisionRecord } from "@cedulon/core";
|
|
12
13
|
export type PermissionCheck = "owner-only" | "not checked on this platform";
|
|
14
|
+
/**
|
|
15
|
+
* A ledger file read from its end through the same seam the reads above use,
|
|
16
|
+
* so a test can count what a window costs.
|
|
17
|
+
*/
|
|
18
|
+
export declare function readLedgerTail<T>(path: string, visit: (row: T) => TailVisit, chunkBytesOrOpts?: number | {
|
|
19
|
+
chunkBytes?: number;
|
|
20
|
+
endOffset?: number;
|
|
21
|
+
}): Promise<T[]>;
|
|
22
|
+
/**
|
|
23
|
+
* How far past a window's lower edge the tail reader keeps looking before it
|
|
24
|
+
* stops. Rows are in file order; their stamps are the clock's, and a clock
|
|
25
|
+
* can be set back. A row stamped a minute before its neighbour is still seen.
|
|
26
|
+
*/
|
|
27
|
+
export declare const WINDOW_SLACK_MS = 60000;
|
|
13
28
|
/** Write then fsync so a crash cannot drop a committed line. */
|
|
14
29
|
export declare function appendDurable(path: string, line: string): Promise<void>;
|
|
15
30
|
export type RemoteWitnessSign = (row: EffectRow, resultHash: string | undefined) => Promise<Pick<LedgerEffect, "receipt" | "attestation" | "witnessClass"> | null>;
|
|
@@ -55,14 +70,37 @@ export declare class MemoryLedger implements Ledger {
|
|
|
55
70
|
lastDecisionHash(): Promise<string | null>;
|
|
56
71
|
exportExtract(window: ExtractWindow, signer: EffectSigner): Promise<SignedEffectExtract>;
|
|
57
72
|
}
|
|
73
|
+
export type FileLedgerOpts = {
|
|
74
|
+
pieceMaxRows?: number;
|
|
75
|
+
pieceMaxBytes?: number;
|
|
76
|
+
onPieceClose?: (window: {
|
|
77
|
+
startMs: number;
|
|
78
|
+
endMs: number;
|
|
79
|
+
pieceId: string;
|
|
80
|
+
}) => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
export type LedgerCounts = {
|
|
83
|
+
decisions: number;
|
|
84
|
+
effects: number;
|
|
85
|
+
lastDecisionMs: number | null;
|
|
86
|
+
activeDecisions: number;
|
|
87
|
+
pieces: number;
|
|
88
|
+
};
|
|
58
89
|
export declare class FileLedger implements Ledger {
|
|
59
90
|
readonly permissionCheck: PermissionCheck;
|
|
60
91
|
readonly dir: string;
|
|
61
92
|
effectSigner?: EffectSigner;
|
|
62
93
|
/** Optional signer in another process. Null means stay `self`. */
|
|
63
94
|
remoteWitness?: RemoteWitnessSign;
|
|
64
|
-
|
|
65
|
-
|
|
95
|
+
/** Asked after a piece closes. A missing or failing hook does not block the close. */
|
|
96
|
+
onPieceClose?: FileLedgerOpts["onPieceClose"];
|
|
97
|
+
pieceMaxRows: number;
|
|
98
|
+
pieceMaxBytes: number;
|
|
99
|
+
private decisionsPath;
|
|
100
|
+
private effectsPath;
|
|
101
|
+
private inputsPath;
|
|
102
|
+
private copyDecisionsPath;
|
|
103
|
+
private copyEffectsPath;
|
|
66
104
|
private readonly lockPath;
|
|
67
105
|
private readonly q;
|
|
68
106
|
private readonly token;
|
|
@@ -75,17 +113,37 @@ export declare class FileLedger implements Ledger {
|
|
|
75
113
|
private readonly reauthByHash;
|
|
76
114
|
private readonly countedAt;
|
|
77
115
|
countsReadable: boolean;
|
|
78
|
-
private readonly copyDir;
|
|
79
116
|
private readonly heartbeatPath;
|
|
80
117
|
private decisionCount;
|
|
81
118
|
private effectCount;
|
|
82
|
-
|
|
119
|
+
private lastDecisionMs;
|
|
120
|
+
private pieces;
|
|
121
|
+
/** Inputs rows appended through this instance, until their decision lands. */
|
|
122
|
+
private readonly recentInputs;
|
|
123
|
+
private activeId;
|
|
124
|
+
private activeDecisionN;
|
|
125
|
+
private activeEffectN;
|
|
126
|
+
constructor(dir: string, opts?: FileLedgerOpts);
|
|
127
|
+
noteInputs(ref: string, inputs: DecisionInputs): void;
|
|
128
|
+
inputsPaths(): {
|
|
129
|
+
active: string;
|
|
130
|
+
all: string[];
|
|
131
|
+
};
|
|
83
132
|
/** After lock handoff a new instance reloads; the lost owner cannot append. */
|
|
84
133
|
private loadCaches;
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
private
|
|
134
|
+
private applyActivePaths;
|
|
135
|
+
private activePiece;
|
|
136
|
+
private applyIndexLine;
|
|
137
|
+
private pruneCounted;
|
|
138
|
+
private rebuildIndex;
|
|
139
|
+
private writeIndexForDecision;
|
|
140
|
+
private writeIndexHasEffect;
|
|
141
|
+
private noteActiveStamp;
|
|
142
|
+
private persistManifest;
|
|
143
|
+
private fsyncPath;
|
|
144
|
+
private ensurePieceFiles;
|
|
145
|
+
private closeActivePiece;
|
|
146
|
+
private maybeRotate;
|
|
89
147
|
private lockBody;
|
|
90
148
|
private ownsLock;
|
|
91
149
|
/** Lock file on disk, not a process counter. */
|
|
@@ -97,6 +155,27 @@ export declare class FileLedger implements Ledger {
|
|
|
97
155
|
close(): void;
|
|
98
156
|
appendDecision(signed: SignedDecisionRecord): Promise<void>;
|
|
99
157
|
appendDecisionChained(build: (prevRecordHash: string | null) => SignedDecisionRecord): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* What /healthz reports, from memory: the counts are kept on load and on
|
|
160
|
+
* every append. Rereading both files to count lines cost a second per call
|
|
161
|
+
* on a 100k-decision ledger (measured 17 Sep 2026), and the panel asks
|
|
162
|
+
* every five seconds. Null when the decision file could not be parsed on
|
|
163
|
+
* load; then the caller reads for itself and fails the way it always did.
|
|
164
|
+
*/
|
|
165
|
+
counts(): LedgerCounts | null;
|
|
166
|
+
/**
|
|
167
|
+
* Decisions stamped in [fromMs, toMs), read from the end of each overlapping
|
|
168
|
+
* piece so the cost is the window's, not the ledger's. With a limit, the
|
|
169
|
+
* newest rows of the window come back and `more` says the window went on,
|
|
170
|
+
* including in a closed piece.
|
|
171
|
+
*/
|
|
172
|
+
decisionsWindow(fromMs: number, toMs: number, limit?: number): Promise<{
|
|
173
|
+
rows: SignedDecisionRecord[];
|
|
174
|
+
more: boolean;
|
|
175
|
+
piecesTouched: string[];
|
|
176
|
+
}>;
|
|
177
|
+
/** Effects stamped in [fromMs, toMs), read from the end of each overlapping piece. */
|
|
178
|
+
effectsWindow(fromMs: number, toMs: number): Promise<LedgerEffect[]>;
|
|
100
179
|
appendEffect(row: EffectRow, witnessClass?: WitnessClass, resultHash?: string): Promise<void>;
|
|
101
180
|
private appendEffectUnlocked;
|
|
102
181
|
decisions(): Promise<SignedDecisionRecord[]>;
|