@tangle-network/agent-eval 0.190.1 → 0.191.0
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 +31 -0
- package/dist/adapters/http.d.ts +2 -2
- package/dist/{agent-profile-DNV_U6QR.d.ts → agent-profile-DDfIjdyS.d.ts} +3 -3
- package/dist/{agent-profile-DNV_U6QR.d.ts.map → agent-profile-DDfIjdyS.d.ts.map} +1 -1
- package/dist/analyst/index.d.ts +11 -11
- package/dist/analyst/index.js +5 -5
- package/dist/{insight-report-CvPkEIcC.d.ts → analyze-runs-CPiKLJFs.d.ts} +73 -3
- package/dist/analyze-runs-CPiKLJFs.d.ts.map +1 -0
- package/dist/{attribute-vocabulary-DLJ6303h.d.ts → attribute-vocabulary-CcXqHODT.d.ts} +23 -12
- package/dist/attribute-vocabulary-CcXqHODT.d.ts.map +1 -0
- package/dist/{benchmark-command-BXIr_k8D.js → benchmark-command-3tZJnD82.js} +8 -8
- package/dist/{benchmark-command-BXIr_k8D.js.map → benchmark-command-3tZJnD82.js.map} +1 -1
- package/dist/benchmarks/index.d.ts +3 -3
- package/dist/benchmarks/index.js +3 -3
- package/dist/campaign/index.d.ts +6 -6
- package/dist/campaign/index.js +9 -7
- package/dist/{campaign-B7q74uIh.js → campaign-C8iWEF0M.js} +10 -8
- package/dist/{campaign-B7q74uIh.js.map → campaign-C8iWEF0M.js.map} +1 -1
- package/dist/{campaign-evidence-D40-LHIL.js → campaign-evidence-BqjlTM3P.js} +10 -111
- package/dist/campaign-evidence-BqjlTM3P.js.map +1 -0
- package/dist/claim-V9Zqbq0J.js +54 -0
- package/dist/claim-V9Zqbq0J.js.map +1 -0
- package/dist/cli.js +99 -2
- package/dist/cli.js.map +1 -1
- package/dist/client-wFyDisxi.d.ts +236 -0
- package/dist/client-wFyDisxi.d.ts.map +1 -0
- package/dist/contract/index.d.ts +10 -11
- package/dist/contract/index.js +9 -139
- package/dist/contract/index.js.map +1 -1
- package/dist/{default-registry-B5HrwrPM.d.ts → default-registry-DK_9aVJf.d.ts} +6 -6
- package/dist/{default-registry-B5HrwrPM.d.ts.map → default-registry-DK_9aVJf.d.ts.map} +1 -1
- package/dist/{default-registry-16I-Bxlf.js → default-registry-xMLX160E.js} +3 -3
- package/dist/{default-registry-16I-Bxlf.js.map → default-registry-xMLX160E.js.map} +1 -1
- package/dist/{define-agent-eval-DPizhHfy.js → define-agent-eval-BE43YTm4.js} +27 -109
- package/dist/define-agent-eval-BE43YTm4.js.map +1 -0
- package/dist/{define-agent-eval-DblGedQk.d.ts → define-agent-eval-P28y7mI1.d.ts} +16 -20
- package/dist/define-agent-eval-P28y7mI1.d.ts.map +1 -0
- package/dist/diagnosis/index.d.ts +2 -2
- package/dist/diagnosis/index.js +1 -1
- package/dist/{dspy-rlm-engine-Cs5LzJ-f.js → dspy-rlm-engine-MfpwoEaj.js} +3 -3
- package/dist/{dspy-rlm-engine-Cs5LzJ-f.js.map → dspy-rlm-engine-MfpwoEaj.js.map} +1 -1
- package/dist/{engine-BkLCnZQX.d.ts → engine-vNiM02Ub.d.ts} +4 -4
- package/dist/{engine-BkLCnZQX.d.ts.map → engine-vNiM02Ub.d.ts.map} +1 -1
- package/dist/evaluation.d.ts +2 -2
- package/dist/{exact-types-CfpP2A4Z.d.ts → exact-types-KGxBWH4b.d.ts} +2 -2
- package/dist/{exact-types-CfpP2A4Z.d.ts.map → exact-types-KGxBWH4b.d.ts.map} +1 -1
- package/dist/experiment/index.d.ts +1 -1
- package/dist/experiment/index.js +2 -1
- package/dist/experiment/index.js.map +1 -1
- package/dist/{external-optimizer-process-BnSmrkgL.js → external-optimizer-process-xFTliuiK.js} +2 -2
- package/dist/{external-optimizer-process-BnSmrkgL.js.map → external-optimizer-process-xFTliuiK.js.map} +1 -1
- package/dist/{external-optimizer-subprocess-kbcPu5po.js → external-optimizer-subprocess-B234V9V0.js} +4 -27
- package/dist/external-optimizer-subprocess-B234V9V0.js.map +1 -0
- package/dist/{extract-usage-BrQ8mCLX.js → extract-usage-DFATEuWW.js} +2 -2
- package/dist/{extract-usage-BrQ8mCLX.js.map → extract-usage-DFATEuWW.js.map} +1 -1
- package/dist/{feedback-trajectory-BYjh7NEM.d.ts → feedback-trajectory-BXCGTRkz.d.ts} +3 -3
- package/dist/{feedback-trajectory-BYjh7NEM.d.ts.map → feedback-trajectory-BXCGTRkz.d.ts.map} +1 -1
- package/dist/hosted/index.d.ts +66 -6
- package/dist/hosted/index.d.ts.map +1 -1
- package/dist/hosted/index.js +2 -2
- package/dist/{index-CJv56rhX.d.ts → index-CzfnASj_.d.ts} +18 -11
- package/dist/index-CzfnASj_.d.ts.map +1 -0
- package/dist/{index-2e_MTznj.d.ts → index-D-TN7ZRM.d.ts} +6 -6
- package/dist/{index-2e_MTznj.d.ts.map → index-D-TN7ZRM.d.ts.map} +1 -1
- package/dist/{index-CWoIJHmy.d.ts → index-DH-MWH5f.d.ts} +10 -104
- package/dist/index-DH-MWH5f.d.ts.map +1 -0
- package/dist/index.d.ts +16 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -11
- package/dist/jev.d.ts +2 -2
- package/dist/{kind-factory-ByZeXDqF.js → kind-factory-CdPPHtGZ.js} +10 -26
- package/dist/kind-factory-CdPPHtGZ.js.map +1 -0
- package/dist/{llm-judge-BnxAv7ed.js → llm-judge-NJOHM0iA.js} +9 -1426
- package/dist/llm-judge-NJOHM0iA.js.map +1 -0
- package/dist/{matrix-CNypzP-v.d.ts → matrix-CgqdFm5N.d.ts} +2 -2
- package/dist/{matrix-CNypzP-v.d.ts.map → matrix-CgqdFm5N.d.ts.map} +1 -1
- package/dist/multishot/golden/index.d.ts +1 -1
- package/dist/multishot/index.d.ts +2 -2
- package/dist/openapi.json +1 -1
- package/dist/{otel-spans-CQY1pNiN.js → otel-spans-DS7t1t1J.js} +8 -19
- package/dist/otel-spans-DS7t1t1J.js.map +1 -0
- package/dist/otlp-attributes-D9ptWaIJ.js +53 -0
- package/dist/otlp-attributes-D9ptWaIJ.js.map +1 -0
- package/dist/{prime-protocol-Fwvctmhm.d.ts → prime-protocol-T0i6xcAs.d.ts} +2 -2
- package/dist/{prime-protocol-Fwvctmhm.d.ts.map → prime-protocol-T0i6xcAs.d.ts.map} +1 -1
- package/dist/{produced-state-BXjXJHJT.js → produced-state-CY-L7CqS.js} +3 -3
- package/dist/{produced-state-BXjXJHJT.js.map → produced-state-CY-L7CqS.js.map} +1 -1
- package/dist/{registry-mAQVtuLx.d.ts → registry-Bu3lNole.d.ts} +3 -3
- package/dist/{registry-mAQVtuLx.d.ts.map → registry-Bu3lNole.d.ts.map} +1 -1
- package/dist/rl.d.ts +1 -1
- package/dist/rl.js +1 -1
- package/dist/{run-record-DnMFyg_g.js → run-record-tNnlQIrD.js} +2 -10
- package/dist/{run-record-DnMFyg_g.js.map → run-record-tNnlQIrD.js.map} +1 -1
- package/dist/search-ledger-CQ8Uy4FK.js +1451 -0
- package/dist/search-ledger-CQ8Uy4FK.js.map +1 -0
- package/dist/search-ledger-file-EvdDZRhv.js +27 -0
- package/dist/search-ledger-file-EvdDZRhv.js.map +1 -0
- package/dist/search-shipper-C55V7HE3.js +781 -0
- package/dist/search-shipper-C55V7HE3.js.map +1 -0
- package/dist/{semantic-concept-judge-ClB73cei.js → semantic-concept-judge-Bt7bXAAo.js} +3 -3
- package/dist/{semantic-concept-judge-ClB73cei.js.map → semantic-concept-judge-Bt7bXAAo.js.map} +1 -1
- package/dist/{skillopt-optimization-method-l2vDBtKp.js → skillopt-optimization-method-DwO3TOAV.js} +6 -5
- package/dist/{skillopt-optimization-method-l2vDBtKp.js.map → skillopt-optimization-method-DwO3TOAV.js.map} +1 -1
- package/dist/{statistical-heldout-CLpjYffJ.d.ts → statistical-heldout-BED3wBtX.d.ts} +2 -2
- package/dist/{statistical-heldout-CLpjYffJ.d.ts.map → statistical-heldout-BED3wBtX.d.ts.map} +1 -1
- package/dist/{store-otlp-84DH0OEH.js → store-otlp-BPowgPm6.js} +2 -2
- package/dist/{store-otlp-84DH0OEH.js.map → store-otlp-BPowgPm6.js.map} +1 -1
- package/dist/{store-tool-spans-DUiqyHvR.js → store-tool-spans-BcC_5ef-.js} +5 -5
- package/dist/{store-tool-spans-DUiqyHvR.js.map → store-tool-spans-BcC_5ef-.js.map} +1 -1
- package/dist/{store-tool-spans-BBXs8hDP.d.ts → store-tool-spans-Cfh9XUCm.d.ts} +5 -9
- package/dist/store-tool-spans-Cfh9XUCm.d.ts.map +1 -0
- package/dist/{task-failure-attributes-B34TiMBe.js → task-failure-attributes-CEqEfuIY.js} +17 -9
- package/dist/task-failure-attributes-CEqEfuIY.js.map +1 -0
- package/dist/{tool-groups-W7MJobX_.d.ts → tool-groups-D-mkeN0N.d.ts} +3 -3
- package/dist/tool-groups-D-mkeN0N.d.ts.map +1 -0
- package/dist/trace-attributes.d.ts +1 -1
- package/dist/trace-attributes.js +23 -75
- package/dist/trace-attributes.js.map +1 -1
- package/dist/trace-repair/index.d.ts +1 -1
- package/dist/traces.d.ts +11 -18
- package/dist/traces.d.ts.map +1 -1
- package/dist/traces.js +16 -16
- package/dist/traces.js.map +1 -1
- package/dist/{types-BvmWNjjs.d.ts → types-BmQRs661.d.ts} +2 -2
- package/dist/{types-BvmWNjjs.d.ts.map → types-BmQRs661.d.ts.map} +1 -1
- package/dist/{types-Bay1dh2o.d.ts → types-CeDfCRXp.d.ts} +11 -16
- package/dist/types-CeDfCRXp.d.ts.map +1 -0
- package/dist/{types-Cvqy7asY.d.ts → types-ClK-sHs8.d.ts} +2 -2
- package/dist/{types-Cvqy7asY.d.ts.map → types-ClK-sHs8.d.ts.map} +1 -1
- package/dist/wire/index.d.ts +1 -1
- package/docs/hosted-ingest-spec.md +92 -133
- package/docs/public-api.md +46 -35
- package/docs/search-ledger.md +17 -0
- package/package.json +2 -2
- package/dist/analyze-runs-BvK4-wSs.d.ts +0 -75
- package/dist/analyze-runs-BvK4-wSs.d.ts.map +0 -1
- package/dist/attribute-vocabulary-DLJ6303h.d.ts.map +0 -1
- package/dist/campaign-evidence-D40-LHIL.js.map +0 -1
- package/dist/client-BhEyAdfe.d.ts +0 -202
- package/dist/client-BhEyAdfe.d.ts.map +0 -1
- package/dist/client-QwJpsNYU.js +0 -655
- package/dist/client-QwJpsNYU.js.map +0 -1
- package/dist/define-agent-eval-DPizhHfy.js.map +0 -1
- package/dist/define-agent-eval-DblGedQk.d.ts.map +0 -1
- package/dist/external-optimizer-subprocess-kbcPu5po.js.map +0 -1
- package/dist/index-CJv56rhX.d.ts.map +0 -1
- package/dist/index-CWoIJHmy.d.ts.map +0 -1
- package/dist/insight-report-CvPkEIcC.d.ts.map +0 -1
- package/dist/kind-factory-ByZeXDqF.js.map +0 -1
- package/dist/llm-judge-BnxAv7ed.js.map +0 -1
- package/dist/otel-spans-CQY1pNiN.js.map +0 -1
- package/dist/otlp-attributes-BpC_blLF.js +0 -83
- package/dist/otlp-attributes-BpC_blLF.js.map +0 -1
- package/dist/store-tool-spans-BBXs8hDP.d.ts.map +0 -1
- package/dist/task-failure-attributes-B34TiMBe.js.map +0 -1
- package/dist/tool-groups-W7MJobX_.d.ts.map +0 -1
- package/dist/types-Bay1dh2o.d.ts.map +0 -1
|
@@ -0,0 +1,1451 @@
|
|
|
1
|
+
import { a as hashCanonical, i as compareCodeUnits, r as canonicalString } from "./canonical-BsZVgBhW.js";
|
|
2
|
+
import { i as modelHasSnapshot, l as searchCellRunId } from "./run-record-BRlM99rX.js";
|
|
3
|
+
import { n as replayLedgerText, t as FileLedgerJournal } from "./journal-c3qY0SDP.js";
|
|
4
|
+
import "./ledger-core/index.js";
|
|
5
|
+
import { a as SearchLedgerError, i as SearchLedgerConflictError, o as SearchLedgerIntegrityError, t as SEARCH_LEDGER_FILE_CONTEXT } from "./search-ledger-file-EvdDZRhv.js";
|
|
6
|
+
import { n as evaluationClaimSchema } from "./claim-V9Zqbq0J.js";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
//#region src/campaign/search-ledger-ordering.ts
|
|
10
|
+
function artifactKey(artifact) {
|
|
11
|
+
return canonicalString(artifact);
|
|
12
|
+
}
|
|
13
|
+
function compareStrings(a, b) {
|
|
14
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/campaign/search-ledger-types.ts
|
|
18
|
+
/**
|
|
19
|
+
* Schema tag every search-ledger entry carries. A ledger written under another
|
|
20
|
+
* tag, such as the retired `tangle.search-ledger.v1` candidate-slot format, is
|
|
21
|
+
* refused with its tag named; it is never translated.
|
|
22
|
+
*/
|
|
23
|
+
const SEARCH_LEDGER_SCHEMA = "tangle.search-ledger.2026-09";
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/campaign/search-state.ts
|
|
26
|
+
/**
|
|
27
|
+
* `SearchState`: the one projection of a search ledger.
|
|
28
|
+
*
|
|
29
|
+
* The same code checks every invariant for the producer's journal, the kernel,
|
|
30
|
+
* and the Intelligence verifier, and builds the read model they share. It keeps
|
|
31
|
+
* compact indexes (nodes, edges, cells, per-unit score sums, running audit
|
|
32
|
+
* totals), not the raw event list, and applies each entry in time proportional
|
|
33
|
+
* to that entry, so a search of any length appends at a constant cost.
|
|
34
|
+
*
|
|
35
|
+
* The journal asks for a snapshot after every append. A snapshot is a
|
|
36
|
+
* `SearchStateView` that reads the live indexes, so taking one costs nothing.
|
|
37
|
+
* The view is valid until the state applies its next entry; a read after that
|
|
38
|
+
* throws rather than returning numbers from a later ledger position. Read the
|
|
39
|
+
* ledger's state again to see newer entries. The audit, header, head and claim
|
|
40
|
+
* on a view are plain values and stay readable.
|
|
41
|
+
*/
|
|
42
|
+
/** Deterministic node id: identical content in one search is one node. */
|
|
43
|
+
function searchNodeId(searchId, artifactDigest) {
|
|
44
|
+
return `node_${shortDigest([searchId, artifactDigest])}`;
|
|
45
|
+
}
|
|
46
|
+
/** Deterministic cell id: one node on one task in one split at one repeat. */
|
|
47
|
+
function searchCellId(searchId, nodeId, taskId, split, rep) {
|
|
48
|
+
return `cell_${shortDigest([
|
|
49
|
+
searchId,
|
|
50
|
+
nodeId,
|
|
51
|
+
taskId,
|
|
52
|
+
split,
|
|
53
|
+
rep
|
|
54
|
+
])}`;
|
|
55
|
+
}
|
|
56
|
+
/** Digest a split's task list in taskId order. */
|
|
57
|
+
function searchTaskSetDigest(tasks) {
|
|
58
|
+
return hashCanonical([...tasks].sort((left, right) => compareCodeUnits(left.taskId, right.taskId)));
|
|
59
|
+
}
|
|
60
|
+
/** 128 bits of SHA-256 over the canonical encoding: collision-safe and short
|
|
61
|
+
* enough for directory names. */
|
|
62
|
+
function shortDigest(value) {
|
|
63
|
+
return createHash("sha256").update(canonicalString(value)).digest("hex").slice(0, 32);
|
|
64
|
+
}
|
|
65
|
+
const MAX_LABEL_CHARS = 200;
|
|
66
|
+
const USD_TOLERANCE = 1e-9;
|
|
67
|
+
const SPLITS = [
|
|
68
|
+
"train",
|
|
69
|
+
"selection",
|
|
70
|
+
"test"
|
|
71
|
+
];
|
|
72
|
+
/**
|
|
73
|
+
* Per-unit means of scored cells, in unitId order. Each unit sums its cells in
|
|
74
|
+
* cellId order, so the result depends on the set of cells alone, never on the
|
|
75
|
+
* order they settled in: a verifier that reads the same cells from a database
|
|
76
|
+
* gets the same bits.
|
|
77
|
+
*/
|
|
78
|
+
function searchUnitScores(cells) {
|
|
79
|
+
const byUnit = /* @__PURE__ */ new Map();
|
|
80
|
+
for (const cell of cells) {
|
|
81
|
+
const unit = byUnit.get(cell.unitId);
|
|
82
|
+
if (unit) unit.push(cell);
|
|
83
|
+
else byUnit.set(cell.unitId, [cell]);
|
|
84
|
+
}
|
|
85
|
+
return [...byUnit.entries()].sort(([left], [right]) => compareCodeUnits(left, right)).map(([unitId, unitCells]) => {
|
|
86
|
+
let sum = 0;
|
|
87
|
+
for (const cell of unitCells.sort((left, right) => compareCodeUnits(left.cellId, right.cellId))) sum += cell.score;
|
|
88
|
+
return {
|
|
89
|
+
unitId,
|
|
90
|
+
sum,
|
|
91
|
+
count: unitCells.length,
|
|
92
|
+
mean: sum / unitCells.length
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The pure, incremental projection of one search ledger. `apply` refuses any
|
|
98
|
+
* entry that breaks an invariant; the journal then discards this instance and
|
|
99
|
+
* replays into a new one, so a refused entry leaves no partial state behind.
|
|
100
|
+
*/
|
|
101
|
+
var SearchState = class {
|
|
102
|
+
searchId;
|
|
103
|
+
version = 0;
|
|
104
|
+
header = null;
|
|
105
|
+
closed = null;
|
|
106
|
+
head = null;
|
|
107
|
+
lastOccurredAt = Number.NEGATIVE_INFINITY;
|
|
108
|
+
lastOccurredAtText = null;
|
|
109
|
+
tasks = /* @__PURE__ */ new Map();
|
|
110
|
+
nodes = /* @__PURE__ */ new Map();
|
|
111
|
+
nodeOrder = [];
|
|
112
|
+
nodeByDigest = /* @__PURE__ */ new Map();
|
|
113
|
+
edges = /* @__PURE__ */ new Map();
|
|
114
|
+
edgeOrder = [];
|
|
115
|
+
cells = /* @__PURE__ */ new Map();
|
|
116
|
+
operations = /* @__PURE__ */ new Map();
|
|
117
|
+
selectedNodeId = null;
|
|
118
|
+
claimUsedUsd = 0;
|
|
119
|
+
audit;
|
|
120
|
+
constructor(searchId) {
|
|
121
|
+
this.searchId = searchId;
|
|
122
|
+
this.audit = {
|
|
123
|
+
searchId,
|
|
124
|
+
eventCount: 0,
|
|
125
|
+
headHash: null,
|
|
126
|
+
status: "open",
|
|
127
|
+
closeReason: null,
|
|
128
|
+
nodes: 0,
|
|
129
|
+
nodesWithoutEdge: 0,
|
|
130
|
+
undecidedNodes: 0,
|
|
131
|
+
selectedNodeId: null,
|
|
132
|
+
edges: {
|
|
133
|
+
explicit: 0,
|
|
134
|
+
correlated: 0,
|
|
135
|
+
unknown: 0,
|
|
136
|
+
reproposals: 0
|
|
137
|
+
},
|
|
138
|
+
cells: {
|
|
139
|
+
allocated: 0,
|
|
140
|
+
settled: 0,
|
|
141
|
+
cancelled: 0,
|
|
142
|
+
open: 0
|
|
143
|
+
},
|
|
144
|
+
attempts: 0,
|
|
145
|
+
outcomes: {
|
|
146
|
+
passed: 0,
|
|
147
|
+
failed: 0,
|
|
148
|
+
errored: 0
|
|
149
|
+
},
|
|
150
|
+
operations: {
|
|
151
|
+
started: 0,
|
|
152
|
+
recorded: 0,
|
|
153
|
+
open: 0
|
|
154
|
+
},
|
|
155
|
+
spend: {
|
|
156
|
+
knownUsd: 0,
|
|
157
|
+
floorUsd: 0,
|
|
158
|
+
unknownCostCells: 0,
|
|
159
|
+
unknownCostOperations: 0,
|
|
160
|
+
committedUsd: 0,
|
|
161
|
+
openReservationUsd: 0,
|
|
162
|
+
overspendUsd: 0,
|
|
163
|
+
unreservedCells: 0,
|
|
164
|
+
boxMinutes: 0
|
|
165
|
+
},
|
|
166
|
+
tokens: {
|
|
167
|
+
inputTokens: 0,
|
|
168
|
+
outputTokens: 0,
|
|
169
|
+
cachedTokens: 0,
|
|
170
|
+
unknownTokenAttempts: 0
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
apply(entry, index) {
|
|
175
|
+
const event = entry.event;
|
|
176
|
+
if (this.closed) throw integrity(`event ${event.eventId} follows search-closed ${this.closed.eventId}`);
|
|
177
|
+
const occurredAt = Date.parse(event.occurredAt);
|
|
178
|
+
if (occurredAt < this.lastOccurredAt) throw integrity(`event ${event.eventId} occurred before the preceding event`);
|
|
179
|
+
assertUnique(event.artifacts.map(artifactKey), "artifact receipt", event.eventId);
|
|
180
|
+
if (event.kind === "search-opened") {
|
|
181
|
+
if (index !== 0 || this.header) throw integrity("search-opened must be the first and only opening event");
|
|
182
|
+
this.open(event);
|
|
183
|
+
} else {
|
|
184
|
+
if (!this.header) throw integrity(`event ${event.eventId} appears before search-opened`);
|
|
185
|
+
this.applyEvent(event, index);
|
|
186
|
+
}
|
|
187
|
+
this.lastOccurredAt = occurredAt;
|
|
188
|
+
this.lastOccurredAtText = event.occurredAt;
|
|
189
|
+
this.head = {
|
|
190
|
+
sequence: index,
|
|
191
|
+
entryHash: entry.entryHash
|
|
192
|
+
};
|
|
193
|
+
this.audit.eventCount = index + 1;
|
|
194
|
+
this.audit.headHash = entry.entryHash;
|
|
195
|
+
this.version += 1;
|
|
196
|
+
}
|
|
197
|
+
snapshot() {
|
|
198
|
+
return new SearchStateView(this, this.version);
|
|
199
|
+
}
|
|
200
|
+
/** @internal Views check this before every read of the live indexes. */
|
|
201
|
+
isAt(version) {
|
|
202
|
+
return this.version === version;
|
|
203
|
+
}
|
|
204
|
+
applyEvent(event, index) {
|
|
205
|
+
switch (event.kind) {
|
|
206
|
+
case "operation-started":
|
|
207
|
+
this.startOperation(event);
|
|
208
|
+
return;
|
|
209
|
+
case "operation-recorded":
|
|
210
|
+
this.recordOperation(event);
|
|
211
|
+
return;
|
|
212
|
+
case "node-registered":
|
|
213
|
+
this.registerNode(event, index);
|
|
214
|
+
return;
|
|
215
|
+
case "edge-recorded":
|
|
216
|
+
this.recordEdge(event, index);
|
|
217
|
+
return;
|
|
218
|
+
case "cell-allocated":
|
|
219
|
+
this.allocateCell(event, index);
|
|
220
|
+
return;
|
|
221
|
+
case "cell-settled":
|
|
222
|
+
this.settleCell(event, index);
|
|
223
|
+
return;
|
|
224
|
+
case "cell-cancelled":
|
|
225
|
+
this.cancelCell(event, index);
|
|
226
|
+
return;
|
|
227
|
+
case "node-decided":
|
|
228
|
+
this.decideNode(event, index);
|
|
229
|
+
return;
|
|
230
|
+
case "search-closed":
|
|
231
|
+
this.close(event);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
open(event) {
|
|
236
|
+
if (event.containment?.searchId === this.searchId) throw integrity("a search cannot contain itself");
|
|
237
|
+
if (event.derivedFrom?.searchId === this.searchId) throw integrity("a search cannot derive from itself");
|
|
238
|
+
const units = {
|
|
239
|
+
train: /* @__PURE__ */ new Set(),
|
|
240
|
+
selection: /* @__PURE__ */ new Set(),
|
|
241
|
+
test: /* @__PURE__ */ new Set()
|
|
242
|
+
};
|
|
243
|
+
for (const split of SPLITS) {
|
|
244
|
+
const { tasks, taskSetDigest } = event.splits[split];
|
|
245
|
+
if (searchTaskSetDigest(tasks) !== taskSetDigest) throw integrity(`the ${split} split's taskSetDigest does not match its tasks`);
|
|
246
|
+
for (const task of tasks) {
|
|
247
|
+
const owner = this.tasks.get(task.taskId);
|
|
248
|
+
if (owner) throw integrity(`task ${task.taskId} is in both the ${owner.split} and ${split} splits`);
|
|
249
|
+
this.tasks.set(task.taskId, {
|
|
250
|
+
split,
|
|
251
|
+
unitId: task.unitId
|
|
252
|
+
});
|
|
253
|
+
units[split].add(task.unitId);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (event.splits.heldOutUnits) {
|
|
257
|
+
const seen = [...units.test].filter((unit) => units.train.has(unit) || units.selection.has(unit));
|
|
258
|
+
if (seen.length > 0) throw integrity(`test units ${seen.join(", ")} also appear in train or selection, but the splits declare heldOutUnits`);
|
|
259
|
+
}
|
|
260
|
+
const { budget } = event;
|
|
261
|
+
if (budget.maxUsd !== null && budget.reservedClaimUsd > budget.maxUsd + USD_TOLERANCE) throw integrity(`the claim reserve $${budget.reservedClaimUsd} exceeds the search cap $${budget.maxUsd}`);
|
|
262
|
+
this.header = event;
|
|
263
|
+
}
|
|
264
|
+
startOperation(event) {
|
|
265
|
+
if (this.operations.has(event.operationId)) throw integrity(`operation ${event.operationId} was started twice`);
|
|
266
|
+
this.admit(event.reservation, false, `operation ${event.operationId}`);
|
|
267
|
+
this.operations.set(event.operationId, {
|
|
268
|
+
operationId: event.operationId,
|
|
269
|
+
operationKind: event.operationKind,
|
|
270
|
+
reservation: event.reservation,
|
|
271
|
+
recorded: false,
|
|
272
|
+
spentUsd: 0
|
|
273
|
+
});
|
|
274
|
+
this.audit.operations.started += 1;
|
|
275
|
+
this.audit.operations.open += 1;
|
|
276
|
+
this.audit.spend.openReservationUsd += event.reservation?.usd ?? 0;
|
|
277
|
+
}
|
|
278
|
+
recordOperation(event) {
|
|
279
|
+
const operation = this.operations.get(event.operationId);
|
|
280
|
+
if (!operation) throw integrity(`operation ${event.operationId} was recorded before it started`);
|
|
281
|
+
if (operation.recorded) throw integrity(`operation ${event.operationId} was recorded twice`);
|
|
282
|
+
if (operation.operationKind !== event.operationKind) throw integrity(`operation ${event.operationId} started as ${operation.operationKind} but recorded as ${event.operationKind}`);
|
|
283
|
+
const cost = this.book(event.accounting);
|
|
284
|
+
if (cost.unknown) this.audit.spend.unknownCostOperations += 1;
|
|
285
|
+
const reserved = operation.reservation?.usd ?? 0;
|
|
286
|
+
this.audit.spend.openReservationUsd -= reserved;
|
|
287
|
+
if (operation.reservation) this.audit.spend.overspendUsd += Math.max(0, cost.usd - reserved);
|
|
288
|
+
operation.recorded = true;
|
|
289
|
+
operation.spentUsd = cost.usd;
|
|
290
|
+
this.audit.operations.recorded += 1;
|
|
291
|
+
this.audit.operations.open -= 1;
|
|
292
|
+
}
|
|
293
|
+
registerNode(event, index) {
|
|
294
|
+
const expected = searchNodeId(this.searchId, event.artifactDigest);
|
|
295
|
+
if (event.nodeId !== expected) throw integrity(`node ${event.nodeId} does not match its artifact digest (expected ${expected})`);
|
|
296
|
+
if (this.nodes.has(event.nodeId)) throw integrity(`node ${event.nodeId} was registered twice; a re-proposal is a second edge into it`);
|
|
297
|
+
assertUnique(event.surfaces.map((surface) => surface.surfaceId), "surfaceId", event.eventId);
|
|
298
|
+
const maxNodes = this.header.budget.maxNodes;
|
|
299
|
+
if (maxNodes !== null && this.nodes.size + 1 > maxNodes) throw integrity(`node ${event.nodeId} exceeds the search's maxNodes ${maxNodes}`);
|
|
300
|
+
this.nodes.set(event.nodeId, {
|
|
301
|
+
event,
|
|
302
|
+
ordinal: this.nodeOrder.length,
|
|
303
|
+
registeredSequence: index,
|
|
304
|
+
updatedSequence: index,
|
|
305
|
+
edgeIds: [],
|
|
306
|
+
parents: [],
|
|
307
|
+
parentKeys: /* @__PURE__ */ new Set(),
|
|
308
|
+
primaryParentId: null,
|
|
309
|
+
depth: null,
|
|
310
|
+
children: [],
|
|
311
|
+
decisions: [],
|
|
312
|
+
finalist: false,
|
|
313
|
+
cellIds: [],
|
|
314
|
+
scoredCells: 0,
|
|
315
|
+
spend: {
|
|
316
|
+
knownUsd: 0,
|
|
317
|
+
floorUsd: 0,
|
|
318
|
+
unknownCostCells: 0,
|
|
319
|
+
boxMinutes: 0
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
this.nodeOrder.push(event.nodeId);
|
|
323
|
+
this.nodeByDigest.set(event.artifactDigest, event.nodeId);
|
|
324
|
+
this.audit.nodes += 1;
|
|
325
|
+
this.audit.nodesWithoutEdge += 1;
|
|
326
|
+
this.audit.undecidedNodes += 1;
|
|
327
|
+
}
|
|
328
|
+
recordEdge(event, index) {
|
|
329
|
+
if (this.edges.has(event.edgeId)) throw integrity(`edge ${event.edgeId} was recorded twice`);
|
|
330
|
+
const child = this.nodes.get(event.childNodeId);
|
|
331
|
+
if (!child) throw integrity(`edge ${event.edgeId} names unregistered child ${event.childNodeId}`);
|
|
332
|
+
if (event.label.length > MAX_LABEL_CHARS) throw integrity(`edge ${event.edgeId} label exceeds ${MAX_LABEL_CHARS} characters`);
|
|
333
|
+
if (event.diffs.length !== event.parents.length) throw integrity(`edge ${event.edgeId} needs one diff per parent`);
|
|
334
|
+
assertUnique(event.parents.map((parent) => canonicalString(parent)), "parent", event.eventId);
|
|
335
|
+
const { operator, attribution, parents } = event;
|
|
336
|
+
if (operator === "seed" !== (event.proposer === null)) throw integrity(`edge ${event.edgeId}: exactly the seed edge has no proposer`);
|
|
337
|
+
if (operator === "seed" && (parents.length > 0 || attribution === "unknown")) throw integrity(`seed edge ${event.edgeId} must have no parents and a known attribution`);
|
|
338
|
+
if (attribution === "unknown" && parents.length > 0) throw integrity(`edge ${event.edgeId} has unknown attribution but names parents`);
|
|
339
|
+
if (operator !== "seed" && attribution !== "unknown" && parents.length === 0) throw integrity(`${operator} edge ${event.edgeId} names no parent`);
|
|
340
|
+
if (operator === "merge" && attribution !== "unknown" && parents.length < 2) throw integrity(`merge edge ${event.edgeId} needs at least two parents`);
|
|
341
|
+
const operationId = event.proposer?.operationId ?? null;
|
|
342
|
+
if (operationId !== null && !this.operations.has(operationId)) throw integrity(`edge ${event.edgeId} names operation ${operationId}, which never started`);
|
|
343
|
+
const inSearch = [];
|
|
344
|
+
for (const parent of parents) {
|
|
345
|
+
if (parent.searchId !== this.searchId) {
|
|
346
|
+
const from = this.header.derivedFrom;
|
|
347
|
+
if (operator !== "derive" || parents.length !== 1 || !from || from.searchId !== parent.searchId || from.nodeId !== parent.nodeId) throw integrity(`edge ${event.edgeId} names a parent in search ${parent.searchId}; only a derive edge matching derivedFrom may`);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (operator === "derive") throw integrity(`derive edge ${event.edgeId} must name the parent in derivedFrom`);
|
|
351
|
+
const record = this.nodes.get(parent.nodeId);
|
|
352
|
+
if (!record) throw integrity(`edge ${event.edgeId} names unregistered parent ${parent.nodeId}`);
|
|
353
|
+
if (record.ordinal >= child.ordinal) throw integrity(`edge ${event.edgeId}: parent ${parent.nodeId} registered after child ${child.event.nodeId}`);
|
|
354
|
+
if (record.edgeIds.length === 0) throw integrity(`edge ${event.edgeId}: parent ${parent.nodeId} has no edge of its own yet`);
|
|
355
|
+
inSearch.push(record);
|
|
356
|
+
}
|
|
357
|
+
if (child.edgeIds.length === 0) {
|
|
358
|
+
const primary = inSearch[0];
|
|
359
|
+
child.primaryParentId = primary?.event.nodeId ?? null;
|
|
360
|
+
child.depth = operator === "seed" || operator === "derive" ? 0 : primary && primary.depth !== null ? primary.depth + 1 : null;
|
|
361
|
+
this.audit.nodesWithoutEdge -= 1;
|
|
362
|
+
} else this.audit.edges.reproposals += 1;
|
|
363
|
+
child.edgeIds.push(event.edgeId);
|
|
364
|
+
for (const parent of parents) {
|
|
365
|
+
const key = canonicalString(parent);
|
|
366
|
+
if (child.parentKeys.has(key)) continue;
|
|
367
|
+
child.parentKeys.add(key);
|
|
368
|
+
child.parents.push(parent);
|
|
369
|
+
}
|
|
370
|
+
for (const parent of inSearch) if (!parent.children.includes(child.event.nodeId)) parent.children.push(child.event.nodeId);
|
|
371
|
+
child.updatedSequence = index;
|
|
372
|
+
this.edges.set(event.edgeId, event);
|
|
373
|
+
this.edgeOrder.push(event.edgeId);
|
|
374
|
+
this.audit.edges[attribution] += 1;
|
|
375
|
+
}
|
|
376
|
+
allocateCell(event, index) {
|
|
377
|
+
const expected = searchCellId(this.searchId, event.nodeId, event.taskId, event.split, event.rep);
|
|
378
|
+
if (event.cellId !== expected) throw integrity(`cell ${event.cellId} does not match its coordinates (expected ${expected})`);
|
|
379
|
+
if (this.cells.has(event.cellId)) throw integrity(`cell ${event.cellId} was allocated twice`);
|
|
380
|
+
const node = this.nodes.get(event.nodeId);
|
|
381
|
+
if (!node) throw integrity(`cell ${event.cellId} names unregistered node ${event.nodeId}`);
|
|
382
|
+
if (node.edgeIds.length === 0) throw integrity(`cell ${event.cellId}: node ${event.nodeId} has no edge yet`);
|
|
383
|
+
const task = this.tasks.get(event.taskId);
|
|
384
|
+
if (!task || task.split !== event.split) throw integrity(`cell ${event.cellId}: task ${event.taskId} is not in the ${event.split} split`);
|
|
385
|
+
if (task.unitId !== event.unitId) throw integrity(`cell ${event.cellId}: task ${event.taskId} samples unit ${task.unitId}`);
|
|
386
|
+
assertStageSplit(event.cellId, event.stage, event.split, node.ordinal === 0);
|
|
387
|
+
if (event.split === "test" && node.ordinal !== 0 && !node.finalist) throw integrity(`cell ${event.cellId}: the test split is sealed; only the root and finalists run on it`);
|
|
388
|
+
const maxCells = this.header.budget.maxCells;
|
|
389
|
+
if (maxCells !== null && this.audit.cells.allocated + 1 > maxCells) throw integrity(`cell ${event.cellId} exceeds the search's maxCells ${maxCells}`);
|
|
390
|
+
this.admit(event.reservation, event.stage === "claim", `cell ${event.cellId}`);
|
|
391
|
+
this.cells.set(event.cellId, {
|
|
392
|
+
cellId: event.cellId,
|
|
393
|
+
nodeId: event.nodeId,
|
|
394
|
+
taskId: event.taskId,
|
|
395
|
+
unitId: event.unitId,
|
|
396
|
+
split: event.split,
|
|
397
|
+
rep: event.rep,
|
|
398
|
+
stage: event.stage,
|
|
399
|
+
lane: event.lane,
|
|
400
|
+
reservation: event.reservation,
|
|
401
|
+
attempts: 0,
|
|
402
|
+
lastRunId: null,
|
|
403
|
+
outcome: null,
|
|
404
|
+
score: null,
|
|
405
|
+
final: false,
|
|
406
|
+
cancelled: null,
|
|
407
|
+
spentUsd: 0,
|
|
408
|
+
costKnown: true,
|
|
409
|
+
traceId: null,
|
|
410
|
+
allocatedSequence: index,
|
|
411
|
+
updatedSequence: index
|
|
412
|
+
});
|
|
413
|
+
node.cellIds.push(event.cellId);
|
|
414
|
+
node.updatedSequence = index;
|
|
415
|
+
this.audit.cells.allocated += 1;
|
|
416
|
+
this.audit.cells.open += 1;
|
|
417
|
+
this.audit.spend.openReservationUsd += event.reservation?.usd ?? 0;
|
|
418
|
+
if (event.stage === "claim") this.claimUsedUsd += event.reservation?.usd ?? 0;
|
|
419
|
+
}
|
|
420
|
+
settleCell(event, index) {
|
|
421
|
+
const cell = this.cells.get(event.cellId);
|
|
422
|
+
if (!cell) throw integrity(`attempt ${event.eventId} names unallocated cell ${event.cellId}`);
|
|
423
|
+
if (cell.cancelled) throw integrity(`cell ${event.cellId} was settled after it was cancelled`);
|
|
424
|
+
if (cell.final) throw integrity(`cell ${event.cellId} was attempted again after a final ${cell.outcome} outcome; only a retryable error admits another attempt`);
|
|
425
|
+
if (event.attempt !== cell.attempts + 1) throw integrity(`cell ${event.cellId} attempt ${event.attempt} is not contiguous (expected ${cell.attempts + 1})`);
|
|
426
|
+
const runId = searchCellRunId({
|
|
427
|
+
cellId: event.cellId,
|
|
428
|
+
attempt: event.attempt
|
|
429
|
+
});
|
|
430
|
+
if (event.runId !== runId) throw integrity(`cell ${event.cellId} attempt ${event.attempt} must have runId ${runId}`);
|
|
431
|
+
const node = this.nodes.get(cell.nodeId);
|
|
432
|
+
const declared = new Set(node.event.surfaces.map((surface) => surface.surfaceId));
|
|
433
|
+
assertUnique(event.surfaceEvidence.map((evidence) => evidence.surfaceId), "surface evidence", event.eventId);
|
|
434
|
+
for (const evidence of event.surfaceEvidence) if (!declared.has(evidence.surfaceId)) throw integrity(`attempt ${event.eventId} reports surface ${evidence.surfaceId}, which node ${cell.nodeId} does not declare`);
|
|
435
|
+
if (event.artifacts.filter((artifact) => artifact.role === "run-record").length > 1) throw integrity(`attempt ${event.eventId} binds more than one RunRecord`);
|
|
436
|
+
const before = this.reservationParts(cell);
|
|
437
|
+
const cost = this.book(event.accounting);
|
|
438
|
+
if (cell.attempts === 0) {
|
|
439
|
+
this.audit.cells.settled += 1;
|
|
440
|
+
this.audit.cells.open -= 1;
|
|
441
|
+
if (cell.reservation === null) this.audit.spend.unreservedCells += 1;
|
|
442
|
+
}
|
|
443
|
+
if (cost.unknown && cell.costKnown) {
|
|
444
|
+
cell.costKnown = false;
|
|
445
|
+
this.audit.spend.unknownCostCells += 1;
|
|
446
|
+
node.spend.unknownCostCells += 1;
|
|
447
|
+
}
|
|
448
|
+
cell.attempts = event.attempt;
|
|
449
|
+
cell.lastRunId = runId;
|
|
450
|
+
cell.outcome = event.outcome.status;
|
|
451
|
+
cell.spentUsd += cost.usd;
|
|
452
|
+
cell.updatedSequence = index;
|
|
453
|
+
if ("traceId" in event.traceRef) cell.traceId = event.traceRef.traceId;
|
|
454
|
+
if (event.outcome.status === "errored") cell.final = !event.outcome.error.retryable;
|
|
455
|
+
else {
|
|
456
|
+
cell.final = true;
|
|
457
|
+
cell.score = event.outcome.score;
|
|
458
|
+
node.scoredCells += 1;
|
|
459
|
+
}
|
|
460
|
+
const after = this.reservationParts(cell);
|
|
461
|
+
this.audit.spend.openReservationUsd += after.open - before.open;
|
|
462
|
+
this.audit.spend.overspendUsd += after.over - before.over;
|
|
463
|
+
if (cell.stage === "claim") this.claimUsedUsd += after.used - before.used;
|
|
464
|
+
if (cost.unknown) node.spend.floorUsd += cost.usd;
|
|
465
|
+
else node.spend.knownUsd += cost.usd;
|
|
466
|
+
const boxMinutes = event.boxMinutes ?? 0;
|
|
467
|
+
node.spend.boxMinutes += boxMinutes;
|
|
468
|
+
this.audit.spend.boxMinutes += boxMinutes;
|
|
469
|
+
node.updatedSequence = index;
|
|
470
|
+
this.audit.attempts += 1;
|
|
471
|
+
this.audit.outcomes[event.outcome.status] += 1;
|
|
472
|
+
}
|
|
473
|
+
cancelCell(event, index) {
|
|
474
|
+
const cell = this.cells.get(event.cellId);
|
|
475
|
+
if (!cell) throw integrity(`cancellation ${event.eventId} names unallocated cell ${event.cellId}`);
|
|
476
|
+
if (cell.cancelled) throw integrity(`cell ${event.cellId} was cancelled twice`);
|
|
477
|
+
if (cell.final) throw integrity(`cell ${event.cellId} was cancelled after a final outcome`);
|
|
478
|
+
const before = this.reservationParts(cell);
|
|
479
|
+
cell.cancelled = event.reason;
|
|
480
|
+
cell.updatedSequence = index;
|
|
481
|
+
const after = this.reservationParts(cell);
|
|
482
|
+
this.audit.spend.openReservationUsd += after.open - before.open;
|
|
483
|
+
this.audit.spend.overspendUsd += after.over - before.over;
|
|
484
|
+
if (cell.stage === "claim") this.claimUsedUsd += after.used - before.used;
|
|
485
|
+
if (cell.attempts === 0) {
|
|
486
|
+
this.audit.cells.cancelled += 1;
|
|
487
|
+
this.audit.cells.open -= 1;
|
|
488
|
+
}
|
|
489
|
+
this.nodes.get(cell.nodeId).updatedSequence = index;
|
|
490
|
+
}
|
|
491
|
+
decideNode(event, index) {
|
|
492
|
+
const node = this.nodes.get(event.nodeId);
|
|
493
|
+
if (!node) throw integrity(`decision ${event.eventId} names unregistered node ${event.nodeId}`);
|
|
494
|
+
if (node.edgeIds.length === 0) throw integrity(`decision ${event.eventId}: node ${event.nodeId} has no edge yet`);
|
|
495
|
+
const status = event.decision.status;
|
|
496
|
+
if (status === "selected") {
|
|
497
|
+
if (this.selectedNodeId !== null && this.selectedNodeId !== event.nodeId) throw integrity(`node ${event.nodeId} was selected while ${this.selectedNodeId} is still selected`);
|
|
498
|
+
if (node.scoredCells === 0) throw integrity(`node ${event.nodeId} cannot be selected without a scored cell`);
|
|
499
|
+
}
|
|
500
|
+
const previous = node.decisions.at(-1)?.decision.status ?? null;
|
|
501
|
+
const wasUndecided = previous === null || previous === "advanced";
|
|
502
|
+
const isUndecided = status === "advanced";
|
|
503
|
+
this.audit.undecidedNodes += (isUndecided ? 1 : 0) - (wasUndecided ? 1 : 0);
|
|
504
|
+
if (previous === "selected" && status !== "selected") this.selectedNodeId = null;
|
|
505
|
+
if (status === "selected") this.selectedNodeId = event.nodeId;
|
|
506
|
+
if (status === "finalist") node.finalist = true;
|
|
507
|
+
node.decisions.push({
|
|
508
|
+
sequence: index,
|
|
509
|
+
decision: event.decision,
|
|
510
|
+
basis: event.basis,
|
|
511
|
+
rule: event.rule,
|
|
512
|
+
reason: event.reason
|
|
513
|
+
});
|
|
514
|
+
node.updatedSequence = index;
|
|
515
|
+
this.audit.selectedNodeId = this.selectedNodeId;
|
|
516
|
+
}
|
|
517
|
+
close(event) {
|
|
518
|
+
const reasons = this.incompleteReasons();
|
|
519
|
+
if (reasons.length > 0) throw integrity(`search-closed while incomplete: ${reasons.join("; ")}`);
|
|
520
|
+
if (event.claim) this.checkClaim(event.claim);
|
|
521
|
+
this.closed = event;
|
|
522
|
+
this.audit.status = "closed";
|
|
523
|
+
this.audit.closeReason = event.reason;
|
|
524
|
+
this.audit.spend.openReservationUsd = 0;
|
|
525
|
+
}
|
|
526
|
+
checkClaim(claim) {
|
|
527
|
+
if (claim.finalists.length > 3) throw integrity(`the claim names ${claim.finalists.length} finalists; at most 3 are allowed`);
|
|
528
|
+
assertUnique(claim.finalists.map((finalist) => finalist.nodeId), "claim finalist", "search-closed");
|
|
529
|
+
for (const finalist of claim.finalists) if (!this.nodes.get(finalist.nodeId)?.finalist) throw integrity(`claim finalist ${finalist.nodeId} was never decided finalist`);
|
|
530
|
+
if (claim.selected !== this.selectedNodeId) throw integrity(`the claim selects ${claim.selected ?? "nothing"} but the selected node is ${this.selectedNodeId ?? "none"}`);
|
|
531
|
+
const root = this.nodeOrder[0] ?? null;
|
|
532
|
+
if (claim.decision === "ship") {
|
|
533
|
+
const shipped = claim.finalists.find((finalist) => finalist.nodeId === claim.selected);
|
|
534
|
+
if (claim.selected === null || claim.selected === root || !shipped?.promote) throw integrity("a ship claim must select a finalist that the paired test promoted");
|
|
535
|
+
if (!this.header.splits.heldOutUnits) throw integrity("a ship claim needs test units held out from train and selection");
|
|
536
|
+
} else if (claim.selected !== null && claim.selected !== root) throw integrity(`a ${claim.decision} claim keeps the root; it cannot select another node`);
|
|
537
|
+
if (claim.decision === "test-cannot-resolve") {
|
|
538
|
+
for (const cell of this.cells.values()) if (cell.split === "test" && cell.attempts > 0) throw integrity("a test-cannot-resolve claim spends nothing on test cells");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/** @internal */
|
|
542
|
+
incompleteReasons() {
|
|
543
|
+
const reasons = [];
|
|
544
|
+
if (!this.header) reasons.push("search-opened is missing");
|
|
545
|
+
if (this.audit.cells.open > 0) reasons.push(count(this.audit.cells.open, "cell", "has no outcome"));
|
|
546
|
+
if (this.audit.operations.open > 0) reasons.push(count(this.audit.operations.open, "operation", "is unrecorded"));
|
|
547
|
+
if (this.audit.nodesWithoutEdge > 0) reasons.push(count(this.audit.nodesWithoutEdge, "node", "has no edge"));
|
|
548
|
+
if (this.audit.undecidedNodes > 0) reasons.push(count(this.audit.undecidedNodes, "node", "has no terminal decision"));
|
|
549
|
+
return reasons;
|
|
550
|
+
}
|
|
551
|
+
/** Book an attempt's or operation's accounting into the running totals. An
|
|
552
|
+
* unknown cost counts as its proven floor. */
|
|
553
|
+
book(accounting) {
|
|
554
|
+
const { tokens, cost } = accounting;
|
|
555
|
+
if (tokens.status === "known") {
|
|
556
|
+
this.audit.tokens.inputTokens += tokens.inputTokens;
|
|
557
|
+
this.audit.tokens.outputTokens += tokens.outputTokens;
|
|
558
|
+
this.audit.tokens.cachedTokens += tokens.cachedTokens;
|
|
559
|
+
} else this.audit.tokens.unknownTokenAttempts += 1;
|
|
560
|
+
const usd = cost.status === "known" ? cost.usd : cost.knownLowerBoundUsd;
|
|
561
|
+
if (cost.status === "known") this.audit.spend.knownUsd += usd;
|
|
562
|
+
else this.audit.spend.floorUsd += usd;
|
|
563
|
+
this.audit.spend.committedUsd += usd;
|
|
564
|
+
return {
|
|
565
|
+
usd,
|
|
566
|
+
unknown: cost.status === "unknown"
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/** A cell's share of the open reservations, its overspend, and what it draws
|
|
570
|
+
* from the claim reserve: the larger of its hold and its spend. */
|
|
571
|
+
reservationParts(cell) {
|
|
572
|
+
const reserved = cell.reservation?.usd ?? 0;
|
|
573
|
+
const holding = !cell.final && cell.cancelled === null;
|
|
574
|
+
return {
|
|
575
|
+
open: holding ? Math.max(0, reserved - cell.spentUsd) : 0,
|
|
576
|
+
over: cell.reservation ? Math.max(0, cell.spentUsd - reserved) : 0,
|
|
577
|
+
used: holding ? Math.max(reserved, cell.spentUsd) : cell.spentUsd
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/** The admission rule: committed spend, open holds, the unspent claim
|
|
581
|
+
* reserve and the new hold stay within the cap. A claim cell draws on the
|
|
582
|
+
* claim reserve instead of adding to it. */
|
|
583
|
+
admit(reservation, claim, subject) {
|
|
584
|
+
const { maxUsd, reservedClaimUsd } = this.header.budget;
|
|
585
|
+
if (maxUsd === null) return;
|
|
586
|
+
const hold = reservation?.usd ?? 0;
|
|
587
|
+
const remainingClaim = Math.max(0, reservedClaimUsd - this.claimUsedUsd);
|
|
588
|
+
if (this.audit.spend.committedUsd + this.audit.spend.openReservationUsd + (claim ? Math.max(0, remainingClaim - hold) : remainingClaim) + hold > maxUsd + USD_TOLERANCE) throw integrity(`${subject} is not admissible: committed $${round(this.audit.spend.committedUsd)} + open $${round(this.audit.spend.openReservationUsd)} + claim reserve $${round(remainingClaim)} + hold $${round(hold)} exceeds the cap $${maxUsd}`);
|
|
589
|
+
}
|
|
590
|
+
/** @internal */
|
|
591
|
+
readHeader() {
|
|
592
|
+
return this.header;
|
|
593
|
+
}
|
|
594
|
+
/** @internal */
|
|
595
|
+
readClosed() {
|
|
596
|
+
return this.closed;
|
|
597
|
+
}
|
|
598
|
+
/** @internal */
|
|
599
|
+
readHead() {
|
|
600
|
+
return this.head;
|
|
601
|
+
}
|
|
602
|
+
/** @internal */
|
|
603
|
+
readLastOccurredAt() {
|
|
604
|
+
return this.lastOccurredAtText;
|
|
605
|
+
}
|
|
606
|
+
/** @internal */
|
|
607
|
+
readAudit() {
|
|
608
|
+
const audit = this.audit;
|
|
609
|
+
return {
|
|
610
|
+
...audit,
|
|
611
|
+
edges: { ...audit.edges },
|
|
612
|
+
cells: { ...audit.cells },
|
|
613
|
+
outcomes: { ...audit.outcomes },
|
|
614
|
+
operations: { ...audit.operations },
|
|
615
|
+
spend: { ...audit.spend },
|
|
616
|
+
tokens: { ...audit.tokens }
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
/** @internal */
|
|
620
|
+
readNode(nodeId) {
|
|
621
|
+
const record = this.nodes.get(nodeId);
|
|
622
|
+
return record ? nodeView(record) : void 0;
|
|
623
|
+
}
|
|
624
|
+
/** @internal */
|
|
625
|
+
readNodeIds() {
|
|
626
|
+
return this.nodeOrder;
|
|
627
|
+
}
|
|
628
|
+
/** @internal */
|
|
629
|
+
readHasNode(nodeId) {
|
|
630
|
+
return this.nodes.has(nodeId);
|
|
631
|
+
}
|
|
632
|
+
/** @internal */
|
|
633
|
+
readNodeIdForDigest(artifactDigest) {
|
|
634
|
+
return this.nodeByDigest.get(artifactDigest);
|
|
635
|
+
}
|
|
636
|
+
/** @internal */
|
|
637
|
+
readEdge(edgeId) {
|
|
638
|
+
return this.edges.get(edgeId);
|
|
639
|
+
}
|
|
640
|
+
/** @internal */
|
|
641
|
+
readEdgeIds() {
|
|
642
|
+
return this.edgeOrder;
|
|
643
|
+
}
|
|
644
|
+
/** @internal */
|
|
645
|
+
readCell(cellId) {
|
|
646
|
+
const cell = this.cells.get(cellId);
|
|
647
|
+
return cell ? { ...cell } : void 0;
|
|
648
|
+
}
|
|
649
|
+
/** @internal */
|
|
650
|
+
readCellIds(nodeId) {
|
|
651
|
+
if (nodeId === void 0) return [...this.cells.keys()];
|
|
652
|
+
return this.nodes.get(nodeId)?.cellIds ?? [];
|
|
653
|
+
}
|
|
654
|
+
/** @internal */
|
|
655
|
+
readOperation(operationId) {
|
|
656
|
+
const operation = this.operations.get(operationId);
|
|
657
|
+
return operation ? { ...operation } : void 0;
|
|
658
|
+
}
|
|
659
|
+
/** @internal */
|
|
660
|
+
readScoredCells(nodeId, split) {
|
|
661
|
+
const scored = [];
|
|
662
|
+
for (const cellId of this.nodes.get(nodeId)?.cellIds ?? []) {
|
|
663
|
+
const cell = this.cells.get(cellId);
|
|
664
|
+
if (cell.split === split && cell.score !== null) scored.push({
|
|
665
|
+
cellId,
|
|
666
|
+
unitId: cell.unitId,
|
|
667
|
+
attempt: cell.attempts,
|
|
668
|
+
score: cell.score
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
return scored;
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
/**
|
|
675
|
+
* A read of `SearchState` at one ledger position. Header, head, audit and
|
|
676
|
+
* claim are values. Node, edge, cell and score reads go to the live indexes and
|
|
677
|
+
* throw once the state has applied a later entry.
|
|
678
|
+
*/
|
|
679
|
+
var SearchStateView = class {
|
|
680
|
+
searchId;
|
|
681
|
+
header;
|
|
682
|
+
closed;
|
|
683
|
+
head;
|
|
684
|
+
/** `occurredAt` of the head entry; a writer stamps later events at or after it. */
|
|
685
|
+
lastOccurredAt;
|
|
686
|
+
audit;
|
|
687
|
+
completion;
|
|
688
|
+
state;
|
|
689
|
+
version;
|
|
690
|
+
constructor(state, version) {
|
|
691
|
+
this.state = state;
|
|
692
|
+
this.version = version;
|
|
693
|
+
this.searchId = state.searchId;
|
|
694
|
+
this.header = state.readHeader();
|
|
695
|
+
this.closed = state.readClosed();
|
|
696
|
+
this.head = state.readHead();
|
|
697
|
+
this.lastOccurredAt = state.readLastOccurredAt();
|
|
698
|
+
this.audit = state.readAudit();
|
|
699
|
+
const reasons = state.incompleteReasons();
|
|
700
|
+
if (!this.closed) reasons.unshift("search is open");
|
|
701
|
+
this.completion = {
|
|
702
|
+
complete: this.closed !== null,
|
|
703
|
+
reasons
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
/** The node the search started from: the first registered. */
|
|
707
|
+
get rootNodeId() {
|
|
708
|
+
return this.live().readNodeIds()[0] ?? null;
|
|
709
|
+
}
|
|
710
|
+
node(nodeId) {
|
|
711
|
+
return this.live().readNode(nodeId);
|
|
712
|
+
}
|
|
713
|
+
hasNode(nodeId) {
|
|
714
|
+
return this.live().readHasNode(nodeId);
|
|
715
|
+
}
|
|
716
|
+
/** Node ids in registration order. */
|
|
717
|
+
nodeIds() {
|
|
718
|
+
return [...this.live().readNodeIds()];
|
|
719
|
+
}
|
|
720
|
+
/** Nodes in registration order. */
|
|
721
|
+
nodes() {
|
|
722
|
+
const state = this.live();
|
|
723
|
+
return state.readNodeIds().map((nodeId) => state.readNode(nodeId));
|
|
724
|
+
}
|
|
725
|
+
nodeIdForDigest(artifactDigest) {
|
|
726
|
+
return this.live().readNodeIdForDigest(artifactDigest);
|
|
727
|
+
}
|
|
728
|
+
edge(edgeId) {
|
|
729
|
+
return this.live().readEdge(edgeId);
|
|
730
|
+
}
|
|
731
|
+
/** Edges in ledger order. */
|
|
732
|
+
edges() {
|
|
733
|
+
const state = this.live();
|
|
734
|
+
return state.readEdgeIds().map((edgeId) => state.readEdge(edgeId));
|
|
735
|
+
}
|
|
736
|
+
cell(cellId) {
|
|
737
|
+
return this.live().readCell(cellId);
|
|
738
|
+
}
|
|
739
|
+
/** Cells in allocation order, optionally of one node. */
|
|
740
|
+
cells(filter = {}) {
|
|
741
|
+
const state = this.live();
|
|
742
|
+
return state.readCellIds(filter.nodeId).map((cellId) => state.readCell(cellId));
|
|
743
|
+
}
|
|
744
|
+
operation(operationId) {
|
|
745
|
+
return this.live().readOperation(operationId);
|
|
746
|
+
}
|
|
747
|
+
/** A node's scored cells on one split, in allocation order. Unscored cells
|
|
748
|
+
* (errored, cancelled or in flight) are absent, never zero. */
|
|
749
|
+
scoredCells(nodeId, split) {
|
|
750
|
+
return this.live().readScoredCells(nodeId, split);
|
|
751
|
+
}
|
|
752
|
+
/** Per-unit score means of a node on one split, in unitId order. */
|
|
753
|
+
unitScores(nodeId, split) {
|
|
754
|
+
return searchUnitScores(this.scoredCells(nodeId, split));
|
|
755
|
+
}
|
|
756
|
+
/** The lineage a RunRecord mint needs for one cell attempt. */
|
|
757
|
+
lineage(coordinates) {
|
|
758
|
+
const state = this.live();
|
|
759
|
+
if (coordinates.searchId !== this.searchId) throw new SearchLedgerError(`coordinates name search ${coordinates.searchId}, not ${this.searchId}`);
|
|
760
|
+
const cell = state.readCell(coordinates.cellId);
|
|
761
|
+
if (!cell || cell.nodeId !== coordinates.nodeId) throw new SearchLedgerError(`cell ${coordinates.cellId} of node ${coordinates.nodeId} is not in search ${this.searchId}`);
|
|
762
|
+
if (coordinates.attempt < 1 || coordinates.attempt > cell.attempts) throw new SearchLedgerError(`cell ${coordinates.cellId} has ${cell.attempts} settled attempt(s), not attempt ${coordinates.attempt}`);
|
|
763
|
+
const node = state.readNode(cell.nodeId);
|
|
764
|
+
if (node.depth === null) throw new SearchLedgerError(`node ${node.nodeId} has unknown lineage, so its depth is unknown`);
|
|
765
|
+
const containment = this.header?.containment ?? null;
|
|
766
|
+
return {
|
|
767
|
+
depth: node.depth,
|
|
768
|
+
ordinal: node.ordinal,
|
|
769
|
+
rep: cell.rep,
|
|
770
|
+
containingRunId: containment ? searchCellRunId({
|
|
771
|
+
cellId: containment.cellId,
|
|
772
|
+
attempt: containment.attempt
|
|
773
|
+
}) : null
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
live() {
|
|
777
|
+
if (!this.state.isAt(this.version)) throw new SearchLedgerError(`this SearchStateView is from sequence ${this.head?.sequence ?? -1}, and the ledger has moved on; read the ledger's state again`);
|
|
778
|
+
return this.state;
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
function nodeView(record) {
|
|
782
|
+
const latest = record.decisions.at(-1)?.decision ?? null;
|
|
783
|
+
return {
|
|
784
|
+
nodeId: record.event.nodeId,
|
|
785
|
+
ordinal: record.ordinal,
|
|
786
|
+
artifactDigest: record.event.artifactDigest,
|
|
787
|
+
artifact: record.event.artifact,
|
|
788
|
+
surfaces: record.event.surfaces,
|
|
789
|
+
primaryParentId: record.primaryParentId,
|
|
790
|
+
depth: record.depth,
|
|
791
|
+
parents: [...record.parents],
|
|
792
|
+
children: [...record.children],
|
|
793
|
+
edgeIds: [...record.edgeIds],
|
|
794
|
+
status: latest?.status ?? null,
|
|
795
|
+
rung: latest?.status === "advanced" ? latest.rung : null,
|
|
796
|
+
finalist: record.finalist,
|
|
797
|
+
decisions: [...record.decisions],
|
|
798
|
+
cellCount: record.cellIds.length,
|
|
799
|
+
spend: { ...record.spend },
|
|
800
|
+
registeredSequence: record.registeredSequence,
|
|
801
|
+
updatedSequence: record.updatedSequence
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
function assertStageSplit(cellId, stage, split, isRoot) {
|
|
805
|
+
if (!{
|
|
806
|
+
root: ["train", "selection"],
|
|
807
|
+
train: ["train"],
|
|
808
|
+
screen: ["selection"],
|
|
809
|
+
rung: ["selection"],
|
|
810
|
+
claim: ["test"],
|
|
811
|
+
external: ["train", "selection"]
|
|
812
|
+
}[stage].includes(split)) throw integrity(`cell ${cellId}: a ${stage} cell cannot run on the ${split} split`);
|
|
813
|
+
if (stage === "root" && !isRoot) throw integrity(`cell ${cellId}: only the root node runs root cells`);
|
|
814
|
+
}
|
|
815
|
+
function assertUnique(values, label, eventId) {
|
|
816
|
+
if (new Set(values).size !== values.length) throw integrity(`event ${eventId} contains duplicate ${label} values`);
|
|
817
|
+
}
|
|
818
|
+
function count(value, noun, predicate) {
|
|
819
|
+
const verb = predicate.replace(/^(has|is) /, (word) => value === 1 ? word : word === "has " ? "have " : "are ");
|
|
820
|
+
return `${value} ${value === 1 ? noun : `${noun}s`} ${verb}`;
|
|
821
|
+
}
|
|
822
|
+
function round(usd) {
|
|
823
|
+
return Math.round(usd * 1e6) / 1e6;
|
|
824
|
+
}
|
|
825
|
+
function integrity(message) {
|
|
826
|
+
return new SearchLedgerIntegrityError(message);
|
|
827
|
+
}
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/campaign/search-ledger.ts
|
|
830
|
+
/**
|
|
831
|
+
* Durable, append-only event log of one search: nodes (content-addressed
|
|
832
|
+
* artifacts), edges (the proposals that derived them), cells (one run of one
|
|
833
|
+
* node on one task), operations, decisions, and the claim.
|
|
834
|
+
*
|
|
835
|
+
* The ledger is the search's only lineage record and its only resume
|
|
836
|
+
* checkpoint. A RunRecord owns one measured run and `CostLedger` owns per-call
|
|
837
|
+
* accounting; this ledger binds them by digest instead of copying them.
|
|
838
|
+
*
|
|
839
|
+
* The file format is canonical JSONL with a SHA-256 hash chain. Every append is
|
|
840
|
+
* serialized across processes, fsynced before acknowledgement, and idempotent
|
|
841
|
+
* by `eventId`. A malformed, non-canonical, truncated, reordered, or conflicting
|
|
842
|
+
* log fails loudly; a bad row is never skipped. A ledger written under another
|
|
843
|
+
* schema tag is refused with that tag named, never translated.
|
|
844
|
+
*
|
|
845
|
+
* The journal machinery is the generic `ledger-core` journal. This module is
|
|
846
|
+
* the search codec: event schemas and canonical ordering. `SearchState` is the
|
|
847
|
+
* state machine and read model.
|
|
848
|
+
*/
|
|
849
|
+
const NON_EMPTY = z.string().min(1).refine((value) => value.trim() === value, "must not contain surrounding whitespace");
|
|
850
|
+
const HASH = z.string().regex(/^sha256:[a-f0-9]{64}$/);
|
|
851
|
+
const IMMUTABLE_REVISION = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|sha256:[a-f0-9]{64}|sha512:[A-Za-z0-9+/=]+)$/);
|
|
852
|
+
const ISO_TIMESTAMP = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/).refine((value) => Number.isFinite(Date.parse(value)), "invalid timestamp");
|
|
853
|
+
const NON_NEGATIVE_INT = z.number().int().nonnegative().safe();
|
|
854
|
+
const POSITIVE_INT = z.number().int().positive().safe();
|
|
855
|
+
const FINITE_NUMBER = z.number().finite();
|
|
856
|
+
const USD = z.number().finite().nonnegative();
|
|
857
|
+
const NODE_ID = z.string().regex(/^node_[a-f0-9]{32}$/);
|
|
858
|
+
const CELL_ID = z.string().regex(/^cell_[a-f0-9]{32}$/);
|
|
859
|
+
const ArtifactRefSchema = z.object({
|
|
860
|
+
role: NON_EMPTY,
|
|
861
|
+
uri: NON_EMPTY,
|
|
862
|
+
sha256: HASH,
|
|
863
|
+
byteLength: NON_NEGATIVE_INT
|
|
864
|
+
}).strict();
|
|
865
|
+
const SourceRefSchema = z.object({
|
|
866
|
+
uri: NON_EMPTY,
|
|
867
|
+
revision: IMMUTABLE_REVISION
|
|
868
|
+
}).strict();
|
|
869
|
+
const UnknownRefSchema = z.object({ unknown: NON_EMPTY }).strict();
|
|
870
|
+
const FailureReasonSchema = z.object({
|
|
871
|
+
code: NON_EMPTY,
|
|
872
|
+
message: NON_EMPTY
|
|
873
|
+
}).strict();
|
|
874
|
+
const ModelIdentitySchema = z.union([z.object({
|
|
875
|
+
provider: NON_EMPTY,
|
|
876
|
+
snapshot: NON_EMPTY.refine(modelHasSnapshot, "model must include an immutable snapshot")
|
|
877
|
+
}).strict(), z.object({
|
|
878
|
+
provider: NON_EMPTY,
|
|
879
|
+
alias: NON_EMPTY,
|
|
880
|
+
unknown: NON_EMPTY
|
|
881
|
+
}).strict()]);
|
|
882
|
+
const ExecutionIdentitySchema = z.object({
|
|
883
|
+
model: ModelIdentitySchema,
|
|
884
|
+
agent: SourceRefSchema,
|
|
885
|
+
benchmark: SourceRefSchema
|
|
886
|
+
}).strict();
|
|
887
|
+
const SURFACE_KINDS = [
|
|
888
|
+
"prompt",
|
|
889
|
+
"tool-contract",
|
|
890
|
+
"runtime-config",
|
|
891
|
+
"memory",
|
|
892
|
+
"knowledge",
|
|
893
|
+
"agent-profile",
|
|
894
|
+
"code",
|
|
895
|
+
"deployment"
|
|
896
|
+
];
|
|
897
|
+
const SPLIT = z.enum([
|
|
898
|
+
"train",
|
|
899
|
+
"selection",
|
|
900
|
+
"test"
|
|
901
|
+
]);
|
|
902
|
+
const ReservationSchema = z.object({
|
|
903
|
+
kind: z.enum(["hard", "estimate"]),
|
|
904
|
+
usd: USD
|
|
905
|
+
}).strict();
|
|
906
|
+
const EventBaseShape = {
|
|
907
|
+
eventId: NON_EMPTY,
|
|
908
|
+
occurredAt: ISO_TIMESTAMP,
|
|
909
|
+
artifacts: z.array(ArtifactRefSchema)
|
|
910
|
+
};
|
|
911
|
+
const OperationKindSchema = z.enum([
|
|
912
|
+
"candidate-generation",
|
|
913
|
+
"analysis",
|
|
914
|
+
"selection",
|
|
915
|
+
"judge",
|
|
916
|
+
"other"
|
|
917
|
+
]);
|
|
918
|
+
const KnownTokensSchema = z.object({
|
|
919
|
+
status: z.literal("known"),
|
|
920
|
+
inputTokens: NON_NEGATIVE_INT,
|
|
921
|
+
outputTokens: NON_NEGATIVE_INT,
|
|
922
|
+
cachedTokens: NON_NEGATIVE_INT
|
|
923
|
+
}).strict();
|
|
924
|
+
const UnknownTokensSchema = z.object({
|
|
925
|
+
status: z.literal("unknown"),
|
|
926
|
+
reason: NON_EMPTY
|
|
927
|
+
}).strict();
|
|
928
|
+
const KnownCostSchema = z.object({
|
|
929
|
+
status: z.literal("known"),
|
|
930
|
+
usd: USD,
|
|
931
|
+
source: z.enum([
|
|
932
|
+
"provider",
|
|
933
|
+
"pricing-table",
|
|
934
|
+
"free"
|
|
935
|
+
])
|
|
936
|
+
}).strict().superRefine((cost, ctx) => {
|
|
937
|
+
if (cost.source === "free" && cost.usd !== 0) ctx.addIssue({
|
|
938
|
+
code: "custom",
|
|
939
|
+
message: "free cost source must have usd 0"
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
const UnknownCostSchema = z.object({
|
|
943
|
+
status: z.literal("unknown"),
|
|
944
|
+
knownLowerBoundUsd: USD,
|
|
945
|
+
reason: NON_EMPTY
|
|
946
|
+
}).strict();
|
|
947
|
+
const AccountingSchema = z.object({
|
|
948
|
+
tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownTokensSchema]),
|
|
949
|
+
cost: z.discriminatedUnion("status", [KnownCostSchema, UnknownCostSchema])
|
|
950
|
+
}).strict();
|
|
951
|
+
const MetricsSchema = z.record(NON_EMPTY, FINITE_NUMBER).superRefine((metrics, ctx) => {
|
|
952
|
+
for (const key of Object.keys(metrics)) if (key === "__proto__" || key === "constructor" || key === "prototype") ctx.addIssue({
|
|
953
|
+
code: "custom",
|
|
954
|
+
message: `unsafe metric key ${key}`
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
const OutcomeSchema = z.discriminatedUnion("status", [
|
|
958
|
+
z.object({
|
|
959
|
+
status: z.literal("passed"),
|
|
960
|
+
score: FINITE_NUMBER,
|
|
961
|
+
metrics: MetricsSchema
|
|
962
|
+
}).strict(),
|
|
963
|
+
z.object({
|
|
964
|
+
status: z.literal("failed"),
|
|
965
|
+
score: FINITE_NUMBER,
|
|
966
|
+
metrics: MetricsSchema,
|
|
967
|
+
failure: FailureReasonSchema
|
|
968
|
+
}).strict(),
|
|
969
|
+
z.object({
|
|
970
|
+
status: z.literal("errored"),
|
|
971
|
+
metrics: MetricsSchema,
|
|
972
|
+
error: FailureReasonSchema.extend({ retryable: z.boolean() }).strict()
|
|
973
|
+
}).strict()
|
|
974
|
+
]);
|
|
975
|
+
const EffectSchema = z.discriminatedUnion("status", [z.object({
|
|
976
|
+
status: z.literal("measured"),
|
|
977
|
+
metric: NON_EMPTY,
|
|
978
|
+
baselineValue: FINITE_NUMBER,
|
|
979
|
+
candidateValue: FINITE_NUMBER,
|
|
980
|
+
delta: FINITE_NUMBER
|
|
981
|
+
}).strict().superRefine((effect, ctx) => {
|
|
982
|
+
const expected = effect.candidateValue - effect.baselineValue;
|
|
983
|
+
const tolerance = Number.EPSILON * Math.max(1, Math.abs(expected), Math.abs(effect.delta)) * 8;
|
|
984
|
+
if (Math.abs(effect.delta - expected) > tolerance) ctx.addIssue({
|
|
985
|
+
code: "custom",
|
|
986
|
+
message: "delta must equal candidateValue - baselineValue"
|
|
987
|
+
});
|
|
988
|
+
}), z.object({
|
|
989
|
+
status: z.literal("not-measured"),
|
|
990
|
+
reason: NON_EMPTY
|
|
991
|
+
}).strict()]);
|
|
992
|
+
const SurfaceEvidenceSchema = z.object({
|
|
993
|
+
surfaceId: NON_EMPTY,
|
|
994
|
+
fired: z.boolean(),
|
|
995
|
+
firingCount: NON_NEGATIVE_INT,
|
|
996
|
+
effect: EffectSchema,
|
|
997
|
+
evidence: z.array(ArtifactRefSchema).min(1)
|
|
998
|
+
}).strict().superRefine((evidence, ctx) => {
|
|
999
|
+
if (evidence.fired !== evidence.firingCount > 0) ctx.addIssue({
|
|
1000
|
+
code: "custom",
|
|
1001
|
+
message: "fired must equal firingCount > 0"
|
|
1002
|
+
});
|
|
1003
|
+
if (!evidence.fired && evidence.effect.status === "measured" && evidence.effect.delta !== 0) ctx.addIssue({
|
|
1004
|
+
code: "custom",
|
|
1005
|
+
message: "a surface that did not fire has no effect"
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
const TaskSchema = z.object({
|
|
1009
|
+
taskId: NON_EMPTY,
|
|
1010
|
+
unitId: NON_EMPTY,
|
|
1011
|
+
source: SourceRefSchema
|
|
1012
|
+
}).strict();
|
|
1013
|
+
const SplitTasksSchema = z.object({
|
|
1014
|
+
taskSetDigest: HASH,
|
|
1015
|
+
tasks: z.array(TaskSchema)
|
|
1016
|
+
}).strict();
|
|
1017
|
+
const SearchOpenedSchema = z.object({
|
|
1018
|
+
...EventBaseShape,
|
|
1019
|
+
kind: z.literal("search-opened"),
|
|
1020
|
+
subject: NON_EMPTY,
|
|
1021
|
+
process: z.object({
|
|
1022
|
+
name: NON_EMPTY,
|
|
1023
|
+
executionRef: SourceRefSchema
|
|
1024
|
+
}).strict(),
|
|
1025
|
+
artifactKind: z.enum([...SURFACE_KINDS, "output"]),
|
|
1026
|
+
objective: z.object({
|
|
1027
|
+
metric: NON_EMPTY,
|
|
1028
|
+
direction: z.enum(["maximize", "minimize"]),
|
|
1029
|
+
judge: z.union([SourceRefSchema, UnknownRefSchema]),
|
|
1030
|
+
claim: evaluationClaimSchema
|
|
1031
|
+
}).strict(),
|
|
1032
|
+
splits: z.object({
|
|
1033
|
+
train: SplitTasksSchema,
|
|
1034
|
+
selection: SplitTasksSchema,
|
|
1035
|
+
test: SplitTasksSchema,
|
|
1036
|
+
heldOutUnits: z.boolean()
|
|
1037
|
+
}).strict(),
|
|
1038
|
+
policy: z.object({
|
|
1039
|
+
expansion: NON_EMPTY,
|
|
1040
|
+
allocation: NON_EMPTY,
|
|
1041
|
+
seed: z.number().int().safe()
|
|
1042
|
+
}).strict(),
|
|
1043
|
+
budget: z.object({
|
|
1044
|
+
maxUsd: USD.nullable(),
|
|
1045
|
+
maxCells: POSITIVE_INT.nullable(),
|
|
1046
|
+
maxNodes: POSITIVE_INT.nullable(),
|
|
1047
|
+
deadline: ISO_TIMESTAMP.nullable(),
|
|
1048
|
+
maxConcurrency: POSITIVE_INT.nullable(),
|
|
1049
|
+
reservedClaimUsd: USD
|
|
1050
|
+
}).strict(),
|
|
1051
|
+
containment: z.object({
|
|
1052
|
+
searchId: NON_EMPTY,
|
|
1053
|
+
cellId: CELL_ID,
|
|
1054
|
+
attempt: POSITIVE_INT
|
|
1055
|
+
}).strict().nullable(),
|
|
1056
|
+
derivedFrom: z.object({
|
|
1057
|
+
searchId: NON_EMPTY,
|
|
1058
|
+
nodeId: NODE_ID,
|
|
1059
|
+
headHash: HASH
|
|
1060
|
+
}).strict().nullable(),
|
|
1061
|
+
identity: ExecutionIdentitySchema
|
|
1062
|
+
}).strict();
|
|
1063
|
+
const OperationStartedSchema = z.object({
|
|
1064
|
+
...EventBaseShape,
|
|
1065
|
+
kind: z.literal("operation-started"),
|
|
1066
|
+
operationId: NON_EMPTY,
|
|
1067
|
+
operationKind: OperationKindSchema,
|
|
1068
|
+
reservation: ReservationSchema.nullable()
|
|
1069
|
+
}).strict();
|
|
1070
|
+
const OperationRecordedSchema = z.object({
|
|
1071
|
+
...EventBaseShape,
|
|
1072
|
+
kind: z.literal("operation-recorded"),
|
|
1073
|
+
operationId: NON_EMPTY,
|
|
1074
|
+
operationKind: OperationKindSchema,
|
|
1075
|
+
execution: z.discriminatedUnion("kind", [z.object({
|
|
1076
|
+
kind: z.literal("model"),
|
|
1077
|
+
model: ModelIdentitySchema,
|
|
1078
|
+
source: SourceRefSchema
|
|
1079
|
+
}).strict(), z.object({
|
|
1080
|
+
kind: z.literal("deterministic"),
|
|
1081
|
+
source: SourceRefSchema
|
|
1082
|
+
}).strict()]),
|
|
1083
|
+
outcome: z.discriminatedUnion("status", [
|
|
1084
|
+
z.object({ status: z.literal("completed") }).strict(),
|
|
1085
|
+
z.object({
|
|
1086
|
+
status: z.literal("partial"),
|
|
1087
|
+
failure: FailureReasonSchema
|
|
1088
|
+
}).strict(),
|
|
1089
|
+
z.object({
|
|
1090
|
+
status: z.literal("failed"),
|
|
1091
|
+
failure: FailureReasonSchema
|
|
1092
|
+
}).strict()
|
|
1093
|
+
]),
|
|
1094
|
+
accounting: AccountingSchema
|
|
1095
|
+
}).strict();
|
|
1096
|
+
const NodeRegisteredSchema = z.object({
|
|
1097
|
+
...EventBaseShape,
|
|
1098
|
+
kind: z.literal("node-registered"),
|
|
1099
|
+
nodeId: NODE_ID,
|
|
1100
|
+
artifactDigest: HASH,
|
|
1101
|
+
artifact: ArtifactRefSchema,
|
|
1102
|
+
surfaces: z.array(z.object({
|
|
1103
|
+
surfaceId: NON_EMPTY,
|
|
1104
|
+
kind: z.enum(SURFACE_KINDS),
|
|
1105
|
+
artifact: ArtifactRefSchema
|
|
1106
|
+
}).strict())
|
|
1107
|
+
}).strict();
|
|
1108
|
+
const NodeRefSchema = z.object({
|
|
1109
|
+
searchId: NON_EMPTY,
|
|
1110
|
+
nodeId: NODE_ID
|
|
1111
|
+
}).strict();
|
|
1112
|
+
const ArtifactOrUnknownSchema = z.union([ArtifactRefSchema, UnknownRefSchema]);
|
|
1113
|
+
const EdgeRecordedSchema = z.object({
|
|
1114
|
+
...EventBaseShape,
|
|
1115
|
+
kind: z.literal("edge-recorded"),
|
|
1116
|
+
edgeId: z.string().regex(/^edge_[a-f0-9]{32}$/),
|
|
1117
|
+
childNodeId: NODE_ID,
|
|
1118
|
+
parents: z.array(NodeRefSchema),
|
|
1119
|
+
operator: z.enum([
|
|
1120
|
+
"seed",
|
|
1121
|
+
"draft",
|
|
1122
|
+
"improve",
|
|
1123
|
+
"debug",
|
|
1124
|
+
"merge",
|
|
1125
|
+
"derive"
|
|
1126
|
+
]),
|
|
1127
|
+
attribution: z.enum([
|
|
1128
|
+
"explicit",
|
|
1129
|
+
"correlated",
|
|
1130
|
+
"unknown"
|
|
1131
|
+
]),
|
|
1132
|
+
proposer: z.object({
|
|
1133
|
+
kind: z.enum([
|
|
1134
|
+
"trace",
|
|
1135
|
+
"frontier-author",
|
|
1136
|
+
"human",
|
|
1137
|
+
"optimizer",
|
|
1138
|
+
"compound"
|
|
1139
|
+
]),
|
|
1140
|
+
name: NON_EMPTY,
|
|
1141
|
+
operationId: NON_EMPTY.nullable(),
|
|
1142
|
+
source: SourceRefSchema
|
|
1143
|
+
}).strict().nullable(),
|
|
1144
|
+
selection: z.object({
|
|
1145
|
+
rule: NON_EMPTY,
|
|
1146
|
+
evidence: z.record(NON_EMPTY, FINITE_NUMBER)
|
|
1147
|
+
}).strict().nullable(),
|
|
1148
|
+
rationale: ArtifactOrUnknownSchema,
|
|
1149
|
+
diffs: z.array(ArtifactOrUnknownSchema),
|
|
1150
|
+
label: z.string()
|
|
1151
|
+
}).strict();
|
|
1152
|
+
const CellAllocatedSchema = z.object({
|
|
1153
|
+
...EventBaseShape,
|
|
1154
|
+
kind: z.literal("cell-allocated"),
|
|
1155
|
+
cellId: CELL_ID,
|
|
1156
|
+
nodeId: NODE_ID,
|
|
1157
|
+
taskId: NON_EMPTY,
|
|
1158
|
+
unitId: NON_EMPTY,
|
|
1159
|
+
split: SPLIT,
|
|
1160
|
+
rep: NON_NEGATIVE_INT,
|
|
1161
|
+
stage: z.enum([
|
|
1162
|
+
"root",
|
|
1163
|
+
"train",
|
|
1164
|
+
"screen",
|
|
1165
|
+
"rung",
|
|
1166
|
+
"claim",
|
|
1167
|
+
"external"
|
|
1168
|
+
]),
|
|
1169
|
+
lane: NON_EMPTY.nullable(),
|
|
1170
|
+
reservation: ReservationSchema.nullable()
|
|
1171
|
+
}).strict();
|
|
1172
|
+
const TraceRefSchema = z.union([z.object({
|
|
1173
|
+
traceId: NON_EMPTY,
|
|
1174
|
+
execRunId: NON_EMPTY.nullable(),
|
|
1175
|
+
spansWritten: NON_NEGATIVE_INT.nullable(),
|
|
1176
|
+
spansDropped: NON_NEGATIVE_INT.nullable()
|
|
1177
|
+
}).strict(), UnknownRefSchema]);
|
|
1178
|
+
const NON_NEGATIVE = z.number().finite().nonnegative();
|
|
1179
|
+
const CellSettledSchema = z.object({
|
|
1180
|
+
...EventBaseShape,
|
|
1181
|
+
kind: z.literal("cell-settled"),
|
|
1182
|
+
cellId: CELL_ID,
|
|
1183
|
+
attempt: POSITIVE_INT,
|
|
1184
|
+
runId: NON_EMPTY,
|
|
1185
|
+
outcome: OutcomeSchema,
|
|
1186
|
+
accounting: AccountingSchema,
|
|
1187
|
+
boxMinutes: NON_NEGATIVE.nullable(),
|
|
1188
|
+
wallMs: NON_NEGATIVE.nullable(),
|
|
1189
|
+
queueMs: NON_NEGATIVE.nullable(),
|
|
1190
|
+
placement: z.object({
|
|
1191
|
+
lane: NON_EMPTY,
|
|
1192
|
+
boxId: NON_EMPTY.nullable()
|
|
1193
|
+
}).strict().nullable(),
|
|
1194
|
+
identity: ExecutionIdentitySchema,
|
|
1195
|
+
surfaceEvidence: z.array(SurfaceEvidenceSchema),
|
|
1196
|
+
traceRef: TraceRefSchema
|
|
1197
|
+
}).strict();
|
|
1198
|
+
const CellCancelledSchema = z.object({
|
|
1199
|
+
...EventBaseShape,
|
|
1200
|
+
kind: z.literal("cell-cancelled"),
|
|
1201
|
+
cellId: CELL_ID,
|
|
1202
|
+
reason: z.enum([
|
|
1203
|
+
"pruned",
|
|
1204
|
+
"budget",
|
|
1205
|
+
"deadline",
|
|
1206
|
+
"aborted"
|
|
1207
|
+
])
|
|
1208
|
+
}).strict();
|
|
1209
|
+
const NodeEstimateSchema = z.object({
|
|
1210
|
+
against: NODE_ID,
|
|
1211
|
+
split: SPLIT,
|
|
1212
|
+
units: NON_NEGATIVE_INT,
|
|
1213
|
+
pairs: NON_NEGATIVE_INT,
|
|
1214
|
+
delta: FINITE_NUMBER.nullable(),
|
|
1215
|
+
interval: z.tuple([FINITE_NUMBER, FINITE_NUMBER]).nullable(),
|
|
1216
|
+
method: z.enum([
|
|
1217
|
+
"none",
|
|
1218
|
+
"insufficient",
|
|
1219
|
+
"descriptive",
|
|
1220
|
+
"bootstrap"
|
|
1221
|
+
]),
|
|
1222
|
+
exactSignP: z.number().min(0).max(1).nullable(),
|
|
1223
|
+
indeterminate: z.boolean(),
|
|
1224
|
+
cellSetDigest: HASH,
|
|
1225
|
+
estimator: SourceRefSchema
|
|
1226
|
+
}).strict();
|
|
1227
|
+
const NodeDecidedSchema = z.object({
|
|
1228
|
+
...EventBaseShape,
|
|
1229
|
+
kind: z.literal("node-decided"),
|
|
1230
|
+
nodeId: NODE_ID,
|
|
1231
|
+
decision: z.discriminatedUnion("status", [
|
|
1232
|
+
z.object({
|
|
1233
|
+
status: z.literal("advanced"),
|
|
1234
|
+
rung: NON_NEGATIVE_INT
|
|
1235
|
+
}).strict(),
|
|
1236
|
+
z.object({ status: z.literal("pruned") }).strict(),
|
|
1237
|
+
z.object({ status: z.literal("invalid") }).strict(),
|
|
1238
|
+
z.object({ status: z.literal("finalist") }).strict(),
|
|
1239
|
+
z.object({ status: z.literal("selected") }).strict(),
|
|
1240
|
+
z.object({ status: z.literal("rejected") }).strict()
|
|
1241
|
+
]),
|
|
1242
|
+
basis: NodeEstimateSchema.nullable(),
|
|
1243
|
+
rule: NON_EMPTY,
|
|
1244
|
+
reason: NON_EMPTY
|
|
1245
|
+
}).strict();
|
|
1246
|
+
const SearchClosedSchema = z.object({
|
|
1247
|
+
...EventBaseShape,
|
|
1248
|
+
kind: z.literal("search-closed"),
|
|
1249
|
+
reason: z.enum([
|
|
1250
|
+
"budget",
|
|
1251
|
+
"deadline",
|
|
1252
|
+
"max-nodes",
|
|
1253
|
+
"patience",
|
|
1254
|
+
"converged",
|
|
1255
|
+
"aborted"
|
|
1256
|
+
]),
|
|
1257
|
+
claim: z.object({
|
|
1258
|
+
power: z.union([z.object({
|
|
1259
|
+
adequate: z.boolean(),
|
|
1260
|
+
minimumEffect: z.number().finite().positive(),
|
|
1261
|
+
powerAtMinimumEffect: z.number().min(0).max(1),
|
|
1262
|
+
units: NON_NEGATIVE_INT
|
|
1263
|
+
}).strict(), UnknownRefSchema]),
|
|
1264
|
+
finalists: z.array(z.object({
|
|
1265
|
+
nodeId: NODE_ID,
|
|
1266
|
+
estimate: NodeEstimateSchema.nullable(),
|
|
1267
|
+
promote: z.boolean()
|
|
1268
|
+
}).strict()),
|
|
1269
|
+
selected: NODE_ID.nullable(),
|
|
1270
|
+
decision: z.enum([
|
|
1271
|
+
"ship",
|
|
1272
|
+
"hold",
|
|
1273
|
+
"test-cannot-resolve"
|
|
1274
|
+
])
|
|
1275
|
+
}).strict().nullable()
|
|
1276
|
+
}).strict();
|
|
1277
|
+
const EventSchema = z.discriminatedUnion("kind", [
|
|
1278
|
+
SearchOpenedSchema,
|
|
1279
|
+
OperationStartedSchema,
|
|
1280
|
+
OperationRecordedSchema,
|
|
1281
|
+
NodeRegisteredSchema,
|
|
1282
|
+
EdgeRecordedSchema,
|
|
1283
|
+
CellAllocatedSchema,
|
|
1284
|
+
CellSettledSchema,
|
|
1285
|
+
CellCancelledSchema,
|
|
1286
|
+
NodeDecidedSchema,
|
|
1287
|
+
SearchClosedSchema
|
|
1288
|
+
]);
|
|
1289
|
+
const EntrySchema = z.object({
|
|
1290
|
+
schema: z.literal(SEARCH_LEDGER_SCHEMA),
|
|
1291
|
+
searchId: NON_EMPTY,
|
|
1292
|
+
sequence: NON_NEGATIVE_INT,
|
|
1293
|
+
previousHash: z.union([HASH, z.null()]),
|
|
1294
|
+
event: EventSchema,
|
|
1295
|
+
entryHash: HASH
|
|
1296
|
+
}).strict();
|
|
1297
|
+
/** Validate and return a canonical copy. Arrays whose order carries no meaning
|
|
1298
|
+
* are sorted, so retries from different processes produce identical bytes. */
|
|
1299
|
+
function validateSearchLedgerEvent(input) {
|
|
1300
|
+
const parsed = EventSchema.safeParse(input);
|
|
1301
|
+
if (!parsed.success) throw new SearchLedgerError(`invalid search ledger event: ${formatZodError(parsed.error)}`);
|
|
1302
|
+
return normalizeEvent(parsed.data);
|
|
1303
|
+
}
|
|
1304
|
+
/** Open a durable filesystem search ledger. Construction performs no I/O; the
|
|
1305
|
+
* first `append` or `state` verifies the whole existing file. */
|
|
1306
|
+
function openSearchLedger(options) {
|
|
1307
|
+
return new FileSearchLedger(options.path, options.searchId, options.trustedHead);
|
|
1308
|
+
}
|
|
1309
|
+
/** Verify and project immutable search-ledger JSONL through the same codec as
|
|
1310
|
+
* `FileSearchLedger`. */
|
|
1311
|
+
function replaySearchLedgerText(text, searchId, source) {
|
|
1312
|
+
return replayLedgerText(text, source, searchLedgerCodec(searchId));
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Verify one stored ledger line in isolation: the schema tag, the entry and
|
|
1316
|
+
* event schemas, canonical bytes, the entry's own hash, and its search. The
|
|
1317
|
+
* chain (sequence and previous hash) and the state machine are the caller's,
|
|
1318
|
+
* because they need the lines before this one. An ingest server runs this on
|
|
1319
|
+
* every received line, so it hashes exactly the bytes the producer hashed.
|
|
1320
|
+
*/
|
|
1321
|
+
function parseSearchLedgerLine(line, searchId, context) {
|
|
1322
|
+
let raw;
|
|
1323
|
+
try {
|
|
1324
|
+
raw = JSON.parse(line);
|
|
1325
|
+
} catch (error) {
|
|
1326
|
+
throw new SearchLedgerIntegrityError(`search ledger ${context.path} has invalid JSON at line ${context.line}`, { cause: error });
|
|
1327
|
+
}
|
|
1328
|
+
const entry = parseSearchLedgerEntry(raw, context);
|
|
1329
|
+
if (entry.searchId !== searchId) throw new SearchLedgerIntegrityError(`search ledger ${context.path} line ${context.line} belongs to search ${entry.searchId}, expected ${searchId}`);
|
|
1330
|
+
if (canonicalString(entry) !== line) throw new SearchLedgerIntegrityError(`search ledger ${context.path} has non-canonical bytes at line ${context.line}`);
|
|
1331
|
+
const { entryHash, ...material } = entry;
|
|
1332
|
+
const expected = hashCanonical(material);
|
|
1333
|
+
if (entryHash !== expected) throw new SearchLedgerIntegrityError(`search ledger ${context.path} line ${context.line} hash mismatch: expected ${expected}, got ${entryHash}`);
|
|
1334
|
+
return entry;
|
|
1335
|
+
}
|
|
1336
|
+
function searchLedgerCodec(searchId) {
|
|
1337
|
+
return {
|
|
1338
|
+
...SEARCH_LEDGER_FILE_CONTEXT,
|
|
1339
|
+
header: {
|
|
1340
|
+
schema: SEARCH_LEDGER_SCHEMA,
|
|
1341
|
+
searchId
|
|
1342
|
+
},
|
|
1343
|
+
conflictError: (message) => new SearchLedgerConflictError(message),
|
|
1344
|
+
parseEntry: parseSearchLedgerEntry,
|
|
1345
|
+
checkEntryHeader: (entry, index) => {
|
|
1346
|
+
if (entry.searchId !== searchId) throw new SearchLedgerIntegrityError(`entry ${index} belongs to search ${entry.searchId}, expected ${searchId}`);
|
|
1347
|
+
},
|
|
1348
|
+
createProjector: () => new SearchState(searchId)
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
/** Append-only file-backed search ledger with idempotent writes. */
|
|
1352
|
+
var FileSearchLedger = class {
|
|
1353
|
+
path;
|
|
1354
|
+
searchId;
|
|
1355
|
+
trustedHeadPath;
|
|
1356
|
+
trustedHeadMode;
|
|
1357
|
+
journal;
|
|
1358
|
+
constructor(path, searchId, trustedHead = "pin") {
|
|
1359
|
+
if (path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
|
|
1360
|
+
if (searchId.length === 0 || searchId.trim() !== searchId) throw new SearchLedgerError("searchId must be non-empty without surrounding whitespace");
|
|
1361
|
+
this.searchId = searchId;
|
|
1362
|
+
this.trustedHeadMode = trustedHead;
|
|
1363
|
+
this.journal = new FileLedgerJournal(path, searchLedgerCodec(searchId), { requireTrustedHead: trustedHead === "require" });
|
|
1364
|
+
this.path = this.journal.path;
|
|
1365
|
+
this.trustedHeadPath = this.journal.trustedHeadPath;
|
|
1366
|
+
}
|
|
1367
|
+
async state() {
|
|
1368
|
+
return this.journal.replay();
|
|
1369
|
+
}
|
|
1370
|
+
async append(input) {
|
|
1371
|
+
const event = validateSearchLedgerEvent(input);
|
|
1372
|
+
const { entry, appended, projection } = await this.journal.append(event, { pinHead: this.trustedHeadMode !== "off" });
|
|
1373
|
+
return {
|
|
1374
|
+
entry,
|
|
1375
|
+
appended,
|
|
1376
|
+
state: projection
|
|
1377
|
+
};
|
|
1378
|
+
}
|
|
1379
|
+
async trustedHead() {
|
|
1380
|
+
return this.journal.trustedHead();
|
|
1381
|
+
}
|
|
1382
|
+
async pinTrustedHead() {
|
|
1383
|
+
return this.journal.pinTrustedHead();
|
|
1384
|
+
}
|
|
1385
|
+
async clearTrustedHead() {
|
|
1386
|
+
return this.journal.clearTrustedHead();
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
function parseSearchLedgerEntry(raw, context) {
|
|
1390
|
+
const schema = raw?.schema;
|
|
1391
|
+
if (schema !== "tangle.search-ledger.2026-09") throw new SearchLedgerIntegrityError(`search ledger ${context.path} line ${context.line} was written under schema ${JSON.stringify(schema)}; this version reads only ${SEARCH_LEDGER_SCHEMA} and does not translate other formats`);
|
|
1392
|
+
const parsed = EntrySchema.safeParse(raw);
|
|
1393
|
+
if (!parsed.success) throw new SearchLedgerIntegrityError(`search ledger ${context.path} has a malformed entry at line ${context.line}: ${formatZodError(parsed.error)}`);
|
|
1394
|
+
const entry = parsed.data;
|
|
1395
|
+
if (canonicalString(validateSearchLedgerEvent(entry.event)) !== canonicalString(entry.event)) throw new SearchLedgerIntegrityError(`search ledger ${context.path} has non-canonical event ordering at line ${context.line}`);
|
|
1396
|
+
return entry;
|
|
1397
|
+
}
|
|
1398
|
+
function normalizeEvent(event) {
|
|
1399
|
+
const artifacts = sortArtifacts(event.artifacts);
|
|
1400
|
+
switch (event.kind) {
|
|
1401
|
+
case "search-opened": return {
|
|
1402
|
+
...event,
|
|
1403
|
+
artifacts,
|
|
1404
|
+
splits: {
|
|
1405
|
+
...event.splits,
|
|
1406
|
+
train: {
|
|
1407
|
+
...event.splits.train,
|
|
1408
|
+
tasks: sortTasks(event.splits.train.tasks)
|
|
1409
|
+
},
|
|
1410
|
+
selection: {
|
|
1411
|
+
...event.splits.selection,
|
|
1412
|
+
tasks: sortTasks(event.splits.selection.tasks)
|
|
1413
|
+
},
|
|
1414
|
+
test: {
|
|
1415
|
+
...event.splits.test,
|
|
1416
|
+
tasks: sortTasks(event.splits.test.tasks)
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
};
|
|
1420
|
+
case "node-registered": return {
|
|
1421
|
+
...event,
|
|
1422
|
+
artifacts,
|
|
1423
|
+
surfaces: [...event.surfaces].sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
|
|
1424
|
+
};
|
|
1425
|
+
case "cell-settled": return {
|
|
1426
|
+
...event,
|
|
1427
|
+
artifacts,
|
|
1428
|
+
surfaceEvidence: [...event.surfaceEvidence].map((evidence) => ({
|
|
1429
|
+
...evidence,
|
|
1430
|
+
evidence: sortArtifacts(evidence.evidence)
|
|
1431
|
+
})).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
|
|
1432
|
+
};
|
|
1433
|
+
default: return {
|
|
1434
|
+
...event,
|
|
1435
|
+
artifacts
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
function sortTasks(tasks) {
|
|
1440
|
+
return [...tasks].sort((a, b) => compareStrings(a.taskId, b.taskId));
|
|
1441
|
+
}
|
|
1442
|
+
function sortArtifacts(artifacts) {
|
|
1443
|
+
return [...artifacts].sort((a, b) => compareStrings(artifactKey(a), artifactKey(b)));
|
|
1444
|
+
}
|
|
1445
|
+
function formatZodError(error) {
|
|
1446
|
+
return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`).join("; ");
|
|
1447
|
+
}
|
|
1448
|
+
//#endregion
|
|
1449
|
+
export { validateSearchLedgerEvent as a, searchCellId as c, searchUnitScores as d, SEARCH_LEDGER_SCHEMA as f, replaySearchLedgerText as i, searchNodeId as l, openSearchLedger as n, SearchState as o, parseSearchLedgerLine as r, SearchStateView as s, FileSearchLedger as t, searchTaskSetDigest as u };
|
|
1450
|
+
|
|
1451
|
+
//# sourceMappingURL=search-ledger-CQ8Uy4FK.js.map
|