@tangle-network/agent-eval 0.125.0 → 0.126.1

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.
Files changed (82) hide show
  1. package/CHANGELOG.md +62 -35
  2. package/README.md +270 -189
  3. package/dist/analyst/index.d.ts +15 -145
  4. package/dist/analyst/index.js +33 -47
  5. package/dist/analyst/index.js.map +1 -1
  6. package/dist/benchmarks/index.d.ts +45 -162
  7. package/dist/benchmarks/index.js +8 -9
  8. package/dist/campaign/index.d.ts +3674 -5393
  9. package/dist/campaign/index.js +21 -95
  10. package/dist/{chunk-R226UZOI.js → chunk-474LBSOX.js} +2 -2
  11. package/dist/{chunk-HM6V7F3M.js → chunk-FO7HEH76.js} +3 -3
  12. package/dist/chunk-IILEIWGW.js +635 -0
  13. package/dist/chunk-IILEIWGW.js.map +1 -0
  14. package/dist/{chunk-EQUK3RFS.js → chunk-J5SQWP6Y.js} +8 -5
  15. package/dist/chunk-J5SQWP6Y.js.map +1 -0
  16. package/dist/{chunk-W5B3ZGP3.js → chunk-KE2VWPZX.js} +8 -6
  17. package/dist/{chunk-W5B3ZGP3.js.map → chunk-KE2VWPZX.js.map} +1 -1
  18. package/dist/{chunk-DT7OXY3C.js → chunk-LUNF2SEL.js} +538 -851
  19. package/dist/chunk-LUNF2SEL.js.map +1 -0
  20. package/dist/chunk-NGUYT5CI.js +4637 -0
  21. package/dist/chunk-NGUYT5CI.js.map +1 -0
  22. package/dist/{chunk-QFQZ3U3X.js → chunk-OCFJACJU.js} +2 -2
  23. package/dist/{chunk-GID26AN4.js → chunk-P22LJ3Y2.js} +4 -6
  24. package/dist/{chunk-GID26AN4.js.map → chunk-P22LJ3Y2.js.map} +1 -1
  25. package/dist/{chunk-SJT4OBVL.js → chunk-SDPM6554.js} +3 -3
  26. package/dist/{chunk-D5JZ7UDZ.js → chunk-UCLVDLCH.js} +136 -50
  27. package/dist/chunk-UCLVDLCH.js.map +1 -0
  28. package/dist/chunk-VMUENW6F.js +7274 -0
  29. package/dist/chunk-VMUENW6F.js.map +1 -0
  30. package/dist/{chunk-JKDNAOF5.js → chunk-W4L6C2XT.js} +2 -2
  31. package/dist/chunk-WGXIEX7P.js +116 -0
  32. package/dist/chunk-WGXIEX7P.js.map +1 -0
  33. package/dist/{chunk-GRCDRKII.js → chunk-WS3NZZQQ.js} +58 -20
  34. package/dist/chunk-WS3NZZQQ.js.map +1 -0
  35. package/dist/cli.js +3 -3
  36. package/dist/contract/index.d.ts +3220 -3094
  37. package/dist/contract/index.js +173 -42
  38. package/dist/contract/index.js.map +1 -1
  39. package/dist/control.js +2 -3
  40. package/dist/fuzz.d.ts +14 -1
  41. package/dist/fuzz.js +1 -1
  42. package/dist/hosted/index.d.ts +8 -1
  43. package/dist/index.d.ts +71 -687
  44. package/dist/index.js +178 -497
  45. package/dist/index.js.map +1 -1
  46. package/dist/openapi.json +1 -1
  47. package/dist/rl.d.ts +5 -100
  48. package/dist/rl.js +4 -5
  49. package/dist/rl.js.map +1 -1
  50. package/dist/{run-campaign-I3JXKVAK.js → run-campaign-LVFKZCEU.js} +3 -3
  51. package/dist/traces.js +2 -3
  52. package/dist/wire/index.d.ts +14 -1
  53. package/dist/wire/index.js +3 -3
  54. package/docs/campaign-proposers.md +363 -168
  55. package/docs/design/loop-taxonomy.md +142 -190
  56. package/docs/design.md +1 -1
  57. package/docs/distributed-driver.md +8 -11
  58. package/docs/feature-guide.md +20 -19
  59. package/docs/knowledge-readiness.md +2 -5
  60. package/docs/multi-shot-optimization.md +35 -27
  61. package/docs/rollout.md +5 -5
  62. package/package.json +4 -4
  63. package/dist/chunk-A62YMFWA.js +0 -9269
  64. package/dist/chunk-A62YMFWA.js.map +0 -1
  65. package/dist/chunk-A6GT67HT.js +0 -550
  66. package/dist/chunk-A6GT67HT.js.map +0 -1
  67. package/dist/chunk-D5JZ7UDZ.js.map +0 -1
  68. package/dist/chunk-DT7OXY3C.js.map +0 -1
  69. package/dist/chunk-EQUK3RFS.js.map +0 -1
  70. package/dist/chunk-GC4ATIKK.js +0 -317
  71. package/dist/chunk-GC4ATIKK.js.map +0 -1
  72. package/dist/chunk-GRCDRKII.js.map +0 -1
  73. package/dist/chunk-LOW3U7JZ.js +0 -328
  74. package/dist/chunk-LOW3U7JZ.js.map +0 -1
  75. package/dist/chunk-PMITBABE.js +0 -3841
  76. package/dist/chunk-PMITBABE.js.map +0 -1
  77. /package/dist/{chunk-R226UZOI.js.map → chunk-474LBSOX.js.map} +0 -0
  78. /package/dist/{chunk-HM6V7F3M.js.map → chunk-FO7HEH76.js.map} +0 -0
  79. /package/dist/{chunk-QFQZ3U3X.js.map → chunk-OCFJACJU.js.map} +0 -0
  80. /package/dist/{chunk-SJT4OBVL.js.map → chunk-SDPM6554.js.map} +0 -0
  81. /package/dist/{chunk-JKDNAOF5.js.map → chunk-W4L6C2XT.js.map} +0 -0
  82. /package/dist/{run-campaign-I3JXKVAK.js.map → run-campaign-LVFKZCEU.js.map} +0 -0
@@ -0,0 +1,4637 @@
1
+ import {
2
+ JudgeParseError,
3
+ assertCodeSurfaceIdentity,
4
+ costReceiptFromTCloud,
5
+ labelTrustRank,
6
+ maximumChargeForTCloudRequest,
7
+ pairHoldout,
8
+ recoverTruncatedJson,
9
+ surfaceContentHash
10
+ } from "./chunk-VMUENW6F.js";
11
+ import {
12
+ SearchLedgerConflictError,
13
+ SearchLedgerError,
14
+ SearchLedgerIntegrityError,
15
+ appendSearchLedgerLine,
16
+ assertRealBackend,
17
+ contentHash,
18
+ planCampaignRun,
19
+ runCampaign,
20
+ summarizeBackendIntegrity,
21
+ withSearchLedgerFileLock
22
+ } from "./chunk-UCLVDLCH.js";
23
+ import {
24
+ Mutex
25
+ } from "./chunk-WGXIEX7P.js";
26
+ import {
27
+ eProcess,
28
+ mcnemar,
29
+ mulberry32,
30
+ pairedBootstrap,
31
+ pairedRiskDifference,
32
+ wilcoxonSignedRank
33
+ } from "./chunk-PJQFMIOX.js";
34
+ import {
35
+ CostLedger
36
+ } from "./chunk-WS3NZZQQ.js";
37
+ import {
38
+ analyzeTraces
39
+ } from "./chunk-NUKSVU3W.js";
40
+ import {
41
+ buildAgentProfileCell,
42
+ modelHasSnapshot,
43
+ validateRunRecord
44
+ } from "./chunk-IILEIWGW.js";
45
+ import {
46
+ canonicalize
47
+ } from "./chunk-VSMTAMNK.js";
48
+ import {
49
+ AgentEvalError,
50
+ ValidationError
51
+ } from "./chunk-ONWEPEDO.js";
52
+
53
+ // src/campaign/analyst-surface.ts
54
+ function surfaceToText(surface) {
55
+ if (typeof surface === "string") return surface;
56
+ throw new Error(
57
+ `buildAnalystSurfaceDispatch: the analyst surface must be a string actorDescription, got a ${surface.kind}-tier surface. The analyst prompt is prompt-tier.`
58
+ );
59
+ }
60
+ function buildAnalystSurfaceDispatch(opts) {
61
+ const analyze = opts.analyze ?? analyzeTraces;
62
+ return async (surface, scenario, _ctx) => {
63
+ const actorDescription = surfaceToText(surface);
64
+ const res = await analyze(
65
+ { question: scenario.question },
66
+ { ...opts.analystOptions, actorDescription, source: scenario.source }
67
+ );
68
+ return {
69
+ answer: res.answer,
70
+ findings: res.findings,
71
+ actorPromptVersion: res.actorPromptVersion
72
+ };
73
+ };
74
+ }
75
+ function failureModeRecallJudge(opts = {}) {
76
+ const recallWeight = opts.recallWeight ?? 0.5;
77
+ return {
78
+ name: "failure-mode-recall",
79
+ dimensions: [
80
+ { key: "recall", description: "fraction of ground-truth failure modes the analyst surfaced" },
81
+ {
82
+ key: "precision",
83
+ description: "1 \u2212 share of findings that named a failure/tool/error absent from this corpus"
84
+ }
85
+ ],
86
+ appliesTo: (s) => s.kind === "analyst-surface",
87
+ score({ artifact, scenario }) {
88
+ const modes = scenario.expectedFailureModes;
89
+ if (modes.length === 0) {
90
+ throw new Error(
91
+ `failureModeRecallJudge: scenario '${scenario.id}' has no expectedFailureModes \u2014 refusing to score (a vacuous 1.0 would corrupt the comparison)`
92
+ );
93
+ }
94
+ const hay = artifact.findings.join("\n").toLowerCase();
95
+ const matched = modes.filter((m) => m.cues.some((c) => hay.includes(c.toLowerCase())));
96
+ const recall = matched.length / modes.length;
97
+ const forbidden = (scenario.forbiddenCues ?? []).map((c) => c.toLowerCase());
98
+ let precision = 1;
99
+ let hallucinated = 0;
100
+ if (forbidden.length > 0) {
101
+ const denom = Math.max(1, artifact.findings.length);
102
+ hallucinated = artifact.findings.filter(
103
+ (f) => forbidden.some((c) => f.toLowerCase().includes(c))
104
+ ).length;
105
+ precision = 1 - hallucinated / denom;
106
+ }
107
+ const composite = forbidden.length > 0 ? recallWeight * recall + (1 - recallWeight) * precision : recall;
108
+ const missed = modes.filter((m) => !matched.includes(m)).map((m) => m.id);
109
+ const notes = `matched ${matched.length}/${modes.length} failure modes` + (missed.length ? `; missed [${missed.join(", ")}]` : "") + (hallucinated ? `; ${hallucinated} out-of-corpus finding(s)` : "");
110
+ return { dimensions: { recall, precision }, composite, notes };
111
+ }
112
+ };
113
+ }
114
+
115
+ // src/paired-arms.ts
116
+ function pairArms(rows, opts) {
117
+ const { baselineArm, treatmentArm } = opts;
118
+ if (baselineArm === treatmentArm) {
119
+ throw new ValidationError(
120
+ `pairArms: baselineArm and treatmentArm are both '${baselineArm}' \u2014 an arm cannot be compared to itself`
121
+ );
122
+ }
123
+ const byArm = /* @__PURE__ */ new Map();
124
+ const armsSeen = /* @__PURE__ */ new Set();
125
+ for (const row of rows) {
126
+ armsSeen.add(row.arm);
127
+ if (row.arm !== baselineArm && row.arm !== treatmentArm) continue;
128
+ const byKey = byArm.get(row.arm) ?? /* @__PURE__ */ new Map();
129
+ const group = byKey.get(row.pairKey) ?? [];
130
+ group.push(row);
131
+ byKey.set(row.pairKey, group);
132
+ byArm.set(row.arm, byKey);
133
+ }
134
+ for (const arm of [baselineArm, treatmentArm]) {
135
+ if (!byArm.has(arm)) {
136
+ const seen = [...armsSeen].sort().join(", ") || "<none>";
137
+ throw new ValidationError(`pairArms: no rows for arm '${arm}' (arms present: ${seen})`);
138
+ }
139
+ }
140
+ const baselineByKey = byArm.get(baselineArm);
141
+ const treatmentByKey = byArm.get(treatmentArm);
142
+ const allKeys = [.../* @__PURE__ */ new Set([...baselineByKey.keys(), ...treatmentByKey.keys()])].sort();
143
+ const pairs = [];
144
+ const unpairedBaseline = [];
145
+ const unpairedTreatment = [];
146
+ for (const pairKey of allKeys) {
147
+ const b = baselineByKey.get(pairKey) ?? [];
148
+ const t = treatmentByKey.get(pairKey) ?? [];
149
+ if (b.length <= 1 && t.length <= 1) {
150
+ if (b.length === 1 && t.length === 1) {
151
+ pairs.push({ pairKey, repIndex: 0, baseline: b[0], treatment: t[0] });
152
+ } else {
153
+ unpairedBaseline.push(...b);
154
+ unpairedTreatment.push(...t);
155
+ }
156
+ continue;
157
+ }
158
+ const bByRep = indexByRepKey(b, pairKey, baselineArm);
159
+ const tByRep = indexByRepKey(t, pairKey, treatmentArm);
160
+ const repKeys = [.../* @__PURE__ */ new Set([...bByRep.keys(), ...tByRep.keys()])].sort();
161
+ let repIndex = 0;
162
+ for (const repKey of repKeys) {
163
+ const baseline = bByRep.get(repKey);
164
+ const treatment = tByRep.get(repKey);
165
+ if (baseline !== void 0 && treatment !== void 0) {
166
+ pairs.push({ pairKey, repIndex: repIndex++, baseline, treatment });
167
+ } else if (baseline !== void 0) {
168
+ unpairedBaseline.push(baseline);
169
+ } else if (treatment !== void 0) {
170
+ unpairedTreatment.push(treatment);
171
+ }
172
+ }
173
+ }
174
+ return { pairs, unpairedBaseline, unpairedTreatment };
175
+ }
176
+ function indexByRepKey(group, pairKey, arm) {
177
+ const byRep = /* @__PURE__ */ new Map();
178
+ for (const row of group) {
179
+ if (row.repKey === void 0) {
180
+ throw new ValidationError(
181
+ `pairArms: pairKey '${pairKey}' has multiple reps in an arm, but a row in arm '${arm}' is missing repKey \u2014 multi-rep items require an explicit repKey on every row so reps pair by identity (pairing reps by outcome or by index would bias the paired statistics)`
182
+ );
183
+ }
184
+ if (byRep.has(row.repKey)) {
185
+ throw new ValidationError(
186
+ `pairArms: duplicate repKey '${row.repKey}' for pairKey '${pairKey}' in arm '${arm}' \u2014 (pairKey, repKey) must uniquely identify a rep within an arm`
187
+ );
188
+ }
189
+ byRep.set(row.repKey, row);
190
+ }
191
+ return byRep;
192
+ }
193
+ function comparePairedArms(rows, opts) {
194
+ const { pairs, unpairedBaseline, unpairedTreatment } = pairArms(rows, opts);
195
+ let correctness = null;
196
+ const baselinePass = [];
197
+ const treatmentPass = [];
198
+ for (const pair of pairs) {
199
+ if (pair.baseline.pass === void 0 || pair.treatment.pass === void 0) continue;
200
+ baselinePass.push(pair.baseline.pass ? 1 : 0);
201
+ treatmentPass.push(pair.treatment.pass ? 1 : 0);
202
+ }
203
+ if (baselinePass.length > 0) {
204
+ const mc = mcnemar(baselinePass, treatmentPass);
205
+ correctness = {
206
+ b10: mc.b,
207
+ b01: mc.c,
208
+ mcnemar: mc,
209
+ riskDifference: pairedRiskDifference(baselinePass, treatmentPass)
210
+ };
211
+ }
212
+ const metricNames = opts.metricNames ?? [
213
+ ...new Set(
214
+ pairs.flatMap((p) => [
215
+ ...Object.keys(p.baseline.metrics ?? {}),
216
+ ...Object.keys(p.treatment.metrics ?? {})
217
+ ])
218
+ )
219
+ ].sort();
220
+ const metricDeltas = metricNames.map((name) => {
221
+ const before = [];
222
+ const after = [];
223
+ let nMissing = 0;
224
+ for (const pair of pairs) {
225
+ const b = metricValue(pair.baseline, name);
226
+ const t = metricValue(pair.treatment, name);
227
+ if (b === void 0 || t === void 0) {
228
+ nMissing++;
229
+ continue;
230
+ }
231
+ before.push(b);
232
+ after.push(t);
233
+ }
234
+ const bootstrapCi = before.length === 0 ? null : pairedBootstrap(before, after, opts.bootstrap);
235
+ return {
236
+ name,
237
+ n: before.length,
238
+ nMissing,
239
+ medianDelta: bootstrapCi === null ? Number.NaN : bootstrapCi.median,
240
+ meanDelta: bootstrapCi === null ? Number.NaN : bootstrapCi.mean,
241
+ bootstrapCi,
242
+ wilcoxon: before.length === 0 ? null : wilcoxonSignedRank(before, after)
243
+ };
244
+ });
245
+ return {
246
+ nPairs: pairs.length,
247
+ nUnpairedBaseline: unpairedBaseline.length,
248
+ nUnpairedTreatment: unpairedTreatment.length,
249
+ correctness,
250
+ metricDeltas
251
+ };
252
+ }
253
+ function metricValue(row, name) {
254
+ const v = row.metrics?.[name];
255
+ if (v === void 0) return void 0;
256
+ if (!Number.isFinite(v)) {
257
+ throw new ValidationError(
258
+ `comparePairedArms: non-finite value for metric '${name}' on pairKey '${row.pairKey}' (arm '${row.arm}'): ${v}`
259
+ );
260
+ }
261
+ return v;
262
+ }
263
+
264
+ // src/campaign/cross-surface-context.ts
265
+ function validateCrossSurfaceInput(input) {
266
+ assertNonEmptyUnique(input.taskOrder, "taskOrder");
267
+ assertNonEmptyUnique(input.componentOrder, "componentOrder");
268
+ if (input.componentOrder.length < 2) {
269
+ throw new ValidationError(
270
+ "analyzeCrossSurfaceInteractions: componentOrder must contain at least two surfaces"
271
+ );
272
+ }
273
+ assertNonEmptyUnique(input.candidateOrder, "candidateOrder");
274
+ assertNonEmptyUnique(input.costMetricOrder, "costMetricOrder");
275
+ if (input.costMetricOrder.includes("score")) {
276
+ throw new ValidationError(
277
+ `analyzeCrossSurfaceInteractions: costMetricOrder cannot contain reserved metric 'score'`
278
+ );
279
+ }
280
+ validateBootstrap(input);
281
+ validateSelection(input);
282
+ const componentById = indexUnique(
283
+ input.components,
284
+ (component) => component.componentId,
285
+ "component"
286
+ );
287
+ assertExactSet(input.componentOrder, componentById.keys(), "componentOrder", "components");
288
+ const surfaces = /* @__PURE__ */ new Set();
289
+ for (const component of componentById.values()) {
290
+ assertNonEmpty(component.componentId, "componentId");
291
+ assertNonEmpty(component.surfaceId, `surfaceId for component '${component.componentId}'`);
292
+ if (surfaces.has(component.surfaceId)) {
293
+ throw new ValidationError(
294
+ `analyzeCrossSurfaceInteractions: surfaceId '${component.surfaceId}' has more than one component; the interaction stage requires one independently selected finalist per surface`
295
+ );
296
+ }
297
+ surfaces.add(component.surfaceId);
298
+ if (typeof component.bestSingleEligible !== "boolean") {
299
+ throw new ValidationError(
300
+ `analyzeCrossSurfaceInteractions: component '${component.componentId}' must declare bestSingleEligible`
301
+ );
302
+ }
303
+ }
304
+ const componentIndex = new Map(input.componentOrder.map((id, index) => [id, index]));
305
+ const candidateById = indexUnique(
306
+ input.candidates,
307
+ (candidate) => candidate.candidateId,
308
+ "candidate"
309
+ );
310
+ assertExactSet(input.candidateOrder, candidateById.keys(), "candidateOrder", "candidates");
311
+ if (!candidateById.has(input.baselineCandidateId)) {
312
+ throw new ValidationError(
313
+ `analyzeCrossSurfaceInteractions: unknown baselineCandidateId '${input.baselineCandidateId}'`
314
+ );
315
+ }
316
+ const candidateByComponents = /* @__PURE__ */ new Map();
317
+ const singleByComponent = /* @__PURE__ */ new Map();
318
+ for (const candidate of candidateById.values()) {
319
+ validateCandidate(candidate, componentById, componentIndex, input.baselineCandidateId);
320
+ const key = crossSurfaceComponentSetKey(candidate.componentIds);
321
+ const duplicate = candidateByComponents.get(key);
322
+ if (duplicate) {
323
+ throw new ValidationError(
324
+ `analyzeCrossSurfaceInteractions: candidates '${duplicate.candidateId}' and '${candidate.candidateId}' materialize the same component set`
325
+ );
326
+ }
327
+ candidateByComponents.set(key, candidate);
328
+ if (candidate.componentIds.length === 1) {
329
+ singleByComponent.set(candidate.componentIds[0], candidate);
330
+ }
331
+ }
332
+ const baseline = candidateById.get(input.baselineCandidateId);
333
+ if (baseline.componentIds.length !== 0) {
334
+ throw new ValidationError(
335
+ `analyzeCrossSurfaceInteractions: baseline '${baseline.candidateId}' must have zero components`
336
+ );
337
+ }
338
+ for (const componentId of input.componentOrder) {
339
+ if (!singleByComponent.has(componentId)) {
340
+ throw new ValidationError(
341
+ `analyzeCrossSurfaceInteractions: no single-surface candidate for component '${componentId}'`
342
+ );
343
+ }
344
+ }
345
+ for (let left = 0; left < input.componentOrder.length; left++) {
346
+ for (let right = left + 1; right < input.componentOrder.length; right++) {
347
+ const ids = [input.componentOrder[left], input.componentOrder[right]];
348
+ if (!candidateByComponents.has(crossSurfaceComponentSetKey(ids))) {
349
+ throw new ValidationError(
350
+ `analyzeCrossSurfaceInteractions: missing pair candidate for components [${ids.join(", ")}]`
351
+ );
352
+ }
353
+ }
354
+ }
355
+ const rowsByCandidate = /* @__PURE__ */ new Map();
356
+ const taskIds = new Set(input.taskOrder);
357
+ for (const row of input.rows) {
358
+ const candidate = candidateById.get(row.candidateId);
359
+ if (!candidate) {
360
+ throw new ValidationError(
361
+ `analyzeCrossSurfaceInteractions: row for task '${row.taskId}' names unknown candidate '${row.candidateId}'`
362
+ );
363
+ }
364
+ if (!taskIds.has(row.taskId)) {
365
+ throw new ValidationError(
366
+ `analyzeCrossSurfaceInteractions: row for candidate '${row.candidateId}' names task '${row.taskId}' outside the declared taskOrder`
367
+ );
368
+ }
369
+ validateRow(input, row, candidate);
370
+ const byTask = rowsByCandidate.get(row.candidateId) ?? /* @__PURE__ */ new Map();
371
+ if (byTask.has(row.taskId)) {
372
+ throw new ValidationError(
373
+ `analyzeCrossSurfaceInteractions: duplicate row for candidate '${row.candidateId}' and task '${row.taskId}'`
374
+ );
375
+ }
376
+ byTask.set(row.taskId, row);
377
+ rowsByCandidate.set(row.candidateId, byTask);
378
+ }
379
+ for (const candidateId of input.candidateOrder) {
380
+ const byTask = rowsByCandidate.get(candidateId);
381
+ const missing = input.taskOrder.filter((taskId) => !byTask?.has(taskId));
382
+ if (missing.length > 0) {
383
+ throw new ValidationError(
384
+ `analyzeCrossSurfaceInteractions: candidate '${candidateId}' is missing declared task row(s) [${missing.join(", ")}]; encode failed attempts explicitly instead of changing the task axis`
385
+ );
386
+ }
387
+ }
388
+ const expectedRows = input.candidateOrder.length * input.taskOrder.length;
389
+ if (input.rows.length !== expectedRows) {
390
+ throw new ValidationError(
391
+ `analyzeCrossSurfaceInteractions: expected exactly ${expectedRows} candidate \xD7 task rows, got ${input.rows.length}`
392
+ );
393
+ }
394
+ return {
395
+ input,
396
+ components: input.componentOrder.map((id) => componentById.get(id)),
397
+ candidates: input.candidateOrder.map((id) => candidateById.get(id)),
398
+ componentById,
399
+ candidateById,
400
+ candidateByComponents,
401
+ singleByComponent,
402
+ rowsByCandidate,
403
+ componentIndex,
404
+ candidateIndex: new Map(input.candidateOrder.map((id, index) => [id, index]))
405
+ };
406
+ }
407
+ function crossSurfaceRowsFor(context, candidateId) {
408
+ return context.input.taskOrder.map((taskId) => crossSurfaceRowFor(context, candidateId, taskId));
409
+ }
410
+ function crossSurfaceRowFor(context, candidateId, taskId) {
411
+ return context.rowsByCandidate.get(candidateId).get(taskId);
412
+ }
413
+ function canonicalCrossSurfaceComponents(context, componentIds) {
414
+ return [...componentIds].sort(
415
+ (left, right) => context.componentIndex.get(left) - context.componentIndex.get(right)
416
+ );
417
+ }
418
+ function crossSurfaceComponentSetKey(componentIds) {
419
+ return JSON.stringify(componentIds);
420
+ }
421
+ function validateBootstrap(input) {
422
+ const { seed, resamples, confidence } = input.bootstrap;
423
+ if (!Number.isInteger(seed)) {
424
+ throw new ValidationError(`analyzeCrossSurfaceInteractions: bootstrap.seed must be an integer`);
425
+ }
426
+ if (!Number.isInteger(resamples) || resamples <= 0) {
427
+ throw new ValidationError(
428
+ `analyzeCrossSurfaceInteractions: bootstrap.resamples must be a positive integer`
429
+ );
430
+ }
431
+ if (!Number.isFinite(confidence) || confidence <= 0 || confidence >= 1) {
432
+ throw new ValidationError(
433
+ `analyzeCrossSurfaceInteractions: bootstrap.confidence must be in (0,1)`
434
+ );
435
+ }
436
+ }
437
+ function validateSelection(input) {
438
+ const policy = input.selection;
439
+ for (const [name, value] of [
440
+ ["minimumFiringTasks", policy.minimumFiringTasks],
441
+ ["minimumEffectTasks", policy.minimumEffectTasks]
442
+ ]) {
443
+ if (!Number.isInteger(value) || value < 0 || value > input.taskOrder.length) {
444
+ throw new ValidationError(
445
+ `analyzeCrossSurfaceInteractions: selection.${name} must be an integer in [0,${input.taskOrder.length}]`
446
+ );
447
+ }
448
+ }
449
+ if (!Number.isInteger(policy.minimumBundleComponents) || policy.minimumBundleComponents < 2 || policy.minimumBundleComponents > input.componentOrder.length) {
450
+ throw new ValidationError(
451
+ `analyzeCrossSurfaceInteractions: selection.minimumBundleComponents must be an integer in [2,${input.componentOrder.length}]`
452
+ );
453
+ }
454
+ for (const [metric, limit] of Object.entries(policy.maximumMedianCostRatioToBaseline)) {
455
+ if (!input.costMetricOrder.includes(metric)) {
456
+ throw new ValidationError(
457
+ `analyzeCrossSurfaceInteractions: cost limit names undeclared metric '${metric}'`
458
+ );
459
+ }
460
+ if (!Number.isFinite(limit) || limit <= 0) {
461
+ throw new ValidationError(
462
+ `analyzeCrossSurfaceInteractions: cost ratio limit for '${metric}' must be positive and finite`
463
+ );
464
+ }
465
+ }
466
+ }
467
+ function validateCandidate(candidate, componentById, componentIndex, baselineCandidateId) {
468
+ assertNonEmpty(candidate.candidateId, "candidateId");
469
+ assertNonEmpty(candidate.contentHash, `contentHash for candidate '${candidate.candidateId}'`);
470
+ if (!Number.isInteger(candidate.artifactBytes) || candidate.artifactBytes < 0) {
471
+ throw new ValidationError(
472
+ `analyzeCrossSurfaceInteractions: artifactBytes for candidate '${candidate.candidateId}' must be a non-negative integer`
473
+ );
474
+ }
475
+ assertUnique(candidate.componentIds, `componentIds for candidate '${candidate.candidateId}'`);
476
+ for (const componentId of candidate.componentIds) {
477
+ if (!componentById.has(componentId)) {
478
+ throw new ValidationError(
479
+ `analyzeCrossSurfaceInteractions: candidate '${candidate.candidateId}' names unknown component '${componentId}'`
480
+ );
481
+ }
482
+ }
483
+ const canonical = [...candidate.componentIds].sort(
484
+ (left, right) => componentIndex.get(left) - componentIndex.get(right)
485
+ );
486
+ if (!arraysEqual(candidate.componentIds, canonical)) {
487
+ throw new ValidationError(
488
+ `analyzeCrossSurfaceInteractions: candidate '${candidate.candidateId}' components must follow componentOrder`
489
+ );
490
+ }
491
+ if (candidate.candidateId !== baselineCandidateId && candidate.componentIds.length === 0) {
492
+ throw new ValidationError(
493
+ `analyzeCrossSurfaceInteractions: only baseline '${baselineCandidateId}' may have zero components`
494
+ );
495
+ }
496
+ }
497
+ function validateRow(input, row, candidate) {
498
+ if (!arraysEqual(row.componentIds, candidate.componentIds)) {
499
+ throw new ValidationError(
500
+ `analyzeCrossSurfaceInteractions: row '${row.candidateId}/${row.taskId}' componentIds do not match its candidate`
501
+ );
502
+ }
503
+ if (!["complete", "missing", "invalid"].includes(row.completeness)) {
504
+ throw new ValidationError(
505
+ `analyzeCrossSurfaceInteractions: row '${row.candidateId}/${row.taskId}' has unknown completeness '${String(row.completeness)}'`
506
+ );
507
+ }
508
+ if (row.completeness === "complete") {
509
+ if (typeof row.pass !== "boolean" || !Number.isFinite(row.score)) {
510
+ throw new ValidationError(
511
+ `analyzeCrossSurfaceInteractions: complete row '${row.candidateId}/${row.taskId}' requires boolean pass and finite score`
512
+ );
513
+ }
514
+ if (row.rejectReason !== null) {
515
+ throw new ValidationError(
516
+ `analyzeCrossSurfaceInteractions: complete row '${row.candidateId}/${row.taskId}' cannot carry rejectReason`
517
+ );
518
+ }
519
+ } else {
520
+ if (row.pass !== null || row.score !== null) {
521
+ throw new ValidationError(
522
+ `analyzeCrossSurfaceInteractions: ${row.completeness} row '${row.candidateId}/${row.taskId}' must use null pass and score`
523
+ );
524
+ }
525
+ if (typeof row.rejectReason !== "string" || row.rejectReason.trim() === "") {
526
+ throw new ValidationError(
527
+ `analyzeCrossSurfaceInteractions: ${row.completeness} row '${row.candidateId}/${row.taskId}' requires rejectReason`
528
+ );
529
+ }
530
+ }
531
+ assertExactSet(
532
+ input.costMetricOrder,
533
+ Object.keys(row.cost),
534
+ `cost keys for row '${row.candidateId}/${row.taskId}'`,
535
+ "costMetricOrder"
536
+ );
537
+ for (const metric of input.costMetricOrder) {
538
+ const value = row.cost[metric];
539
+ if (value === null || value === void 0 || !Number.isFinite(value) || value < 0) {
540
+ throw new ValidationError(
541
+ `analyzeCrossSurfaceInteractions: unknown or invalid cost '${metric}' on row '${row.candidateId}/${row.taskId}'; every attempt must report a non-negative finite value`
542
+ );
543
+ }
544
+ }
545
+ const evidenceByComponent = indexUnique(
546
+ row.componentEvidence,
547
+ (evidence) => evidence.componentId,
548
+ `componentEvidence on row '${row.candidateId}/${row.taskId}'`
549
+ );
550
+ assertExactSet(
551
+ candidate.componentIds,
552
+ evidenceByComponent.keys(),
553
+ `componentEvidence on row '${row.candidateId}/${row.taskId}'`,
554
+ "candidate components"
555
+ );
556
+ for (const evidence of evidenceByComponent.values()) {
557
+ assertTriState(evidence.fired, "fired", row);
558
+ assertTriState(evidence.effectObserved, "effectObserved", row);
559
+ if (evidence.effectObserved === true && evidence.fired !== true) {
560
+ throw new ValidationError(
561
+ `analyzeCrossSurfaceInteractions: effectObserved=true requires fired=true for component '${evidence.componentId}' on row '${row.candidateId}/${row.taskId}'`
562
+ );
563
+ }
564
+ }
565
+ }
566
+ function assertTriState(value, field, row) {
567
+ if (value !== true && value !== false && value !== null) {
568
+ throw new ValidationError(
569
+ `analyzeCrossSurfaceInteractions: ${field} on row '${row.candidateId}/${row.taskId}' must be boolean or null`
570
+ );
571
+ }
572
+ }
573
+ function indexUnique(items, id, label) {
574
+ const result = /* @__PURE__ */ new Map();
575
+ for (const item of items) {
576
+ const key = id(item);
577
+ assertNonEmpty(key, `${label} id`);
578
+ if (result.has(key)) {
579
+ throw new ValidationError(`analyzeCrossSurfaceInteractions: duplicate ${label} id '${key}'`);
580
+ }
581
+ result.set(key, item);
582
+ }
583
+ return result;
584
+ }
585
+ function assertNonEmptyUnique(values, label) {
586
+ if (values.length === 0) {
587
+ throw new ValidationError(`analyzeCrossSurfaceInteractions: ${label} is empty`);
588
+ }
589
+ assertUnique(values, label);
590
+ for (const value of values) assertNonEmpty(value, label);
591
+ }
592
+ function assertUnique(values, label) {
593
+ if (new Set(values).size !== values.length) {
594
+ throw new ValidationError(`analyzeCrossSurfaceInteractions: ${label} contains duplicates`);
595
+ }
596
+ }
597
+ function assertNonEmpty(value, label) {
598
+ if (typeof value !== "string" || value.trim() === "") {
599
+ throw new ValidationError(
600
+ `analyzeCrossSurfaceInteractions: ${label} must be a non-empty string`
601
+ );
602
+ }
603
+ }
604
+ function assertExactSet(expected, actualIterable, actualLabel, expectedLabel) {
605
+ const actual = [...actualIterable];
606
+ const expectedSet = new Set(expected);
607
+ const actualSet = new Set(actual);
608
+ const missing = expected.filter((value) => !actualSet.has(value));
609
+ const extra = actual.filter((value) => !expectedSet.has(value));
610
+ if (missing.length > 0 || extra.length > 0 || actual.length !== expected.length) {
611
+ throw new ValidationError(
612
+ `analyzeCrossSurfaceInteractions: ${actualLabel} does not match ${expectedLabel}; missing=[${missing.join(", ")}], extra=[${extra.join(", ")}]`
613
+ );
614
+ }
615
+ }
616
+ function arraysEqual(left, right) {
617
+ return left.length === right.length && left.every((value, index) => value === right[index]);
618
+ }
619
+
620
+ // src/campaign/cross-surface-interaction.ts
621
+ function analyzeCrossSurfaceInteractions(input) {
622
+ const context = validateCrossSurfaceInput(input);
623
+ const baseline = context.candidateById.get(input.baselineCandidateId);
624
+ const preliminary = context.candidates.map(
625
+ (candidate) => summarizeCandidate(context, candidate, baseline)
626
+ );
627
+ const baselineSummary = preliminary.find(
628
+ (summary) => summary.candidate.candidateId === input.baselineCandidateId
629
+ );
630
+ const summaries = preliminary.map(
631
+ (summary) => summary.candidate.candidateId === input.baselineCandidateId ? summary : {
632
+ ...summary,
633
+ eligibility: candidateEligibility(context, summary, baselineSummary)
634
+ }
635
+ );
636
+ const summaryById = new Map(summaries.map((summary) => [summary.candidate.candidateId, summary]));
637
+ const eligibleSingles = rankedEligibleSingles(context, summaryById);
638
+ const interactionReadySingles = rankedInteractionReadySingles(context, summaryById);
639
+ const pairwise = buildPairwise(context, summaryById, interactionReadySingles, baselineSummary);
640
+ const selections = selectCandidates(
641
+ context,
642
+ summaryById,
643
+ eligibleSingles,
644
+ interactionReadySingles,
645
+ pairwise
646
+ );
647
+ const rows = context.candidates.flatMap(
648
+ (candidate) => input.taskOrder.map(
649
+ (taskId) => context.rowsByCandidate.get(candidate.candidateId).get(taskId)
650
+ )
651
+ );
652
+ return {
653
+ taskIds: [...input.taskOrder],
654
+ componentIds: [...input.componentOrder],
655
+ candidateIds: [...input.candidateOrder],
656
+ costMetrics: [...input.costMetricOrder],
657
+ rows,
658
+ missingAttempts: rows.filter((row) => row.completeness === "missing"),
659
+ invalidAttempts: rows.filter((row) => row.completeness === "invalid"),
660
+ candidates: summaries,
661
+ pairwise,
662
+ selections
663
+ };
664
+ }
665
+ function summarizeCandidate(context, candidate, baseline) {
666
+ const rows = crossSurfaceRowsFor(context, candidate.candidateId);
667
+ const baselineRows = crossSurfaceRowsFor(context, baseline.candidateId);
668
+ const outcome = summarizeOutcome(
669
+ context,
670
+ rows,
671
+ baselineRows,
672
+ candidate.candidateId === baseline.candidateId
673
+ );
674
+ const completeScores = rows.filter((row) => row.completeness === "complete").map((row) => row.score);
675
+ const costs = Object.fromEntries(
676
+ context.input.costMetricOrder.map((metric) => [
677
+ metric,
678
+ distribution(rows.map((row) => row.cost[metric]))
679
+ ])
680
+ );
681
+ return {
682
+ candidate,
683
+ outcome,
684
+ score: completeScores.length === 0 ? null : distribution(completeScores),
685
+ costs,
686
+ firing: summarizeCandidateEvidence(rows, candidate.componentIds, "fired"),
687
+ effect: summarizeCandidateEvidence(rows, candidate.componentIds, "effectObserved"),
688
+ comparisonToBaseline: candidate.candidateId === baseline.candidateId ? null : pairedComparison(context, baseline.candidateId, candidate.candidateId),
689
+ eligibility: null
690
+ };
691
+ }
692
+ function summarizeOutcome(context, rows, baselineRows, isBaseline) {
693
+ const resolvedTaskIds = [];
694
+ const failedTaskIds = [];
695
+ const missingTaskIds = [];
696
+ const invalidTaskIds = [];
697
+ const benefitTaskIds = [];
698
+ const regressionTaskIds = [];
699
+ const comparisonMissingTaskIds = [];
700
+ for (let index = 0; index < context.input.taskOrder.length; index++) {
701
+ const row = rows[index];
702
+ const baseline = baselineRows[index];
703
+ if (row.completeness === "missing") missingTaskIds.push(row.taskId);
704
+ else if (row.completeness === "invalid") invalidTaskIds.push(row.taskId);
705
+ else if (row.pass) resolvedTaskIds.push(row.taskId);
706
+ else failedTaskIds.push(row.taskId);
707
+ if (isBaseline) continue;
708
+ if (row.completeness !== "complete" || baseline.completeness !== "complete") {
709
+ comparisonMissingTaskIds.push(row.taskId);
710
+ } else if (row.pass && !baseline.pass) benefitTaskIds.push(row.taskId);
711
+ else if (!row.pass && baseline.pass) regressionTaskIds.push(row.taskId);
712
+ }
713
+ return {
714
+ resolvedTaskIds,
715
+ failedTaskIds,
716
+ missingTaskIds,
717
+ invalidTaskIds,
718
+ benefitTaskIds,
719
+ regressionTaskIds,
720
+ comparisonMissingTaskIds,
721
+ netBenefit: benefitTaskIds.length - regressionTaskIds.length
722
+ };
723
+ }
724
+ function summarizeCandidateEvidence(rows, componentIds, field) {
725
+ if (componentIds.length === 0) {
726
+ return {
727
+ byComponent: [],
728
+ allObservedTaskIds: [],
729
+ someObservedTaskIds: [],
730
+ noneObservedTaskIds: [],
731
+ unobservedTaskIds: []
732
+ };
733
+ }
734
+ const byComponent = componentIds.map((componentId) => {
735
+ const observedTaskIds = [];
736
+ const notObservedTaskIds = [];
737
+ const unobservedTaskIds2 = [];
738
+ for (const row of rows) {
739
+ const value = evidenceValue(row, componentId, field);
740
+ if (value === true) observedTaskIds.push(row.taskId);
741
+ else if (value === false) notObservedTaskIds.push(row.taskId);
742
+ else unobservedTaskIds2.push(row.taskId);
743
+ }
744
+ return { componentId, observedTaskIds, notObservedTaskIds, unobservedTaskIds: unobservedTaskIds2 };
745
+ });
746
+ const allObservedTaskIds = [];
747
+ const someObservedTaskIds = [];
748
+ const noneObservedTaskIds = [];
749
+ const unobservedTaskIds = [];
750
+ for (const row of rows) {
751
+ const values = componentIds.map((componentId) => evidenceValue(row, componentId, field));
752
+ if (values.some((value) => value === null)) unobservedTaskIds.push(row.taskId);
753
+ else if (values.every(Boolean)) allObservedTaskIds.push(row.taskId);
754
+ else if (values.some(Boolean)) someObservedTaskIds.push(row.taskId);
755
+ else noneObservedTaskIds.push(row.taskId);
756
+ }
757
+ return {
758
+ byComponent,
759
+ allObservedTaskIds,
760
+ someObservedTaskIds,
761
+ noneObservedTaskIds,
762
+ unobservedTaskIds
763
+ };
764
+ }
765
+ function candidateEligibility(context, summary, baseline) {
766
+ const reasons = [];
767
+ if (summary.outcome.missingTaskIds.length > 0) reasons.push("missing_attempt");
768
+ if (summary.outcome.invalidTaskIds.length > 0) reasons.push("invalid_attempt");
769
+ if (summary.outcome.comparisonMissingTaskIds.length > 0) reasons.push("baseline_outcome_missing");
770
+ if (summary.outcome.benefitTaskIds.length <= summary.outcome.regressionTaskIds.length) {
771
+ reasons.push("benefit_not_greater_than_regression");
772
+ }
773
+ appendEvidenceEligibilityReasons(
774
+ reasons,
775
+ summary.firing,
776
+ context.input.selection.minimumFiringTasks,
777
+ context.input.selection.requireObservedFiring,
778
+ "firing_below_minimum",
779
+ "firing_unobserved"
780
+ );
781
+ appendEvidenceEligibilityReasons(
782
+ reasons,
783
+ summary.effect,
784
+ context.input.selection.minimumEffectTasks,
785
+ context.input.selection.requireObservedEffect,
786
+ "effect_below_minimum",
787
+ "effect_unobserved"
788
+ );
789
+ if (!withinCostLimits(context, summary, baseline)) reasons.push("cost_limit_exceeded");
790
+ return { eligible: reasons.length === 0, reasons: unique(reasons) };
791
+ }
792
+ function appendEvidenceEligibilityReasons(reasons, evidence, minimum, requireObserved, belowReason, unobservedReason) {
793
+ if (evidence.byComponent.some((component) => component.observedTaskIds.length < minimum)) {
794
+ reasons.push(belowReason);
795
+ }
796
+ if (requireObserved && evidence.byComponent.some((component) => component.unobservedTaskIds.length > 0)) {
797
+ reasons.push(unobservedReason);
798
+ }
799
+ }
800
+ function rankedEligibleSingles(context, summaryById) {
801
+ return [...context.singleByComponent.values()].map((candidate) => summaryById.get(candidate.candidateId)).filter((summary) => summary.eligibility?.eligible).sort((left, right) => compareSingleSummaries(context, left, right));
802
+ }
803
+ function rankedInteractionReadySingles(context, summaryById) {
804
+ return [...context.singleByComponent.values()].map((candidate) => summaryById.get(candidate.candidateId)).filter(
805
+ (summary) => summary.outcome.regressionTaskIds.length === 0 && summary.eligibility?.reasons.every(
806
+ (reason) => reason === "benefit_not_greater_than_regression"
807
+ )
808
+ ).sort((left, right) => compareSingleSummaries(context, left, right));
809
+ }
810
+ function compareSingleSummaries(context, left, right) {
811
+ const byNetBenefit = right.outcome.netBenefit - left.outcome.netBenefit;
812
+ if (byNetBenefit !== 0) return byNetBenefit;
813
+ const byBenefit = right.outcome.benefitTaskIds.length - left.outcome.benefitTaskIds.length;
814
+ if (byBenefit !== 0) return byBenefit;
815
+ const byRegression = left.outcome.regressionTaskIds.length - right.outcome.regressionTaskIds.length;
816
+ if (byRegression !== 0) return byRegression;
817
+ for (const metric of context.input.costMetricOrder) {
818
+ const byCost = left.costs[metric].median - right.costs[metric].median;
819
+ if (byCost !== 0) return byCost;
820
+ }
821
+ const byBytes = left.candidate.artifactBytes - right.candidate.artifactBytes;
822
+ if (byBytes !== 0) return byBytes;
823
+ return context.candidateIndex.get(left.candidate.candidateId) - context.candidateIndex.get(right.candidate.candidateId);
824
+ }
825
+ function buildPairwise(context, summaryById, interactionReadySingles, baseline) {
826
+ const interactionReadyIds = new Set(
827
+ interactionReadySingles.map((summary) => summary.candidate.candidateId)
828
+ );
829
+ const entries = [];
830
+ for (let leftIndex = 0; leftIndex < context.components.length; leftIndex++) {
831
+ for (let rightIndex = leftIndex + 1; rightIndex < context.components.length; rightIndex++) {
832
+ const leftComponent = context.components[leftIndex];
833
+ const rightComponent = context.components[rightIndex];
834
+ const leftSingle = context.singleByComponent.get(leftComponent.componentId);
835
+ const rightSingle = context.singleByComponent.get(rightComponent.componentId);
836
+ const pair = context.candidateByComponents.get(
837
+ crossSurfaceComponentSetKey([leftComponent.componentId, rightComponent.componentId])
838
+ );
839
+ const pairSummary = summaryById.get(pair.candidateId);
840
+ const leftSummary = summaryById.get(leftSingle.candidateId);
841
+ const rightSummary = summaryById.get(rightSingle.candidateId);
842
+ const synergyTaskIds = [];
843
+ const interferenceTaskIds = [];
844
+ for (const taskId of context.input.taskOrder) {
845
+ const pairRow = crossSurfaceRowFor(context, pair.candidateId, taskId);
846
+ const leftRow = crossSurfaceRowFor(context, leftSingle.candidateId, taskId);
847
+ const rightRow = crossSurfaceRowFor(context, rightSingle.candidateId, taskId);
848
+ if (![pairRow, leftRow, rightRow].every((row) => row.completeness === "complete")) continue;
849
+ if (pairRow.pass && !leftRow.pass && !rightRow.pass) synergyTaskIds.push(taskId);
850
+ if (!pairRow.pass && (leftRow.pass || rightRow.pass)) interferenceTaskIds.push(taskId);
851
+ }
852
+ const incrementalVsConstituents = [
853
+ compareCandidates(context, summaryById, leftSingle.candidateId, pair.candidateId),
854
+ compareCandidates(context, summaryById, rightSingle.candidateId, pair.candidateId)
855
+ ];
856
+ const betterSingle = compareSingleSummaries(context, leftSummary, rightSummary) <= 0 ? leftSummary : rightSummary;
857
+ const firing = summarizePairEvidence(
858
+ crossSurfaceRowsFor(context, pair.candidateId),
859
+ leftComponent.componentId,
860
+ rightComponent.componentId,
861
+ "fired"
862
+ );
863
+ const effect = summarizePairEvidence(
864
+ crossSurfaceRowsFor(context, pair.candidateId),
865
+ leftComponent.componentId,
866
+ rightComponent.componentId,
867
+ "effectObserved"
868
+ );
869
+ const compatibility = pairCompatibility(
870
+ context,
871
+ pairSummary,
872
+ baseline,
873
+ betterSingle,
874
+ incrementalVsConstituents,
875
+ interactionReadyIds.has(leftSingle.candidateId) && interactionReadyIds.has(rightSingle.candidateId),
876
+ interferenceTaskIds,
877
+ firing,
878
+ effect
879
+ );
880
+ entries.push({
881
+ componentIds: [leftComponent.componentId, rightComponent.componentId],
882
+ singleCandidateIds: [leftSingle.candidateId, rightSingle.candidateId],
883
+ compositionCandidateId: pair.candidateId,
884
+ benefitTaskIds: [...pairSummary.outcome.benefitTaskIds],
885
+ regressionTaskIds: [...pairSummary.outcome.regressionTaskIds],
886
+ synergyTaskIds,
887
+ interferenceTaskIds,
888
+ incrementalVsConstituents,
889
+ relativeCostToBaseline: relativeCosts(context, pairSummary, baseline),
890
+ firing,
891
+ effect,
892
+ interaction: interactionEffect(
893
+ context,
894
+ baseline.candidate,
895
+ [leftSingle, rightSingle],
896
+ pair
897
+ ),
898
+ compatibility
899
+ });
900
+ }
901
+ }
902
+ return entries;
903
+ }
904
+ function pairCompatibility(context, pair, baseline, betterSingle, comparisons, constituentsReady, interferenceTaskIds, firing, effect) {
905
+ const reasons = [];
906
+ if (!constituentsReady) reasons.push("constituent_not_ready");
907
+ if (pair.outcome.missingTaskIds.length > 0 || pair.outcome.invalidTaskIds.length > 0 || pair.outcome.comparisonMissingTaskIds.length > 0) {
908
+ reasons.push("pair_incomplete");
909
+ }
910
+ if (pair.outcome.regressionTaskIds.length > 0) reasons.push("baseline_regression");
911
+ if (interferenceTaskIds.length > 0) reasons.push("interference");
912
+ const betterComparison = comparisons.find(
913
+ (comparison) => comparison.comparatorCandidateId === betterSingle.candidate.candidateId
914
+ );
915
+ if (betterComparison.winsTaskIds.length === 0) reasons.push("no_incremental_resolution");
916
+ appendPairEvidenceReasons(
917
+ reasons,
918
+ firing,
919
+ context.input.selection.minimumFiringTasks,
920
+ context.input.selection.requireObservedFiring,
921
+ "firing_below_minimum",
922
+ "firing_unobserved"
923
+ );
924
+ appendPairEvidenceReasons(
925
+ reasons,
926
+ effect,
927
+ context.input.selection.minimumEffectTasks,
928
+ context.input.selection.requireObservedEffect,
929
+ "effect_below_minimum",
930
+ "effect_unobserved"
931
+ );
932
+ if (!withinCostLimits(context, pair, baseline)) reasons.push("cost_limit_exceeded");
933
+ return {
934
+ compatible: reasons.length === 0,
935
+ reasons: unique(reasons),
936
+ betterSingleCandidateId: betterSingle.candidate.candidateId
937
+ };
938
+ }
939
+ function appendPairEvidenceReasons(reasons, evidence, minimum, requireObserved, belowReason, unobservedReason) {
940
+ const leftObserved = evidence.bothTaskIds.length + evidence.leftOnlyTaskIds.length;
941
+ const rightObserved = evidence.bothTaskIds.length + evidence.rightOnlyTaskIds.length;
942
+ if (leftObserved < minimum || rightObserved < minimum) reasons.push(belowReason);
943
+ if (requireObserved && evidence.unobservedTaskIds.length > 0) reasons.push(unobservedReason);
944
+ }
945
+ function interactionEffect(context, baseline, singles, composition) {
946
+ const perTask = context.input.taskOrder.map((taskId) => {
947
+ const baselineRow = crossSurfaceRowFor(context, baseline.candidateId, taskId);
948
+ const singleRows = singles.map((single) => crossSurfaceRowFor(context, single.candidateId, taskId));
949
+ const compositionRow = crossSurfaceRowFor(context, composition.candidateId, taskId);
950
+ const rows = [baselineRow, ...singleRows, compositionRow];
951
+ if (!rows.every((row) => row.completeness === "complete")) {
952
+ return { taskId, passInteraction: null, scoreInteraction: null };
953
+ }
954
+ const additivePass = singleRows.reduce((sum, row) => sum + Number(row.pass), 0) - (singles.length - 1) * Number(baselineRow.pass);
955
+ const additiveScore = singleRows.reduce((sum, row) => sum + row.score, 0) - (singles.length - 1) * baselineRow.score;
956
+ return {
957
+ taskId,
958
+ passInteraction: Number(compositionRow.pass) - additivePass,
959
+ scoreInteraction: compositionRow.score - additiveScore
960
+ };
961
+ });
962
+ const complete = perTask.filter(
963
+ (row) => row.passInteraction !== null && row.scoreInteraction !== null
964
+ );
965
+ const passInteractions = complete.map((row) => row.passInteraction);
966
+ const scoreInteractions = complete.map((row) => row.scoreInteraction);
967
+ return {
968
+ perTask,
969
+ n: complete.length,
970
+ nMissing: perTask.length - complete.length,
971
+ meanPassInteraction: meanOrNull(passInteractions),
972
+ meanScoreInteraction: meanOrNull(scoreInteractions),
973
+ passBootstrap: bootstrapInteraction(passInteractions, context),
974
+ scoreBootstrap: bootstrapInteraction(scoreInteractions, context)
975
+ };
976
+ }
977
+ function bootstrapInteraction(interactions, context) {
978
+ if (interactions.length === 0) return null;
979
+ return pairedBootstrap(new Array(interactions.length).fill(0), interactions, {
980
+ ...context.input.bootstrap,
981
+ statistic: "mean"
982
+ });
983
+ }
984
+ function selectCandidates(context, summaryById, eligibleSingles, interactionReadySingles, pairwise) {
985
+ const bestSingleRanking = eligibleSingles.filter((summary) => {
986
+ const componentId = summary.candidate.componentIds[0];
987
+ return context.componentById.get(componentId).bestSingleEligible;
988
+ });
989
+ const ranking = bestSingleRanking.map((summary, index) => ({
990
+ rank: index + 1,
991
+ candidateId: summary.candidate.candidateId,
992
+ componentId: summary.candidate.componentIds[0]
993
+ }));
994
+ const best = bestSingleRanking[0];
995
+ const bestSingle = best ? {
996
+ candidateId: best.candidate.candidateId,
997
+ componentId: best.candidate.componentIds[0],
998
+ ranking
999
+ } : null;
1000
+ const naiveStack = selectNaiveStack(context, eligibleSingles);
1001
+ const interactionAware = selectInteractionAware(
1002
+ context,
1003
+ summaryById,
1004
+ interactionReadySingles,
1005
+ pairwise
1006
+ );
1007
+ return { bestSingle, naiveStack, interactionAware };
1008
+ }
1009
+ function selectNaiveStack(context, eligibleSingles) {
1010
+ if (eligibleSingles.length === 0) return null;
1011
+ const componentIds = eligibleSingles.map((summary) => summary.candidate.componentIds[0]).sort((left, right) => context.componentIndex.get(left) - context.componentIndex.get(right));
1012
+ const candidate = context.candidateByComponents.get(crossSurfaceComponentSetKey(componentIds));
1013
+ if (!candidate) {
1014
+ throw new ValidationError(
1015
+ `analyzeCrossSurfaceInteractions: naive stack [${componentIds.join(", ")}] was not evaluated`
1016
+ );
1017
+ }
1018
+ return { candidateId: candidate.candidateId, componentIds };
1019
+ }
1020
+ function selectInteractionAware(context, summaryById, interactionReadySingles, pairwise) {
1021
+ const baseline = summaryById.get(context.input.baselineCandidateId);
1022
+ const pairByComponents = new Map(
1023
+ pairwise.map((entry) => [crossSurfaceComponentSetKey(entry.componentIds), entry])
1024
+ );
1025
+ const paths = pairwise.filter((entry) => entry.compatibility.compatible).map(
1026
+ (entry) => growInteractionPath(
1027
+ context,
1028
+ summaryById,
1029
+ interactionReadySingles,
1030
+ pairByComponents,
1031
+ baseline,
1032
+ summaryById.get(entry.compositionCandidateId)
1033
+ )
1034
+ );
1035
+ if (paths.length === 0) return null;
1036
+ const qualified = paths.filter((path) => path.qualified).sort((left, right) => compareInteractionPaths(context, summaryById, left, right));
1037
+ const winning = qualified[0] ?? [...paths].sort((left, right) => compareInteractionPaths(context, summaryById, left, right))[0];
1038
+ return {
1039
+ seedCandidateId: winning.seedCandidateId,
1040
+ terminalCandidateId: winning.terminalCandidateId,
1041
+ terminalComponentIds: [...winning.terminalComponentIds],
1042
+ selectedCandidateId: qualified.length > 0 ? winning.terminalCandidateId : null,
1043
+ qualified: qualified.length > 0,
1044
+ evaluatedPaths: paths,
1045
+ steps: winning.steps
1046
+ };
1047
+ }
1048
+ function growInteractionPath(context, summaryById, interactionReadySingles, pairByComponents, baseline, seed) {
1049
+ let current = seed;
1050
+ let retained = [...seed.candidate.componentIds];
1051
+ let remaining = interactionReadySingles.filter(
1052
+ (summary) => !retained.includes(summary.candidate.componentIds[0])
1053
+ );
1054
+ const steps = [];
1055
+ while (remaining.length > 0) {
1056
+ const decisions = remaining.map(
1057
+ (addition) => evaluateAddition(
1058
+ context,
1059
+ summaryById,
1060
+ pairByComponents,
1061
+ baseline,
1062
+ current,
1063
+ retained,
1064
+ addition
1065
+ )
1066
+ );
1067
+ const eligible = decisions.filter((decision) => decision.eligible).sort((left, right) => compareAdditionDecisions(context, summaryById, left, right));
1068
+ const selected = eligible[0];
1069
+ if (selected) selected.selected = true;
1070
+ steps.push({
1071
+ fromCandidateId: current.candidate.candidateId,
1072
+ retainedComponentIds: [...retained],
1073
+ considered: decisions.sort(
1074
+ (left, right) => context.candidateIndex.get(left.additionCandidateId) - context.candidateIndex.get(right.additionCandidateId)
1075
+ ),
1076
+ selectedCandidateId: selected?.bundleCandidateId ?? null
1077
+ });
1078
+ if (!selected?.bundleCandidateId) break;
1079
+ current = summaryById.get(selected.bundleCandidateId);
1080
+ retained = [...current.candidate.componentIds];
1081
+ remaining = remaining.filter(
1082
+ (summary) => summary.candidate.candidateId !== selected.additionCandidateId
1083
+ );
1084
+ }
1085
+ const qualified = retained.length >= context.input.selection.minimumBundleComponents;
1086
+ return {
1087
+ seedCandidateId: seed.candidate.candidateId,
1088
+ terminalCandidateId: current.candidate.candidateId,
1089
+ terminalComponentIds: retained,
1090
+ qualified,
1091
+ steps
1092
+ };
1093
+ }
1094
+ function compareInteractionPaths(context, summaryById, left, right) {
1095
+ const leftSummary = summaryById.get(left.terminalCandidateId);
1096
+ const rightSummary = summaryById.get(right.terminalCandidateId);
1097
+ const byNetBenefit = rightSummary.outcome.netBenefit - leftSummary.outcome.netBenefit;
1098
+ if (byNetBenefit !== 0) return byNetBenefit;
1099
+ const byBenefit = rightSummary.outcome.benefitTaskIds.length - leftSummary.outcome.benefitTaskIds.length;
1100
+ if (byBenefit !== 0) return byBenefit;
1101
+ const byRegression = leftSummary.outcome.regressionTaskIds.length - rightSummary.outcome.regressionTaskIds.length;
1102
+ if (byRegression !== 0) return byRegression;
1103
+ for (const metric of context.input.costMetricOrder) {
1104
+ const byCost = leftSummary.costs[metric].median - rightSummary.costs[metric].median;
1105
+ if (byCost !== 0) return byCost;
1106
+ }
1107
+ const byBytes = leftSummary.candidate.artifactBytes - rightSummary.candidate.artifactBytes;
1108
+ if (byBytes !== 0) return byBytes;
1109
+ const byCandidateOrder = context.candidateIndex.get(left.terminalCandidateId) - context.candidateIndex.get(right.terminalCandidateId);
1110
+ if (byCandidateOrder !== 0) return byCandidateOrder;
1111
+ return context.candidateIndex.get(left.seedCandidateId) - context.candidateIndex.get(right.seedCandidateId);
1112
+ }
1113
+ function evaluateAddition(context, summaryById, pairByComponents, baseline, current, retained, addition) {
1114
+ const additionComponentId = addition.candidate.componentIds[0];
1115
+ const reasons = [];
1116
+ for (const retainedComponentId of retained) {
1117
+ const pair = pairByComponents.get(
1118
+ crossSurfaceComponentSetKey(canonicalCrossSurfaceComponents(context, [retainedComponentId, additionComponentId]))
1119
+ );
1120
+ if (!pair?.compatibility.compatible) reasons.push("pair_incompatible");
1121
+ }
1122
+ const bundleComponents = canonicalCrossSurfaceComponents(context, [...retained, additionComponentId]);
1123
+ const bundle = context.candidateByComponents.get(crossSurfaceComponentSetKey(bundleComponents));
1124
+ if (!bundle) {
1125
+ reasons.push("full_bundle_not_evaluated");
1126
+ return emptyAdditionDecision(addition, additionComponentId, reasons);
1127
+ }
1128
+ const bundleSummary = summaryById.get(bundle.candidateId);
1129
+ if (bundleSummary.outcome.missingTaskIds.length > 0 || bundleSummary.outcome.invalidTaskIds.length > 0 || bundleSummary.outcome.comparisonMissingTaskIds.length > 0) {
1130
+ reasons.push("bundle_incomplete");
1131
+ }
1132
+ if (bundleSummary.outcome.regressionTaskIds.length > 0) reasons.push("baseline_regression");
1133
+ const comparison = compareCandidates(
1134
+ context,
1135
+ summaryById,
1136
+ current.candidate.candidateId,
1137
+ bundle.candidateId
1138
+ );
1139
+ if (comparison.winsTaskIds.length === 0) reasons.push("no_incremental_resolution");
1140
+ if (comparison.regressionTaskIds.length > 0) reasons.push("incremental_regression");
1141
+ appendBundleEvidenceReasons(
1142
+ reasons,
1143
+ bundleSummary.firing,
1144
+ context.input.selection.minimumFiringTasks,
1145
+ context.input.selection.requireObservedFiring,
1146
+ "firing_below_minimum",
1147
+ "firing_unobserved"
1148
+ );
1149
+ appendBundleEvidenceReasons(
1150
+ reasons,
1151
+ bundleSummary.effect,
1152
+ context.input.selection.minimumEffectTasks,
1153
+ context.input.selection.requireObservedEffect,
1154
+ "effect_below_minimum",
1155
+ "effect_unobserved"
1156
+ );
1157
+ if (!withinCostLimits(context, bundleSummary, baseline)) reasons.push("cost_limit_exceeded");
1158
+ return {
1159
+ additionCandidateId: addition.candidate.candidateId,
1160
+ additionComponentId,
1161
+ bundleCandidateId: bundle.candidateId,
1162
+ incrementalResolutionTaskIds: comparison.winsTaskIds,
1163
+ incrementalRegressionTaskIds: comparison.regressionTaskIds,
1164
+ incrementalMedianCost: Object.fromEntries(
1165
+ context.input.costMetricOrder.map((metric) => [
1166
+ metric,
1167
+ bundleSummary.costs[metric].median - current.costs[metric].median
1168
+ ])
1169
+ ),
1170
+ eligible: reasons.length === 0,
1171
+ selected: false,
1172
+ reasons: unique(reasons)
1173
+ };
1174
+ }
1175
+ function emptyAdditionDecision(addition, additionComponentId, reasons) {
1176
+ return {
1177
+ additionCandidateId: addition.candidate.candidateId,
1178
+ additionComponentId,
1179
+ bundleCandidateId: null,
1180
+ incrementalResolutionTaskIds: [],
1181
+ incrementalRegressionTaskIds: [],
1182
+ incrementalMedianCost: null,
1183
+ eligible: false,
1184
+ selected: false,
1185
+ reasons: unique(reasons)
1186
+ };
1187
+ }
1188
+ function appendBundleEvidenceReasons(reasons, evidence, minimum, requireObserved, belowReason, unobservedReason) {
1189
+ if (evidence.byComponent.some((component) => component.observedTaskIds.length < minimum)) {
1190
+ reasons.push(belowReason);
1191
+ }
1192
+ if (requireObserved && evidence.byComponent.some((component) => component.unobservedTaskIds.length > 0)) {
1193
+ reasons.push(unobservedReason);
1194
+ }
1195
+ }
1196
+ function compareAdditionDecisions(context, summaryById, left, right) {
1197
+ const byWins = right.incrementalResolutionTaskIds.length - left.incrementalResolutionTaskIds.length;
1198
+ if (byWins !== 0) return byWins;
1199
+ for (const metric of context.input.costMetricOrder) {
1200
+ const byCost = left.incrementalMedianCost[metric] - right.incrementalMedianCost[metric];
1201
+ if (byCost !== 0) return byCost;
1202
+ }
1203
+ const leftBytes = summaryById.get(left.additionCandidateId).candidate.artifactBytes;
1204
+ const rightBytes = summaryById.get(right.additionCandidateId).candidate.artifactBytes;
1205
+ const byBytes = leftBytes - rightBytes;
1206
+ if (byBytes !== 0) return byBytes;
1207
+ return context.candidateIndex.get(left.additionCandidateId) - context.candidateIndex.get(right.additionCandidateId);
1208
+ }
1209
+ function compareCandidates(context, summaryById, comparatorCandidateId, treatmentCandidateId) {
1210
+ const winsTaskIds = [];
1211
+ const regressionTaskIds = [];
1212
+ const missingTaskIds = [];
1213
+ for (const taskId of context.input.taskOrder) {
1214
+ const comparator = crossSurfaceRowFor(context, comparatorCandidateId, taskId);
1215
+ const treatment = crossSurfaceRowFor(context, treatmentCandidateId, taskId);
1216
+ if (comparator.completeness !== "complete" || treatment.completeness !== "complete") {
1217
+ missingTaskIds.push(taskId);
1218
+ } else if (treatment.pass && !comparator.pass) winsTaskIds.push(taskId);
1219
+ else if (!treatment.pass && comparator.pass) regressionTaskIds.push(taskId);
1220
+ }
1221
+ return {
1222
+ comparatorCandidateId,
1223
+ treatmentCandidateId,
1224
+ winsTaskIds,
1225
+ regressionTaskIds,
1226
+ missingTaskIds,
1227
+ paired: pairedComparison(context, comparatorCandidateId, treatmentCandidateId),
1228
+ relativeCost: relativeCosts(
1229
+ context,
1230
+ summaryById.get(treatmentCandidateId),
1231
+ summaryById.get(comparatorCandidateId)
1232
+ )
1233
+ };
1234
+ }
1235
+ function pairedComparison(context, comparatorCandidateId, treatmentCandidateId) {
1236
+ const rows = [];
1237
+ for (const taskId of context.input.taskOrder) {
1238
+ rows.push(toPairedArmRow(context, crossSurfaceRowFor(context, comparatorCandidateId, taskId)));
1239
+ rows.push(toPairedArmRow(context, crossSurfaceRowFor(context, treatmentCandidateId, taskId)));
1240
+ }
1241
+ return comparePairedArms(rows, {
1242
+ baselineArm: comparatorCandidateId,
1243
+ treatmentArm: treatmentCandidateId,
1244
+ metricNames: ["score", ...context.input.costMetricOrder],
1245
+ bootstrap: { ...context.input.bootstrap, statistic: "mean" }
1246
+ });
1247
+ }
1248
+ function toPairedArmRow(context, row) {
1249
+ const metrics = Object.fromEntries(
1250
+ context.input.costMetricOrder.map((metric) => [metric, row.cost[metric]])
1251
+ );
1252
+ if (row.completeness === "complete") metrics.score = row.score;
1253
+ return {
1254
+ pairKey: row.taskId,
1255
+ arm: row.candidateId,
1256
+ ...row.completeness === "complete" ? { pass: row.pass } : {},
1257
+ metrics
1258
+ };
1259
+ }
1260
+ function summarizePairEvidence(rows, leftComponentId, rightComponentId, field) {
1261
+ const bothTaskIds = [];
1262
+ const leftOnlyTaskIds = [];
1263
+ const rightOnlyTaskIds = [];
1264
+ const neitherTaskIds = [];
1265
+ const unobservedTaskIds = [];
1266
+ for (const row of rows) {
1267
+ const left = evidenceValue(row, leftComponentId, field);
1268
+ const right = evidenceValue(row, rightComponentId, field);
1269
+ if (left === null || right === null) unobservedTaskIds.push(row.taskId);
1270
+ else if (left && right) bothTaskIds.push(row.taskId);
1271
+ else if (left) leftOnlyTaskIds.push(row.taskId);
1272
+ else if (right) rightOnlyTaskIds.push(row.taskId);
1273
+ else neitherTaskIds.push(row.taskId);
1274
+ }
1275
+ return {
1276
+ bothTaskIds,
1277
+ leftOnlyTaskIds,
1278
+ rightOnlyTaskIds,
1279
+ neitherTaskIds,
1280
+ unobservedTaskIds
1281
+ };
1282
+ }
1283
+ function relativeCosts(context, treatment, comparator) {
1284
+ return Object.fromEntries(
1285
+ context.input.costMetricOrder.map((metric) => {
1286
+ const treatmentMedian = treatment.costs[metric].median;
1287
+ const comparatorMedian = comparator.costs[metric].median;
1288
+ return [
1289
+ metric,
1290
+ {
1291
+ treatmentMedian,
1292
+ comparatorMedian,
1293
+ medianDelta: treatmentMedian - comparatorMedian,
1294
+ medianRatio: comparatorMedian === 0 ? treatmentMedian === 0 ? 1 : null : treatmentMedian / comparatorMedian
1295
+ }
1296
+ ];
1297
+ })
1298
+ );
1299
+ }
1300
+ function withinCostLimits(context, treatment, baseline) {
1301
+ const relative3 = relativeCosts(context, treatment, baseline);
1302
+ return Object.entries(context.input.selection.maximumMedianCostRatioToBaseline).every(
1303
+ ([metric, limit]) => {
1304
+ const ratio = relative3[metric].medianRatio;
1305
+ return ratio !== null && ratio <= limit;
1306
+ }
1307
+ );
1308
+ }
1309
+ function distribution(values) {
1310
+ const sorted = [...values].sort((left, right) => left - right);
1311
+ const total = sorted.reduce((sum, value) => sum + value, 0);
1312
+ const middle = Math.floor(sorted.length / 2);
1313
+ const median = sorted.length % 2 === 1 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
1314
+ return {
1315
+ n: sorted.length,
1316
+ min: sorted[0],
1317
+ median,
1318
+ mean: total / sorted.length,
1319
+ max: sorted[sorted.length - 1],
1320
+ total
1321
+ };
1322
+ }
1323
+ function evidenceValue(row, componentId, field) {
1324
+ return row.componentEvidence.find((evidence) => evidence.componentId === componentId)[field];
1325
+ }
1326
+ function meanOrNull(values) {
1327
+ return values.length === 0 ? null : values.reduce((sum, value) => sum + value, 0) / values.length;
1328
+ }
1329
+ function unique(values) {
1330
+ return [...new Set(values)];
1331
+ }
1332
+
1333
+ // src/campaign/fixtures.ts
1334
+ import { createHash } from "crypto";
1335
+ import { existsSync, readdirSync, readFileSync, statSync } from "fs";
1336
+ import { isAbsolute, join, relative, resolve } from "path";
1337
+ function discoverEvalFixtures(evalsDir) {
1338
+ const root = resolve(evalsDir);
1339
+ if (!existsSync(root)) throw new Error(`discoverEvalFixtures: evalsDir not found: ${root}`);
1340
+ const fixtures = [];
1341
+ const walk = (dir, base = "") => {
1342
+ for (const entry of readdirSync(dir).sort()) {
1343
+ if (entry.startsWith(".") || entry === "node_modules") continue;
1344
+ const fullPath = join(dir, entry);
1345
+ if (!statSync(fullPath).isDirectory()) continue;
1346
+ const name = base ? `${base}/${entry}` : entry;
1347
+ if (existsWithExactCase(fullPath, "PROMPT.md")) {
1348
+ fixtures.push(name);
1349
+ } else {
1350
+ walk(fullPath, name);
1351
+ }
1352
+ }
1353
+ };
1354
+ walk(root);
1355
+ return fixtures;
1356
+ }
1357
+ function loadEvalFixture(evalsDir, name, options = {}) {
1358
+ const validation = options.validation ?? "vitest";
1359
+ const root = resolve(evalsDir);
1360
+ const fixturePath = resolve(root, name);
1361
+ assertInside(root, fixturePath, name);
1362
+ if (!existsSync(fixturePath) || !statSync(fixturePath).isDirectory()) {
1363
+ throw new Error(`loadEvalFixture: fixture not found: ${name}`);
1364
+ }
1365
+ if (!existsWithExactCase(fixturePath, "PROMPT.md")) {
1366
+ throw new Error(`loadEvalFixture: ${name} is missing exact-case PROMPT.md`);
1367
+ }
1368
+ const promptPath = join(fixturePath, "PROMPT.md");
1369
+ const evalPath = resolveEvalPath(fixturePath);
1370
+ const packageJsonPath = existsWithExactCase(fixturePath, "package.json") ? join(fixturePath, "package.json") : void 0;
1371
+ if (validation !== "none") {
1372
+ if (!evalPath)
1373
+ throw new Error(`loadEvalFixture: ${name} is missing exact-case EVAL.ts or EVAL.tsx`);
1374
+ if (!packageJsonPath)
1375
+ throw new Error(`loadEvalFixture: ${name} is missing exact-case package.json`);
1376
+ assertModulePackage(packageJsonPath, name);
1377
+ }
1378
+ const files = collectFixtureFiles(fixturePath);
1379
+ return {
1380
+ name,
1381
+ path: fixturePath,
1382
+ promptPath,
1383
+ evalPath,
1384
+ packageJsonPath,
1385
+ prompt: readFileSync(promptPath, "utf8"),
1386
+ files,
1387
+ fingerprint: contentHash({
1388
+ files,
1389
+ config: options.fingerprintConfig ?? null
1390
+ })
1391
+ };
1392
+ }
1393
+ function loadEvalFixtureScenarios(evalsDir, options = {}) {
1394
+ const names = options.names ?? discoverEvalFixtures(evalsDir);
1395
+ return names.map((name) => {
1396
+ const fixture = loadEvalFixture(evalsDir, name, options);
1397
+ return {
1398
+ id: fixture.name,
1399
+ kind: "eval-fixture",
1400
+ tags: ["eval-fixture"],
1401
+ fixtureName: fixture.name,
1402
+ fixturePath: fixture.path,
1403
+ promptPath: fixture.promptPath,
1404
+ evalPath: fixture.evalPath,
1405
+ packageJsonPath: fixture.packageJsonPath,
1406
+ prompt: fixture.prompt,
1407
+ fingerprint: fixture.fingerprint
1408
+ };
1409
+ });
1410
+ }
1411
+ function planEvalFixtureRun(options) {
1412
+ const scenarios = loadEvalFixtureScenarios(options.evalsDir, {
1413
+ names: options.names,
1414
+ validation: options.validation,
1415
+ fingerprintConfig: options.fingerprintConfig
1416
+ });
1417
+ const plan = planCampaignRun({
1418
+ scenarios,
1419
+ dispatchRef: options.dispatchRef ?? "eval-fixture-dispatch",
1420
+ judges: options.judges,
1421
+ seed: options.seed,
1422
+ reps: options.reps,
1423
+ resumable: options.resumable,
1424
+ runDir: options.runDir,
1425
+ storage: options.storage
1426
+ });
1427
+ return {
1428
+ ...plan,
1429
+ fixtures: scenarios.map((scenario) => ({
1430
+ fixtureName: scenario.fixtureName,
1431
+ fixturePath: scenario.fixturePath,
1432
+ fingerprint: scenario.fingerprint
1433
+ }))
1434
+ };
1435
+ }
1436
+ function existsWithExactCase(dirPath, fileName) {
1437
+ try {
1438
+ return readdirSync(dirPath).includes(fileName);
1439
+ } catch {
1440
+ return false;
1441
+ }
1442
+ }
1443
+ function resolveEvalPath(fixturePath) {
1444
+ if (existsWithExactCase(fixturePath, "EVAL.ts")) return join(fixturePath, "EVAL.ts");
1445
+ if (existsWithExactCase(fixturePath, "EVAL.tsx")) return join(fixturePath, "EVAL.tsx");
1446
+ return void 0;
1447
+ }
1448
+ function assertModulePackage(packageJsonPath, name) {
1449
+ let parsed;
1450
+ try {
1451
+ parsed = JSON.parse(readFileSync(packageJsonPath, "utf8"));
1452
+ } catch (err) {
1453
+ throw new Error(
1454
+ `loadEvalFixture: ${name} package.json is invalid JSON: ${err instanceof Error ? err.message : String(err)}`
1455
+ );
1456
+ }
1457
+ if (typeof parsed !== "object" || parsed === null || parsed.type !== "module") {
1458
+ throw new Error(`loadEvalFixture: ${name} package.json must set "type": "module"`);
1459
+ }
1460
+ }
1461
+ function collectFixtureFiles(fixturePath, base = "") {
1462
+ const files = [];
1463
+ for (const entry of readdirSync(join(fixturePath, base)).sort()) {
1464
+ if (entry === "node_modules" || entry === ".git") continue;
1465
+ const relativePath = base ? `${base}/${entry}` : entry;
1466
+ const fullPath = join(fixturePath, relativePath);
1467
+ const stat = statSync(fullPath);
1468
+ if (stat.isDirectory()) {
1469
+ files.push(...collectFixtureFiles(fixturePath, relativePath));
1470
+ continue;
1471
+ }
1472
+ const bytes = readFileSync(fullPath);
1473
+ files.push({
1474
+ path: relativePath,
1475
+ sha256: createHash("sha256").update(bytes).digest("hex"),
1476
+ bytes: bytes.byteLength
1477
+ });
1478
+ }
1479
+ return files;
1480
+ }
1481
+ function assertInside(root, target, label) {
1482
+ const rel = relative(root, target);
1483
+ if (rel === "" || !rel.startsWith("..") && !isAbsolute(rel)) return;
1484
+ throw new Error(`loadEvalFixture: fixture path escapes evalsDir: ${label}`);
1485
+ }
1486
+
1487
+ // src/campaign/gates/neutralization-gate.ts
1488
+ function mean(xs) {
1489
+ return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
1490
+ }
1491
+ function pairedLift(arm, baseline, scenarioIds) {
1492
+ const paired = pairHoldout(arm, baseline, scenarioIds, (s) => s.composite);
1493
+ const deltas = paired.after.map((a, i) => a - (paired.before[i] ?? 0));
1494
+ return { lift: mean(deltas), n: deltas.length };
1495
+ }
1496
+ function neutralizationGate(options) {
1497
+ const maxDecorativeFraction = options.maxDecorativeFraction ?? 0.5;
1498
+ return {
1499
+ name: "neutralizationGate",
1500
+ async decide(ctx) {
1501
+ if (!ctx.baselineJudgeScores) {
1502
+ throw new Error(
1503
+ "neutralizationGate: ctx.baselineJudgeScores is required \u2014 the placebo control measures lift OVER baseline."
1504
+ );
1505
+ }
1506
+ if (!ctx.neutralizedJudgeScores) {
1507
+ throw new Error(
1508
+ "neutralizationGate: ctx.neutralizedJudgeScores is required. It is populated by runImprovementLoop only when a `neutralize` function is supplied \u2014 composing this gate without that wiring would pass an unproven candidate."
1509
+ );
1510
+ }
1511
+ const scenarioIds = new Set(options.scenarios.map((s) => s.id));
1512
+ const cand = pairedLift(
1513
+ ctx.judgeScores,
1514
+ ctx.baselineJudgeScores,
1515
+ scenarioIds
1516
+ );
1517
+ const neut = pairedLift(
1518
+ ctx.neutralizedJudgeScores,
1519
+ ctx.baselineJudgeScores,
1520
+ scenarioIds
1521
+ );
1522
+ if (cand.lift <= 0) {
1523
+ return {
1524
+ decision: "hold",
1525
+ reasons: [
1526
+ `neutralization: candidate held-out lift ${cand.lift.toFixed(3)} \u2264 0 \u2014 no positive lift to attribute to content`
1527
+ ],
1528
+ contributingGates: [
1529
+ {
1530
+ name: "neutralizationGate",
1531
+ passed: false,
1532
+ detail: { candidateLift: cand.lift, neutralizedLift: neut.lift, n: cand.n }
1533
+ }
1534
+ ],
1535
+ delta: cand.lift
1536
+ };
1537
+ }
1538
+ const decorativeFraction = neut.lift / cand.lift;
1539
+ const passed = decorativeFraction < maxDecorativeFraction;
1540
+ const pct = (decorativeFraction * 100).toFixed(0);
1541
+ return {
1542
+ decision: passed ? "ship" : "hold",
1543
+ reasons: passed ? [
1544
+ `neutralization: content is causal \u2014 blanked variant reproduces ${pct}% of the lift (< ${(maxDecorativeFraction * 100).toFixed(0)}%); candidate \u0394 ${cand.lift.toFixed(3)}, neutralized \u0394 ${neut.lift.toFixed(3)}`
1545
+ ] : [
1546
+ `neutralization: lift is DECORATIVE \u2014 blanking the content (footprint-matched) reproduces ${pct}% of the lift (\u2265 ${(maxDecorativeFraction * 100).toFixed(0)}%); candidate \u0394 ${cand.lift.toFixed(3)}, neutralized \u0394 ${neut.lift.toFixed(3)}`
1547
+ ],
1548
+ contributingGates: [
1549
+ {
1550
+ name: "neutralizationGate",
1551
+ passed,
1552
+ detail: {
1553
+ candidateLift: cand.lift,
1554
+ neutralizedLift: neut.lift,
1555
+ decorativeFraction,
1556
+ maxDecorativeFraction,
1557
+ n: cand.n
1558
+ }
1559
+ }
1560
+ ],
1561
+ delta: cand.lift
1562
+ };
1563
+ }
1564
+ };
1565
+ }
1566
+
1567
+ // src/campaign/gates/sequential.ts
1568
+ import { createHash as createHash2 } from "crypto";
1569
+ function verifyManifestSync(m) {
1570
+ if (m.algo !== void 0 && m.algo !== "sha256-content") {
1571
+ throw new Error(`sequentialPairedGate: unrecognized manifest hash algo '${m.algo}'`);
1572
+ }
1573
+ const { contentHash: contentHash2, algo: _algo, ...rest } = m;
1574
+ void _algo;
1575
+ const bytes = JSON.stringify(canonicalize(rest));
1576
+ const hash = createHash2("sha256").update(bytes, "utf8").digest("hex");
1577
+ return hash === contentHash2;
1578
+ }
1579
+ function resolveConfig(opts) {
1580
+ const m = opts.preRegistration;
1581
+ let alpha;
1582
+ let maxN;
1583
+ let direction;
1584
+ let minEffect;
1585
+ if (m) {
1586
+ if (!verifyManifestSync(m)) {
1587
+ throw new Error(
1588
+ `sequentialPairedGate: pre-registration manifest '${m.id}' content hash mismatch (tampered)`
1589
+ );
1590
+ }
1591
+ if (opts.alpha !== void 0 && opts.alpha !== m.alpha) {
1592
+ throw new Error(
1593
+ `sequentialPairedGate: alpha ${opts.alpha} conflicts with pre-registered alpha ${m.alpha} \u2014 the registered statistic is the only one anytime validity covers`
1594
+ );
1595
+ }
1596
+ if (opts.maxN !== void 0 && opts.maxN !== m.preRegisteredN) {
1597
+ throw new Error(
1598
+ `sequentialPairedGate: maxN ${opts.maxN} conflicts with pre-registered N ${m.preRegisteredN}`
1599
+ );
1600
+ }
1601
+ alpha = m.alpha;
1602
+ maxN = m.preRegisteredN;
1603
+ direction = m.direction;
1604
+ minEffect = m.minEffect;
1605
+ } else {
1606
+ alpha = opts.alpha ?? 0.05;
1607
+ if (opts.maxN === void 0) {
1608
+ throw new Error(
1609
+ "sequentialPairedGate: maxN is required (or bind a preRegistration manifest whose preRegisteredN is the budget) \u2014 an unbounded stream has no pre-registered budget"
1610
+ );
1611
+ }
1612
+ maxN = opts.maxN;
1613
+ direction = "increase";
1614
+ minEffect = 0;
1615
+ }
1616
+ const scale = opts.scale ?? 1;
1617
+ if (!Number.isFinite(scale) || scale <= 0) {
1618
+ throw new Error(`sequentialPairedGate: scale must be > 0, got ${scale}`);
1619
+ }
1620
+ if (!Number.isInteger(maxN) || maxN < 1) {
1621
+ throw new Error(`sequentialPairedGate: maxN must be a positive integer, got ${maxN}`);
1622
+ }
1623
+ const minN = opts.minN ?? 5;
1624
+ if (!Number.isInteger(minN) || minN < 1 || minN > maxN) {
1625
+ throw new Error(
1626
+ `sequentialPairedGate: minN must be an integer in [1, maxN=${maxN}], got ${minN}`
1627
+ );
1628
+ }
1629
+ if (!Number.isFinite(minEffect) || minEffect < 0 || minEffect >= scale) {
1630
+ throw new Error(
1631
+ `sequentialPairedGate: minEffect must be in [0, scale=${scale}), got ${minEffect}`
1632
+ );
1633
+ }
1634
+ const nullMean = 0.5 + minEffect / (2 * scale);
1635
+ return {
1636
+ alpha,
1637
+ minN,
1638
+ maxN,
1639
+ maxBet: opts.maxBet ?? 0.5,
1640
+ scale,
1641
+ shuffleSeed: opts.shuffleSeed ?? 1337,
1642
+ direction,
1643
+ nullMean,
1644
+ minEffect
1645
+ };
1646
+ }
1647
+ function makeStream(cfg) {
1648
+ const proc = eProcess({ alpha: cfg.alpha, maxBet: cfg.maxBet, nullMean: cfg.nullMean });
1649
+ const threshold = 1 / cfg.alpha;
1650
+ let terminal;
1651
+ return {
1652
+ observe(delta) {
1653
+ if (terminal === "undecided-at-maxN") {
1654
+ throw new Error(
1655
+ `sequentialPairedGate: pre-registered maxN=${cfg.maxN} exhausted \u2014 extending the stream after seeing the result reopens optional stopping; start a NEW pre-registered test`
1656
+ );
1657
+ }
1658
+ if (!Number.isFinite(delta) || Math.abs(delta) > cfg.scale) {
1659
+ throw new Error(
1660
+ `sequentialPairedGate: delta ${delta} outside \xB1scale=${cfg.scale} \u2014 pass the judge's native scale explicitly (detectScale helps pick 1 vs 100)`
1661
+ );
1662
+ }
1663
+ const d = cfg.direction === "decrease" ? -delta : delta;
1664
+ const step = proc.update((d / cfg.scale + 1) / 2);
1665
+ if (terminal === void 0 && step.n >= cfg.minN && step.wealth >= threshold) {
1666
+ terminal = "promote";
1667
+ }
1668
+ if (terminal === "promote") {
1669
+ return {
1670
+ decision: "promote",
1671
+ eValue: step.wealth,
1672
+ n: step.n,
1673
+ reason: `e-value ${step.wealth.toFixed(2)} \u2265 1/\u03B1=${threshold.toFixed(2)} at n=${step.n} (minN=${cfg.minN}): the paired improvement exceeds ${cfg.minEffect} at anytime-valid level \u03B1=${cfg.alpha}`
1674
+ };
1675
+ }
1676
+ if (step.n >= cfg.maxN) {
1677
+ terminal = "undecided-at-maxN";
1678
+ return {
1679
+ decision: "undecided-at-maxN",
1680
+ eValue: step.wealth,
1681
+ n: step.n,
1682
+ reason: `undecided at pre-registered maxN=${cfg.maxN} (e-value ${step.wealth.toFixed(2)} < 1/\u03B1=${threshold.toFixed(2)}). This is NOT evidence of no effect \u2014 the effect may be real but smaller than this budget can detect; re-register with a larger N to test that`
1683
+ };
1684
+ }
1685
+ return {
1686
+ decision: "continue",
1687
+ eValue: step.wealth,
1688
+ n: step.n,
1689
+ reason: `e-value ${step.wealth.toFixed(2)} < 1/\u03B1=${threshold.toFixed(2)} at n=${step.n}/${cfg.maxN} \u2014 keep observing`
1690
+ };
1691
+ },
1692
+ state() {
1693
+ return { ...proc.state(), decision: terminal ?? "continue" };
1694
+ }
1695
+ };
1696
+ }
1697
+ function seededShuffle(items, seed) {
1698
+ const rng = mulberry32(seed);
1699
+ for (let i = items.length - 1; i > 0; i--) {
1700
+ const j = Math.floor(rng() * (i + 1));
1701
+ const tmp = items[i];
1702
+ items[i] = items[j];
1703
+ items[j] = tmp;
1704
+ }
1705
+ return items;
1706
+ }
1707
+ function sequentialPairedGate(options) {
1708
+ const cfg = resolveConfig(options);
1709
+ const name = options.name ?? "sequentialPairedGate";
1710
+ const manifest = options.preRegistration;
1711
+ const observeStream = makeStream(cfg);
1712
+ return {
1713
+ name,
1714
+ observe(delta) {
1715
+ return observeStream.observe(delta);
1716
+ },
1717
+ state() {
1718
+ return observeStream.state();
1719
+ },
1720
+ async decide(ctx) {
1721
+ if (!ctx.baselineJudgeScores) {
1722
+ throw new Error(
1723
+ `${name}: ctx.baselineJudgeScores is required \u2014 falling back to the candidate's own scores would compare the candidate against itself (delta 0, silent no-op)`
1724
+ );
1725
+ }
1726
+ const scenarioIds = new Set(ctx.scenarios.map((s) => s.id));
1727
+ const paired = pairHoldout(
1728
+ ctx.judgeScores,
1729
+ ctx.baselineJudgeScores,
1730
+ scenarioIds,
1731
+ (s) => s.composite
1732
+ );
1733
+ const deltas = paired.after.map((a, i) => a - paired.before[i]);
1734
+ seededShuffle(deltas, cfg.shuffleSeed);
1735
+ const stream = makeStream(cfg);
1736
+ let last;
1737
+ for (const d of deltas) {
1738
+ last = stream.observe(d);
1739
+ if (last.decision !== "continue") break;
1740
+ }
1741
+ const detail = {
1742
+ ...stream.state(),
1743
+ minN: cfg.minN,
1744
+ maxN: cfg.maxN,
1745
+ scale: cfg.scale,
1746
+ shuffleSeed: cfg.shuffleSeed,
1747
+ direction: cfg.direction,
1748
+ minEffect: cfg.minEffect,
1749
+ pairedN: deltas.length,
1750
+ ...manifest ? { preRegisteredId: manifest.id, metric: manifest.metric } : {}
1751
+ };
1752
+ const meanDelta = deltas.length === 0 ? void 0 : deltas.reduce((s, d) => s + d, 0) / deltas.length;
1753
+ if (last === void 0) {
1754
+ return {
1755
+ decision: "need_more_work",
1756
+ reasons: [`${name}: no paired holdout observations \u2014 nothing to test`],
1757
+ contributingGates: [{ name, passed: false, detail }]
1758
+ };
1759
+ }
1760
+ const decision = last.decision === "promote" ? "ship" : last.decision === "continue" ? "need_more_work" : "hold";
1761
+ return {
1762
+ decision,
1763
+ reasons: [`${name}: ${last.reason}`],
1764
+ contributingGates: [{ name, passed: decision === "ship", detail }],
1765
+ delta: meanDelta
1766
+ };
1767
+ }
1768
+ };
1769
+ }
1770
+ function sequentialDecide(options = {}) {
1771
+ const alpha = options.alpha ?? 0.05;
1772
+ const minN = options.minN ?? 5;
1773
+ const scale = options.scale ?? 1;
1774
+ if (!Number.isFinite(scale) || scale <= 0) {
1775
+ throw new Error(`sequentialDecide: scale must be > 0, got ${scale}`);
1776
+ }
1777
+ const proc = eProcess({ alpha, maxBet: options.maxBet ?? 0.5, nullMean: 0.5 });
1778
+ const threshold = 1 / alpha;
1779
+ let processedGenerations = 0;
1780
+ let reference;
1781
+ let stopped;
1782
+ const topCandidate = (record) => {
1783
+ if (record.candidates.length === 0) {
1784
+ throw new Error(
1785
+ `sequentialDecide: generation ${record.generationIndex} has no candidates \u2014 cannot extract a top candidate`
1786
+ );
1787
+ }
1788
+ return record.candidates.reduce((best, c) => c.composite > best.composite ? c : best);
1789
+ };
1790
+ const decide = ({ history }) => {
1791
+ if (stopped) return stopped;
1792
+ if (history.length === 0) return { stop: false };
1793
+ if (reference === void 0) {
1794
+ reference = new Map(
1795
+ topCandidate(history[0]).scenarios.map((s) => [s.scenarioId, s.composite])
1796
+ );
1797
+ }
1798
+ for (let g = Math.max(1, processedGenerations); g < history.length; g++) {
1799
+ const top = topCandidate(history[g]);
1800
+ const byScenario = new Map(top.scenarios.map((s) => [s.scenarioId, s.composite]));
1801
+ for (const [scenarioId, refComposite] of reference) {
1802
+ const candComposite = byScenario.get(scenarioId);
1803
+ if (candComposite === void 0) {
1804
+ throw new Error(
1805
+ `sequentialDecide: generation ${history[g].generationIndex} top candidate is missing scenario '${scenarioId}' \u2014 generations must score the same scenario set to pair`
1806
+ );
1807
+ }
1808
+ const delta = candComposite - refComposite;
1809
+ if (!Number.isFinite(delta) || Math.abs(delta) > scale) {
1810
+ throw new Error(
1811
+ `sequentialDecide: paired delta ${delta} outside \xB1scale=${scale} on scenario '${scenarioId}' \u2014 pass the composite scale explicitly`
1812
+ );
1813
+ }
1814
+ const step = proc.update((delta / scale + 1) / 2);
1815
+ if (step.n >= minN && step.wealth >= threshold) {
1816
+ stopped = {
1817
+ stop: true,
1818
+ reason: `sequential e-process decided at generation ${history[g].generationIndex}: e-value ${step.wealth.toFixed(2)} \u2265 1/\u03B1=${threshold.toFixed(2)} after n=${step.n} paired deltas vs the generation-0 incumbent \u2014 the improvement is real at \u03B1=${alpha}; stop exploring and promote via the gate`
1819
+ };
1820
+ processedGenerations = history.length;
1821
+ return stopped;
1822
+ }
1823
+ }
1824
+ }
1825
+ processedGenerations = history.length;
1826
+ return { stop: false };
1827
+ };
1828
+ decide.state = () => proc.state();
1829
+ return decide;
1830
+ }
1831
+
1832
+ // src/campaign/grounded-reflection.ts
1833
+ function rolloutArgumentDiff(rollouts, opts = {}) {
1834
+ const passThreshold = opts.passThreshold ?? 1;
1835
+ const maxValues = opts.maxValuesPerField ?? 4;
1836
+ const collect = (pass) => {
1837
+ const byField = /* @__PURE__ */ new Map();
1838
+ for (const r of rollouts) {
1839
+ if (pass !== r.score >= passThreshold) continue;
1840
+ for (const c of r.calls) {
1841
+ for (const [k, v] of Object.entries(c.args)) {
1842
+ const set = byField.get(k) ?? /* @__PURE__ */ new Set();
1843
+ set.add(String(v));
1844
+ byField.set(k, set);
1845
+ }
1846
+ }
1847
+ }
1848
+ return byField;
1849
+ };
1850
+ const passing = collect(true);
1851
+ const failing = collect(false);
1852
+ const lines = [];
1853
+ for (const field of /* @__PURE__ */ new Set([...passing.keys(), ...failing.keys()])) {
1854
+ const pv = [...passing.get(field) ?? []].slice(0, maxValues);
1855
+ const fv = [...failing.get(field) ?? []].slice(0, maxValues);
1856
+ const render = (vals) => vals.length ? JSON.stringify(vals) : "NOT SET (omitted)";
1857
+ lines.push(` ${field}: passing runs -> ${render(pv)} | failing runs -> ${render(fv)}`);
1858
+ }
1859
+ const lower = (m) => {
1860
+ const out = /* @__PURE__ */ new Set();
1861
+ for (const vals of m.values()) for (const v of vals) out.add(v.toLowerCase());
1862
+ return out;
1863
+ };
1864
+ return {
1865
+ text: lines.join("\n") || " (no calls observed)",
1866
+ passingValues: lower(passing),
1867
+ failingValues: lower(failing)
1868
+ };
1869
+ }
1870
+ function classifyUngroundedLiterals(text, diff) {
1871
+ const ungrounded = /* @__PURE__ */ new Set();
1872
+ for (const m of text.matchAll(/'([a-z][a-z_-]{1,19})'/gi)) {
1873
+ const w = m[1].toLowerCase();
1874
+ if (!diff.passingValues.has(w)) ungrounded.add(w);
1875
+ }
1876
+ const all = [...ungrounded];
1877
+ return {
1878
+ ungrounded: all,
1879
+ harmful: all.filter((w) => diff.failingValues.has(w))
1880
+ };
1881
+ }
1882
+
1883
+ // src/campaign/labeled-store/fs-adapter.ts
1884
+ import { createHash as createHash3 } from "crypto";
1885
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
1886
+ import { join as join2 } from "path";
1887
+ var LabeledScenarioStoreError = class extends Error {
1888
+ constructor(code, message) {
1889
+ super(message);
1890
+ this.code = code;
1891
+ this.name = "LabeledScenarioStoreError";
1892
+ }
1893
+ code;
1894
+ };
1895
+ var FsLabeledScenarioStore = class {
1896
+ constructor(options) {
1897
+ this.options = options;
1898
+ if (!existsSync2(options.root)) mkdirSync(options.root, { recursive: true });
1899
+ this.now = options.now ?? Date.now;
1900
+ }
1901
+ options;
1902
+ now;
1903
+ rateLimits = /* @__PURE__ */ new Map();
1904
+ async observe(write) {
1905
+ this.assertProvenance(write);
1906
+ this.assertRateLimit(write);
1907
+ const record = this.toRecord(write);
1908
+ const path = this.pathForSource(write.source);
1909
+ const line = `${JSON.stringify(record)}
1910
+ `;
1911
+ appendLine(path, line);
1912
+ }
1913
+ async sample(args) {
1914
+ if (!args.split) {
1915
+ throw new LabeledScenarioStoreError(
1916
+ "split_required",
1917
+ "sample() requires an explicit `split` (train | test) \u2014 substrate refuses ambiguous reads"
1918
+ );
1919
+ }
1920
+ if (!args.capturedBefore) {
1921
+ throw new LabeledScenarioStoreError(
1922
+ "capturedBefore_required",
1923
+ "sample() requires an explicit `capturedBefore` timestamp for temporal-split discipline"
1924
+ );
1925
+ }
1926
+ const all = [];
1927
+ for (const source of ALL_SOURCES) {
1928
+ if (args.split === "train" && source === "production-trace") {
1929
+ const explicit = sourceFilterContains(args.filter?.source, "production-trace");
1930
+ if (!explicit) continue;
1931
+ }
1932
+ const path = this.pathForSource(source);
1933
+ if (!existsSync2(path)) continue;
1934
+ const lines = readFileSync2(path, "utf8").split("\n").filter(Boolean);
1935
+ for (const line of lines) {
1936
+ let record;
1937
+ try {
1938
+ record = JSON.parse(line);
1939
+ } catch {
1940
+ continue;
1941
+ }
1942
+ if (!matchesFilter(record, args, source)) continue;
1943
+ all.push(record);
1944
+ }
1945
+ }
1946
+ all.sort((a, b) => {
1947
+ if (a.capturedAt !== b.capturedAt) return a.capturedAt.localeCompare(b.capturedAt);
1948
+ return a.recordHash.localeCompare(b.recordHash);
1949
+ });
1950
+ return all.slice(0, args.count);
1951
+ }
1952
+ async size() {
1953
+ const bySource = {};
1954
+ const byTrust = {
1955
+ unverified: 0,
1956
+ "verified-signal": 0,
1957
+ "human-rated": 0
1958
+ };
1959
+ let total = 0;
1960
+ for (const source of ALL_SOURCES) {
1961
+ const path = this.pathForSource(source);
1962
+ if (!existsSync2(path)) {
1963
+ bySource[source] = 0;
1964
+ continue;
1965
+ }
1966
+ const lines = readFileSync2(path, "utf8").split("\n").filter(Boolean);
1967
+ bySource[source] = lines.length;
1968
+ total += lines.length;
1969
+ for (const line of lines) {
1970
+ let trust = "unverified";
1971
+ try {
1972
+ trust = JSON.parse(line).labelTrust ?? "unverified";
1973
+ } catch {
1974
+ }
1975
+ byTrust[trust] += 1;
1976
+ }
1977
+ }
1978
+ return { train: total, test: total, bySource, byTrust };
1979
+ }
1980
+ assertProvenance(write) {
1981
+ if (!write.source) {
1982
+ throw new LabeledScenarioStoreError(
1983
+ "missing_source",
1984
+ "LabeledScenarioWrite requires `source`"
1985
+ );
1986
+ }
1987
+ if (!write.sourceVersionHash || write.sourceVersionHash.length === 0) {
1988
+ throw new LabeledScenarioStoreError(
1989
+ "missing_source_version",
1990
+ "LabeledScenarioWrite requires `sourceVersionHash` (git sha or substrate version)"
1991
+ );
1992
+ }
1993
+ if (!write.capturedAt) {
1994
+ throw new LabeledScenarioStoreError(
1995
+ "missing_captured_at",
1996
+ "LabeledScenarioWrite requires `capturedAt` ISO timestamp"
1997
+ );
1998
+ }
1999
+ if (!write.redactionStatus) {
2000
+ throw new LabeledScenarioStoreError(
2001
+ "missing_redaction_status",
2002
+ "LabeledScenarioWrite requires explicit `redactionStatus` \u2014 raw / redacted-pii / redacted-secrets / fully-redacted"
2003
+ );
2004
+ }
2005
+ if (!ALL_SOURCES.includes(write.source)) {
2006
+ throw new LabeledScenarioStoreError(
2007
+ "unknown_source",
2008
+ `LabeledScenarioWrite.source must be one of: ${ALL_SOURCES.join(", ")}`
2009
+ );
2010
+ }
2011
+ }
2012
+ assertRateLimit(write) {
2013
+ const cap = this.options.maxWritesPerMinutePerBucket;
2014
+ if (!cap || !write.rateLimitBucket) return;
2015
+ const now = this.now();
2016
+ const windowMs = 6e4;
2017
+ let state = this.rateLimits.get(write.rateLimitBucket);
2018
+ if (!state || now - state.windowStartMs >= windowMs) {
2019
+ state = { bucket: write.rateLimitBucket, windowStartMs: now, count: 0 };
2020
+ this.rateLimits.set(write.rateLimitBucket, state);
2021
+ }
2022
+ if (state.count >= cap) {
2023
+ throw new LabeledScenarioStoreError(
2024
+ "rate_limit_exceeded",
2025
+ `LabeledScenarioStore: bucket ${write.rateLimitBucket} exceeded ${cap} writes/min`
2026
+ );
2027
+ }
2028
+ state.count += 1;
2029
+ }
2030
+ toRecord(write) {
2031
+ const recordHash = sha256(
2032
+ JSON.stringify({
2033
+ id: write.scenario.id,
2034
+ src: write.source,
2035
+ at: write.capturedAt,
2036
+ ver: write.sourceVersionHash
2037
+ })
2038
+ );
2039
+ return {
2040
+ ...write,
2041
+ recordHash,
2042
+ split: "train"
2043
+ };
2044
+ }
2045
+ pathForSource(source) {
2046
+ return join2(this.options.root, `${source}.jsonl`);
2047
+ }
2048
+ };
2049
+ var ALL_SOURCES = [
2050
+ "production-trace",
2051
+ "eval-run",
2052
+ "manual",
2053
+ "red-team",
2054
+ "synthetic"
2055
+ ];
2056
+ function sourceFilterContains(filter, needle) {
2057
+ if (!filter) return false;
2058
+ if (Array.isArray(filter)) return filter.includes(needle);
2059
+ return filter === needle;
2060
+ }
2061
+ function matchesFilter(record, args, source) {
2062
+ if (args.split === "train" && record.capturedAt >= args.capturedBefore) return false;
2063
+ if (args.split === "test" && record.capturedAt < args.capturedBefore) return false;
2064
+ const f = args.filter;
2065
+ if (!f) return true;
2066
+ if (f.kind && record.scenario.kind !== f.kind) return false;
2067
+ if (f.source) {
2068
+ const sources = Array.isArray(f.source) ? f.source : [f.source];
2069
+ if (!sources.includes(source)) return false;
2070
+ }
2071
+ if (f.minComposite !== void 0 || f.maxComposite !== void 0) {
2072
+ const composites = Object.values(record.judgeScores).map((s) => s.composite);
2073
+ const max = composites.length === 0 ? 0 : Math.max(...composites);
2074
+ if (f.minComposite !== void 0 && max < f.minComposite) return false;
2075
+ if (f.maxComposite !== void 0 && max > f.maxComposite) return false;
2076
+ }
2077
+ if (f.minTrust !== void 0 && labelTrustRank(record.labelTrust) < labelTrustRank(f.minTrust)) {
2078
+ return false;
2079
+ }
2080
+ return true;
2081
+ }
2082
+ function sha256(input) {
2083
+ return createHash3("sha256").update(input).digest("hex").slice(0, 16);
2084
+ }
2085
+ function appendLine(path, line) {
2086
+ if (existsSync2(path)) {
2087
+ const existing = readFileSync2(path, "utf8");
2088
+ writeFileSync(path, existing + line);
2089
+ } else {
2090
+ writeFileSync(path, line);
2091
+ }
2092
+ }
2093
+
2094
+ // src/campaign/neutralize.ts
2095
+ var FILLER = "#";
2096
+ function neutralizeText(content) {
2097
+ return content.replace(/\S/g, FILLER);
2098
+ }
2099
+
2100
+ // src/completion-verifier.ts
2101
+ import { randomUUID } from "crypto";
2102
+ function completionVerdict(input) {
2103
+ if (input.requirements.length === 0) {
2104
+ throw new Error(
2105
+ `completionVerdict: task '${input.taskId}' has no requirement checks \u2014 nothing to derive a verdict from`
2106
+ );
2107
+ }
2108
+ const measurable = input.requirements.filter((r) => !r.unmeasured);
2109
+ const unmeasuredCount = input.requirements.length - measurable.length;
2110
+ if (measurable.length === 0) {
2111
+ throw new Error(
2112
+ `completionVerdict: task '${input.taskId}' has no measurable requirements \u2014 all ${input.requirements.length} correctness checks failed (${input.requirements[0]?.unmeasuredReason ?? "unknown reason"})`
2113
+ );
2114
+ }
2115
+ const satisfiedCount = measurable.filter((r) => r.satisfied).length;
2116
+ const completionRate = satisfiedCount / measurable.length;
2117
+ const fullyComplete = unmeasuredCount === 0 && satisfiedCount === measurable.length;
2118
+ return {
2119
+ taskId: input.taskId,
2120
+ requirements: input.requirements,
2121
+ completionRate,
2122
+ fullyComplete,
2123
+ unmeasuredCount,
2124
+ valid: fullyComplete,
2125
+ score: completionRate
2126
+ };
2127
+ }
2128
+ var STOPWORDS = /* @__PURE__ */ new Set([
2129
+ "the",
2130
+ "a",
2131
+ "an",
2132
+ "of",
2133
+ "for",
2134
+ "and",
2135
+ "or",
2136
+ "to",
2137
+ "in",
2138
+ "on",
2139
+ "with",
2140
+ "by"
2141
+ ]);
2142
+ var REQUIREMENT_FORM_STOPWORDS = /* @__PURE__ */ new Set([
2143
+ "generated",
2144
+ "generate",
2145
+ "view",
2146
+ "render",
2147
+ "rendered",
2148
+ "persisted",
2149
+ "persist",
2150
+ "artifact",
2151
+ "file",
2152
+ "document",
2153
+ "note",
2154
+ "proposal",
2155
+ "deliverable",
2156
+ "output",
2157
+ "created",
2158
+ "create",
2159
+ "produce",
2160
+ "produced",
2161
+ "flag"
2162
+ ]);
2163
+ var MATCH_THRESHOLD = 0.5;
2164
+ var MIN_CONTENT_CHARS = 50;
2165
+ function tokens(s, extraStop) {
2166
+ return new Set(
2167
+ s.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 1 && !STOPWORDS.has(t) && !extraStop?.has(t))
2168
+ );
2169
+ }
2170
+ function tokenRecall(requirementText, candidateText) {
2171
+ const req = tokens(requirementText, REQUIREMENT_FORM_STOPWORDS);
2172
+ if (req.size === 0) return 0;
2173
+ const cand = tokens(candidateText);
2174
+ let hit = 0;
2175
+ for (const t of req) if (cand.has(t)) hit++;
2176
+ return hit / req.size;
2177
+ }
2178
+ function artifactCandidates(req, reqIndex, artifacts) {
2179
+ const reqText = `${req.title} ${req.category ?? ""}`;
2180
+ const out = [];
2181
+ artifacts.forEach((a, i) => {
2182
+ if ((a.content ?? "").trim().length < MIN_CONTENT_CHARS) return;
2183
+ let score = tokenRecall(
2184
+ reqText,
2185
+ `${a.path ?? ""} ${a.kind} ${(a.content ?? "").slice(0, 4e3)}`
2186
+ );
2187
+ if (req.category && a.kind && req.category.toLowerCase() === a.kind.toLowerCase()) {
2188
+ score = Math.max(score, 1);
2189
+ }
2190
+ if (score < MATCH_THRESHOLD) return;
2191
+ out.push({
2192
+ reqIndex,
2193
+ itemKey: `artifact:${i}`,
2194
+ score,
2195
+ evidence: `artifact '${a.path ?? a.kind}' matched (token recall ${score.toFixed(2)})`,
2196
+ content: a.content ?? null
2197
+ });
2198
+ });
2199
+ return out;
2200
+ }
2201
+ function proposalCandidates(req, reqIndex, proposals) {
2202
+ const reqText = `${req.title} ${req.category ?? ""}`;
2203
+ const out = [];
2204
+ for (const p of proposals) {
2205
+ if (p.status !== "approved") continue;
2206
+ const body = (p.content ?? "").trim();
2207
+ if (body.length < MIN_CONTENT_CHARS) continue;
2208
+ const score = tokenRecall(reqText, `${p.title} ${body}`);
2209
+ if (score < MATCH_THRESHOLD) continue;
2210
+ out.push({
2211
+ reqIndex,
2212
+ itemKey: `proposal:${p.id}`,
2213
+ score,
2214
+ evidence: `approved proposal '${p.title}' matched (token recall ${score.toFixed(2)})`,
2215
+ content: body
2216
+ });
2217
+ }
2218
+ return out;
2219
+ }
2220
+ function toolCallCandidates(req, reqIndex, toolCalls) {
2221
+ const out = [];
2222
+ toolCalls.forEach((name, i) => {
2223
+ const score = tokenRecall(req.title, name);
2224
+ if (score < MATCH_THRESHOLD) return;
2225
+ out.push({
2226
+ reqIndex,
2227
+ itemKey: `tool:${i}`,
2228
+ score,
2229
+ evidence: `tool call '${name}' matched (token recall ${score.toFixed(2)})`,
2230
+ content: null
2231
+ });
2232
+ });
2233
+ return out;
2234
+ }
2235
+ async function verifyCompletion(gold, state, checkCorrectness) {
2236
+ if (gold.requirements.length === 0) {
2237
+ throw new Error(
2238
+ `verifyCompletion: task '${gold.taskId}' has no requirements \u2014 malformed gold spec`
2239
+ );
2240
+ }
2241
+ const candidates = [];
2242
+ gold.requirements.forEach((req, i) => {
2243
+ const by = req.satisfiedBy ?? "any";
2244
+ if (by === "artifact" || by === "any") {
2245
+ candidates.push(...artifactCandidates(req, i, state.artifacts));
2246
+ }
2247
+ if (by === "proposal" || by === "any") {
2248
+ candidates.push(...proposalCandidates(req, i, state.proposals));
2249
+ }
2250
+ if (by === "tool-call" || by === "any") {
2251
+ candidates.push(...toolCallCandidates(req, i, state.toolCalls));
2252
+ }
2253
+ });
2254
+ candidates.sort((a, b) => b.score - a.score);
2255
+ const assigned = /* @__PURE__ */ new Map();
2256
+ const itemTaken = /* @__PURE__ */ new Set();
2257
+ for (const c of candidates) {
2258
+ if (assigned.has(c.reqIndex) || itemTaken.has(c.itemKey)) continue;
2259
+ assigned.set(c.reqIndex, c);
2260
+ itemTaken.add(c.itemKey);
2261
+ }
2262
+ const requirements = [];
2263
+ for (let i = 0; i < gold.requirements.length; i++) {
2264
+ const req = gold.requirements[i];
2265
+ const match = assigned.get(i);
2266
+ const evidence = [];
2267
+ let correct = null;
2268
+ let unmeasuredReason;
2269
+ if (match) {
2270
+ evidence.push(match.evidence);
2271
+ if (match.content !== null) {
2272
+ try {
2273
+ const r = await checkCorrectness(req, match.content);
2274
+ correct = r.correct;
2275
+ evidence.push(`correctness: ${r.correct ? "pass" : "fail"} \u2014 ${r.reason}`);
2276
+ } catch (err) {
2277
+ unmeasuredReason = err instanceof JudgeParseError ? `checker response unparseable after retry: ${err.raw.slice(0, 200)}` : `checker call failed: ${err instanceof Error ? err.message : String(err)}`;
2278
+ evidence.push(`correctness: UNMEASURED \u2014 ${unmeasuredReason}`);
2279
+ }
2280
+ } else {
2281
+ evidence.push("correctness: not assessed \u2014 matched item carries no content");
2282
+ }
2283
+ } else {
2284
+ const by = req.satisfiedBy ?? "any";
2285
+ const kind = by === "any" ? "artifact/proposal/tool-call" : by;
2286
+ evidence.push(`no produced ${kind} matched this requirement`);
2287
+ }
2288
+ const structurallyPresent = match !== void 0;
2289
+ const unmeasured = unmeasuredReason !== void 0;
2290
+ const satisfied = structurallyPresent && !unmeasured && correct !== false;
2291
+ requirements.push({
2292
+ reqId: req.reqId,
2293
+ title: req.title,
2294
+ structurallyPresent,
2295
+ correct,
2296
+ satisfied,
2297
+ ...unmeasured ? { unmeasured: true, unmeasuredReason } : {},
2298
+ evidence
2299
+ });
2300
+ }
2301
+ return completionVerdict({ taskId: gold.taskId, requirements });
2302
+ }
2303
+ function parseCorrectnessResponse(raw) {
2304
+ const readVerdict = (candidate) => {
2305
+ if (candidate === null || typeof candidate !== "object") return null;
2306
+ const { correct, reason } = candidate;
2307
+ if (typeof correct !== "boolean") return null;
2308
+ return { correct, reason: typeof reason === "string" ? reason : "" };
2309
+ };
2310
+ const match = raw.match(/\{[\s\S]*\}/);
2311
+ if (match) {
2312
+ try {
2313
+ const strict = readVerdict(JSON.parse(match[0]));
2314
+ if (strict) return strict;
2315
+ } catch {
2316
+ }
2317
+ }
2318
+ const start = raw.indexOf("{");
2319
+ if (start !== -1) {
2320
+ const recovered = readVerdict(recoverTruncatedJson(raw.slice(start)));
2321
+ if (recovered) return recovered;
2322
+ }
2323
+ throw new JudgeParseError("correctness-checker", raw);
2324
+ }
2325
+ function createLlmCorrectnessChecker(tc, opts = {}) {
2326
+ const model = opts.model ?? "claude-sonnet-4-6";
2327
+ const maxContentChars = opts.maxContentChars ?? 8e3;
2328
+ const maxAttempts = opts.maxAttempts ?? 2;
2329
+ const costLedger = opts.costLedger ?? new CostLedger();
2330
+ const sink = opts.rawSink;
2331
+ const record = async (event) => {
2332
+ try {
2333
+ await sink?.record(event);
2334
+ } catch {
2335
+ }
2336
+ };
2337
+ return async (requirement, content) => {
2338
+ const request = {
2339
+ model,
2340
+ messages: [
2341
+ {
2342
+ role: "system",
2343
+ content: 'You verify whether a produced work artifact actually fulfils a stated requirement. Judge fulfilment only \u2014 is the deliverable substantively present and on-point \u2014 not polish. A plan to do it later, a vague gesture, or a description of what should be done does NOT fulfil a requirement; the artifact must BE the deliverable. Respond with a single JSON object: {"correct": boolean, "reason": string (<= 30 words)}.'
2344
+ },
2345
+ {
2346
+ role: "user",
2347
+ content: `Requirement: ${requirement.title}
2348
+ ${requirement.category ? `Category: ${requirement.category}
2349
+ ` : ""}
2350
+ Produced artifact:
2351
+ ${content.slice(0, maxContentChars)}`
2352
+ }
2353
+ ],
2354
+ temperature: 0,
2355
+ maxTokens: 200
2356
+ };
2357
+ let lastErr;
2358
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2359
+ const started = Date.now();
2360
+ await record({
2361
+ eventId: randomUUID(),
2362
+ provider: "correctness-checker",
2363
+ model,
2364
+ endpoint: "/chat",
2365
+ baseUrl: "",
2366
+ attemptIndex: attempt,
2367
+ direction: "request",
2368
+ timestamp: started,
2369
+ requestBody: request,
2370
+ redactedFields: []
2371
+ });
2372
+ try {
2373
+ const paid = await costLedger.runPaidCall({
2374
+ channel: "verifier",
2375
+ phase: opts.costPhase ?? "completion.correctness",
2376
+ actor: "correctness-checker",
2377
+ model,
2378
+ maximumCharge: maximumChargeForTCloudRequest(request, opts.tcloudMaximumAttempts),
2379
+ tags: {
2380
+ ...opts.costTags,
2381
+ requirementId: requirement.reqId,
2382
+ attempt: String(attempt)
2383
+ },
2384
+ signal: opts.signal,
2385
+ execute: () => tc.chat(request),
2386
+ receipt: (response) => costReceiptFromTCloud(response, model),
2387
+ receiptFromError: (error) => opts.receiptFromError?.(error, attempt)
2388
+ });
2389
+ if (!paid.succeeded) throw paid.error;
2390
+ const resp = paid.value;
2391
+ const raw = resp.choices?.[0]?.message?.content ?? "";
2392
+ await record({
2393
+ eventId: randomUUID(),
2394
+ provider: "correctness-checker",
2395
+ model,
2396
+ endpoint: "/chat",
2397
+ baseUrl: "",
2398
+ attemptIndex: attempt,
2399
+ direction: "response",
2400
+ timestamp: Date.now(),
2401
+ durationMs: Date.now() - started,
2402
+ responseBody: resp,
2403
+ redactedFields: []
2404
+ });
2405
+ return parseCorrectnessResponse(raw);
2406
+ } catch (err) {
2407
+ lastErr = err;
2408
+ await record({
2409
+ eventId: randomUUID(),
2410
+ provider: "correctness-checker",
2411
+ model,
2412
+ endpoint: "/chat",
2413
+ baseUrl: "",
2414
+ attemptIndex: attempt,
2415
+ direction: "error",
2416
+ timestamp: Date.now(),
2417
+ durationMs: Date.now() - started,
2418
+ errorMessage: err instanceof Error ? err.message : String(err),
2419
+ redactedFields: []
2420
+ });
2421
+ }
2422
+ }
2423
+ throw lastErr instanceof Error ? lastErr : new Error(String(lastErr));
2424
+ };
2425
+ }
2426
+ var TITLE_STOPWORDS = /* @__PURE__ */ new Set([
2427
+ "the",
2428
+ "a",
2429
+ "an",
2430
+ "and",
2431
+ "or",
2432
+ "for",
2433
+ "to",
2434
+ "of",
2435
+ "in",
2436
+ "on",
2437
+ "with",
2438
+ "review",
2439
+ "update",
2440
+ "new",
2441
+ "proposed"
2442
+ ]);
2443
+ function createTokenRecallChecker(opts = {}) {
2444
+ const minRecall = opts.minRecall ?? 0.5;
2445
+ const minLen = opts.minContentLength ?? 120;
2446
+ return async (requirement, content) => {
2447
+ const body = content.trim();
2448
+ if (body.length < minLen)
2449
+ return {
2450
+ correct: false,
2451
+ reason: `content too thin (${body.length} chars) to be the deliverable`
2452
+ };
2453
+ const titleTokens = requirement.title.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length > 2 && !TITLE_STOPWORDS.has(t));
2454
+ if (titleTokens.length === 0)
2455
+ return {
2456
+ correct: true,
2457
+ reason: "requirement title has no significant tokens \u2014 structural match accepted"
2458
+ };
2459
+ const lower = body.toLowerCase();
2460
+ const hits = titleTokens.filter((t) => lower.includes(t)).length;
2461
+ const recall = hits / titleTokens.length;
2462
+ return recall >= minRecall ? {
2463
+ correct: true,
2464
+ reason: `content recalls ${hits}/${titleTokens.length} requirement tokens`
2465
+ } : {
2466
+ correct: false,
2467
+ reason: `content recalls only ${hits}/${titleTokens.length} requirement tokens`
2468
+ };
2469
+ };
2470
+ }
2471
+
2472
+ // src/produced-state.ts
2473
+ function artifactKind(mimeType) {
2474
+ if (!mimeType) return "file";
2475
+ if (mimeType.includes("json")) return "json";
2476
+ if (mimeType.startsWith("text/")) return "text";
2477
+ return "file";
2478
+ }
2479
+ function extractProducedState(events) {
2480
+ const artifacts = [];
2481
+ const proposals = [];
2482
+ const toolCalls = [];
2483
+ const seenTools = /* @__PURE__ */ new Set();
2484
+ for (const ev of events) {
2485
+ if (ev.type === "tool_call") {
2486
+ const name = ev.toolName;
2487
+ if (name && !seenTools.has(name)) {
2488
+ seenTools.add(name);
2489
+ toolCalls.push(name);
2490
+ }
2491
+ } else if (ev.type === "artifact") {
2492
+ const a = ev;
2493
+ artifacts.push({
2494
+ kind: artifactKind(a.mimeType),
2495
+ path: a.name ?? a.uri ?? a.artifactId,
2496
+ content: a.content ?? ""
2497
+ });
2498
+ } else if (ev.type === "proposal_created") {
2499
+ const p = ev;
2500
+ proposals.push({
2501
+ id: p.proposalId,
2502
+ title: p.title,
2503
+ status: p.status ?? "pending",
2504
+ ...p.content !== void 0 ? { content: p.content } : {}
2505
+ });
2506
+ }
2507
+ }
2508
+ return { artifacts, proposals, toolCalls };
2509
+ }
2510
+
2511
+ // src/campaign/presets/playback.ts
2512
+ function makePlaybackDispatch(driver) {
2513
+ return async (profile, scenario, ctx) => {
2514
+ const events = await driver.run(scenario, { ...ctx, profile });
2515
+ return extractProducedState(events);
2516
+ };
2517
+ }
2518
+ async function scoreUserStory(story, state, checkCorrectness) {
2519
+ const verdict = await verifyCompletion(
2520
+ { taskId: story.id, requirements: story.requirements },
2521
+ state,
2522
+ checkCorrectness
2523
+ );
2524
+ return { ...verdict, title: story.title };
2525
+ }
2526
+ function userStoryScoreboard(verdicts) {
2527
+ const rows = [];
2528
+ for (const v of verdicts) {
2529
+ for (const r of v.requirements) {
2530
+ rows.push({
2531
+ storyId: v.taskId,
2532
+ storyTitle: v.title,
2533
+ reqId: r.reqId,
2534
+ reqTitle: r.title,
2535
+ status: r.satisfied ? "PASS" : "FAIL",
2536
+ evidence: r.evidence
2537
+ });
2538
+ }
2539
+ }
2540
+ return rows;
2541
+ }
2542
+ function scoreboardSummary(rows) {
2543
+ const byStory = /* @__PURE__ */ new Map();
2544
+ let passed = 0;
2545
+ for (const r of rows) {
2546
+ const s = byStory.get(r.storyId) ?? { total: 0, passed: 0 };
2547
+ s.total++;
2548
+ if (r.status === "PASS") {
2549
+ s.passed++;
2550
+ passed++;
2551
+ }
2552
+ byStory.set(r.storyId, s);
2553
+ }
2554
+ let storiesFullyComplete = 0;
2555
+ for (const s of byStory.values()) if (s.total > 0 && s.passed === s.total) storiesFullyComplete++;
2556
+ return {
2557
+ stories: byStory.size,
2558
+ storiesFullyComplete,
2559
+ requirements: rows.length,
2560
+ passed,
2561
+ failed: rows.length - passed,
2562
+ passRate: rows.length === 0 ? 0 : passed / rows.length
2563
+ };
2564
+ }
2565
+ function escapeCell(s) {
2566
+ return s.replace(/\|/g, "\\|").replace(/\r?\n/g, " ");
2567
+ }
2568
+ function truncate(s, max) {
2569
+ return s.length <= max ? s : `${s.slice(0, Math.max(0, max - 1))}\u2026`;
2570
+ }
2571
+ function renderScoreboardMarkdown(rows, opts = {}) {
2572
+ const maxEv = opts.maxEvidenceChars ?? 160;
2573
+ const sum = scoreboardSummary(rows);
2574
+ const pct = (n) => `${Math.round(n * 100)}%`;
2575
+ const ev = (e) => escapeCell(truncate(e.join("; "), maxEv)) || "\u2014";
2576
+ const out = [`# ${opts.title ?? "Product-flow playback scoreboard"}`, ""];
2577
+ if (opts.meta) {
2578
+ for (const [k, v] of Object.entries(opts.meta)) out.push(`- **${k}:** ${v}`);
2579
+ out.push("");
2580
+ }
2581
+ out.push(
2582
+ `**${sum.storiesFullyComplete}/${sum.stories}** user stories fully shipped \xB7 **${sum.passed}/${sum.requirements}** requirements passing (${pct(sum.passRate)}) \xB7 **${sum.failed}** open`,
2583
+ ""
2584
+ );
2585
+ const fails = rows.filter((r) => r.status === "FAIL");
2586
+ if (fails.length > 0) {
2587
+ out.push("## Open tickets", "", "| Story | Requirement | Evidence |", "| --- | --- | --- |");
2588
+ for (const r of fails) {
2589
+ out.push(`| ${escapeCell(r.storyTitle)} | ${escapeCell(r.reqTitle)} | ${ev(r.evidence)} |`);
2590
+ }
2591
+ out.push("");
2592
+ } else {
2593
+ out.push("_All requirements passing \u2014 no open tickets._", "");
2594
+ }
2595
+ out.push("## Per-story tick-off", "");
2596
+ for (const storyId of [...new Set(rows.map((r) => r.storyId))]) {
2597
+ const storyRows = rows.filter((r) => r.storyId === storyId);
2598
+ const passed = storyRows.filter((r) => r.status === "PASS").length;
2599
+ const mark = passed === storyRows.length ? "\u2705" : "\u26A0\uFE0F";
2600
+ out.push(
2601
+ `### ${escapeCell(storyRows[0].storyTitle)} \u2014 ${passed}/${storyRows.length} ${mark}`,
2602
+ "",
2603
+ "| Requirement | Status | Evidence |",
2604
+ "| --- | --- | --- |"
2605
+ );
2606
+ for (const r of storyRows) {
2607
+ out.push(
2608
+ `| ${escapeCell(r.reqTitle)} | ${r.status === "PASS" ? "\u2705 PASS" : "\u274C FAIL"} | ${ev(r.evidence)} |`
2609
+ );
2610
+ }
2611
+ out.push("");
2612
+ }
2613
+ return out.join("\n");
2614
+ }
2615
+
2616
+ // src/campaign/presets/run-profile-matrix.ts
2617
+ import { createHash as createHash5 } from "crypto";
2618
+ import { join as join3 } from "path";
2619
+
2620
+ // src/agent-profile.ts
2621
+ import { createHash as createHash4 } from "crypto";
2622
+ import { harnessSupportsModel } from "@tangle-network/agent-interface";
2623
+ var CODING_HARNESSES = [
2624
+ "opencode",
2625
+ "claude-code",
2626
+ "codex",
2627
+ "kimi-code"
2628
+ ];
2629
+ var HARNESS_NATIVE_MODEL = "default";
2630
+ function expandProfileAxes(spec) {
2631
+ const harnesses = spec.harnesses ?? CODING_HARNESSES;
2632
+ if (harnesses.length === 0) throw new ValidationError("expandProfileAxes: no harnesses to sweep");
2633
+ const baseModel = spec.base.model?.default;
2634
+ const models = spec.models ?? (baseModel ? [baseModel] : []);
2635
+ if (models.length === 0) {
2636
+ throw new ValidationError(
2637
+ "expandProfileAxes: no models to sweep \u2014 base profile has no model.default and none were supplied"
2638
+ );
2639
+ }
2640
+ const out = [];
2641
+ const seen = /* @__PURE__ */ new Set();
2642
+ for (const harness of harnesses) {
2643
+ const supported = spec.keepIncompatible ? models : models.filter((model) => harnessSupportsModel(harness, model));
2644
+ const effective = supported.length > 0 ? supported : [HARNESS_NATIVE_MODEL];
2645
+ for (const model of effective) {
2646
+ const profile = {
2647
+ ...spec.base,
2648
+ name: `${spec.base.name ?? "agent"}/${harness}/${model}`,
2649
+ model: { ...spec.base.model, default: model },
2650
+ metadata: { ...spec.base.metadata ?? {}, harness, harnessModel: model }
2651
+ };
2652
+ const id = agentProfileId(profile);
2653
+ if (seen.has(id)) continue;
2654
+ seen.add(id);
2655
+ out.push(profile);
2656
+ }
2657
+ }
2658
+ if (out.length === 0) {
2659
+ throw new ValidationError(
2660
+ `expandProfileAxes: produced no profiles (harnesses=[${harnesses.join(", ")}], models=[${models.join(", ")}]).`
2661
+ );
2662
+ }
2663
+ return out;
2664
+ }
2665
+ function harnessAxisOf(profile) {
2666
+ const m = profile.metadata;
2667
+ const harness = m?.harness;
2668
+ const model = m?.harnessModel;
2669
+ if (typeof harness === "string" && typeof model === "string") {
2670
+ return { harness, model };
2671
+ }
2672
+ return void 0;
2673
+ }
2674
+ function agentProfileId(profile) {
2675
+ const label = pathSafeProfileLabel(agentProfileDisplayLabel(profile)) ?? "profile";
2676
+ return `${label}-${agentProfileHash(profile).slice(0, 16)}`;
2677
+ }
2678
+ function agentProfileModelId(profile) {
2679
+ const model = profile.model?.default?.trim();
2680
+ if (!model) {
2681
+ const label = agentProfileDisplayLabel(profile) ?? "unnamed profile";
2682
+ throw new ValidationError(
2683
+ `AgentProfile "${label}" has no model.default \u2014 cannot record eval run`
2684
+ );
2685
+ }
2686
+ return model;
2687
+ }
2688
+ function agentProfileDisplayLabel(profile) {
2689
+ return profile.name?.trim() || profile.version?.trim() || void 0;
2690
+ }
2691
+ function pathSafeProfileLabel(label) {
2692
+ const safe = label?.trim().replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2693
+ return safe || void 0;
2694
+ }
2695
+ function compact(input) {
2696
+ const out = {};
2697
+ for (const [key, value] of Object.entries(input)) {
2698
+ if (value !== void 0) out[key] = value;
2699
+ }
2700
+ return out;
2701
+ }
2702
+ function agentProfileHash(profile) {
2703
+ const model = agentProfileModelId(profile);
2704
+ const behaviour = {
2705
+ ...profile,
2706
+ name: void 0,
2707
+ description: void 0,
2708
+ tags: profile.tags ? [...profile.tags].sort() : void 0,
2709
+ model: compact({ ...profile.model, default: model })
2710
+ };
2711
+ return createHash4("sha256").update(JSON.stringify(canonicalize(behaviour))).digest("hex");
2712
+ }
2713
+
2714
+ // src/campaign/presets/run-profile-matrix.ts
2715
+ var ProfileMatrixError = class extends AgentEvalError {
2716
+ constructor(message) {
2717
+ super("profile_matrix", message);
2718
+ }
2719
+ };
2720
+ function sanitize(id) {
2721
+ return id.replace(/[^a-zA-Z0-9_-]/g, "_");
2722
+ }
2723
+ function sha(input) {
2724
+ return createHash5("sha256").update(JSON.stringify(input)).digest("hex");
2725
+ }
2726
+ function mean2(xs) {
2727
+ return xs.length === 0 ? 0 : xs.reduce((a, b) => a + b, 0) / xs.length;
2728
+ }
2729
+ function cellComposite(cell) {
2730
+ const composites = Object.values(cell.judgeScores).map((s) => s.composite);
2731
+ return composites.length === 0 ? 0 : mean2(composites);
2732
+ }
2733
+ function requireResolvedModel(cell, profileId) {
2734
+ const resolved = cell.resolvedModel?.trim();
2735
+ if (!resolved) {
2736
+ throw new ProfileMatrixError(
2737
+ `profile '${profileId}' declared the '${HARNESS_NATIVE_MODEL}' runtime-resolved model but its dispatch reported no resolved model for cell '${cell.cellId}' \u2014 return it in the ctx.cost.runPaidCall receipt so the RunRecord pins the real model (never records '${HARNESS_NATIVE_MODEL}')`
2738
+ );
2739
+ }
2740
+ if (!modelHasSnapshot(resolved)) {
2741
+ throw new ProfileMatrixError(
2742
+ `profile '${profileId}' resolved to model '${resolved}' for cell '${cell.cellId}', which lacks a snapshot version \u2014 pin it (name@YYYY-MM-DD or name-YYYYMMDD) in the paid-call receipt`
2743
+ );
2744
+ }
2745
+ return resolved;
2746
+ }
2747
+ function buildRunRecord(args) {
2748
+ const { cell, profile, profileHash, configHash, experimentId, splitTag, commitSha, matrixId } = args;
2749
+ const profileId = agentProfileId(profile);
2750
+ const declaredModel = agentProfileModelId(profile);
2751
+ const model = declaredModel === HARNESS_NATIVE_MODEL ? requireResolvedModel(cell, profileId) : declaredModel;
2752
+ const composite = cellComposite(cell);
2753
+ const raw = { composite };
2754
+ const perJudge = {};
2755
+ const dimAccum = {};
2756
+ const notes = [];
2757
+ for (const [judgeName, js] of Object.entries(cell.judgeScores)) {
2758
+ perJudge[judgeName] = { ...js.dimensions };
2759
+ for (const [dim, value] of Object.entries(js.dimensions)) {
2760
+ raw[`${judgeName}.${dim}`] = value;
2761
+ dimAccum[dim] ??= [];
2762
+ dimAccum[dim].push(value);
2763
+ }
2764
+ if (js.notes) notes.push(`${judgeName}: ${js.notes}`);
2765
+ }
2766
+ const perDimMean = {};
2767
+ for (const [dim, values] of Object.entries(dimAccum)) perDimMean[dim] = mean2(values);
2768
+ const costUsd = cell.costUsd;
2769
+ raw.cost_usd = costUsd;
2770
+ raw.cost_estimated = cell.costEstimated ? 1 : 0;
2771
+ raw.tokens_input = cell.tokenUsage.input;
2772
+ raw.tokens_output = cell.tokenUsage.output;
2773
+ if (typeof cell.tokenUsage.cached === "number") raw.tokens_cached = cell.tokenUsage.cached;
2774
+ raw.latency_ms = cell.durationMs;
2775
+ if (costUsd > 0) {
2776
+ raw.tokens_per_dollar = (cell.tokenUsage.input + cell.tokenUsage.output) / costUsd;
2777
+ }
2778
+ if (composite > 0.01) raw.cost_per_quality = costUsd / composite;
2779
+ const outcome = splitTag === "holdout" ? { holdoutScore: composite, raw } : { searchScore: composite, raw };
2780
+ if (Object.keys(perJudge).length > 0) {
2781
+ outcome.judgeScores = {
2782
+ perJudge,
2783
+ perDimMean,
2784
+ composite,
2785
+ ...notes.length > 0 ? { notes: notes.join(" | ") } : {}
2786
+ };
2787
+ }
2788
+ const record = {
2789
+ runId: `${matrixId}:${profileId}:${cell.cellId}`,
2790
+ experimentId,
2791
+ candidateId: profileId,
2792
+ seed: cell.seed,
2793
+ model,
2794
+ promptHash: profileHash,
2795
+ configHash,
2796
+ commitSha,
2797
+ wallMs: cell.durationMs,
2798
+ costUsd,
2799
+ tokenUsage: cell.tokenUsage,
2800
+ outcome,
2801
+ splitTag,
2802
+ scenarioId: cell.scenarioId,
2803
+ ...args.agentProfileCell ? { agentProfile: args.agentProfileCell } : {},
2804
+ ...cell.error ? { failureMode: cell.error } : {}
2805
+ };
2806
+ if (args.corpusText && args.scenario) {
2807
+ try {
2808
+ const text = args.corpusText(cell.artifact, args.scenario);
2809
+ if (text && typeof text.prompt === "string" && typeof text.completion === "string") {
2810
+ record.prompt = text.prompt;
2811
+ record.completion = text.completion;
2812
+ }
2813
+ } catch {
2814
+ }
2815
+ }
2816
+ return record;
2817
+ }
2818
+ async function runProfileMatrix(opts) {
2819
+ if (opts.profiles.length === 0) throw new ProfileMatrixError("profiles must not be empty");
2820
+ if (opts.scenarios.length === 0) throw new ProfileMatrixError("scenarios must not be empty");
2821
+ const splitTag = opts.splitTag ?? "search";
2822
+ const seed = opts.seed ?? 42;
2823
+ const validate = opts.validate ?? true;
2824
+ const integrityMode = opts.integrity ?? "assert";
2825
+ const profileIds = opts.profiles.map(agentProfileId);
2826
+ const experimentId = opts.experimentId ?? `pm_${sha({ profileIds, scenarios: opts.scenarios.map((s) => s.id) }).slice(0, 16)}`;
2827
+ const matrixId = `mtx_${sha({ experimentId, profileIds, seed, splitTag }).slice(0, 16)}`;
2828
+ const scenarioById = new Map(opts.scenarios.map((s) => [s.id, s]));
2829
+ for (const profile of opts.profiles) {
2830
+ const profileHash = agentProfileHash(profile);
2831
+ const profileId = agentProfileId(profile);
2832
+ const declaredModel = agentProfileModelId(profile);
2833
+ const model = declaredModel === HARNESS_NATIVE_MODEL ? `${HARNESS_NATIVE_MODEL}@runtime-resolved` : declaredModel;
2834
+ try {
2835
+ validateRunRecord({
2836
+ runId: `${matrixId}:${profileId}:probe`,
2837
+ experimentId,
2838
+ candidateId: profileId,
2839
+ seed,
2840
+ model,
2841
+ promptHash: profileHash,
2842
+ configHash: profileHash,
2843
+ commitSha: opts.commitSha,
2844
+ wallMs: 0,
2845
+ costUsd: 0,
2846
+ tokenUsage: { input: 0, output: 0 },
2847
+ outcome: splitTag === "holdout" ? { holdoutScore: 0, raw: {} } : { searchScore: 0, raw: {} },
2848
+ splitTag
2849
+ });
2850
+ } catch (err) {
2851
+ throw new ProfileMatrixError(
2852
+ `profile '${profileId}' is not recordable: ${err instanceof Error ? err.message : String(err)}`
2853
+ );
2854
+ }
2855
+ }
2856
+ const records = [];
2857
+ const campaigns = {};
2858
+ const byProfile = {};
2859
+ for (const profile of opts.profiles) {
2860
+ const profileHash = agentProfileHash(profile);
2861
+ const profileId = agentProfileId(profile);
2862
+ const declaredModel = agentProfileModelId(profile);
2863
+ const configHash = sha({
2864
+ profile: profileHash,
2865
+ judges: (opts.judges ?? []).map((j) => j.name),
2866
+ seed,
2867
+ splitTag
2868
+ });
2869
+ const dispatch = (scenario, ctx) => opts.dispatch(profile, scenario, ctx);
2870
+ Object.defineProperty(dispatch, "name", { value: `profile_${sanitize(profileId)}` });
2871
+ const campaign = await runCampaign({
2872
+ scenarios: opts.scenarios,
2873
+ dispatch,
2874
+ judges: opts.judges,
2875
+ seed,
2876
+ reps: opts.reps,
2877
+ maxConcurrency: opts.maxConcurrency,
2878
+ costCeiling: opts.costCeiling,
2879
+ labeledStore: opts.labeledStore,
2880
+ captureSource: opts.captureSource,
2881
+ storage: opts.storage,
2882
+ now: opts.now,
2883
+ runDir: join3(opts.runDir, sanitize(profileId))
2884
+ });
2885
+ const axis = harnessAxisOf(profile);
2886
+ const buildCellIdentity = (cellModel) => buildAgentProfileCell({
2887
+ profileId,
2888
+ sourceProfile: { kind: "agent-interface-profile", hash: profileHash },
2889
+ model: cellModel,
2890
+ ...axis ? { harness: { id: axis.harness } } : {}
2891
+ });
2892
+ const sharedCellIdentity = declaredModel === HARNESS_NATIVE_MODEL ? void 0 : await buildCellIdentity(declaredModel);
2893
+ const profileRecords = [];
2894
+ for (const cell of campaign.cells) {
2895
+ const agentProfileCell = sharedCellIdentity ?? await buildCellIdentity(requireResolvedModel(cell, profileId));
2896
+ const record = buildRunRecord({
2897
+ cell,
2898
+ profile,
2899
+ profileHash,
2900
+ configHash,
2901
+ experimentId,
2902
+ splitTag,
2903
+ commitSha: opts.commitSha,
2904
+ matrixId,
2905
+ agentProfileCell,
2906
+ scenario: scenarioById.get(cell.scenarioId),
2907
+ corpusText: opts.corpusText
2908
+ });
2909
+ if (validate) validateRunRecord(record);
2910
+ profileRecords.push(record);
2911
+ records.push(record);
2912
+ }
2913
+ const totalCostUsd = campaign.aggregates.totalCostUsd;
2914
+ campaigns[profileId] = campaign;
2915
+ byProfile[profileId] = {
2916
+ profileId,
2917
+ profileHash,
2918
+ // The declared model, unless it snapped to the sentinel — then the
2919
+ // resolved model the cells actually ran on (all cells of a profile share
2920
+ // one harness, so the first record's model is representative).
2921
+ model: declaredModel === HARNESS_NATIVE_MODEL ? profileRecords[0]?.model ?? declaredModel : declaredModel,
2922
+ records: profileRecords.length,
2923
+ meanComposite: mean2(profileRecords.map(compositeOf)),
2924
+ totalCostUsd,
2925
+ integrity: summarizeBackendIntegrity(profileRecords)
2926
+ };
2927
+ }
2928
+ const integrity = summarizeBackendIntegrity(records);
2929
+ if (integrityMode === "assert") {
2930
+ assertRealBackend(records, { allowMixed: opts.allowMixed ?? true });
2931
+ } else if (integrityMode === "warn" && integrity.verdict !== "real") {
2932
+ console.warn(
2933
+ `[runProfileMatrix] backend integrity: ${integrity.verdict} \u2014 ${integrity.diagnosis}`
2934
+ );
2935
+ }
2936
+ const byScenario = rollup(records, (r) => r.scenarioId);
2937
+ const byPersona = opts.personaOf ? rollupByPersona(records, opts.scenarios, opts.personaOf) : void 0;
2938
+ return { matrixId, experimentId, records, byProfile, byScenario, byPersona, integrity, campaigns };
2939
+ }
2940
+ function compositeOf(r) {
2941
+ return r.outcome.holdoutScore ?? r.outcome.searchScore ?? 0;
2942
+ }
2943
+ function rollup(records, keyOf) {
2944
+ const groups = /* @__PURE__ */ new Map();
2945
+ for (const r of records) {
2946
+ const key = keyOf(r);
2947
+ if (key === void 0) continue;
2948
+ const arr = groups.get(key) ?? [];
2949
+ arr.push(compositeOf(r));
2950
+ groups.set(key, arr);
2951
+ }
2952
+ const out = {};
2953
+ for (const [key, xs] of groups) out[key] = { meanComposite: mean2(xs), n: xs.length };
2954
+ return out;
2955
+ }
2956
+ function rollupByPersona(records, scenarios, personaOf) {
2957
+ const personaByScenarioId = /* @__PURE__ */ new Map();
2958
+ for (const s of scenarios) personaByScenarioId.set(s.id, personaOf(s));
2959
+ return rollup(records, (r) => r.scenarioId ? personaByScenarioId.get(r.scenarioId) : void 0);
2960
+ }
2961
+
2962
+ // src/campaign/scenario-selection.ts
2963
+ var DEFAULT_SATURATION_CEILING = 0.999;
2964
+ var VARIANCE_EPSILON = 1e-9;
2965
+ function moments(scores) {
2966
+ const n = scores.length;
2967
+ if (n === 0) return { meanScore: 0, variance: 0 };
2968
+ let sum = 0;
2969
+ for (const s of scores) sum += s;
2970
+ const meanScore = sum / n;
2971
+ let sqDev = 0;
2972
+ for (const s of scores) {
2973
+ const d = s - meanScore;
2974
+ sqDev += d * d;
2975
+ }
2976
+ return { meanScore, variance: sqDev / n };
2977
+ }
2978
+ function compareDiscrimination(a, b) {
2979
+ if (b.discrimination !== a.discrimination) return b.discrimination - a.discrimination;
2980
+ if (a.meanScore !== b.meanScore) return a.meanScore - b.meanScore;
2981
+ return a.scenarioId < b.scenarioId ? -1 : a.scenarioId > b.scenarioId ? 1 : 0;
2982
+ }
2983
+ function scoreDiscrimination(signals, opts) {
2984
+ const saturationCeiling = opts?.saturationCeiling ?? DEFAULT_SATURATION_CEILING;
2985
+ const scored = signals.map((signal) => {
2986
+ const { meanScore, variance } = moments(signal.scores);
2987
+ const tied = variance < VARIANCE_EPSILON && meanScore >= saturationCeiling;
2988
+ return {
2989
+ scenarioId: signal.scenarioId,
2990
+ discrimination: variance,
2991
+ meanScore,
2992
+ variance,
2993
+ tied
2994
+ };
2995
+ });
2996
+ return scored.sort(compareDiscrimination);
2997
+ }
2998
+ function selectDiscriminative(signals, k, opts) {
2999
+ if (k < 1) throw new Error(`selectDiscriminative: k must be >= 1 (got ${k})`);
3000
+ const ranked = scoreDiscrimination(signals, opts);
3001
+ if (ranked.length <= k) return ranked.map((s) => s.scenarioId);
3002
+ const nonTied = ranked.filter((s) => !s.tied);
3003
+ if (nonTied.length >= k) return nonTied.slice(0, k).map((s) => s.scenarioId);
3004
+ const tied = ranked.filter((s) => s.tied);
3005
+ const fill = tied.slice(0, k - nonTied.length);
3006
+ return [...nonTied, ...fill].map((s) => s.scenarioId);
3007
+ }
3008
+
3009
+ // src/campaign/search-ledger.ts
3010
+ import { createHash as createHash6 } from "crypto";
3011
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
3012
+ import { resolve as resolve2 } from "path";
3013
+ import { z } from "zod";
3014
+ var SEARCH_LEDGER_SCHEMA = "tangle.search-ledger.v1";
3015
+ var NON_EMPTY = z.string().min(1).refine((value) => value.trim() === value, "must not contain surrounding whitespace");
3016
+ var HASH = z.string().regex(/^sha256:[a-f0-9]{64}$/);
3017
+ var LINEAGE_NODE_ID = z.string().regex(/^[a-f0-9]{16}$/);
3018
+ var IMMUTABLE_REVISION = z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|sha256:[a-f0-9]{64}|sha512:[A-Za-z0-9+/=]+)$/);
3019
+ var 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");
3020
+ var NON_NEGATIVE_INT = z.number().int().nonnegative().safe();
3021
+ var FINITE_NUMBER = z.number().finite();
3022
+ var ArtifactRefSchema = z.object({
3023
+ role: NON_EMPTY,
3024
+ uri: NON_EMPTY,
3025
+ sha256: HASH,
3026
+ byteLength: NON_NEGATIVE_INT
3027
+ }).strict();
3028
+ var SourceRefSchema = z.object({
3029
+ uri: NON_EMPTY,
3030
+ revision: IMMUTABLE_REVISION
3031
+ }).strict();
3032
+ var FailureReasonSchema = z.object({
3033
+ code: NON_EMPTY,
3034
+ message: NON_EMPTY
3035
+ }).strict();
3036
+ var EventBaseShape = {
3037
+ eventId: NON_EMPTY,
3038
+ occurredAt: ISO_TIMESTAMP,
3039
+ artifacts: z.array(ArtifactRefSchema).min(1)
3040
+ };
3041
+ var OperationKindSchema = z.enum([
3042
+ "candidate-generation",
3043
+ "analysis",
3044
+ "selection",
3045
+ "judge",
3046
+ "other"
3047
+ ]);
3048
+ var SearchPlannedSchema = z.object({
3049
+ ...EventBaseShape,
3050
+ kind: z.literal("search-planned"),
3051
+ plan: z.object({
3052
+ candidateSlots: z.array(
3053
+ z.object({
3054
+ slotId: NON_EMPTY,
3055
+ generationOperationId: NON_EMPTY
3056
+ }).strict()
3057
+ ).min(1),
3058
+ tasks: z.array(
3059
+ z.object({
3060
+ taskId: NON_EMPTY,
3061
+ source: SourceRefSchema,
3062
+ benchmark: SourceRefSchema,
3063
+ maxAttempts: z.number().int().positive().safe()
3064
+ }).strict()
3065
+ ).min(1),
3066
+ operations: z.array(
3067
+ z.object({
3068
+ operationId: NON_EMPTY,
3069
+ kind: OperationKindSchema
3070
+ }).strict()
3071
+ ).min(1)
3072
+ }).strict()
3073
+ }).strict();
3074
+ var CandidateRegisteredSchema = z.object({
3075
+ ...EventBaseShape,
3076
+ kind: z.literal("candidate-registered"),
3077
+ slotId: NON_EMPTY,
3078
+ generationOperationId: NON_EMPTY,
3079
+ candidateId: NON_EMPTY,
3080
+ lineage: z.object({
3081
+ lineageNodeId: LINEAGE_NODE_ID,
3082
+ parentCandidateIds: z.array(NON_EMPTY),
3083
+ generation: NON_NEGATIVE_INT,
3084
+ proposer: NON_EMPTY,
3085
+ proposerSource: SourceRefSchema
3086
+ }).strict(),
3087
+ surfaces: z.array(
3088
+ z.object({
3089
+ surfaceId: NON_EMPTY,
3090
+ kind: z.enum([
3091
+ "prompt",
3092
+ "tool-contract",
3093
+ "runtime-config",
3094
+ "memory",
3095
+ "knowledge",
3096
+ "agent-profile",
3097
+ "code",
3098
+ "deployment"
3099
+ ]),
3100
+ artifact: ArtifactRefSchema
3101
+ }).strict()
3102
+ ).min(1)
3103
+ }).strict();
3104
+ var CandidateSlotClosedSchema = z.object({
3105
+ ...EventBaseShape,
3106
+ kind: z.literal("candidate-slot-closed"),
3107
+ slotId: NON_EMPTY,
3108
+ generationOperationId: NON_EMPTY,
3109
+ reason: FailureReasonSchema
3110
+ }).strict();
3111
+ var KnownTokensSchema = z.object({
3112
+ status: z.literal("known"),
3113
+ inputTokens: NON_NEGATIVE_INT,
3114
+ outputTokens: NON_NEGATIVE_INT,
3115
+ cachedTokens: NON_NEGATIVE_INT
3116
+ }).strict();
3117
+ var UnknownSchema = z.object({
3118
+ status: z.literal("unknown"),
3119
+ reason: NON_EMPTY
3120
+ }).strict();
3121
+ var KnownCostSchema = z.object({
3122
+ status: z.literal("known"),
3123
+ usd: z.number().finite().nonnegative(),
3124
+ source: z.enum(["provider", "pricing-table", "free"])
3125
+ }).strict().superRefine((cost, ctx) => {
3126
+ if (cost.source === "free" && cost.usd !== 0) {
3127
+ ctx.addIssue({ code: "custom", message: "free cost source must have usd 0" });
3128
+ }
3129
+ });
3130
+ var UnknownCostSchema = z.object({
3131
+ status: z.literal("unknown"),
3132
+ knownLowerBoundUsd: z.number().finite().nonnegative(),
3133
+ reason: NON_EMPTY
3134
+ }).strict();
3135
+ var AccountingSchema = z.object({
3136
+ tokens: z.discriminatedUnion("status", [KnownTokensSchema, UnknownSchema]),
3137
+ cost: z.discriminatedUnion("status", [KnownCostSchema, UnknownCostSchema])
3138
+ }).strict();
3139
+ var MetricsSchema = z.record(NON_EMPTY, FINITE_NUMBER).superRefine((metrics, ctx) => {
3140
+ for (const key of Object.keys(metrics)) {
3141
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
3142
+ ctx.addIssue({ code: "custom", message: `unsafe metric key ${key}` });
3143
+ }
3144
+ }
3145
+ });
3146
+ var OutcomeSchema = z.discriminatedUnion("status", [
3147
+ z.object({
3148
+ status: z.literal("passed"),
3149
+ score: FINITE_NUMBER,
3150
+ metrics: MetricsSchema
3151
+ }).strict(),
3152
+ z.object({
3153
+ status: z.literal("failed"),
3154
+ score: FINITE_NUMBER,
3155
+ metrics: MetricsSchema,
3156
+ failure: FailureReasonSchema
3157
+ }).strict(),
3158
+ z.object({
3159
+ status: z.literal("errored"),
3160
+ metrics: MetricsSchema,
3161
+ error: FailureReasonSchema.extend({ retryable: z.boolean() }).strict()
3162
+ }).strict()
3163
+ ]);
3164
+ var EffectSchema = z.discriminatedUnion("status", [
3165
+ z.object({
3166
+ status: z.literal("measured"),
3167
+ metric: NON_EMPTY,
3168
+ baselineValue: FINITE_NUMBER,
3169
+ candidateValue: FINITE_NUMBER,
3170
+ delta: FINITE_NUMBER
3171
+ }).strict().superRefine((effect, ctx) => {
3172
+ const expected = effect.candidateValue - effect.baselineValue;
3173
+ const tolerance = Number.EPSILON * Math.max(1, Math.abs(expected), Math.abs(effect.delta)) * 8;
3174
+ if (Math.abs(effect.delta - expected) > tolerance) {
3175
+ ctx.addIssue({ code: "custom", message: "delta must equal candidateValue - baselineValue" });
3176
+ }
3177
+ }),
3178
+ z.object({
3179
+ status: z.literal("not-measured"),
3180
+ reason: NON_EMPTY
3181
+ }).strict()
3182
+ ]);
3183
+ var SurfaceEvidenceSchema = z.object({
3184
+ surfaceId: NON_EMPTY,
3185
+ fired: z.boolean(),
3186
+ firingCount: NON_NEGATIVE_INT,
3187
+ effect: EffectSchema,
3188
+ evidence: z.array(ArtifactRefSchema).min(1)
3189
+ }).strict().superRefine((evidence, ctx) => {
3190
+ if (evidence.fired && evidence.firingCount === 0) {
3191
+ ctx.addIssue({ code: "custom", message: "a fired surface must have firingCount >= 1" });
3192
+ }
3193
+ if (!evidence.fired && evidence.firingCount !== 0) {
3194
+ ctx.addIssue({
3195
+ code: "custom",
3196
+ message: "a surface that did not fire must have firingCount 0"
3197
+ });
3198
+ }
3199
+ if (!evidence.fired && evidence.effect.status === "measured" && evidence.effect.delta !== 0) {
3200
+ ctx.addIssue({
3201
+ code: "custom",
3202
+ message: "a surface that did not fire cannot claim non-zero effect"
3203
+ });
3204
+ }
3205
+ });
3206
+ var TaskAttemptedSchema = z.object({
3207
+ ...EventBaseShape,
3208
+ kind: z.literal("task-attempted"),
3209
+ candidateId: NON_EMPTY,
3210
+ runId: NON_EMPTY,
3211
+ attemptIndex: NON_NEGATIVE_INT,
3212
+ task: z.object({ taskId: NON_EMPTY, source: SourceRefSchema }).strict(),
3213
+ identity: z.object({
3214
+ model: z.object({
3215
+ provider: NON_EMPTY,
3216
+ snapshot: NON_EMPTY.refine(
3217
+ modelHasSnapshot,
3218
+ "model must include an immutable snapshot"
3219
+ )
3220
+ }).strict(),
3221
+ agent: SourceRefSchema,
3222
+ benchmark: SourceRefSchema
3223
+ }).strict(),
3224
+ outcome: OutcomeSchema,
3225
+ accounting: AccountingSchema,
3226
+ surfaceEvidence: z.array(SurfaceEvidenceSchema).min(1)
3227
+ }).strict();
3228
+ var SearchOperationRecordedSchema = z.object({
3229
+ ...EventBaseShape,
3230
+ kind: z.literal("search-operation-recorded"),
3231
+ operationId: NON_EMPTY,
3232
+ operationKind: OperationKindSchema,
3233
+ execution: z.discriminatedUnion("kind", [
3234
+ z.object({
3235
+ kind: z.literal("model"),
3236
+ model: z.object({
3237
+ provider: NON_EMPTY,
3238
+ snapshot: NON_EMPTY.refine(
3239
+ modelHasSnapshot,
3240
+ "model must include an immutable snapshot"
3241
+ )
3242
+ }).strict(),
3243
+ source: SourceRefSchema
3244
+ }).strict(),
3245
+ z.object({
3246
+ kind: z.literal("deterministic"),
3247
+ source: SourceRefSchema
3248
+ }).strict()
3249
+ ]),
3250
+ outcome: z.discriminatedUnion("status", [
3251
+ z.object({ status: z.literal("completed") }).strict(),
3252
+ z.object({
3253
+ status: z.literal("partial"),
3254
+ failure: FailureReasonSchema
3255
+ }).strict(),
3256
+ z.object({
3257
+ status: z.literal("failed"),
3258
+ failure: FailureReasonSchema
3259
+ }).strict()
3260
+ ]),
3261
+ accounting: AccountingSchema
3262
+ }).strict();
3263
+ var CandidateDecidedSchema = z.object({
3264
+ ...EventBaseShape,
3265
+ kind: z.literal("candidate-decided"),
3266
+ candidateId: NON_EMPTY,
3267
+ decision: z.discriminatedUnion("status", [
3268
+ z.object({ status: z.literal("selected") }).strict(),
3269
+ z.object({
3270
+ status: z.literal("rejected"),
3271
+ reason: FailureReasonSchema
3272
+ }).strict()
3273
+ ])
3274
+ }).strict();
3275
+ var SearchCompletedSchema = z.object({
3276
+ ...EventBaseShape,
3277
+ kind: z.literal("search-completed"),
3278
+ result: z.discriminatedUnion("status", [
3279
+ z.object({
3280
+ status: z.literal("selected"),
3281
+ candidateId: NON_EMPTY
3282
+ }).strict(),
3283
+ z.object({
3284
+ status: z.literal("all-rejected"),
3285
+ reason: FailureReasonSchema
3286
+ }).strict()
3287
+ ])
3288
+ }).strict();
3289
+ var EventSchema = z.discriminatedUnion("kind", [
3290
+ SearchPlannedSchema,
3291
+ CandidateRegisteredSchema,
3292
+ CandidateSlotClosedSchema,
3293
+ TaskAttemptedSchema,
3294
+ SearchOperationRecordedSchema,
3295
+ CandidateDecidedSchema,
3296
+ SearchCompletedSchema
3297
+ ]);
3298
+ var EntrySchema = z.object({
3299
+ schema: z.literal(SEARCH_LEDGER_SCHEMA),
3300
+ campaignId: NON_EMPTY,
3301
+ sequence: NON_NEGATIVE_INT,
3302
+ previousHash: z.union([HASH, z.null()]),
3303
+ event: EventSchema,
3304
+ entryHash: HASH
3305
+ }).strict();
3306
+ function validateSearchLedgerEvent(input) {
3307
+ const parsed = EventSchema.safeParse(input);
3308
+ if (!parsed.success) {
3309
+ throw new SearchLedgerError(`invalid search ledger event: ${formatZodError(parsed.error)}`);
3310
+ }
3311
+ return normalizeEvent(parsed.data);
3312
+ }
3313
+ function openSearchLedger(options) {
3314
+ if (options.path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3315
+ return new FileSearchLedger(options.path, options.campaignId);
3316
+ }
3317
+ var ledgerMutexes = /* @__PURE__ */ new Map();
3318
+ function mutexFor(path) {
3319
+ const existing = ledgerMutexes.get(path);
3320
+ if (existing) return existing;
3321
+ const mutex = new Mutex();
3322
+ ledgerMutexes.set(path, mutex);
3323
+ return mutex;
3324
+ }
3325
+ var FileSearchLedger = class {
3326
+ path;
3327
+ campaignId;
3328
+ mutex;
3329
+ constructor(path, campaignId) {
3330
+ if (path.trim().length === 0) throw new SearchLedgerError("ledger path is empty");
3331
+ if (campaignId.length === 0) throw new SearchLedgerError("campaignId is empty");
3332
+ if (campaignId.trim() !== campaignId) {
3333
+ throw new SearchLedgerError("campaignId must not contain surrounding whitespace");
3334
+ }
3335
+ this.path = resolve2(path);
3336
+ this.campaignId = campaignId;
3337
+ this.mutex = mutexFor(this.path);
3338
+ }
3339
+ async replay() {
3340
+ return this.mutex.runExclusive(
3341
+ () => withSearchLedgerFileLock(this.path, () => replayFile(this.path, this.campaignId))
3342
+ );
3343
+ }
3344
+ async append(input) {
3345
+ const event = validateSearchLedgerEvent(input);
3346
+ return this.mutex.runExclusive(
3347
+ () => withSearchLedgerFileLock(this.path, () => {
3348
+ const before = replayFile(this.path, this.campaignId);
3349
+ const existing = before.entries.find((entry2) => entry2.event.eventId === event.eventId);
3350
+ if (existing) {
3351
+ if (canonicalString(existing.event) !== canonicalString(event)) {
3352
+ throw new SearchLedgerConflictError(
3353
+ `eventId ${event.eventId} already exists with different content`
3354
+ );
3355
+ }
3356
+ return { entry: existing, appended: false, replay: before };
3357
+ }
3358
+ const previousHash = before.audit.headHash;
3359
+ const material = {
3360
+ schema: SEARCH_LEDGER_SCHEMA,
3361
+ campaignId: this.campaignId,
3362
+ sequence: before.entries.length,
3363
+ previousHash,
3364
+ event
3365
+ };
3366
+ const entry = {
3367
+ ...material,
3368
+ entryHash: hashCanonical(material)
3369
+ };
3370
+ const replay = replayEntries([...before.entries, entry], this.campaignId);
3371
+ appendSearchLedgerLine(this.path, `${canonicalString(entry)}
3372
+ `);
3373
+ return { entry, appended: true, replay };
3374
+ })
3375
+ );
3376
+ }
3377
+ };
3378
+ function replayFile(path, campaignId) {
3379
+ if (!existsSync3(path)) return replayEntries([], campaignId);
3380
+ const text = readFileSync3(path, "utf8");
3381
+ if (text.length === 0) return replayEntries([], campaignId);
3382
+ if (!text.endsWith("\n")) {
3383
+ throw new SearchLedgerIntegrityError(
3384
+ `search ledger ${path} has a truncated final record (missing newline)`
3385
+ );
3386
+ }
3387
+ const lines = text.slice(0, -1).split("\n");
3388
+ const entries = [];
3389
+ for (let index = 0; index < lines.length; index += 1) {
3390
+ const line = lines[index];
3391
+ if (line.length === 0) {
3392
+ throw new SearchLedgerIntegrityError(
3393
+ `search ledger ${path} has a blank row at line ${index + 1}`
3394
+ );
3395
+ }
3396
+ let raw;
3397
+ try {
3398
+ raw = JSON.parse(line);
3399
+ } catch (error) {
3400
+ throw new SearchLedgerIntegrityError(
3401
+ `search ledger ${path} has invalid JSON at line ${index + 1}`,
3402
+ { cause: error }
3403
+ );
3404
+ }
3405
+ const parsed = EntrySchema.safeParse(raw);
3406
+ if (!parsed.success) {
3407
+ throw new SearchLedgerIntegrityError(
3408
+ `search ledger ${path} has a malformed entry at line ${index + 1}: ${formatZodError(parsed.error)}`
3409
+ );
3410
+ }
3411
+ const entry = parsed.data;
3412
+ const normalizedEvent = validateSearchLedgerEvent(entry.event);
3413
+ if (canonicalString(normalizedEvent) !== canonicalString(entry.event)) {
3414
+ throw new SearchLedgerIntegrityError(
3415
+ `search ledger ${path} has non-canonical event ordering at line ${index + 1}`
3416
+ );
3417
+ }
3418
+ if (line !== canonicalString(entry)) {
3419
+ throw new SearchLedgerIntegrityError(
3420
+ `search ledger ${path} has non-canonical bytes at line ${index + 1}`
3421
+ );
3422
+ }
3423
+ entries.push(entry);
3424
+ }
3425
+ return replayEntries(entries, campaignId);
3426
+ }
3427
+ function replayEntries(entries, campaignId) {
3428
+ const candidates = /* @__PURE__ */ new Map();
3429
+ const candidateBySlot = /* @__PURE__ */ new Map();
3430
+ const closedSlots = /* @__PURE__ */ new Map();
3431
+ const lineageNodes = /* @__PURE__ */ new Map();
3432
+ const eventIds = /* @__PURE__ */ new Set();
3433
+ const runIds = /* @__PURE__ */ new Set();
3434
+ const attemptKeys = /* @__PURE__ */ new Set();
3435
+ const candidateEvents = [];
3436
+ const closedSlotEvents = [];
3437
+ const attempts = [];
3438
+ const operationEvents = [];
3439
+ const operationsById = /* @__PURE__ */ new Map();
3440
+ const decisions = [];
3441
+ let planEvent = null;
3442
+ let completion = null;
3443
+ let expectedPrevious = null;
3444
+ let previousOccurredAt = Number.NEGATIVE_INFINITY;
3445
+ for (let index = 0; index < entries.length; index += 1) {
3446
+ const entry = entries[index];
3447
+ if (entry.campaignId !== campaignId) {
3448
+ throw new SearchLedgerIntegrityError(
3449
+ `entry ${index} belongs to campaign ${entry.campaignId}, expected ${campaignId}`
3450
+ );
3451
+ }
3452
+ if (entry.sequence !== index) {
3453
+ throw new SearchLedgerIntegrityError(
3454
+ `entry ${entry.event.eventId} has sequence ${entry.sequence}, expected ${index}`
3455
+ );
3456
+ }
3457
+ if (entry.previousHash !== expectedPrevious) {
3458
+ throw new SearchLedgerIntegrityError(
3459
+ `entry ${entry.event.eventId} does not extend the previous hash`
3460
+ );
3461
+ }
3462
+ const { entryHash: _entryHash, ...material } = entry;
3463
+ const expectedHash = hashCanonical(material);
3464
+ if (entry.entryHash !== expectedHash) {
3465
+ throw new SearchLedgerIntegrityError(
3466
+ `entry ${entry.event.eventId} hash mismatch: expected ${expectedHash}, got ${entry.entryHash}`
3467
+ );
3468
+ }
3469
+ expectedPrevious = entry.entryHash;
3470
+ const event = entry.event;
3471
+ if (eventIds.has(event.eventId)) {
3472
+ throw new SearchLedgerIntegrityError(`duplicate eventId ${event.eventId} in durable ledger`);
3473
+ }
3474
+ eventIds.add(event.eventId);
3475
+ if (completion) {
3476
+ throw new SearchLedgerIntegrityError(
3477
+ `event ${event.eventId} appears after terminal event ${completion.eventId}`
3478
+ );
3479
+ }
3480
+ const occurredAt = Date.parse(event.occurredAt);
3481
+ if (occurredAt < previousOccurredAt) {
3482
+ throw new SearchLedgerIntegrityError(
3483
+ `event ${event.eventId} occurred before the preceding durable event`
3484
+ );
3485
+ }
3486
+ previousOccurredAt = occurredAt;
3487
+ assertUnique2(event.artifacts.map(artifactKey), "artifact receipt", event.eventId);
3488
+ if (event.kind === "search-planned") {
3489
+ if (index !== 0 || planEvent) {
3490
+ throw new SearchLedgerIntegrityError("search plan must be the first and only plan event");
3491
+ }
3492
+ assertUnique2(
3493
+ event.plan.candidateSlots.map((slot) => slot.slotId),
3494
+ "candidate slot",
3495
+ event.eventId
3496
+ );
3497
+ assertUnique2(
3498
+ event.plan.tasks.map((task) => task.taskId),
3499
+ "planned taskId",
3500
+ event.eventId
3501
+ );
3502
+ assertUnique2(
3503
+ event.plan.operations.map((operation) => operation.operationId),
3504
+ "planned operationId",
3505
+ event.eventId
3506
+ );
3507
+ for (const slot of event.plan.candidateSlots) {
3508
+ const generationOperation = event.plan.operations.find(
3509
+ (operation) => operation.operationId === slot.generationOperationId
3510
+ );
3511
+ if (!generationOperation || generationOperation.kind !== "candidate-generation") {
3512
+ throw new SearchLedgerIntegrityError(
3513
+ `candidate slot ${slot.slotId} references unplanned candidate-generation operation ${slot.generationOperationId}`
3514
+ );
3515
+ }
3516
+ }
3517
+ planEvent = event;
3518
+ continue;
3519
+ }
3520
+ if (!planEvent) {
3521
+ throw new SearchLedgerIntegrityError(
3522
+ `event ${event.eventId} appears before the required search plan`
3523
+ );
3524
+ }
3525
+ if (event.kind === "candidate-registered") {
3526
+ if (candidates.has(event.candidateId)) {
3527
+ throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was registered twice`);
3528
+ }
3529
+ const plannedSlot = planEvent.plan.candidateSlots.find((slot) => slot.slotId === event.slotId);
3530
+ if (!plannedSlot) {
3531
+ throw new SearchLedgerIntegrityError(
3532
+ `candidate ${event.candidateId} binds unknown slot ${event.slotId}`
3533
+ );
3534
+ }
3535
+ if (candidateBySlot.has(event.slotId)) {
3536
+ throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was bound twice`);
3537
+ }
3538
+ if (closedSlots.has(event.slotId)) {
3539
+ throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was already closed`);
3540
+ }
3541
+ if (event.generationOperationId !== plannedSlot.generationOperationId) {
3542
+ throw new SearchLedgerIntegrityError(
3543
+ `candidate ${event.candidateId} generation operation ${event.generationOperationId} does not match slot ${event.slotId} plan ${plannedSlot.generationOperationId}`
3544
+ );
3545
+ }
3546
+ const generationOperation = operationsById.get(event.generationOperationId);
3547
+ if (!generationOperation) {
3548
+ throw new SearchLedgerIntegrityError(
3549
+ `candidate ${event.candidateId} precedes generation operation ${event.generationOperationId}`
3550
+ );
3551
+ }
3552
+ if (generationOperation.outcome.status === "failed") {
3553
+ throw new SearchLedgerIntegrityError(
3554
+ `candidate ${event.candidateId} cannot bind failed generation operation ${event.generationOperationId}`
3555
+ );
3556
+ }
3557
+ const previousCandidate = lineageNodes.get(event.lineage.lineageNodeId);
3558
+ if (previousCandidate) {
3559
+ throw new SearchLedgerIntegrityError(
3560
+ `lineage node ${event.lineage.lineageNodeId} is already bound to ${previousCandidate}`
3561
+ );
3562
+ }
3563
+ assertUnique2(event.lineage.parentCandidateIds, "parentCandidateId", event.eventId);
3564
+ const parents = event.lineage.parentCandidateIds.map((id) => {
3565
+ const parent = candidates.get(id);
3566
+ if (!parent) {
3567
+ throw new SearchLedgerIntegrityError(
3568
+ `candidate ${event.candidateId} references unknown parent ${id}`
3569
+ );
3570
+ }
3571
+ return parent;
3572
+ });
3573
+ const expectedGeneration = parents.length === 0 ? 0 : Math.max(...parents.map((parent) => parent.registered.lineage.generation)) + 1;
3574
+ if (event.lineage.generation !== expectedGeneration) {
3575
+ throw new SearchLedgerIntegrityError(
3576
+ `candidate ${event.candidateId} generation ${event.lineage.generation} does not follow its parents (expected ${expectedGeneration})`
3577
+ );
3578
+ }
3579
+ assertUnique2(
3580
+ event.surfaces.map((surface) => surface.surfaceId),
3581
+ "surfaceId",
3582
+ event.eventId
3583
+ );
3584
+ candidates.set(event.candidateId, { registered: event, attempts: [], decision: null });
3585
+ candidateBySlot.set(event.slotId, event.candidateId);
3586
+ lineageNodes.set(event.lineage.lineageNodeId, event.candidateId);
3587
+ candidateEvents.push(event);
3588
+ continue;
3589
+ }
3590
+ if (event.kind === "task-attempted") {
3591
+ const candidate = candidates.get(event.candidateId);
3592
+ if (!candidate) {
3593
+ throw new SearchLedgerIntegrityError(
3594
+ `attempt ${event.eventId} references unknown candidate ${event.candidateId}`
3595
+ );
3596
+ }
3597
+ if (candidate.decision) {
3598
+ throw new SearchLedgerIntegrityError(
3599
+ `attempt ${event.eventId} appears after candidate ${event.candidateId} was decided`
3600
+ );
3601
+ }
3602
+ const plannedTask = planEvent.plan.tasks.find((task) => task.taskId === event.task.taskId);
3603
+ if (!plannedTask) {
3604
+ throw new SearchLedgerIntegrityError(
3605
+ `attempt ${event.eventId} references unplanned task ${event.task.taskId}`
3606
+ );
3607
+ }
3608
+ if (canonicalString(plannedTask.source) !== canonicalString(event.task.source) || canonicalString(plannedTask.benchmark) !== canonicalString(event.identity.benchmark)) {
3609
+ throw new SearchLedgerIntegrityError(
3610
+ `task ${event.task.taskId} does not match its planned source identity`
3611
+ );
3612
+ }
3613
+ if (event.attemptIndex >= plannedTask.maxAttempts) {
3614
+ throw new SearchLedgerIntegrityError(
3615
+ `task ${event.task.taskId} attempt ${event.attemptIndex} exceeds planned maxAttempts ${plannedTask.maxAttempts}`
3616
+ );
3617
+ }
3618
+ if (runIds.has(event.runId)) {
3619
+ throw new SearchLedgerIntegrityError(`runId ${event.runId} was recorded twice`);
3620
+ }
3621
+ runIds.add(event.runId);
3622
+ const attemptKey = canonicalString([event.candidateId, event.task.taskId, event.attemptIndex]);
3623
+ if (attemptKeys.has(attemptKey)) {
3624
+ throw new SearchLedgerIntegrityError(
3625
+ `candidate ${event.candidateId} task ${event.task.taskId} attempt ${event.attemptIndex} was recorded twice`
3626
+ );
3627
+ }
3628
+ const expectedAttemptIndex = candidate.attempts.filter(
3629
+ (attempt) => attempt.task.taskId === event.task.taskId
3630
+ ).length;
3631
+ if (event.attemptIndex !== expectedAttemptIndex) {
3632
+ throw new SearchLedgerIntegrityError(
3633
+ `candidate ${event.candidateId} task ${event.task.taskId} attempt index ${event.attemptIndex} is not contiguous (expected ${expectedAttemptIndex})`
3634
+ );
3635
+ }
3636
+ const previousAttempt = candidate.attempts.find(
3637
+ (attempt) => attempt.task.taskId === event.task.taskId
3638
+ );
3639
+ if (previousAttempt?.outcome.status !== void 0 && previousAttempt.outcome.status !== "errored") {
3640
+ throw new SearchLedgerIntegrityError(
3641
+ `task ${event.task.taskId} was retried after a measured outcome`
3642
+ );
3643
+ }
3644
+ if (previousAttempt && canonicalString({ task: previousAttempt.task, identity: previousAttempt.identity }) !== canonicalString({ task: event.task, identity: event.identity })) {
3645
+ throw new SearchLedgerIntegrityError(
3646
+ `candidate ${event.candidateId} task ${event.task.taskId} changed immutable execution identity between attempts`
3647
+ );
3648
+ }
3649
+ attemptKeys.add(attemptKey);
3650
+ const declared = candidate.registered.surfaces.map((surface) => surface.surfaceId).sort();
3651
+ const observed = event.surfaceEvidence.map((surface) => surface.surfaceId).sort();
3652
+ assertUnique2(observed, "surface evidence", event.eventId);
3653
+ for (const evidence of event.surfaceEvidence) {
3654
+ assertUnique2(evidence.evidence.map(artifactKey), "surface evidence receipt", event.eventId);
3655
+ }
3656
+ if (canonicalString(declared) !== canonicalString(observed)) {
3657
+ throw new SearchLedgerIntegrityError(
3658
+ `attempt ${event.eventId} surface evidence does not exactly cover candidate ${event.candidateId}`
3659
+ );
3660
+ }
3661
+ candidate.attempts.push(event);
3662
+ attempts.push(event);
3663
+ continue;
3664
+ }
3665
+ if (event.kind === "search-operation-recorded") {
3666
+ const plannedOperation = planEvent.plan.operations.find(
3667
+ (operation) => operation.operationId === event.operationId
3668
+ );
3669
+ if (!plannedOperation) {
3670
+ throw new SearchLedgerIntegrityError(
3671
+ `operation ${event.operationId} was not declared in the search plan`
3672
+ );
3673
+ }
3674
+ if (plannedOperation.kind !== event.operationKind) {
3675
+ throw new SearchLedgerIntegrityError(
3676
+ `operation ${event.operationId} kind ${event.operationKind} does not match planned ${plannedOperation.kind}`
3677
+ );
3678
+ }
3679
+ if (operationsById.has(event.operationId)) {
3680
+ throw new SearchLedgerIntegrityError(`operation ${event.operationId} was recorded twice`);
3681
+ }
3682
+ operationsById.set(event.operationId, event);
3683
+ operationEvents.push(event);
3684
+ continue;
3685
+ }
3686
+ if (event.kind === "candidate-slot-closed") {
3687
+ const plannedSlot = planEvent.plan.candidateSlots.find((slot) => slot.slotId === event.slotId);
3688
+ if (!plannedSlot) {
3689
+ throw new SearchLedgerIntegrityError(
3690
+ `candidate slot closure ${event.eventId} references unknown slot ${event.slotId}`
3691
+ );
3692
+ }
3693
+ if (event.generationOperationId !== plannedSlot.generationOperationId) {
3694
+ throw new SearchLedgerIntegrityError(
3695
+ `candidate slot closure ${event.eventId} generation operation ${event.generationOperationId} does not match slot ${event.slotId} plan ${plannedSlot.generationOperationId}`
3696
+ );
3697
+ }
3698
+ if (candidateBySlot.has(event.slotId)) {
3699
+ throw new SearchLedgerIntegrityError(
3700
+ `candidate slot ${event.slotId} was already bound to a candidate`
3701
+ );
3702
+ }
3703
+ if (closedSlots.has(event.slotId)) {
3704
+ throw new SearchLedgerIntegrityError(`candidate slot ${event.slotId} was closed twice`);
3705
+ }
3706
+ const operation = operationsById.get(event.generationOperationId);
3707
+ if (!operation) {
3708
+ throw new SearchLedgerIntegrityError(
3709
+ `candidate slot closure ${event.eventId} precedes operation ${event.generationOperationId}`
3710
+ );
3711
+ }
3712
+ if (operation.outcome.status === "completed") {
3713
+ throw new SearchLedgerIntegrityError(
3714
+ `candidate slot ${event.slotId} cannot close from completed operation ${event.generationOperationId}`
3715
+ );
3716
+ }
3717
+ closedSlots.set(event.slotId, event);
3718
+ closedSlotEvents.push(event);
3719
+ continue;
3720
+ }
3721
+ if (event.kind === "candidate-decided") {
3722
+ const candidate = candidates.get(event.candidateId);
3723
+ if (!candidate) {
3724
+ throw new SearchLedgerIntegrityError(
3725
+ `decision ${event.eventId} references unknown candidate ${event.candidateId}`
3726
+ );
3727
+ }
3728
+ if (candidate.decision) {
3729
+ throw new SearchLedgerIntegrityError(`candidate ${event.candidateId} was decided twice`);
3730
+ }
3731
+ if (event.decision.status === "selected") {
3732
+ if (!candidate.attempts.some((attempt) => attempt.outcome.status !== "errored")) {
3733
+ throw new SearchLedgerIntegrityError(
3734
+ `candidate ${event.candidateId} cannot be selected without a measured task outcome`
3735
+ );
3736
+ }
3737
+ if (decisions.some((decision) => decision.decision.status === "selected")) {
3738
+ throw new SearchLedgerIntegrityError("more than one candidate was selected");
3739
+ }
3740
+ }
3741
+ candidate.decision = event;
3742
+ decisions.push(event);
3743
+ continue;
3744
+ }
3745
+ const missingCandidateSlots2 = planEvent.plan.candidateSlots.filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId);
3746
+ if (missingCandidateSlots2.length > 0) {
3747
+ throw new SearchLedgerIntegrityError(
3748
+ `search completed with missing candidate slots: ${missingCandidateSlots2.join(", ")}`
3749
+ );
3750
+ }
3751
+ const missingTaskOutcomes2 = plannedTaskOutcomeKeys(planEvent, candidates);
3752
+ if (missingTaskOutcomes2.length > 0) {
3753
+ throw new SearchLedgerIntegrityError(
3754
+ `search completed with missing task outcomes: ${missingTaskOutcomes2.join(", ")}`
3755
+ );
3756
+ }
3757
+ const missingOperations2 = planEvent.plan.operations.filter((operation) => !operationsById.has(operation.operationId)).map((operation) => operation.operationId);
3758
+ if (missingOperations2.length > 0) {
3759
+ throw new SearchLedgerIntegrityError(
3760
+ `search completed with missing search operations: ${missingOperations2.join(", ")}`
3761
+ );
3762
+ }
3763
+ for (const operation of planEvent.plan.operations) {
3764
+ if (operation.kind !== "candidate-generation") continue;
3765
+ const generationOutcome = operationsById.get(operation.operationId).outcome.status;
3766
+ const slots = planEvent.plan.candidateSlots.filter(
3767
+ (slot) => slot.generationOperationId === operation.operationId
3768
+ );
3769
+ if (slots.length === 0) continue;
3770
+ const registeredCount = slots.filter((slot) => candidateBySlot.has(slot.slotId)).length;
3771
+ const closedCount = slots.filter((slot) => closedSlots.has(slot.slotId)).length;
3772
+ if (generationOutcome === "completed" && closedCount > 0) {
3773
+ throw new SearchLedgerIntegrityError(
3774
+ `completed generation operation ${operation.operationId} contains ${closedCount} closed slot(s)`
3775
+ );
3776
+ }
3777
+ if (generationOutcome === "failed" && registeredCount > 0) {
3778
+ throw new SearchLedgerIntegrityError(
3779
+ `failed generation operation ${operation.operationId} contains ${registeredCount} registered candidate(s)`
3780
+ );
3781
+ }
3782
+ if (generationOutcome === "partial" && (registeredCount === 0 || closedCount === 0)) {
3783
+ throw new SearchLedgerIntegrityError(
3784
+ `partial generation operation ${operation.operationId} must contain both a registered candidate and a closed slot`
3785
+ );
3786
+ }
3787
+ }
3788
+ const pending = [...candidates.values()].filter((candidate) => candidate.decision === null);
3789
+ if (pending.length > 0) {
3790
+ throw new SearchLedgerIntegrityError(
3791
+ `search completed with ${pending.length} candidate decision(s) missing`
3792
+ );
3793
+ }
3794
+ const selected = decisions.filter((decision) => decision.decision.status === "selected");
3795
+ if (event.result.status === "selected") {
3796
+ if (selected.length !== 1 || selected[0].candidateId !== event.result.candidateId) {
3797
+ throw new SearchLedgerIntegrityError(
3798
+ `search completion winner ${event.result.candidateId} does not match candidate decisions`
3799
+ );
3800
+ }
3801
+ } else if (selected.length !== 0) {
3802
+ throw new SearchLedgerIntegrityError("all-rejected completion contains a selected candidate");
3803
+ }
3804
+ completion = event;
3805
+ }
3806
+ const selectedDecisions = decisions.filter((decision) => decision.decision.status === "selected");
3807
+ const rejectedDecisions = decisions.filter((decision) => decision.decision.status === "rejected");
3808
+ const outcomeCounts = { passed: 0, failed: 0, errored: 0 };
3809
+ const operationOutcomeCounts = { completed: 0, partial: 0, failed: 0 };
3810
+ let inputTokens = 0;
3811
+ let outputTokens = 0;
3812
+ let cachedTokens = 0;
3813
+ let costUsd = 0;
3814
+ const unknownTokenEventIds = [];
3815
+ const unknownCostEventIds = [];
3816
+ for (const attempt of attempts) {
3817
+ outcomeCounts[attempt.outcome.status] += 1;
3818
+ }
3819
+ for (const operation of operationEvents) {
3820
+ operationOutcomeCounts[operation.outcome.status] += 1;
3821
+ }
3822
+ for (const costedEvent of [...attempts, ...operationEvents]) {
3823
+ if (costedEvent.accounting.tokens.status === "known") {
3824
+ inputTokens += costedEvent.accounting.tokens.inputTokens;
3825
+ outputTokens += costedEvent.accounting.tokens.outputTokens;
3826
+ cachedTokens += costedEvent.accounting.tokens.cachedTokens;
3827
+ } else {
3828
+ unknownTokenEventIds.push(costedEvent.eventId);
3829
+ }
3830
+ if (costedEvent.accounting.cost.status === "known") {
3831
+ costUsd += costedEvent.accounting.cost.usd;
3832
+ } else {
3833
+ costUsd += costedEvent.accounting.cost.knownLowerBoundUsd;
3834
+ unknownCostEventIds.push(costedEvent.eventId);
3835
+ }
3836
+ }
3837
+ const accounting = unknownTokenEventIds.length === 0 && unknownCostEventIds.length === 0 ? {
3838
+ status: "known",
3839
+ inputTokens,
3840
+ outputTokens,
3841
+ cachedTokens,
3842
+ costUsd
3843
+ } : {
3844
+ status: "partial",
3845
+ knownInputTokens: inputTokens,
3846
+ knownOutputTokens: outputTokens,
3847
+ knownCachedTokens: cachedTokens,
3848
+ knownCostUsd: costUsd,
3849
+ unknownTokenEventIds,
3850
+ unknownCostEventIds
3851
+ };
3852
+ const selectedCandidateId = completion?.result.status === "selected" ? completion.result.candidateId : null;
3853
+ const status = completion?.result.status === "selected" ? "selected" : completion?.result.status === "all-rejected" ? "all-rejected" : "in-progress";
3854
+ const missingCandidateSlots = planEvent?.plan.candidateSlots.filter((slot) => !candidateBySlot.has(slot.slotId) && !closedSlots.has(slot.slotId)).map((slot) => slot.slotId) ?? [];
3855
+ const missingTaskOutcomes = planEvent ? plannedTaskOutcomeKeys(planEvent, candidates) : [];
3856
+ const missingOperations = planEvent?.plan.operations.filter((operation) => !operationsById.has(operation.operationId)).map((operation) => operation.operationId) ?? [];
3857
+ return {
3858
+ entries: [...entries],
3859
+ plan: planEvent,
3860
+ candidates: candidateEvents,
3861
+ closedCandidateSlots: closedSlotEvents,
3862
+ attempts,
3863
+ operations: operationEvents,
3864
+ decisions,
3865
+ completion,
3866
+ audit: {
3867
+ campaignId,
3868
+ eventCount: entries.length,
3869
+ candidateCount: candidates.size,
3870
+ closedCandidateSlotCount: closedSlots.size,
3871
+ attemptCount: attempts.length,
3872
+ operationCount: operationEvents.length,
3873
+ outcomes: outcomeCounts,
3874
+ operationOutcomes: operationOutcomeCounts,
3875
+ decisions: {
3876
+ selected: selectedDecisions.length,
3877
+ rejected: rejectedDecisions.length,
3878
+ pending: candidates.size - decisions.length
3879
+ },
3880
+ expected: {
3881
+ candidateSlots: planEvent?.plan.candidateSlots.length ?? 0,
3882
+ taskOutcomes: candidates.size * (planEvent?.plan.tasks.length ?? 0),
3883
+ operations: planEvent?.plan.operations.length ?? 0,
3884
+ missingCandidateSlots,
3885
+ missingTaskOutcomes,
3886
+ missingOperations
3887
+ },
3888
+ status,
3889
+ selectedCandidateId,
3890
+ accounting,
3891
+ headHash: entries.at(-1)?.entryHash ?? null
3892
+ }
3893
+ };
3894
+ }
3895
+ function plannedTaskOutcomeKeys(planEvent, candidates) {
3896
+ const missing = [];
3897
+ const registeredCandidates = [...candidates.values()].sort(
3898
+ (a, b) => compareStrings(a.registered.slotId, b.registered.slotId)
3899
+ );
3900
+ for (const candidate of registeredCandidates) {
3901
+ const slotId = candidate.registered.slotId;
3902
+ for (const task of planEvent.plan.tasks) {
3903
+ const measured = candidate.attempts.some(
3904
+ (attempt) => attempt.task.taskId === task.taskId && attempt.outcome.status !== "errored"
3905
+ );
3906
+ if (!measured) missing.push(`${slotId}/${task.taskId}`);
3907
+ }
3908
+ }
3909
+ return missing;
3910
+ }
3911
+ function normalizeEvent(event) {
3912
+ const artifacts = sortArtifacts(event.artifacts);
3913
+ if (event.kind === "search-planned") {
3914
+ return {
3915
+ ...event,
3916
+ artifacts,
3917
+ plan: {
3918
+ candidateSlots: [...event.plan.candidateSlots].sort(
3919
+ (a, b) => compareStrings(a.slotId, b.slotId)
3920
+ ),
3921
+ tasks: [...event.plan.tasks].sort((a, b) => compareStrings(a.taskId, b.taskId)),
3922
+ operations: [...event.plan.operations].sort(
3923
+ (a, b) => compareStrings(a.operationId, b.operationId)
3924
+ )
3925
+ }
3926
+ };
3927
+ }
3928
+ if (event.kind === "candidate-registered") {
3929
+ return {
3930
+ ...event,
3931
+ artifacts,
3932
+ lineage: {
3933
+ ...event.lineage,
3934
+ parentCandidateIds: sortedStrings(event.lineage.parentCandidateIds)
3935
+ },
3936
+ surfaces: [...event.surfaces].map((surface) => ({ ...surface, artifact: { ...surface.artifact } })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3937
+ };
3938
+ }
3939
+ if (event.kind === "task-attempted") {
3940
+ return {
3941
+ ...event,
3942
+ artifacts,
3943
+ surfaceEvidence: [...event.surfaceEvidence].map((evidence) => ({ ...evidence, evidence: sortArtifacts(evidence.evidence) })).sort((a, b) => compareStrings(a.surfaceId, b.surfaceId))
3944
+ };
3945
+ }
3946
+ return { ...event, artifacts };
3947
+ }
3948
+ function sortArtifacts(artifacts) {
3949
+ return [...artifacts].map((artifact) => ({ ...artifact })).sort((a, b) => compareStrings(artifactKey(a), artifactKey(b)));
3950
+ }
3951
+ function artifactKey(artifact) {
3952
+ return canonicalString(artifact);
3953
+ }
3954
+ function compareStrings(a, b) {
3955
+ return a < b ? -1 : a > b ? 1 : 0;
3956
+ }
3957
+ function sortedStrings(values) {
3958
+ return [...values].sort();
3959
+ }
3960
+ function assertUnique2(values, label, eventId) {
3961
+ if (new Set(values).size !== values.length) {
3962
+ throw new SearchLedgerIntegrityError(`event ${eventId} contains duplicate ${label} values`);
3963
+ }
3964
+ }
3965
+ function canonicalString(value) {
3966
+ return JSON.stringify(canonicalize(value));
3967
+ }
3968
+ function hashCanonical(value) {
3969
+ return `sha256:${createHash6("sha256").update(canonicalString(value)).digest("hex")}`;
3970
+ }
3971
+ function formatZodError(error) {
3972
+ return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<root>"}: ${issue.message}`).join("; ");
3973
+ }
3974
+
3975
+ // src/campaign/transient-failure.ts
3976
+ var BASE_TRANSIENT = /\b50[234]\b|no stream output|produced no stream|admission timed out|admission_rejected|queue_timeout|fetch failed|ECONNRESET|This operation was aborted/i;
3977
+ var TIMEOUT_PATTERN = /timeout after \d+ ?ms|cli-bridge timeout/i;
3978
+ function isTransientTransportFailure(message, opts = {}) {
3979
+ if (!message) return false;
3980
+ if (BASE_TRANSIENT.test(message)) return true;
3981
+ if ((opts.retryFullDurationTimeouts ?? false) && TIMEOUT_PATTERN.test(message)) return true;
3982
+ for (const p of opts.extraPatterns ?? []) if (p.test(message)) return true;
3983
+ return false;
3984
+ }
3985
+
3986
+ // src/campaign/worktree/index.ts
3987
+ import { execFileSync } from "child_process";
3988
+ import { createHash as createHash7 } from "crypto";
3989
+ import {
3990
+ closeSync,
3991
+ existsSync as existsSync4,
3992
+ constants as fsConstants,
3993
+ fstatSync,
3994
+ lstatSync,
3995
+ mkdirSync as mkdirSync2,
3996
+ mkdtempSync,
3997
+ openSync,
3998
+ readlinkSync,
3999
+ readSync,
4000
+ realpathSync,
4001
+ rmSync
4002
+ } from "fs";
4003
+ import { devNull, tmpdir } from "os";
4004
+ import { basename, dirname, isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3, sep } from "path";
4005
+ var MAX_GIT_OUTPUT_BYTES = 256 * 1024 * 1024;
4006
+ var FILE_HASH_CHUNK_BYTES = 1024 * 1024;
4007
+ var GIT_REPOSITORY_ENV = /* @__PURE__ */ new Set([
4008
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
4009
+ "GIT_COMMON_DIR",
4010
+ "GIT_DIR",
4011
+ "GIT_INDEX_FILE",
4012
+ "GIT_NAMESPACE",
4013
+ "GIT_OBJECT_DIRECTORY",
4014
+ "GIT_PREFIX",
4015
+ "GIT_QUARANTINE_PATH",
4016
+ "GIT_WORK_TREE"
4017
+ ]);
4018
+ var WorktreeAdapterError = class extends Error {
4019
+ constructor(message, cause) {
4020
+ super(message);
4021
+ this.cause = cause;
4022
+ this.name = "WorktreeAdapterError";
4023
+ }
4024
+ cause;
4025
+ };
4026
+ function defaultGit(args, cwd, overrides) {
4027
+ try {
4028
+ const env = { ...process.env };
4029
+ for (const key of Object.keys(env)) {
4030
+ if (GIT_REPOSITORY_ENV.has(key) || key === "GIT_CONFIG" || key.startsWith("GIT_CONFIG_") || key.startsWith("GIT_ATTR_") || key === "GIT_DIFF_OPTS" || key === "GIT_EXTERNAL_DIFF") {
4031
+ delete env[key];
4032
+ }
4033
+ }
4034
+ Object.assign(env, overrides);
4035
+ env.GIT_NO_REPLACE_OBJECTS = "1";
4036
+ env.LC_ALL = "C";
4037
+ env.LANG = "C";
4038
+ return execFileSync("git", args, { cwd, env, maxBuffer: MAX_GIT_OUTPUT_BYTES });
4039
+ } catch (err) {
4040
+ const stderr = err && typeof err === "object" && "stderr" in err ? String(err.stderr) : "";
4041
+ throw new WorktreeAdapterError(`git ${args.join(" ")} failed: ${stderr || String(err)}`, err);
4042
+ }
4043
+ }
4044
+ function gitBytes(git, args, cwd, env) {
4045
+ try {
4046
+ const output = git(args, cwd, env);
4047
+ return typeof output === "string" ? Buffer.from(output, "utf8") : Buffer.from(output);
4048
+ } catch (err) {
4049
+ if (err instanceof WorktreeAdapterError) throw err;
4050
+ throw new WorktreeAdapterError(`git ${args.join(" ")} failed: ${String(err)}`, err);
4051
+ }
4052
+ }
4053
+ function gitText(git, args, cwd, env) {
4054
+ return gitBytes(git, args, cwd, env).toString("utf8").trim();
4055
+ }
4056
+ function hasRegisteredWorktree(git, repoRoot, path) {
4057
+ const expected = Buffer.from(`worktree ${resolve3(path)}`, "utf8");
4058
+ const records = gitBytes(git, ["worktree", "list", "--porcelain", "-z"], repoRoot);
4059
+ let start = 0;
4060
+ while (start < records.length) {
4061
+ const end = records.indexOf(0, start);
4062
+ if (end < 0) {
4063
+ throw new WorktreeAdapterError("Git worktree list output was not NUL-terminated");
4064
+ }
4065
+ if (records.subarray(start, end).equals(expected)) return true;
4066
+ start = end + 1;
4067
+ }
4068
+ return false;
4069
+ }
4070
+ function hasLocalBranch(git, repoRoot, branch) {
4071
+ const ref = `refs/heads/${branch}`;
4072
+ return gitText(git, ["for-each-ref", "--format=%(refname)", "--", ref], repoRoot).split("\n").some((candidate) => candidate === ref);
4073
+ }
4074
+ function reconcileAbsent(exists, remove) {
4075
+ try {
4076
+ if (!exists()) return void 0;
4077
+ } catch (err) {
4078
+ return err;
4079
+ }
4080
+ try {
4081
+ remove();
4082
+ return void 0;
4083
+ } catch (removeError) {
4084
+ try {
4085
+ if (!exists()) return void 0;
4086
+ } catch (recheckError) {
4087
+ return new AggregateError(
4088
+ [removeError, recheckError],
4089
+ "Removal failed and the resulting resource state could not be checked"
4090
+ );
4091
+ }
4092
+ return removeError;
4093
+ }
4094
+ }
4095
+ function sha2562(bytes) {
4096
+ return `sha256:${createHash7("sha256").update(bytes).digest("hex")}`;
4097
+ }
4098
+ var PATCH_ARGS = [
4099
+ // Git binary patches embed a zlib stream. Pin no-compression at command
4100
+ // scope so repository/global config and compressor heuristics cannot change
4101
+ // the candidate identity for the same base and final tree.
4102
+ "-c",
4103
+ "core.compression=0",
4104
+ "-c",
4105
+ `core.attributesFile=${devNull}`,
4106
+ "-c",
4107
+ "core.quotePath=true",
4108
+ "-c",
4109
+ "diff.suppressBlankEmpty=false",
4110
+ "diff",
4111
+ "--unified=3",
4112
+ "--inter-hunk-context=0",
4113
+ `-O${devNull}`,
4114
+ "--binary",
4115
+ "--full-index",
4116
+ "--no-color",
4117
+ "--no-ext-diff",
4118
+ "--no-textconv",
4119
+ "--no-renames",
4120
+ "--diff-algorithm=myers",
4121
+ "--no-indent-heuristic",
4122
+ "--src-prefix=a/",
4123
+ "--dst-prefix=b/"
4124
+ ];
4125
+ var CANONICAL_GIT_ENV = {
4126
+ GIT_ATTR_GLOBAL: devNull,
4127
+ GIT_ATTR_NOSYSTEM: "1",
4128
+ GIT_CONFIG_GLOBAL: devNull,
4129
+ GIT_CONFIG_NOSYSTEM: "1",
4130
+ GIT_CONFIG_SYSTEM: devNull
4131
+ };
4132
+ function patchBytes(git, cwd, baseCommit, candidateCommit) {
4133
+ const scratch = mkdtempSync(join4(tmpdir(), "agent-eval-patch-"));
4134
+ const bareRepo = join4(scratch, "repo.git");
4135
+ const emptyTemplate = join4(scratch, "empty-template");
4136
+ mkdirSync2(emptyTemplate);
4137
+ try {
4138
+ const objectFormat = gitObjectHashAlgorithm(candidateCommit);
4139
+ const sourceObjects = realpathSync(gitText(git, ["rev-parse", "--git-path", "objects"], cwd));
4140
+ gitText(
4141
+ git,
4142
+ [
4143
+ "init",
4144
+ "--bare",
4145
+ "--quiet",
4146
+ ...objectFormat === "sha256" ? ["--object-format=sha256"] : [],
4147
+ `--template=${emptyTemplate}`,
4148
+ bareRepo
4149
+ ],
4150
+ scratch,
4151
+ CANONICAL_GIT_ENV
4152
+ );
4153
+ return gitBytes(
4154
+ git,
4155
+ [`--git-dir=${bareRepo}`, ...PATCH_ARGS, baseCommit, candidateCommit, "--"],
4156
+ scratch,
4157
+ {
4158
+ ...CANONICAL_GIT_ENV,
4159
+ GIT_ALTERNATE_OBJECT_DIRECTORIES: sourceObjects
4160
+ }
4161
+ );
4162
+ } finally {
4163
+ rmSync(scratch, { recursive: true, force: true });
4164
+ }
4165
+ }
4166
+ function resolveCommit(git, cwd, ref) {
4167
+ return gitText(git, ["rev-parse", "--verify", `${ref}^{commit}`], cwd);
4168
+ }
4169
+ function unresolvedWorktreePath(surface, worktreeDir) {
4170
+ if (isAbsolute2(surface.worktreeRef)) return surface.worktreeRef;
4171
+ if (worktreeDir) return join4(worktreeDir, basename(surface.worktreeRef));
4172
+ return surface.worktreeRef;
4173
+ }
4174
+ function displayGitPath(path) {
4175
+ return JSON.stringify(path);
4176
+ }
4177
+ function parseGitTreeEntries(bytes) {
4178
+ const input = Buffer.from(bytes);
4179
+ const entries = [];
4180
+ let start = 0;
4181
+ while (start < input.length) {
4182
+ const end = input.indexOf(0, start);
4183
+ if (end < 0) throw new WorktreeAdapterError("Git tree output was not NUL-terminated");
4184
+ const record = input.subarray(start, end);
4185
+ start = end + 1;
4186
+ if (record.length === 0) continue;
4187
+ const tab = record.indexOf(9);
4188
+ if (tab < 0) throw new WorktreeAdapterError("Git tree entry did not contain a path");
4189
+ const metadata = record.subarray(0, tab).toString("ascii").split(" ");
4190
+ if (metadata.length !== 3) throw new WorktreeAdapterError("Git tree entry was malformed");
4191
+ const mode = metadata[0];
4192
+ const type = metadata[1];
4193
+ const objectId = metadata[2];
4194
+ if (!mode || !type || !objectId) {
4195
+ throw new WorktreeAdapterError("Git tree entry was malformed");
4196
+ }
4197
+ const pathBytes = record.subarray(tab + 1);
4198
+ const path = pathBytes.toString("utf8");
4199
+ if (!Buffer.from(path, "utf8").equals(pathBytes)) {
4200
+ throw new WorktreeAdapterError("CodeSurface paths must be valid UTF-8");
4201
+ }
4202
+ if (type !== "blob" && type !== "commit") {
4203
+ throw new WorktreeAdapterError(
4204
+ `CodeSurface contains unsupported Git object type ${type} at ${displayGitPath(path)}`
4205
+ );
4206
+ }
4207
+ entries.push({ mode, objectId, path });
4208
+ }
4209
+ return entries;
4210
+ }
4211
+ function parseGitPathList(bytes) {
4212
+ const input = Buffer.from(bytes);
4213
+ const paths = [];
4214
+ let start = 0;
4215
+ while (start < input.length) {
4216
+ const end = input.indexOf(0, start);
4217
+ if (end < 0) throw new WorktreeAdapterError("Git path output was not NUL-terminated");
4218
+ const pathBytes = input.subarray(start, end);
4219
+ start = end + 1;
4220
+ if (pathBytes.length === 0) continue;
4221
+ const path = pathBytes.toString("utf8");
4222
+ if (!Buffer.from(path, "utf8").equals(pathBytes)) {
4223
+ throw new WorktreeAdapterError("CodeSurface paths must be valid UTF-8");
4224
+ }
4225
+ paths.push(path);
4226
+ }
4227
+ return paths;
4228
+ }
4229
+ function isWithinRoot(root, candidate) {
4230
+ const rel = relative2(root, candidate);
4231
+ return rel === "" || !isAbsolute2(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
4232
+ }
4233
+ function assertSafeRelativePath(root, path) {
4234
+ if (path.length === 0 || isAbsolute2(path)) {
4235
+ throw new WorktreeAdapterError(`CodeSurface contains unsafe path ${displayGitPath(path)}`);
4236
+ }
4237
+ const absolutePath = resolve3(root, path);
4238
+ if (!isWithinRoot(root, absolutePath) || absolutePath === root) {
4239
+ throw new WorktreeAdapterError(`CodeSurface path escapes its worktree: ${displayGitPath(path)}`);
4240
+ }
4241
+ const segments = path.split(/[\\/]/u);
4242
+ let parent = root;
4243
+ for (const segment of segments.slice(0, -1)) {
4244
+ if (segment.length === 0 || segment === "." || segment === "..") {
4245
+ throw new WorktreeAdapterError(`CodeSurface contains unsafe path ${displayGitPath(path)}`);
4246
+ }
4247
+ parent = join4(parent, segment);
4248
+ const stat = lstatSync(parent);
4249
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
4250
+ throw new WorktreeAdapterError(
4251
+ `CodeSurface path traverses a non-directory or symbolic link: ${displayGitPath(path)}`
4252
+ );
4253
+ }
4254
+ }
4255
+ return absolutePath;
4256
+ }
4257
+ function gitObjectHashAlgorithm(objectId) {
4258
+ if (/^[a-f0-9]{40}$/.test(objectId)) return "sha1";
4259
+ if (/^[a-f0-9]{64}$/.test(objectId)) return "sha256";
4260
+ throw new WorktreeAdapterError(`Unsupported Git object id: ${objectId}`);
4261
+ }
4262
+ function hashGitBlobBytes(bytes, objectId) {
4263
+ const hash = createHash7(gitObjectHashAlgorithm(objectId));
4264
+ hash.update(`blob ${bytes.byteLength}\0`);
4265
+ hash.update(bytes);
4266
+ return hash.digest("hex");
4267
+ }
4268
+ function hashGitBlobFile(path, objectId) {
4269
+ const before = lstatSync(path);
4270
+ if (!before.isFile() || before.isSymbolicLink()) {
4271
+ throw new WorktreeAdapterError(`CodeSurface expected a regular file at ${displayGitPath(path)}`);
4272
+ }
4273
+ const noFollow = process.platform === "win32" ? 0 : fsConstants.O_NOFOLLOW;
4274
+ const fd = openSync(path, fsConstants.O_RDONLY | noFollow);
4275
+ try {
4276
+ const opened = fstatSync(fd);
4277
+ if (!opened.isFile() || opened.dev !== before.dev || opened.ino !== before.ino || opened.size !== before.size) {
4278
+ throw new WorktreeAdapterError(
4279
+ `CodeSurface file changed while it was being verified: ${displayGitPath(path)}`
4280
+ );
4281
+ }
4282
+ const hash = createHash7(gitObjectHashAlgorithm(objectId));
4283
+ hash.update(`blob ${opened.size}\0`);
4284
+ const chunk = Buffer.allocUnsafe(FILE_HASH_CHUNK_BYTES);
4285
+ let total = 0;
4286
+ while (true) {
4287
+ const count = readSync(fd, chunk, 0, chunk.length, null);
4288
+ if (count === 0) break;
4289
+ total += count;
4290
+ hash.update(chunk.subarray(0, count));
4291
+ }
4292
+ if (total !== opened.size) {
4293
+ throw new WorktreeAdapterError(
4294
+ `CodeSurface file changed while it was being verified: ${displayGitPath(path)}`
4295
+ );
4296
+ }
4297
+ return { hash: hash.digest("hex"), executable: (opened.mode & 73) !== 0 };
4298
+ } finally {
4299
+ closeSync(fd);
4300
+ }
4301
+ }
4302
+ function assertSymlinkTargetIsBound(root, linkPath, trackedPaths) {
4303
+ const targetBytes = readlinkSync(linkPath, { encoding: "buffer" });
4304
+ const target = targetBytes.toString("utf8");
4305
+ if (!Buffer.from(target, "utf8").equals(targetBytes)) {
4306
+ throw new WorktreeAdapterError(
4307
+ `CodeSurface symbolic link has an unsafe target at ${displayGitPath(linkPath)}`
4308
+ );
4309
+ }
4310
+ if (isAbsolute2(target)) {
4311
+ throw new WorktreeAdapterError(
4312
+ `CodeSurface symbolic link escapes its worktree at ${displayGitPath(linkPath)}`
4313
+ );
4314
+ }
4315
+ const lexicalTarget = resolve3(dirname(linkPath), target);
4316
+ if (!isWithinRoot(root, lexicalTarget)) {
4317
+ throw new WorktreeAdapterError(
4318
+ `CodeSurface symbolic link escapes its worktree at ${displayGitPath(linkPath)}`
4319
+ );
4320
+ }
4321
+ let resolvedTarget;
4322
+ try {
4323
+ resolvedTarget = realpathSync(linkPath);
4324
+ } catch (err) {
4325
+ throw new WorktreeAdapterError(
4326
+ `CodeSurface symbolic link target is missing or cyclic at ${displayGitPath(linkPath)}`,
4327
+ err
4328
+ );
4329
+ }
4330
+ if (!isWithinRoot(root, resolvedTarget)) {
4331
+ throw new WorktreeAdapterError(
4332
+ `CodeSurface symbolic link escapes its worktree at ${displayGitPath(linkPath)}`
4333
+ );
4334
+ }
4335
+ const targetRelative = relative2(root, resolvedTarget).split(sep).join("/");
4336
+ const targetIsTracked = trackedPaths.has(targetRelative) || [...trackedPaths].some((trackedPath) => trackedPath.startsWith(`${targetRelative}/`));
4337
+ if (!targetIsTracked) {
4338
+ throw new WorktreeAdapterError(
4339
+ `CodeSurface symbolic link resolves to untracked content at ${displayGitPath(linkPath)}`
4340
+ );
4341
+ }
4342
+ }
4343
+ function assertRawTreeMatchesWorktree(git, root, candidateCommit) {
4344
+ const entries = parseGitTreeEntries(
4345
+ gitBytes(git, ["ls-tree", "-r", "-z", "--full-tree", candidateCommit], root)
4346
+ );
4347
+ const trackedPaths = new Set(entries.map((entry) => entry.path));
4348
+ for (const entry of entries) {
4349
+ const absolutePath = assertSafeRelativePath(root, entry.path);
4350
+ if (entry.mode === "160000" || entry.mode === "040000") {
4351
+ throw new WorktreeAdapterError(
4352
+ `CodeSurface contains a Git submodule whose executable bytes are not bound: ${displayGitPath(entry.path)}`
4353
+ );
4354
+ }
4355
+ if (entry.mode === "120000") {
4356
+ const stat = lstatSync(absolutePath);
4357
+ if (!stat.isSymbolicLink()) {
4358
+ throw new WorktreeAdapterError(
4359
+ `CodeSurface expected a symbolic link at ${displayGitPath(entry.path)}`
4360
+ );
4361
+ }
4362
+ const targetBytes = readlinkSync(absolutePath, { encoding: "buffer" });
4363
+ const actualObjectId = hashGitBlobBytes(targetBytes, entry.objectId);
4364
+ if (actualObjectId !== entry.objectId) {
4365
+ throw new WorktreeAdapterError(
4366
+ `CodeSurface raw symbolic-link bytes differ from the candidate tree at ${displayGitPath(entry.path)}`
4367
+ );
4368
+ }
4369
+ assertSymlinkTargetIsBound(root, absolutePath, trackedPaths);
4370
+ continue;
4371
+ }
4372
+ if (entry.mode !== "100644" && entry.mode !== "100755") {
4373
+ throw new WorktreeAdapterError(
4374
+ `CodeSurface contains unsupported Git mode ${entry.mode} at ${displayGitPath(entry.path)}`
4375
+ );
4376
+ }
4377
+ const actual = hashGitBlobFile(absolutePath, entry.objectId);
4378
+ if (actual.hash !== entry.objectId) {
4379
+ throw new WorktreeAdapterError(
4380
+ `CodeSurface raw tracked bytes differ from the candidate tree because the worktree changed after finalization at ${displayGitPath(entry.path)}`
4381
+ );
4382
+ }
4383
+ if (process.platform !== "win32" && actual.executable !== (entry.mode === "100755")) {
4384
+ throw new WorktreeAdapterError(
4385
+ `CodeSurface executable mode differs from the candidate tree at ${displayGitPath(entry.path)}`
4386
+ );
4387
+ }
4388
+ }
4389
+ }
4390
+ function verifyCodeSurfaceWithGit(surface, path, git) {
4391
+ assertCodeSurfaceIdentity(surface);
4392
+ if (!existsSync4(path)) {
4393
+ throw new WorktreeAdapterError(`CodeSurface worktree does not exist: ${path}`);
4394
+ }
4395
+ const lexicalRoot = resolve3(path);
4396
+ const canonicalRoot = realpathSync(path);
4397
+ if (lstatSync(lexicalRoot).isSymbolicLink() || process.platform !== "win32" && lexicalRoot !== canonicalRoot) {
4398
+ throw new WorktreeAdapterError(
4399
+ `CodeSurface worktree locator must not contain a symbolic link: ${path}`
4400
+ );
4401
+ }
4402
+ const repoRoot = gitText(git, ["rev-parse", "--show-toplevel"], path);
4403
+ const canonicalRepoRoot = realpathSync(repoRoot);
4404
+ if (canonicalRepoRoot !== canonicalRoot) {
4405
+ throw new WorktreeAdapterError(
4406
+ `CodeSurface worktree locator is not the repository root: expected ${repoRoot}, got ${path}`
4407
+ );
4408
+ }
4409
+ const indexFlags = gitText(git, ["ls-files", "-v"], path).split("\n").filter((line) => line.length > 0 && (line.startsWith("S ") || /^[a-z]/.test(line)));
4410
+ if (indexFlags.length > 0) {
4411
+ throw new WorktreeAdapterError(
4412
+ `CodeSurface worktree uses hidden index entries that cannot be verified: ${path}
4413
+ ${indexFlags.join("\n")}`
4414
+ );
4415
+ }
4416
+ const extraPaths = [
4417
+ ...parseGitPathList(gitBytes(git, ["ls-files", "--others", "--exclude-standard", "-z"], path)),
4418
+ ...parseGitPathList(
4419
+ gitBytes(git, ["ls-files", "--others", "--ignored", "--exclude-standard", "-z"], path)
4420
+ )
4421
+ ];
4422
+ if (extraPaths.length > 0) {
4423
+ throw new WorktreeAdapterError(
4424
+ `CodeSurface worktree changed after finalization: ${path}
4425
+ ${extraPaths.join("\n")}`
4426
+ );
4427
+ }
4428
+ const candidateCommit = resolveCommit(git, path, "HEAD");
4429
+ if (candidateCommit !== surface.candidateCommit) {
4430
+ throw new WorktreeAdapterError(
4431
+ `CodeSurface candidate commit mismatch: expected ${surface.candidateCommit}, got ${candidateCommit}`
4432
+ );
4433
+ }
4434
+ const baseCommit = resolveCommit(git, path, surface.baseCommit);
4435
+ if (baseCommit !== surface.baseCommit) {
4436
+ throw new WorktreeAdapterError(
4437
+ `CodeSurface base commit mismatch: expected ${surface.baseCommit}, got ${baseCommit}`
4438
+ );
4439
+ }
4440
+ const baseTree = gitText(git, ["rev-parse", "--verify", `${surface.baseCommit}^{tree}`], path);
4441
+ if (baseTree !== surface.baseTree) {
4442
+ throw new WorktreeAdapterError(
4443
+ `CodeSurface base tree mismatch: expected ${surface.baseTree}, got ${baseTree}`
4444
+ );
4445
+ }
4446
+ const candidateTree = gitText(
4447
+ git,
4448
+ ["rev-parse", "--verify", `${surface.candidateCommit}^{tree}`],
4449
+ path
4450
+ );
4451
+ if (candidateTree !== surface.candidateTree) {
4452
+ throw new WorktreeAdapterError(
4453
+ `CodeSurface tree mismatch: expected ${surface.candidateTree}, got ${candidateTree}`
4454
+ );
4455
+ }
4456
+ try {
4457
+ gitText(
4458
+ git,
4459
+ [
4460
+ "diff-index",
4461
+ "--cached",
4462
+ "--quiet",
4463
+ "--no-ext-diff",
4464
+ "--no-textconv",
4465
+ surface.candidateCommit,
4466
+ "--"
4467
+ ],
4468
+ path
4469
+ );
4470
+ } catch (err) {
4471
+ throw new WorktreeAdapterError(
4472
+ `CodeSurface index differs from finalized candidate: ${path}`,
4473
+ err
4474
+ );
4475
+ }
4476
+ try {
4477
+ assertRawTreeMatchesWorktree(git, canonicalRepoRoot, surface.candidateCommit);
4478
+ } catch (err) {
4479
+ if (err instanceof WorktreeAdapterError) throw err;
4480
+ throw new WorktreeAdapterError(`CodeSurface raw tree verification failed: ${path}`, err);
4481
+ }
4482
+ const mergeBase = gitText(git, ["merge-base", surface.baseCommit, surface.candidateCommit], path);
4483
+ if (mergeBase !== surface.baseCommit) {
4484
+ throw new WorktreeAdapterError(
4485
+ `CodeSurface candidate ${surface.candidateCommit} does not descend from base ${surface.baseCommit}`
4486
+ );
4487
+ }
4488
+ const patch = patchBytes(git, path, surface.baseCommit, surface.candidateCommit);
4489
+ const actualPatchHash = sha2562(patch);
4490
+ if (actualPatchHash !== surface.patch.sha256 || patch.byteLength !== surface.patch.byteLength) {
4491
+ throw new WorktreeAdapterError(
4492
+ `CodeSurface patch mismatch: expected ${surface.patch.sha256}/${surface.patch.byteLength} bytes, got ${actualPatchHash}/${patch.byteLength} bytes`
4493
+ );
4494
+ }
4495
+ return {
4496
+ path,
4497
+ repoRoot: canonicalRepoRoot,
4498
+ contentHash: surfaceContentHash(surface),
4499
+ patchBytes: new Uint8Array(patch)
4500
+ };
4501
+ }
4502
+ function slug(label) {
4503
+ return label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "candidate";
4504
+ }
4505
+ function gitWorktreeAdapter(opts) {
4506
+ const git = opts.git ?? defaultGit;
4507
+ const worktreeDir = opts.worktreeDir ?? join4(opts.repoRoot, ".worktrees");
4508
+ const branchPrefix = opts.branchPrefix ?? "improve";
4509
+ return {
4510
+ async create({ baseRef, label }) {
4511
+ const id = `${slug(label)}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
4512
+ const branch = `${branchPrefix}/${id}`;
4513
+ const path = join4(worktreeDir, id);
4514
+ const baseCommit = resolveCommit(git, opts.repoRoot, baseRef);
4515
+ const baseTree = gitText(
4516
+ git,
4517
+ ["rev-parse", "--verify", `${baseCommit}^{tree}`],
4518
+ opts.repoRoot
4519
+ );
4520
+ gitText(git, ["worktree", "add", "-b", branch, path, baseCommit], opts.repoRoot);
4521
+ return { path, branch, baseRef, baseCommit, baseTree };
4522
+ },
4523
+ async finalize(worktree, summary) {
4524
+ const status = gitText(
4525
+ git,
4526
+ ["status", "--porcelain=v1", "--untracked-files=all"],
4527
+ worktree.path
4528
+ );
4529
+ if (status.length > 0) {
4530
+ gitText(git, ["add", "-A"], worktree.path);
4531
+ gitText(git, ["commit", "-m", summary], worktree.path);
4532
+ }
4533
+ const candidateCommit = resolveCommit(git, worktree.path, "HEAD");
4534
+ const candidateTree = gitText(
4535
+ git,
4536
+ ["rev-parse", "--verify", `${candidateCommit}^{tree}`],
4537
+ worktree.path
4538
+ );
4539
+ const patch = patchBytes(git, worktree.path, worktree.baseCommit, candidateCommit);
4540
+ const surface = {
4541
+ kind: "code",
4542
+ worktreeRef: worktree.path,
4543
+ baseRef: worktree.baseRef,
4544
+ baseCommit: worktree.baseCommit,
4545
+ baseTree: worktree.baseTree,
4546
+ candidateCommit,
4547
+ candidateTree,
4548
+ patch: {
4549
+ format: "git-diff-binary",
4550
+ sha256: sha2562(patch),
4551
+ byteLength: patch.byteLength
4552
+ },
4553
+ summary
4554
+ };
4555
+ verifyCodeSurfaceWithGit(surface, worktree.path, git);
4556
+ return surface;
4557
+ },
4558
+ async discard(worktree) {
4559
+ const failures = [
4560
+ reconcileAbsent(
4561
+ () => hasRegisteredWorktree(git, opts.repoRoot, worktree.path),
4562
+ () => gitText(git, ["worktree", "remove", "--force", "--", worktree.path], opts.repoRoot)
4563
+ ),
4564
+ reconcileAbsent(
4565
+ () => hasLocalBranch(git, opts.repoRoot, worktree.branch),
4566
+ () => gitText(git, ["branch", "-D", "--", worktree.branch], opts.repoRoot)
4567
+ )
4568
+ ].filter((failure) => failure !== void 0);
4569
+ if (failures.length > 0) {
4570
+ const cause = failures.length === 1 ? failures[0] : new AggregateError(failures, "Multiple Git resources could not be removed");
4571
+ throw new WorktreeAdapterError(
4572
+ `Failed to discard worktree ${worktree.path} and branch ${worktree.branch}`,
4573
+ cause
4574
+ );
4575
+ }
4576
+ }
4577
+ };
4578
+ }
4579
+ function verifyCodeSurface(surface, worktreeDir) {
4580
+ const path = unresolvedWorktreePath(surface, worktreeDir);
4581
+ return verifyCodeSurfaceWithGit(surface, path, defaultGit);
4582
+ }
4583
+ function resolveWorktreePath(surface, worktreeDir) {
4584
+ return verifyCodeSurface(surface, worktreeDir).path;
4585
+ }
4586
+
4587
+ export {
4588
+ pairArms,
4589
+ comparePairedArms,
4590
+ completionVerdict,
4591
+ verifyCompletion,
4592
+ parseCorrectnessResponse,
4593
+ createLlmCorrectnessChecker,
4594
+ createTokenRecallChecker,
4595
+ extractProducedState,
4596
+ CODING_HARNESSES,
4597
+ HARNESS_NATIVE_MODEL,
4598
+ expandProfileAxes,
4599
+ harnessAxisOf,
4600
+ agentProfileId,
4601
+ agentProfileModelId,
4602
+ agentProfileHash,
4603
+ buildAnalystSurfaceDispatch,
4604
+ failureModeRecallJudge,
4605
+ analyzeCrossSurfaceInteractions,
4606
+ discoverEvalFixtures,
4607
+ loadEvalFixture,
4608
+ loadEvalFixtureScenarios,
4609
+ planEvalFixtureRun,
4610
+ neutralizationGate,
4611
+ sequentialPairedGate,
4612
+ sequentialDecide,
4613
+ rolloutArgumentDiff,
4614
+ classifyUngroundedLiterals,
4615
+ LabeledScenarioStoreError,
4616
+ FsLabeledScenarioStore,
4617
+ neutralizeText,
4618
+ makePlaybackDispatch,
4619
+ scoreUserStory,
4620
+ userStoryScoreboard,
4621
+ scoreboardSummary,
4622
+ renderScoreboardMarkdown,
4623
+ ProfileMatrixError,
4624
+ runProfileMatrix,
4625
+ scoreDiscrimination,
4626
+ selectDiscriminative,
4627
+ SEARCH_LEDGER_SCHEMA,
4628
+ validateSearchLedgerEvent,
4629
+ openSearchLedger,
4630
+ FileSearchLedger,
4631
+ isTransientTransportFailure,
4632
+ WorktreeAdapterError,
4633
+ gitWorktreeAdapter,
4634
+ verifyCodeSurface,
4635
+ resolveWorktreePath
4636
+ };
4637
+ //# sourceMappingURL=chunk-NGUYT5CI.js.map