@hraness/direct 0.7.5

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +436 -0
  3. package/dist/core/index.js +162 -0
  4. package/dist/index-1csg00w4.js +1167 -0
  5. package/dist/index-6mdfd2ey.js +464 -0
  6. package/dist/index-7n1h75n6.js +616 -0
  7. package/dist/index.js +232 -0
  8. package/dist/react.js +32 -0
  9. package/dist/testing/index.js +1069 -0
  10. package/dist/tooling/bombadil.js +2117 -0
  11. package/dist/tooling/browser-verification-entry.js +1499 -0
  12. package/dist/tooling/bundle-boundary.js +119 -0
  13. package/dist/web.js +605 -0
  14. package/package.json +179 -0
  15. package/skills/direct/AGENTS.md +13 -0
  16. package/skills/direct/SKILL.md +49 -0
  17. package/skills/direct/agents/openai.yaml +4 -0
  18. package/skills/direct/references/adoption.md +131 -0
  19. package/skills/direct/references/install.md +91 -0
  20. package/skills/direct/references/verification.md +247 -0
  21. package/src/core/coverage.ts +336 -0
  22. package/src/core/definition.ts +378 -0
  23. package/src/core/effects.ts +88 -0
  24. package/src/core/fixture.ts +185 -0
  25. package/src/core/ids.ts +77 -0
  26. package/src/core/index.ts +13 -0
  27. package/src/core/json-value.ts +7 -0
  28. package/src/core/json.ts +593 -0
  29. package/src/core/query.ts +230 -0
  30. package/src/core/reason.ts +16 -0
  31. package/src/core/resource.ts +10 -0
  32. package/src/core/result.ts +19 -0
  33. package/src/core/runtime.ts +229 -0
  34. package/src/core/scenario.ts +149 -0
  35. package/src/core/store.ts +784 -0
  36. package/src/index.ts +51 -0
  37. package/src/react.ts +54 -0
  38. package/src/testing/activity.ts +228 -0
  39. package/src/testing/coverage-binding.ts +99 -0
  40. package/src/testing/evidence.ts +59 -0
  41. package/src/testing/index.ts +22 -0
  42. package/src/testing/manifest.ts +559 -0
  43. package/src/testing/probe.ts +446 -0
  44. package/src/testing/scripted-transport.ts +775 -0
  45. package/src/testing/session.ts +525 -0
  46. package/src/tooling/bombadil-campaign.ts +288 -0
  47. package/src/tooling/bombadil-internal.d.ts +46 -0
  48. package/src/tooling/bombadil-runner.ts +1424 -0
  49. package/src/tooling/bombadil.ts +27 -0
  50. package/src/tooling/browser-verification-entry.ts +32 -0
  51. package/src/tooling/browser-verification.ts +916 -0
  52. package/src/tooling/bundle-boundary.ts +159 -0
  53. package/src/web/browser-bridge.ts +296 -0
  54. package/src/web/browser.ts +277 -0
  55. package/src/web/fetch-firewall.ts +251 -0
  56. package/src/web.ts +27 -0
@@ -0,0 +1,2117 @@
1
+ // @bun
2
+ // src/tooling/bombadil-runner.ts
3
+ import { createReadStream } from "fs";
4
+ import { readFile, realpath, stat, writeFile as writeFile2 } from "fs/promises";
5
+ import { isAbsolute, join as join2, relative, resolve } from "path";
6
+ import process2 from "process";
7
+ import { createInterface } from "readline";
8
+
9
+ // src/core/result.ts
10
+ function ok(value) {
11
+ return { ok: true, value };
12
+ }
13
+ function err(error) {
14
+ return { ok: false, error };
15
+ }
16
+ function isRecord(value) {
17
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18
+ }
19
+
20
+ // src/core/ids.ts
21
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[._/-][a-z0-9]+)*$/u;
22
+ var MAX_IDENTIFIER_LENGTH = 120;
23
+ function parseIdentifier(input, kind) {
24
+ if (typeof input !== "string" || input.length === 0 || input.length > MAX_IDENTIFIER_LENGTH || !IDENTIFIER_PATTERN.test(input)) {
25
+ return err({
26
+ code: "invalid-identifier",
27
+ kind,
28
+ value: input,
29
+ message: `${kind} identifiers must be 1-${MAX_IDENTIFIER_LENGTH} lowercase ASCII characters with separated alphanumeric segments`
30
+ });
31
+ }
32
+ return ok(input);
33
+ }
34
+ function parseScenarioId(input) {
35
+ const parsed = parseIdentifier(input, "scenario");
36
+ return parsed.ok ? ok(parsed.value) : parsed;
37
+ }
38
+ function parseCoverageKey(input) {
39
+ const parsed = parseIdentifier(input, "coverage");
40
+ return parsed.ok ? ok(parsed.value) : parsed;
41
+ }
42
+
43
+ // src/core/reason.ts
44
+ function renderUnknownReason(reason, fallback = "Unknown failure") {
45
+ try {
46
+ if (typeof reason === "object" && reason !== null || typeof reason === "function") {
47
+ const message = Reflect.get(reason, "message");
48
+ if (typeof message === "string")
49
+ return message;
50
+ }
51
+ } catch {}
52
+ try {
53
+ return String(reason);
54
+ } catch {
55
+ return fallback;
56
+ }
57
+ }
58
+
59
+ // src/core/json.ts
60
+ var DEFAULT_JSON_LIMITS = Object.freeze({
61
+ maxDepth: 64,
62
+ maxNodes: 1e5,
63
+ maxStringBytes: 1048576
64
+ });
65
+ var PARSED_JSON_OPTIONS = Object.freeze({
66
+ freeze: false,
67
+ normalizeNegativeZero: false,
68
+ objectPrototype: "null",
69
+ sortObjectKeys: false
70
+ });
71
+ var CLONED_JSON_OPTIONS = Object.freeze({
72
+ freeze: false,
73
+ normalizeNegativeZero: true,
74
+ objectPrototype: "ordinary",
75
+ sortObjectKeys: true
76
+ });
77
+ var FROZEN_CLONED_JSON_OPTIONS = Object.freeze({
78
+ freeze: true,
79
+ normalizeNegativeZero: true,
80
+ objectPrototype: "ordinary",
81
+ sortObjectKeys: true
82
+ });
83
+ function jsonError(code, path, message) {
84
+ return { code, path, message };
85
+ }
86
+ function utf8ByteLength(value) {
87
+ let bytes = 0;
88
+ for (let index = 0;index < value.length; index += 1) {
89
+ const code = value.charCodeAt(index);
90
+ if (code <= 127) {
91
+ bytes += 1;
92
+ } else if (code <= 2047) {
93
+ bytes += 2;
94
+ } else if (code >= 55296 && code <= 56319 && index + 1 < value.length) {
95
+ const next = value.charCodeAt(index + 1);
96
+ if (next >= 56320 && next <= 57343) {
97
+ bytes += 4;
98
+ index += 1;
99
+ } else {
100
+ bytes += 3;
101
+ }
102
+ } else {
103
+ bytes += 3;
104
+ }
105
+ }
106
+ return bytes;
107
+ }
108
+ function parseJsonAt(input, path, depth, limits, budget, ancestors, options) {
109
+ budget.nodes += 1;
110
+ if (budget.nodes > limits.maxNodes) {
111
+ return err(jsonError("node-limit-exceeded", path, `JSON value exceeds ${limits.maxNodes} nodes`));
112
+ }
113
+ if (depth > limits.maxDepth) {
114
+ return err(jsonError("depth-exceeded", path, `JSON value exceeds depth ${limits.maxDepth}`));
115
+ }
116
+ if (input === null || typeof input === "boolean") {
117
+ return ok(input);
118
+ }
119
+ if (typeof input === "string") {
120
+ budget.stringBytes += utf8ByteLength(input);
121
+ if (budget.stringBytes > limits.maxStringBytes) {
122
+ return err(jsonError("string-limit-exceeded", path, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
123
+ }
124
+ return ok(input);
125
+ }
126
+ if (typeof input === "number") {
127
+ return Number.isFinite(input) ? ok(options.normalizeNegativeZero && Object.is(input, -0) ? 0 : input) : err(jsonError("invalid-number", path, "JSON numbers must be finite"));
128
+ }
129
+ if (typeof input !== "object") {
130
+ return err(jsonError("invalid-type", path, `${typeof input} is not a JSON value`));
131
+ }
132
+ if (ancestors.has(input)) {
133
+ return err(jsonError("cycle", path, "JSON values cannot contain cycles"));
134
+ }
135
+ const nextAncestors = new Set(ancestors);
136
+ nextAncestors.add(input);
137
+ if (Array.isArray(input)) {
138
+ if (Object.getPrototypeOf(input) !== Array.prototype) {
139
+ return err(jsonError("invalid-object", path, "JSON arrays must have the standard Array prototype"));
140
+ }
141
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(input, "length");
142
+ if (lengthDescriptor === undefined || lengthDescriptor.get !== undefined || lengthDescriptor.set !== undefined || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) {
143
+ return err(jsonError("invalid-object", path, "JSON arrays must have a valid data length"));
144
+ }
145
+ const length = lengthDescriptor.value;
146
+ for (const key of Reflect.ownKeys(input)) {
147
+ if (typeof key === "symbol") {
148
+ return err(jsonError("symbol-key", path, "JSON arrays cannot have symbol keys"));
149
+ }
150
+ if (key === "length")
151
+ continue;
152
+ const index = Number(key);
153
+ if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== key) {
154
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON arrays cannot have extra properties"));
155
+ }
156
+ }
157
+ const output2 = [];
158
+ for (let index = 0;index < length; index += 1) {
159
+ const descriptor = Object.getOwnPropertyDescriptor(input, index);
160
+ if (descriptor === undefined) {
161
+ return err(jsonError("invalid-object", `${path}[${index}]`, "Sparse arrays are not exact JSON values"));
162
+ }
163
+ if (descriptor.get !== undefined || descriptor.set !== undefined) {
164
+ return err(jsonError("accessor-property", `${path}[${index}]`, "JSON arrays must use data elements"));
165
+ }
166
+ if (!descriptor.enumerable) {
167
+ return err(jsonError("invalid-object", `${path}[${index}]`, "JSON array elements must be enumerable"));
168
+ }
169
+ const item = parseJsonAt(descriptor.value, `${path}[${index}]`, depth + 1, limits, budget, nextAncestors, options);
170
+ if (!item.ok) {
171
+ return item;
172
+ }
173
+ output2.push(item.value);
174
+ }
175
+ return ok(options.freeze ? Object.freeze(output2) : output2);
176
+ }
177
+ const prototype = Object.getPrototypeOf(input);
178
+ if (prototype !== Object.prototype && prototype !== null) {
179
+ return err(jsonError("invalid-object", path, "JSON objects must have Object or null prototypes"));
180
+ }
181
+ const output = options.objectPrototype === "ordinary" ? {} : Object.create(null);
182
+ const entries = options.sortObjectKeys ? [] : null;
183
+ for (const key of Reflect.ownKeys(input)) {
184
+ if (typeof key === "symbol") {
185
+ return err(jsonError("symbol-key", path, "JSON objects cannot have symbol keys"));
186
+ }
187
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
188
+ if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined) {
189
+ return err(jsonError("accessor-property", `${path}.${key}`, "JSON objects must use data properties"));
190
+ }
191
+ if (!descriptor.enumerable) {
192
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON object properties must be enumerable"));
193
+ }
194
+ budget.stringBytes += utf8ByteLength(key);
195
+ if (budget.stringBytes > limits.maxStringBytes) {
196
+ return err(jsonError("string-limit-exceeded", `${path}.${key}`, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
197
+ }
198
+ const child = parseJsonAt(descriptor.value, `${path}.${key}`, depth + 1, limits, budget, nextAncestors, options);
199
+ if (!child.ok) {
200
+ return child;
201
+ }
202
+ if (entries === null) {
203
+ output[key] = child.value;
204
+ } else {
205
+ entries.push([key, child.value]);
206
+ }
207
+ }
208
+ if (entries !== null) {
209
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
210
+ for (const [key, value] of entries) {
211
+ Object.defineProperty(output, key, {
212
+ configurable: true,
213
+ enumerable: true,
214
+ value,
215
+ writable: true
216
+ });
217
+ }
218
+ }
219
+ return ok(options.freeze ? Object.freeze(output) : output);
220
+ }
221
+ function validateAndCloneJson(input, limits, options) {
222
+ if (!Number.isSafeInteger(limits.maxDepth) || limits.maxDepth < 0 || !Number.isSafeInteger(limits.maxNodes) || limits.maxNodes < 1 || !Number.isSafeInteger(limits.maxStringBytes) || limits.maxStringBytes < 0) {
223
+ throw new Error("JSON limits must be non-negative safe integers and allow at least one node");
224
+ }
225
+ try {
226
+ return parseJsonAt(input, "$", 0, limits, { nodes: 0, stringBytes: 0 }, new Set, options);
227
+ } catch (reason) {
228
+ return err(jsonError("invalid-object", "$", renderUnknownReason(reason, "JSON object inspection failed")));
229
+ }
230
+ }
231
+ function parseJsonValue(input, limits = DEFAULT_JSON_LIMITS) {
232
+ return validateAndCloneJson(input, limits, PARSED_JSON_OPTIONS);
233
+ }
234
+ function canonicalize(value) {
235
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
236
+ return JSON.stringify(value);
237
+ }
238
+ if (Array.isArray(value)) {
239
+ return `[${value.map(canonicalize).join(",")}]`;
240
+ }
241
+ const entries = Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => `${JSON.stringify(key)}:${canonicalize(child)}`);
242
+ return `{${entries.join(",")}}`;
243
+ }
244
+ function canonicalJson(input, limits = DEFAULT_JSON_LIMITS) {
245
+ const parsed = parseJsonValue(input, limits);
246
+ return parsed.ok ? ok(canonicalize(parsed.value)) : parsed;
247
+ }
248
+ function freezeJson(value) {
249
+ if (value !== null && typeof value === "object") {
250
+ for (const child of Array.isArray(value) ? value : Object.values(value)) {
251
+ freezeJson(child);
252
+ }
253
+ Object.freeze(value);
254
+ }
255
+ return value;
256
+ }
257
+ var STABLE_HASH_ALGORITHM = "fnv1a-64";
258
+ var TAGGED_STABLE_HASH_PATTERN = /^fnv1a-64:[0-9a-f]{16}$/u;
259
+ function tagStableHash(hash) {
260
+ return `${hash.algorithm}:${hash.value}`;
261
+ }
262
+ function parseTaggedStableHash(input) {
263
+ return typeof input === "string" && TAGGED_STABLE_HASH_PATTERN.test(input) ? ok(input) : err({
264
+ code: "invalid-stable-hash",
265
+ message: `Stable hashes must use ${STABLE_HASH_ALGORITHM} with 16 lowercase hexadecimal digits`
266
+ });
267
+ }
268
+ function updateFnvByte(hash, byte) {
269
+ return BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
270
+ }
271
+ function stableHash(input, limits = DEFAULT_JSON_LIMITS) {
272
+ const serialized = canonicalJson(input, limits);
273
+ if (!serialized.ok) {
274
+ return serialized;
275
+ }
276
+ let hash = 0xcbf29ce484222325n;
277
+ for (let index = 0;index < serialized.value.length; index += 1) {
278
+ const code = serialized.value.charCodeAt(index);
279
+ if (code <= 127) {
280
+ hash = updateFnvByte(hash, code);
281
+ } else if (code <= 2047) {
282
+ hash = updateFnvByte(hash, 192 | code >> 6);
283
+ hash = updateFnvByte(hash, 128 | code & 63);
284
+ } else if (code >= 55296 && code <= 56319 && index + 1 < serialized.value.length) {
285
+ const next = serialized.value.charCodeAt(index + 1);
286
+ if (next >= 56320 && next <= 57343) {
287
+ const point = 65536 + (code - 55296 << 10) + (next - 56320);
288
+ hash = updateFnvByte(hash, 240 | point >> 18);
289
+ hash = updateFnvByte(hash, 128 | point >> 12 & 63);
290
+ hash = updateFnvByte(hash, 128 | point >> 6 & 63);
291
+ hash = updateFnvByte(hash, 128 | point & 63);
292
+ index += 1;
293
+ } else {
294
+ hash = updateFnvByte(hash, 239);
295
+ hash = updateFnvByte(hash, 191);
296
+ hash = updateFnvByte(hash, 189);
297
+ }
298
+ } else {
299
+ hash = updateFnvByte(hash, 224 | code >> 12);
300
+ hash = updateFnvByte(hash, 128 | code >> 6 & 63);
301
+ hash = updateFnvByte(hash, 128 | code & 63);
302
+ }
303
+ }
304
+ return ok({
305
+ algorithm: STABLE_HASH_ALGORITHM,
306
+ value: hash.toString(16).padStart(16, "0")
307
+ });
308
+ }
309
+
310
+ // src/core/coverage.ts
311
+ var DIRECT_COVERAGE_SCHEMA = "direct.coverage/v2";
312
+ var MAX_DIRECT_COVERAGE_ENTRIES = 256;
313
+ var DIRECT_COVERAGE_JSON_LIMITS = Object.freeze({
314
+ ...DEFAULT_JSON_LIMITS,
315
+ maxStringBytes: 16777216
316
+ });
317
+ var EMPTY_COVERAGE_CATALOG_SNAPSHOT = Object.freeze({
318
+ schema: DIRECT_COVERAGE_SCHEMA,
319
+ entries: Object.freeze([])
320
+ });
321
+ function coverageError(code, message, keys = []) {
322
+ return { code, message, keys };
323
+ }
324
+ function hasControlCharacters(value) {
325
+ for (const character of value) {
326
+ const code = character.charCodeAt(0);
327
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) {
328
+ return true;
329
+ }
330
+ }
331
+ return false;
332
+ }
333
+ var COVERAGE_ENTRY_KEYS = new Set(["key", "mode", "claim", "scenarios"]);
334
+ var COVERAGE_SNAPSHOT_KEYS = new Set(["schema", "entries"]);
335
+ function isStringArray(value) {
336
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
337
+ }
338
+ function createCoverageCatalogSnapshot(catalog) {
339
+ return Object.freeze({
340
+ schema: DIRECT_COVERAGE_SCHEMA,
341
+ entries: catalog.list()
342
+ });
343
+ }
344
+ function parseCoverageCatalogSnapshot(input, limits = DIRECT_COVERAGE_JSON_LIMITS) {
345
+ const parsed = parseJsonValue(input, limits);
346
+ if (!parsed.ok || !isRecord(parsed.value)) {
347
+ return err(coverageError("invalid-coverage", parsed.ok ? "Coverage snapshot must be an object" : parsed.error.message));
348
+ }
349
+ for (const key of Object.keys(parsed.value)) {
350
+ if (!COVERAGE_SNAPSHOT_KEYS.has(key)) {
351
+ return err(coverageError("invalid-coverage", `Unknown coverage snapshot key: ${key}`));
352
+ }
353
+ }
354
+ if (parsed.value.schema !== DIRECT_COVERAGE_SCHEMA) {
355
+ return err(coverageError("invalid-coverage", `Coverage snapshot schema must be ${DIRECT_COVERAGE_SCHEMA}`));
356
+ }
357
+ if (!Array.isArray(parsed.value.entries)) {
358
+ return err(coverageError("invalid-coverage", "Coverage snapshot entries must be an array"));
359
+ }
360
+ const entries = [];
361
+ for (const [index, candidate] of parsed.value.entries.entries()) {
362
+ if (!isRecord(candidate)) {
363
+ return err(coverageError("invalid-coverage", `Coverage entry ${String(index)} must be an object`));
364
+ }
365
+ for (const key of Object.keys(candidate)) {
366
+ if (!COVERAGE_ENTRY_KEYS.has(key)) {
367
+ return err(coverageError("invalid-coverage", `Unknown coverage entry key at ${String(index)}: ${key}`));
368
+ }
369
+ }
370
+ if (typeof candidate.key !== "string" || typeof candidate.claim !== "string" || candidate.mode !== "fixture" && candidate.mode !== "mixed" && candidate.mode !== "direct" || !isStringArray(candidate.scenarios)) {
371
+ return err(coverageError("invalid-coverage", `Coverage entry ${String(index)} has an invalid wire shape`));
372
+ }
373
+ if (candidate.mode === "direct") {
374
+ if (candidate.scenarios.length > 0) {
375
+ return err(coverageError("invalid-mode", `Direct coverage ${candidate.key} cannot cite fixture scenarios`, [candidate.key]));
376
+ }
377
+ entries.push({
378
+ key: candidate.key,
379
+ mode: candidate.mode,
380
+ claim: candidate.claim,
381
+ scenarios: []
382
+ });
383
+ } else {
384
+ const firstScenario = candidate.scenarios[0];
385
+ if (typeof firstScenario !== "string") {
386
+ return err(coverageError("invalid-mode", `${candidate.mode} coverage ${candidate.key} must cite at least one scenario`, [candidate.key]));
387
+ }
388
+ entries.push({
389
+ key: candidate.key,
390
+ mode: candidate.mode,
391
+ claim: candidate.claim,
392
+ scenarios: [firstScenario, ...candidate.scenarios.slice(1)]
393
+ });
394
+ }
395
+ }
396
+ const catalog = createCoverageCatalog(entries);
397
+ return catalog.ok ? ok(createCoverageCatalogSnapshot(catalog.value)) : catalog;
398
+ }
399
+ function createCoverageCatalog(inputs, scenarios) {
400
+ if (inputs.length > MAX_DIRECT_COVERAGE_ENTRIES) {
401
+ return err(coverageError("too-many-coverage-entries", `Direct definitions support at most ${String(MAX_DIRECT_COVERAGE_ENTRIES)} coverage entries`));
402
+ }
403
+ const entries = [];
404
+ const byKey = new Map;
405
+ for (const input of inputs) {
406
+ const key = parseCoverageKey(input.key);
407
+ if (!key.ok) {
408
+ return err(coverageError("invalid-coverage", key.error.message, [String(input.key)]));
409
+ }
410
+ if (byKey.has(key.value)) {
411
+ return err(coverageError("duplicate-coverage", `Duplicate coverage key: ${key.value}`, [key.value]));
412
+ }
413
+ if (input.claim.trim().length === 0 || input.claim.length > 1000 || hasControlCharacters(input.claim)) {
414
+ return err(coverageError("invalid-claim", `Coverage ${key.value} needs a 1-1000 character claim`, [key.value]));
415
+ }
416
+ if (input.mode !== "fixture" && input.mode !== "mixed" && input.mode !== "direct") {
417
+ return err(coverageError("invalid-mode", `Coverage ${key.value} has an unknown proof mode`, [key.value]));
418
+ }
419
+ if (input.mode === "direct" && input.scenarios.length > 0) {
420
+ return err(coverageError("invalid-mode", `Direct coverage ${key.value} cannot cite fixture scenarios`, [key.value]));
421
+ }
422
+ if (input.mode !== "direct" && input.scenarios.length === 0) {
423
+ return err(coverageError("invalid-mode", `${input.mode} coverage ${key.value} must cite at least one scenario`, [key.value]));
424
+ }
425
+ const scenarioIds = [];
426
+ const seenScenarios = new Set;
427
+ for (const candidate of input.scenarios) {
428
+ const id = parseScenarioId(candidate);
429
+ if (!id.ok) {
430
+ return err(coverageError("invalid-scenario", id.error.message, [String(candidate)]));
431
+ }
432
+ if (seenScenarios.has(id.value)) {
433
+ return err(coverageError("invalid-scenario", `Coverage ${key.value} repeats scenario ${id.value}`, [id.value]));
434
+ }
435
+ if (scenarios !== undefined && scenarios.get(id.value) === undefined) {
436
+ return err(coverageError("unknown-scenario", `Coverage ${key.value} cites unknown scenario ${id.value}`, [id.value]));
437
+ }
438
+ seenScenarios.add(id.value);
439
+ scenarioIds.push(id.value);
440
+ }
441
+ let entry;
442
+ if (input.mode === "direct") {
443
+ const scenarios2 = Object.freeze([]);
444
+ entry = Object.freeze({
445
+ key: key.value,
446
+ mode: input.mode,
447
+ claim: input.claim,
448
+ scenarios: scenarios2
449
+ });
450
+ } else {
451
+ const firstScenarioId = scenarioIds[0];
452
+ if (firstScenarioId === undefined) {
453
+ return err(coverageError("invalid-mode", `${input.mode} coverage ${key.value} must cite at least one scenario`, [key.value]));
454
+ }
455
+ const scenarios2 = Object.freeze([
456
+ firstScenarioId,
457
+ ...scenarioIds.slice(1)
458
+ ]);
459
+ entry = Object.freeze({
460
+ key: key.value,
461
+ mode: input.mode,
462
+ claim: input.claim,
463
+ scenarios: scenarios2
464
+ });
465
+ }
466
+ entries.push(entry);
467
+ byKey.set(key.value, entry);
468
+ }
469
+ const frozenEntries = Object.freeze(entries);
470
+ const keys = Object.freeze(frozenEntries.map((entry) => entry.key));
471
+ const catalog = {
472
+ size: frozenEntries.length,
473
+ keys: () => keys,
474
+ list: () => frozenEntries,
475
+ get: (key) => byKey.get(key),
476
+ resolve: (input) => {
477
+ const key = parseCoverageKey(input);
478
+ if (!key.ok) {
479
+ return err(coverageError("invalid-coverage", key.error.message, [String(input)]));
480
+ }
481
+ const entry = byKey.get(key.value);
482
+ return entry === undefined ? err(coverageError("unknown-coverage", `Unknown coverage key: ${key.value}`, [key.value])) : ok(entry);
483
+ },
484
+ requireExactKeys: (expected) => {
485
+ const expectedKeys = [];
486
+ const seen = new Set;
487
+ for (const candidate of expected) {
488
+ const parsed = parseCoverageKey(candidate);
489
+ if (!parsed.ok) {
490
+ return err(coverageError("invalid-coverage", parsed.error.message, [String(candidate)]));
491
+ }
492
+ if (seen.has(parsed.value)) {
493
+ return err(coverageError("duplicate-expected-key", `Expected coverage repeats ${parsed.value}`, [parsed.value]));
494
+ }
495
+ seen.add(parsed.value);
496
+ expectedKeys.push(parsed.value);
497
+ }
498
+ const missing = expectedKeys.filter((key) => !byKey.has(key));
499
+ if (missing.length > 0) {
500
+ return err(coverageError("missing-coverage", `Missing coverage keys: ${missing.join(", ")}`, missing));
501
+ }
502
+ const unexpected = keys.filter((key) => !seen.has(key));
503
+ if (unexpected.length > 0) {
504
+ return err(coverageError("unexpected-coverage", `Unexpected coverage keys: ${unexpected.join(", ")}`, unexpected));
505
+ }
506
+ return ok(true);
507
+ }
508
+ };
509
+ return ok(Object.freeze(catalog));
510
+ }
511
+ // src/core/fixture.ts
512
+ var DEFAULT_MAX_FIXTURE_BYTES = 65536;
513
+ var FIXTURE_KEYS = new Set(["schema", "scenario", "route", "world", "runtime"]);
514
+
515
+ // src/core/query.ts
516
+ var SCENARIO_QUERY_KEY = "__direct_scenario";
517
+ var FIXTURE_QUERY_KEY = "__direct_fixture";
518
+ var FIXTURE_QUERY_PREFIX_BYTES = utf8ByteLength(`?${FIXTURE_QUERY_KEY}=`);
519
+ function maximumFixtureQueryBytes(maxFixtureBytes) {
520
+ return maxFixtureBytes * 3 + FIXTURE_QUERY_PREFIX_BYTES;
521
+ }
522
+ var DEFAULT_MAX_QUERY_BYTES = maximumFixtureQueryBytes(DEFAULT_MAX_FIXTURE_BYTES);
523
+
524
+ // src/core/scenario.ts
525
+ var MAX_DIRECT_SCENARIOS = 256;
526
+
527
+ // src/testing/manifest.ts
528
+ var DIRECT_SESSION_MANIFEST_SCHEMA = "direct.session-manifest/v1";
529
+ var DIRECT_CATALOG_HASH_ALGORITHM = STABLE_HASH_ALGORITHM;
530
+ var DIRECT_SESSION_MANIFEST_JSON_LIMITS = Object.freeze({
531
+ maxDepth: 64,
532
+ maxNodes: 1e5,
533
+ maxStringBytes: 16777216
534
+ });
535
+ var MANIFEST_KEYS = new Set([
536
+ "active",
537
+ "catalogHash",
538
+ "coverage",
539
+ "defaultScenario",
540
+ "queries",
541
+ "scenarios",
542
+ "schema"
543
+ ]);
544
+ var QUERY_KEYS = new Set(["fixture", "scenario"]);
545
+ var ACTIVE_KEYS = new Set([
546
+ "activationHash",
547
+ "route",
548
+ "scenario",
549
+ "selectionHash",
550
+ "source"
551
+ ]);
552
+ var SCENARIO_KEYS = new Set([
553
+ "description",
554
+ "id",
555
+ "route",
556
+ "title"
557
+ ]);
558
+ function manifestError(code, message) {
559
+ return Object.freeze({ code, message });
560
+ }
561
+ function exactKeys(input, expected, label) {
562
+ for (const key of Object.keys(input)) {
563
+ if (!expected.has(key))
564
+ throw new Error(`Unknown ${label} key: ${key}`);
565
+ }
566
+ for (const key of expected) {
567
+ if (!Object.hasOwn(input, key))
568
+ throw new Error(`Missing ${label} key: ${key}`);
569
+ }
570
+ }
571
+ function hasControlCharacters2(value) {
572
+ for (const character of value) {
573
+ const code = character.charCodeAt(0);
574
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) {
575
+ return true;
576
+ }
577
+ }
578
+ return false;
579
+ }
580
+ function validText(value, maximum) {
581
+ return value.trim().length > 0 && value.length <= maximum && !hasControlCharacters2(value);
582
+ }
583
+ function validRoute(value) {
584
+ if (value.trim().length === 0 || value.length > 256)
585
+ return false;
586
+ for (const character of value) {
587
+ const code = character.charCodeAt(0);
588
+ if (code < 32 || code === 127)
589
+ return false;
590
+ }
591
+ return true;
592
+ }
593
+ function parseTaggedHash(value, label) {
594
+ const parsed = parseTaggedStableHash(value);
595
+ if (!parsed.ok)
596
+ throw new Error(`${label}: ${parsed.error.message}`);
597
+ return parsed.value;
598
+ }
599
+ function selectionHash(payload) {
600
+ const hashed = stableHash(payload, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
601
+ if (!hashed.ok) {
602
+ return err(manifestError("invalid-manifest", hashed.error.message));
603
+ }
604
+ return ok(tagStableHash(hashed.value));
605
+ }
606
+ function catalogHash(payload) {
607
+ const hashed = stableHash(payload, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
608
+ if (!hashed.ok) {
609
+ return err(manifestError("invalid-manifest", hashed.error.message));
610
+ }
611
+ return ok(tagStableHash(hashed.value));
612
+ }
613
+ function parseManifestUnchecked(input) {
614
+ const parsedJson = parseJsonValue(input, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
615
+ if (!parsedJson.ok || !isRecord(parsedJson.value)) {
616
+ return err(manifestError("invalid-manifest", parsedJson.ok ? "Direct session manifest must be an object" : parsedJson.error.message));
617
+ }
618
+ const candidate = parsedJson.value;
619
+ exactKeys(candidate, MANIFEST_KEYS, "Direct session manifest");
620
+ if (candidate.schema !== DIRECT_SESSION_MANIFEST_SCHEMA) {
621
+ throw new Error(`Direct session manifest schema must be ${DIRECT_SESSION_MANIFEST_SCHEMA}`);
622
+ }
623
+ if (!isRecord(candidate.queries)) {
624
+ throw new Error("Direct session manifest queries must be an object");
625
+ }
626
+ exactKeys(candidate.queries, QUERY_KEYS, "Direct session manifest queries");
627
+ if (candidate.queries.scenario !== SCENARIO_QUERY_KEY || candidate.queries.fixture !== FIXTURE_QUERY_KEY) {
628
+ throw new Error("Direct session manifest query keys do not match Direct");
629
+ }
630
+ const queries = Object.freeze({
631
+ scenario: SCENARIO_QUERY_KEY,
632
+ fixture: FIXTURE_QUERY_KEY
633
+ });
634
+ const defaultScenario = parseScenarioId(candidate.defaultScenario);
635
+ if (!defaultScenario.ok) {
636
+ throw new Error(`Invalid default scenario: ${defaultScenario.error.message}`);
637
+ }
638
+ if (!Array.isArray(candidate.scenarios)) {
639
+ throw new Error("Direct session manifest scenarios must be an array");
640
+ }
641
+ if (candidate.scenarios.length > MAX_DIRECT_SCENARIOS) {
642
+ throw new Error(`Direct session manifests support at most ${String(MAX_DIRECT_SCENARIOS)} scenarios`);
643
+ }
644
+ const scenarios = [];
645
+ const byId = new Map;
646
+ for (const [index, rawScenario] of candidate.scenarios.entries()) {
647
+ if (!isRecord(rawScenario)) {
648
+ throw new Error(`Direct session manifest scenario ${String(index)} must be an object`);
649
+ }
650
+ exactKeys(rawScenario, SCENARIO_KEYS, `Direct session manifest scenario ${String(index)}`);
651
+ const id = parseScenarioId(rawScenario.id);
652
+ if (!id.ok) {
653
+ throw new Error(`Invalid Direct session manifest scenario ${String(index)}: ${id.error.message}`);
654
+ }
655
+ if (byId.has(id.value)) {
656
+ return err(manifestError("duplicate-scenario", `Duplicate Direct session manifest scenario: ${id.value}`));
657
+ }
658
+ if (typeof rawScenario.title !== "string" || !validText(rawScenario.title, 160)) {
659
+ throw new Error(`Direct session manifest scenario ${id.value} title must contain 1-160 visible characters`);
660
+ }
661
+ if (rawScenario.description !== null && (typeof rawScenario.description !== "string" || !validText(rawScenario.description, 2000))) {
662
+ throw new Error(`Direct session manifest scenario ${id.value} description must be null or contain 1-2000 visible characters`);
663
+ }
664
+ if (typeof rawScenario.route !== "string" || !validRoute(rawScenario.route)) {
665
+ throw new Error(`Direct session manifest scenario ${id.value} route must contain 1-256 visible characters`);
666
+ }
667
+ const scenario = Object.freeze({
668
+ id: id.value,
669
+ title: rawScenario.title,
670
+ description: rawScenario.description,
671
+ route: rawScenario.route
672
+ });
673
+ scenarios.push(scenario);
674
+ byId.set(id.value, scenario);
675
+ }
676
+ const frozenScenarios = Object.freeze(scenarios);
677
+ if (!byId.has(defaultScenario.value)) {
678
+ return err(manifestError("unknown-scenario", `Direct session manifest default scenario is missing: ${defaultScenario.value}`));
679
+ }
680
+ if (!isRecord(candidate.active)) {
681
+ throw new Error("Direct session manifest active selection must be an object");
682
+ }
683
+ exactKeys(candidate.active, ACTIVE_KEYS, "Direct session manifest active selection");
684
+ if (candidate.active.source !== "scenario" && candidate.active.source !== "fixture") {
685
+ throw new Error("Direct session manifest active source must be scenario or fixture");
686
+ }
687
+ const activeScenario = parseScenarioId(candidate.active.scenario);
688
+ if (!activeScenario.ok) {
689
+ throw new Error(`Invalid active scenario: ${activeScenario.error.message}`);
690
+ }
691
+ const activeDefinition = byId.get(activeScenario.value);
692
+ if (activeDefinition === undefined) {
693
+ return err(manifestError("unknown-scenario", `Direct session manifest active scenario is missing: ${activeScenario.value}`));
694
+ }
695
+ if (typeof candidate.active.route !== "string" || !validRoute(candidate.active.route)) {
696
+ throw new Error("Direct session manifest active route is invalid");
697
+ }
698
+ if (candidate.active.route !== activeDefinition.route) {
699
+ return err(manifestError("route-mismatch", `Direct session manifest active route does not match scenario ${activeScenario.value}`));
700
+ }
701
+ const activationHash = parseTaggedHash(candidate.active.activationHash, "Direct session manifest activationHash");
702
+ let suppliedSelectionHash;
703
+ try {
704
+ suppliedSelectionHash = parseTaggedHash(candidate.active.selectionHash, "Direct session manifest selectionHash");
705
+ } catch (reason) {
706
+ return err(manifestError("invalid-selection-hash", renderUnknownReason(reason, "Direct session manifest selectionHash is invalid")));
707
+ }
708
+ const expectedSelectionHash = selectionHash({
709
+ source: candidate.active.source,
710
+ scenario: activeScenario.value,
711
+ route: activeDefinition.route,
712
+ activationHash
713
+ });
714
+ if (!expectedSelectionHash.ok)
715
+ return expectedSelectionHash;
716
+ if (suppliedSelectionHash !== expectedSelectionHash.value) {
717
+ return err(manifestError("selection-hash-mismatch", "Direct session manifest selectionHash does not match its active selection"));
718
+ }
719
+ const active = Object.freeze({
720
+ source: candidate.active.source,
721
+ scenario: activeScenario.value,
722
+ route: activeDefinition.route,
723
+ activationHash,
724
+ selectionHash: expectedSelectionHash.value
725
+ });
726
+ const coverage = parseCoverageCatalogSnapshot(candidate.coverage, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
727
+ if (!coverage.ok) {
728
+ throw new Error(coverage.error.message);
729
+ }
730
+ for (const entry of coverage.value.entries) {
731
+ for (const scenario of entry.scenarios) {
732
+ if (!byId.has(scenario)) {
733
+ return err(manifestError("unknown-coverage-scenario", `Coverage ${entry.key} cites unknown Direct session manifest scenario ${scenario}`));
734
+ }
735
+ }
736
+ }
737
+ let suppliedCatalogHash;
738
+ try {
739
+ const parsedHash = parseTaggedHash(candidate.catalogHash, "Direct session manifest catalogHash");
740
+ const separator = parsedHash.indexOf(":");
741
+ suppliedCatalogHash = `${DIRECT_CATALOG_HASH_ALGORITHM}:${parsedHash.slice(separator + 1)}`;
742
+ } catch (reason) {
743
+ return err(manifestError("invalid-catalog-hash", renderUnknownReason(reason, "Direct session manifest catalogHash is invalid")));
744
+ }
745
+ const expectedCatalogHash = catalogHash({
746
+ queries,
747
+ defaultScenario: defaultScenario.value,
748
+ scenarios: frozenScenarios,
749
+ coverage: coverage.value
750
+ });
751
+ if (!expectedCatalogHash.ok)
752
+ return expectedCatalogHash;
753
+ if (suppliedCatalogHash !== expectedCatalogHash.value) {
754
+ return err(manifestError("catalog-hash-mismatch", "Direct session manifest catalogHash does not match its public catalog"));
755
+ }
756
+ return ok(Object.freeze({
757
+ schema: DIRECT_SESSION_MANIFEST_SCHEMA,
758
+ catalogHash: expectedCatalogHash.value,
759
+ queries,
760
+ defaultScenario: defaultScenario.value,
761
+ active,
762
+ scenarios: frozenScenarios,
763
+ coverage: coverage.value
764
+ }));
765
+ }
766
+ function parseDirectSessionManifest(input) {
767
+ try {
768
+ return parseManifestUnchecked(input);
769
+ } catch (reason) {
770
+ return err(manifestError("invalid-manifest", renderUnknownReason(reason, "Direct session manifest is invalid")));
771
+ }
772
+ }
773
+ // src/testing/probe.ts
774
+ var DIRECT_PROBE_SCHEMA = "direct.probe/v1";
775
+ var MAX_DIRECT_PROBE_COUNTERS = 128;
776
+ var COUNTER_NAME_PATTERN = /^[a-z][A-Za-z0-9]*(?:[.-][A-Za-z0-9]+)*$/u;
777
+ var SNAPSHOT_KEYS = new Set([
778
+ "schema",
779
+ "activationHash",
780
+ "generation",
781
+ "revision",
782
+ "activity",
783
+ "pending",
784
+ "violations",
785
+ "remainingWork",
786
+ "isQuiescent"
787
+ ]);
788
+ var ACTIVITY_KEYS = new Set(["active", "started", "settled"]);
789
+ function probeError(code, message, counter = null) {
790
+ return Object.freeze({ code, message, counter });
791
+ }
792
+ function readNonNegativeInteger(input) {
793
+ return typeof input === "number" && Number.isSafeInteger(input) && input >= 0 ? input : null;
794
+ }
795
+ function parseSnapshotCounters(input, category) {
796
+ if (!isRecord(input)) {
797
+ return err(probeError("invalid-snapshot", `Probe ${category} counters must be an object`));
798
+ }
799
+ const output = Object.create(null);
800
+ for (const [name, candidate] of Object.entries(input)) {
801
+ if (name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
802
+ return err(probeError("invalid-counter-name", "Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens", name));
803
+ }
804
+ const value = readNonNegativeInteger(candidate);
805
+ if (value === null) {
806
+ return err(probeError("invalid-counter", `Counter ${name} must be a non-negative safe integer`, name));
807
+ }
808
+ output[name] = value;
809
+ }
810
+ return ok(Object.freeze(output));
811
+ }
812
+ function parseDirectProbeSnapshot(input) {
813
+ const parsed = parseJsonValue(input);
814
+ if (!parsed.ok || !isRecord(parsed.value)) {
815
+ return err(probeError("invalid-snapshot", parsed.ok ? "Direct probe snapshot must be an object" : parsed.error.message));
816
+ }
817
+ const record = parsed.value;
818
+ for (const key of Object.keys(record)) {
819
+ if (!SNAPSHOT_KEYS.has(key)) {
820
+ return err(probeError("invalid-snapshot", `Unknown Direct probe snapshot key: ${key}`));
821
+ }
822
+ }
823
+ if (record.schema !== DIRECT_PROBE_SCHEMA) {
824
+ return err(probeError("invalid-snapshot", `Direct probe schema must be ${DIRECT_PROBE_SCHEMA}`));
825
+ }
826
+ const activationHash = parseTaggedStableHash(record.activationHash);
827
+ if (!activationHash.ok) {
828
+ return err(probeError("invalid-activation-hash", "Direct probe activation hash is invalid"));
829
+ }
830
+ const generation = readNonNegativeInteger(record.generation);
831
+ const revision = readNonNegativeInteger(record.revision);
832
+ if (generation === null || generation < 1 || revision === null) {
833
+ return err(probeError("invalid-snapshot", "Direct probe generation must be positive and revision must be non-negative"));
834
+ }
835
+ if (generation - 1 > revision) {
836
+ return err(probeError("invalid-snapshot", "Direct probe generation cannot exceed revision plus one"));
837
+ }
838
+ if (!isRecord(record.activity)) {
839
+ return err(probeError("invalid-snapshot", "Direct probe activity must be an object"));
840
+ }
841
+ for (const key of Object.keys(record.activity)) {
842
+ if (!ACTIVITY_KEYS.has(key)) {
843
+ return err(probeError("invalid-snapshot", `Unknown Direct activity key: ${key}`));
844
+ }
845
+ }
846
+ const active = readNonNegativeInteger(record.activity.active);
847
+ const started = readNonNegativeInteger(record.activity.started);
848
+ const settled = readNonNegativeInteger(record.activity.settled);
849
+ if (active === null || started === null || settled === null || settled > started || active !== started - settled) {
850
+ return err(probeError("invalid-snapshot", "Direct activity counters must be non-negative and conserve started work"));
851
+ }
852
+ if (started > revision || settled > revision - started) {
853
+ return err(probeError("invalid-snapshot", "Direct activity transitions cannot exceed the store revision"));
854
+ }
855
+ const pending = parseSnapshotCounters(record.pending, "pending");
856
+ if (!pending.ok)
857
+ return pending;
858
+ const violations = parseSnapshotCounters(record.violations, "violation");
859
+ if (!violations.ok)
860
+ return violations;
861
+ if (Object.keys(pending.value).length + Object.keys(violations.value).length > MAX_DIRECT_PROBE_COUNTERS) {
862
+ return err(probeError("too-many-counters", `A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`));
863
+ }
864
+ if (record.remainingWork === undefined) {
865
+ return err(probeError("invalid-snapshot", "Direct probe snapshot requires remainingWork"));
866
+ }
867
+ if (typeof record.isQuiescent !== "boolean") {
868
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent must be boolean"));
869
+ }
870
+ const expectedQuiescence = active === 0 && Object.values(pending.value).every((value) => value === 0);
871
+ if (record.isQuiescent !== expectedQuiescence) {
872
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent does not match its activity and pending counters"));
873
+ }
874
+ return ok(Object.freeze({
875
+ schema: DIRECT_PROBE_SCHEMA,
876
+ activationHash: activationHash.value,
877
+ generation,
878
+ revision,
879
+ activity: Object.freeze({ active, started, settled }),
880
+ pending: pending.value,
881
+ violations: violations.value,
882
+ remainingWork: freezeJson(record.remainingWork),
883
+ isQuiescent: record.isQuiescent
884
+ }));
885
+ }
886
+ // src/index.ts
887
+ var FIXTURE_QUERY_KEY2 = "__direct_fixture";
888
+ var SCENARIO_QUERY_KEY2 = "__direct_scenario";
889
+
890
+ // src/tooling/browser-verification.ts
891
+ import { randomUUID } from "crypto";
892
+ import { mkdir, rename, rm, writeFile } from "fs/promises";
893
+ import { dirname, join } from "path";
894
+ var DEFAULT_LOG_LIMIT = 12000;
895
+ var DEFAULT_PROBE_TIMEOUT_MS = 1500;
896
+ var DEFAULT_REUSE_PROBE_INTERVAL_MS = 250;
897
+ var DEFAULT_STOP_TIMEOUT_MS = 3000;
898
+ var MAX_RENDERED_ERROR_LENGTH = 4096;
899
+ var MAX_ERROR_CAUSE_DEPTH = 8;
900
+ function truncateRenderedError(value) {
901
+ if (value.length <= MAX_RENDERED_ERROR_LENGTH)
902
+ return value;
903
+ return `${value.slice(0, MAX_RENDERED_ERROR_LENGTH - 1)}\u2026`;
904
+ }
905
+ function readForeignProperty(value, key) {
906
+ try {
907
+ return { ok: true, value: Reflect.get(value, key) };
908
+ } catch {
909
+ return { ok: false };
910
+ }
911
+ }
912
+ function renderUnknownAtDepth(value, seen, depth) {
913
+ if (typeof value === "string")
914
+ return truncateRenderedError(value);
915
+ if (typeof value === "object" && value !== null || typeof value === "function") {
916
+ if (seen.has(value))
917
+ return "[Circular]";
918
+ if (depth >= MAX_ERROR_CAUSE_DEPTH)
919
+ return "[Cause depth exceeded]";
920
+ seen.add(value);
921
+ const message = readForeignProperty(value, "message");
922
+ if (message.ok && typeof message.value === "string") {
923
+ const name = readForeignProperty(value, "name");
924
+ const label = name.ok && typeof name.value === "string" && name.value.length > 0 ? name.value : "Error";
925
+ const cause = readForeignProperty(value, "cause");
926
+ const renderedCause = cause.ok && cause.value !== undefined ? `; caused by ${renderUnknownAtDepth(cause.value, seen, depth + 1)}` : "";
927
+ return truncateRenderedError(`${label}: ${message.value}${renderedCause}`);
928
+ }
929
+ }
930
+ try {
931
+ const encoded = JSON.stringify(value);
932
+ if (encoded !== undefined)
933
+ return truncateRenderedError(encoded);
934
+ } catch {}
935
+ try {
936
+ return truncateRenderedError(String(value));
937
+ } catch {
938
+ return "Unknown failure";
939
+ }
940
+ }
941
+ function renderUnknown(value) {
942
+ return renderUnknownAtDepth(value, new WeakSet, 0);
943
+ }
944
+ function tail(value, maximumLength = DEFAULT_LOG_LIMIT) {
945
+ return value.length <= maximumLength ? value : value.slice(-maximumLength);
946
+ }
947
+ function normalizeRootHttpOrigin(input) {
948
+ let url;
949
+ try {
950
+ url = new URL(input);
951
+ } catch {
952
+ throw new Error("--base-url must be an absolute HTTP URL");
953
+ }
954
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
955
+ throw new Error("--base-url must use http: or https:");
956
+ }
957
+ if (url.username !== "" || url.password !== "") {
958
+ throw new Error("--base-url cannot contain credentials");
959
+ }
960
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
961
+ throw new Error("--base-url must point to the server root without a query string or fragment");
962
+ }
963
+ return url.origin;
964
+ }
965
+ function canAutomaticallyStartLocalServer(baseUrl, localHosts = new Set(["127.0.0.1", "localhost"])) {
966
+ const url = new URL(normalizeRootHttpOrigin(baseUrl));
967
+ return url.protocol === "http:" && localHosts.has(url.hostname);
968
+ }
969
+ async function collectStream(stream, logLimit) {
970
+ const reader = stream.getReader();
971
+ const decoder = new TextDecoder;
972
+ let output = "";
973
+ for (;; ) {
974
+ const chunk = await reader.read();
975
+ if (chunk.done)
976
+ return tail(`${output}${decoder.decode()}`, logLimit);
977
+ output = tail(`${output}${decoder.decode(chunk.value, { stream: true })}`, logLimit);
978
+ }
979
+ }
980
+ function spawnVerificationServer(options) {
981
+ const process_ = Bun.spawn([...options.command], {
982
+ cwd: options.cwd,
983
+ env: { ...process.env, ...options.env },
984
+ stdin: "ignore",
985
+ stdout: "pipe",
986
+ stderr: "pipe"
987
+ });
988
+ const logLimit = options.logLimit ?? DEFAULT_LOG_LIMIT;
989
+ const output = Promise.all([
990
+ collectStream(process_.stdout, logLimit),
991
+ collectStream(process_.stderr, logLimit)
992
+ ]).then(([stdout, stderr]) => tail(`${stdout}
993
+ ${stderr}`.trim(), logLimit));
994
+ return {
995
+ exited: process_.exited,
996
+ exitCode: () => process_.exitCode,
997
+ output,
998
+ terminate: () => process_.kill("SIGTERM"),
999
+ kill: () => process_.kill("SIGKILL")
1000
+ };
1001
+ }
1002
+ async function settleWithin(promise, timeoutMs) {
1003
+ return await Promise.race([
1004
+ promise.then((value) => ({ settled: true, value })),
1005
+ Bun.sleep(timeoutMs).then(() => ({ settled: false }))
1006
+ ]);
1007
+ }
1008
+ async function serverIsReachable(baseUrl, probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS, readinessPath = "/") {
1009
+ if (!readinessPath.startsWith("/") || readinessPath.startsWith("//")) {
1010
+ throw new Error(`readinessPath must be an origin-relative path, received ${JSON.stringify(readinessPath)}`);
1011
+ }
1012
+ const probeUrl = new URL(readinessPath, `${normalizeRootHttpOrigin(baseUrl)}/`);
1013
+ if (probeUrl.hash !== "")
1014
+ throw new Error("readinessPath cannot contain a fragment");
1015
+ try {
1016
+ const response = await fetch(probeUrl, {
1017
+ signal: AbortSignal.timeout(probeTimeoutMs)
1018
+ });
1019
+ await response.body?.cancel();
1020
+ return response.ok;
1021
+ } catch {
1022
+ return false;
1023
+ }
1024
+ }
1025
+ async function stopVerificationServerWithOutput(server, stopTimeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
1026
+ if (!Number.isFinite(stopTimeoutMs) || stopTimeoutMs < 0) {
1027
+ throw new Error("verification server stop timeout must be a finite nonnegative duration");
1028
+ }
1029
+ if (server.exitCode() === null)
1030
+ server.terminate();
1031
+ const stopped = await settleWithin(server.exited, stopTimeoutMs);
1032
+ if (!stopped.settled) {
1033
+ server.kill();
1034
+ const killed = await settleWithin(server.exited, stopTimeoutMs);
1035
+ if (!killed.settled) {
1036
+ throw new Error(`verification server did not exit within ${stopTimeoutMs}ms after SIGKILL`);
1037
+ }
1038
+ }
1039
+ const output = await settleWithin(server.output, stopTimeoutMs);
1040
+ if (!output.settled) {
1041
+ throw new Error(`verification server output did not settle within ${stopTimeoutMs}ms after exit`);
1042
+ }
1043
+ return output.value;
1044
+ }
1045
+ async function stopVerificationServer(server, stopTimeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
1046
+ await stopVerificationServerWithOutput(server, stopTimeoutMs);
1047
+ }
1048
+ async function acquireVerificationServer(options) {
1049
+ const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
1050
+ const readinessPath = options.readinessPath ?? "/";
1051
+ const isReachable = options.isReachable ?? serverIsReachable;
1052
+ const canStartLocally = canAutomaticallyStartLocalServer(options.baseUrl, options.localHosts);
1053
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1054
+ if (canStartLocally && options.reuseExistingLocalServer === false) {
1055
+ throw new Error(`A local server is already reachable at ${options.baseUrl}; ` + "verification will not reuse a server whose worktree ownership is unknown");
1056
+ }
1057
+ await Bun.sleep(options.reuseProbeIntervalMs ?? DEFAULT_REUSE_PROBE_INTERVAL_MS);
1058
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1059
+ return { source: "reused" };
1060
+ }
1061
+ }
1062
+ if (!canStartLocally) {
1063
+ throw new Error(`No server is reachable at ${options.baseUrl}; automatic startup is limited to local HTTP URLs`);
1064
+ }
1065
+ const server = options.startServer();
1066
+ let exitedWithCode = null;
1067
+ try {
1068
+ const deadline = Date.now() + options.startupTimeoutMs;
1069
+ while (Date.now() < deadline) {
1070
+ const exitCode = server.exitCode();
1071
+ if (exitCode !== null) {
1072
+ exitedWithCode = exitCode;
1073
+ break;
1074
+ }
1075
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1076
+ return { source: "started", server };
1077
+ }
1078
+ await Bun.sleep(options.pollIntervalMs ?? 200);
1079
+ }
1080
+ } catch (error) {
1081
+ await stopVerificationServer(server);
1082
+ throw error;
1083
+ }
1084
+ if (exitedWithCode !== null) {
1085
+ const output2 = tail(await stopVerificationServerWithOutput(server));
1086
+ throw new Error(`${options.label} exited with ${exitedWithCode}:
1087
+ ${output2}`);
1088
+ }
1089
+ const timeoutMessage = `${options.label} did not become reachable at ${new URL(readinessPath, `${options.baseUrl}/`).href} within ${options.startupTimeoutMs}ms`;
1090
+ const output = tail(await stopVerificationServerWithOutput(server));
1091
+ throw new Error(output === "" ? timeoutMessage : `${timeoutMessage}:
1092
+ ${output}`);
1093
+ }
1094
+ async function createArtifactRun(options) {
1095
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
1096
+ const processId = options.processId ?? process.pid;
1097
+ const runId = `${generatedAt.replaceAll(/[^0-9A-Za-z]/gu, "-")}-${processId}`;
1098
+ const runDirectory = join(options.artifactRoot, runId);
1099
+ await mkdir(runDirectory, { recursive: true });
1100
+ return {
1101
+ artifactRoot: options.artifactRoot,
1102
+ generatedAt,
1103
+ manifestPath: join(options.artifactRoot, "manifest.json"),
1104
+ runDirectory
1105
+ };
1106
+ }
1107
+ async function writeJsonAtomically(path, value) {
1108
+ const temporaryPath = join(dirname(path), `.${process.pid}-${randomUUID()}.tmp`);
1109
+ try {
1110
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
1111
+ `, "utf8");
1112
+ await rename(temporaryPath, path);
1113
+ } catch (error) {
1114
+ await rm(temporaryPath, { force: true });
1115
+ throw error;
1116
+ }
1117
+ }
1118
+
1119
+ // src/tooling/bombadil-runner.ts
1120
+ var EXPECTED_BOMBADIL_VERSION = "0.7.2";
1121
+ var DEFAULT_TIME_LIMIT_SECONDS = 20;
1122
+ var MIN_TIME_LIMIT_SECONDS = 12;
1123
+ var MAX_TIME_LIMIT_SECONDS = 300;
1124
+ var DEFAULT_STARTUP_TIMEOUT_MS = 60000;
1125
+ var MAX_STARTUP_TIMEOUT_MS = 120000;
1126
+ var LOG_LIMIT = 24000;
1127
+ var ARTIFACT_SCHEMA = "direct.bombadil-run/v1";
1128
+ var SCENARIO_PATTERN = /^[a-z][a-z0-9]*(?:[._/-][a-z0-9]+)*$/u;
1129
+ var ARTIFACT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
1130
+ var ENVIRONMENT_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/u;
1131
+ var QUERY_PARAMETER_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]*$/u;
1132
+ var PROTOTYPE_PROPERTY_NAMES = new Set(["__proto__", "constructor", "prototype"]);
1133
+ var TRACE_MAX_BYTES = 64 * 1024 * 1024;
1134
+ var TRACE_MAX_LINE_BYTES = 16 * 1024 * 1024;
1135
+ var TRACE_MAX_LINES = 1e4;
1136
+ var TRACE_MAX_SNAPSHOTS_PER_LINE = 4096;
1137
+ var RANDOM_RUN_OVERHEAD_MS = 30000;
1138
+ var REPLAY_WALL_CLOCK_TIMEOUT_MS = MAX_TIME_LIMIT_SECONDS * 1000 + RANDOM_RUN_OVERHEAD_MS;
1139
+ var PROCESS_TERMINATION_GRACE_MS = 5000;
1140
+ var MIN_PROCESS_OUTPUT_DRAIN_MS = 500;
1141
+ var SERVER_OUTPUT_TIMEOUT_MS = 3000;
1142
+ var DIRECT_BROWSER_BRIDGE_SCHEMA = "direct.browser-bridge/v2";
1143
+ var TRACE_LINE_KEYS = new Set(["action", "snapshots", "state", "timestamp", "violations"]);
1144
+ var TRACE_SNAPSHOT_KEYS = new Set(["index", "name", "time", "value"]);
1145
+ var DIRECT_OBSERVATION_KEYS = new Set([
1146
+ "activationHash",
1147
+ "activeRoute",
1148
+ "activeScenario",
1149
+ "activeSource",
1150
+ "bridgePresent",
1151
+ "bridgeSchema",
1152
+ "catalogHash",
1153
+ "contractValid",
1154
+ "isQuiescent",
1155
+ "manifest",
1156
+ "probe",
1157
+ "violations",
1158
+ "violationsValid"
1159
+ ]);
1160
+ function readOptionValue(arguments_, index, option) {
1161
+ const value = arguments_[index + 1];
1162
+ if (value === undefined || value.startsWith("-")) {
1163
+ throw new Error(`${option} requires a value`);
1164
+ }
1165
+ return { index: index + 1, value };
1166
+ }
1167
+ function parseTimeLimit(value) {
1168
+ const match = /^([1-9][0-9]*)s$/u.exec(value);
1169
+ if (match === null) {
1170
+ throw new Error("--time-limit must be a whole number of seconds such as 20s");
1171
+ }
1172
+ const seconds = Number(match[1]);
1173
+ if (!Number.isSafeInteger(seconds) || seconds < MIN_TIME_LIMIT_SECONDS || seconds > MAX_TIME_LIMIT_SECONDS) {
1174
+ throw new Error(`--time-limit must be between ${String(MIN_TIME_LIMIT_SECONDS)}s and ${String(MAX_TIME_LIMIT_SECONDS)}s`);
1175
+ }
1176
+ return seconds;
1177
+ }
1178
+ function bombadilNativeBinary(repositoryRoot) {
1179
+ let binary;
1180
+ if (process2.platform === "darwin" && process2.arch === "arm64") {
1181
+ binary = "bombadil-darwin-arm64";
1182
+ } else if (process2.platform === "linux" && process2.arch === "x64") {
1183
+ binary = "bombadil-linux-x64";
1184
+ } else if (process2.platform === "linux" && process2.arch === "arm64") {
1185
+ binary = "bombadil-linux-arm64";
1186
+ } else {
1187
+ throw new Error(`Bombadil 0.7.2 does not support ${process2.platform}-${process2.arch}`);
1188
+ }
1189
+ return join2(repositoryRoot, "node_modules", "@antithesishq", "bombadil", "binaries", binary);
1190
+ }
1191
+ function requireLocalRootHttpOrigin(value) {
1192
+ const baseUrl = normalizeRootHttpOrigin(value);
1193
+ const url = new URL(baseUrl);
1194
+ if (!canAutomaticallyStartLocalServer(baseUrl)) {
1195
+ throw new Error("--base-url must use HTTP on 127.0.0.1 or localhost");
1196
+ }
1197
+ if (url.port === "") {
1198
+ throw new Error("--base-url must include an explicit local server port");
1199
+ }
1200
+ if (Number(url.port) < 1) {
1201
+ throw new Error("--base-url port must be between 1 and 65535");
1202
+ }
1203
+ return baseUrl;
1204
+ }
1205
+ function hasControlCharacters3(value) {
1206
+ for (const character of value) {
1207
+ const code = character.charCodeAt(0);
1208
+ if (code < 32 || code === 127)
1209
+ return true;
1210
+ }
1211
+ return false;
1212
+ }
1213
+ function isRecord2(value) {
1214
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1215
+ }
1216
+ function hasExactKeys(value, expected) {
1217
+ const keys = Object.keys(value);
1218
+ return keys.length === expected.size && keys.every((key) => expected.has(key));
1219
+ }
1220
+ function parseTraceDirectObservation(value) {
1221
+ if (!isRecord2(value) || !hasExactKeys(value, DIRECT_OBSERVATION_KEYS)) {
1222
+ throw new Error("Bombadil trace has an invalid named direct observation");
1223
+ }
1224
+ const stringKeys = [
1225
+ "activationHash",
1226
+ "activeRoute",
1227
+ "activeScenario",
1228
+ "activeSource",
1229
+ "bridgeSchema",
1230
+ "catalogHash"
1231
+ ];
1232
+ for (const key of stringKeys) {
1233
+ if (typeof value[key] !== "string") {
1234
+ throw new Error(`Bombadil trace direct observation has an invalid ${key}`);
1235
+ }
1236
+ }
1237
+ const booleanKeys = [
1238
+ "bridgePresent",
1239
+ "contractValid",
1240
+ "isQuiescent",
1241
+ "violationsValid"
1242
+ ];
1243
+ for (const key of booleanKeys) {
1244
+ if (typeof value[key] !== "boolean") {
1245
+ throw new Error(`Bombadil trace direct observation has an invalid ${key}`);
1246
+ }
1247
+ }
1248
+ if (!Array.isArray(value.violations) || !value.violations.every((candidate) => typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate >= 0)) {
1249
+ throw new Error("Bombadil trace direct observation has invalid violation counters");
1250
+ }
1251
+ const activationHash = value.activationHash;
1252
+ const activeRoute = value.activeRoute;
1253
+ const activeScenario = value.activeScenario;
1254
+ const activeSource = value.activeSource;
1255
+ const bridgePresent = value.bridgePresent;
1256
+ const bridgeSchema = value.bridgeSchema;
1257
+ const catalogHash2 = value.catalogHash;
1258
+ const contractValid = value.contractValid;
1259
+ const isQuiescent = value.isQuiescent;
1260
+ const violationsValid = value.violationsValid;
1261
+ if (typeof activationHash !== "string" || typeof activeRoute !== "string" || typeof activeScenario !== "string" || typeof activeSource !== "string" || typeof bridgePresent !== "boolean" || typeof bridgeSchema !== "string" || typeof catalogHash2 !== "string" || typeof contractValid !== "boolean" || typeof isQuiescent !== "boolean" || typeof violationsValid !== "boolean") {
1262
+ throw new Error("Bombadil trace direct observation could not be narrowed");
1263
+ }
1264
+ return {
1265
+ activationHash,
1266
+ activeRoute,
1267
+ activeScenario,
1268
+ activeSource,
1269
+ bridgePresent,
1270
+ bridgeSchema,
1271
+ catalogHash: catalogHash2,
1272
+ contractValid,
1273
+ isQuiescent,
1274
+ manifest: value.manifest,
1275
+ probe: value.probe,
1276
+ violations: value.violations,
1277
+ violationsValid
1278
+ };
1279
+ }
1280
+ function exactTraceDirectObservation(observation) {
1281
+ if (!observation.bridgePresent) {
1282
+ if (observation.bridgeSchema !== "" || observation.manifest !== null || observation.probe !== null || observation.contractValid || observation.violationsValid || observation.isQuiescent || observation.activationHash !== "" || observation.activeRoute !== "" || observation.activeScenario !== "" || observation.activeSource !== "" || observation.catalogHash !== "" || observation.violations.length !== 0) {
1283
+ throw new Error("Bombadil trace has a malformed bridge-absent Direct observation");
1284
+ }
1285
+ return null;
1286
+ }
1287
+ if (observation.bridgeSchema !== DIRECT_BROWSER_BRIDGE_SCHEMA) {
1288
+ throw new Error("Bombadil trace Direct bridge schema is invalid");
1289
+ }
1290
+ const manifest2 = parseDirectSessionManifest(observation.manifest);
1291
+ if (!manifest2.ok) {
1292
+ throw new Error(`Bombadil trace Direct manifest is invalid: ${manifest2.error.message}`);
1293
+ }
1294
+ const probe2 = parseDirectProbeSnapshot(observation.probe);
1295
+ if (!probe2.ok) {
1296
+ throw new Error(`Bombadil trace Direct probe is invalid: ${probe2.error.message}`);
1297
+ }
1298
+ if (manifest2.value.active.activationHash !== probe2.value.activationHash) {
1299
+ throw new Error("Bombadil trace Direct manifest and probe activation hashes differ");
1300
+ }
1301
+ const violationValues = Object.values(probe2.value.violations);
1302
+ if (!observation.contractValid || !observation.violationsValid || observation.catalogHash !== manifest2.value.catalogHash || observation.activationHash !== manifest2.value.active.activationHash || observation.activeRoute !== manifest2.value.active.route || observation.activeScenario !== manifest2.value.active.scenario || observation.activeSource !== manifest2.value.active.source || observation.isQuiescent !== probe2.value.isQuiescent || observation.violations.length !== violationValues.length || observation.violations.some((value, index) => value !== violationValues[index])) {
1303
+ throw new Error("Bombadil trace Direct summary does not match its exact manifest and probe");
1304
+ }
1305
+ return {
1306
+ activationHash: manifest2.value.active.activationHash,
1307
+ catalogHash: manifest2.value.catalogHash,
1308
+ route: manifest2.value.active.route,
1309
+ scenario: manifest2.value.active.scenario,
1310
+ source: manifest2.value.active.source,
1311
+ isQuiescent: probe2.value.isQuiescent
1312
+ };
1313
+ }
1314
+ function parseTraceLine(line, lineNumber) {
1315
+ let input;
1316
+ try {
1317
+ input = JSON.parse(line);
1318
+ } catch {
1319
+ throw new Error(`Bombadil trace line ${String(lineNumber)} is not valid JSON`);
1320
+ }
1321
+ if (!isRecord2(input) || !hasExactKeys(input, TRACE_LINE_KEYS)) {
1322
+ throw new Error(`Bombadil trace line ${String(lineNumber)} has an invalid 0.7.2 envelope`);
1323
+ }
1324
+ if (!Number.isSafeInteger(input.timestamp) || typeof input.timestamp !== "number" || input.timestamp < 0 || !Array.isArray(input.snapshots) || input.snapshots.length > TRACE_MAX_SNAPSHOTS_PER_LINE || !Array.isArray(input.violations)) {
1325
+ throw new Error(`Bombadil trace line ${String(lineNumber)} has invalid state fields`);
1326
+ }
1327
+ const snapshots = input.snapshots;
1328
+ const directSnapshots = snapshots.filter((snapshot2) => isRecord2(snapshot2) && snapshot2.name === "direct");
1329
+ if (directSnapshots.length !== 1) {
1330
+ throw new Error(`Bombadil trace line ${String(lineNumber)} must contain one named direct snapshot`);
1331
+ }
1332
+ const snapshot = directSnapshots[0];
1333
+ if (snapshot === undefined || !hasExactKeys(snapshot, TRACE_SNAPSHOT_KEYS) || !Number.isSafeInteger(snapshot.index) || !Number.isSafeInteger(snapshot.time) || snapshot.index < 0 || snapshot.time < 0) {
1334
+ throw new Error(`Bombadil trace line ${String(lineNumber)} has an invalid direct snapshot`);
1335
+ }
1336
+ return parseTraceDirectObservation(snapshot.value);
1337
+ }
1338
+ async function attestDirectBombadilTrace(options) {
1339
+ const metadata = await stat(options.tracePath).catch(() => null);
1340
+ if (metadata === null || !metadata.isFile() || metadata.size === 0) {
1341
+ throw new Error("Bombadil did not produce a nonempty trace.jsonl");
1342
+ }
1343
+ if (metadata.size > TRACE_MAX_BYTES) {
1344
+ throw new Error(`Bombadil trace exceeds ${String(TRACE_MAX_BYTES)} bytes`);
1345
+ }
1346
+ const stream = createReadStream(options.tracePath, { encoding: "utf8" });
1347
+ const lines = createInterface({ input: stream, crlfDelay: Infinity });
1348
+ let observationCount = 0;
1349
+ let invalidObservationCount = 0;
1350
+ let validObservationCount = 0;
1351
+ let initial = null;
1352
+ let final = null;
1353
+ let finalWasInvalid = false;
1354
+ try {
1355
+ for await (const line of lines) {
1356
+ observationCount += 1;
1357
+ if (observationCount > TRACE_MAX_LINES) {
1358
+ throw new Error(`Bombadil trace exceeds ${String(TRACE_MAX_LINES)} lines`);
1359
+ }
1360
+ if (Buffer.byteLength(line, "utf8") > TRACE_MAX_LINE_BYTES) {
1361
+ throw new Error(`Bombadil trace line ${String(observationCount)} is too large`);
1362
+ }
1363
+ const observation = parseTraceLine(line, observationCount);
1364
+ const exact = exactTraceDirectObservation(observation);
1365
+ if (exact === null) {
1366
+ if (initial !== null) {
1367
+ throw new Error("Bombadil trace lost the Direct bridge after exact activation");
1368
+ }
1369
+ invalidObservationCount += 1;
1370
+ finalWasInvalid = true;
1371
+ continue;
1372
+ }
1373
+ validObservationCount += 1;
1374
+ final = exact;
1375
+ finalWasInvalid = false;
1376
+ if (initial === null) {
1377
+ if (exact.source !== "scenario" || exact.scenario !== options.expectedScenario || exact.route !== options.expectedRoute) {
1378
+ throw new Error("Bombadil trace first valid Direct activation does not match the requested scenario and route");
1379
+ }
1380
+ initial = {
1381
+ activationHash: exact.activationHash,
1382
+ catalogHash: exact.catalogHash,
1383
+ route: exact.route,
1384
+ scenario: exact.scenario,
1385
+ source: exact.source
1386
+ };
1387
+ }
1388
+ if (exact.source !== "scenario") {
1389
+ throw new Error("Bombadil trace left scenario activation during the run");
1390
+ }
1391
+ if (exact.scenario !== initial.scenario || exact.route !== initial.route || exact.activationHash !== initial.activationHash) {
1392
+ throw new Error("Bombadil trace Direct activation changed during the run");
1393
+ }
1394
+ if (exact.catalogHash !== initial.catalogHash) {
1395
+ throw new Error("Bombadil trace Direct catalog changed during the run");
1396
+ }
1397
+ if (observation.violations.some((value) => value !== 0)) {
1398
+ throw new Error("Bombadil trace contains a nonzero Direct violation counter");
1399
+ }
1400
+ }
1401
+ } finally {
1402
+ lines.close();
1403
+ stream.destroy();
1404
+ }
1405
+ if (initial === null || final === null) {
1406
+ throw new Error("Bombadil trace never reached a valid Direct contract");
1407
+ }
1408
+ if (finalWasInvalid) {
1409
+ throw new Error("Bombadil trace ended without an installed valid Direct bridge");
1410
+ }
1411
+ if (!final.isQuiescent) {
1412
+ throw new Error("Bombadil trace final Direct observation is not quiescent");
1413
+ }
1414
+ return {
1415
+ schema: "direct.bombadil-trace-attestation/v1",
1416
+ catalogHash: initial.catalogHash,
1417
+ initial,
1418
+ final: {
1419
+ activationHash: final.activationHash,
1420
+ catalogHash: final.catalogHash,
1421
+ route: final.route,
1422
+ scenario: final.scenario,
1423
+ source: final.source,
1424
+ isQuiescent: true
1425
+ },
1426
+ observationCount,
1427
+ invalidObservationCount,
1428
+ validObservationCount
1429
+ };
1430
+ }
1431
+ function parseDirectBombadilFuzzArguments(arguments_, defaultBaseUrl) {
1432
+ let baseUrl = defaultBaseUrl;
1433
+ let timeLimitSeconds = DEFAULT_TIME_LIMIT_SECONDS;
1434
+ let replayPath = null;
1435
+ let receivedBaseUrl = false;
1436
+ let receivedTimeLimit = false;
1437
+ let receivedReplay = false;
1438
+ for (let index = 0;index < arguments_.length; index += 1) {
1439
+ const argument = arguments_[index];
1440
+ if (argument === undefined)
1441
+ continue;
1442
+ if (argument === "--help" || argument === "-h")
1443
+ return { kind: "help" };
1444
+ if (argument === "--base-url" || argument.startsWith("--base-url=")) {
1445
+ if (receivedBaseUrl)
1446
+ throw new Error("--base-url may be provided only once");
1447
+ receivedBaseUrl = true;
1448
+ if (argument === "--base-url") {
1449
+ const next = readOptionValue(arguments_, index, "--base-url");
1450
+ baseUrl = next.value;
1451
+ index = next.index;
1452
+ } else {
1453
+ baseUrl = argument.slice("--base-url=".length);
1454
+ }
1455
+ continue;
1456
+ }
1457
+ if (argument === "--time-limit" || argument.startsWith("--time-limit=")) {
1458
+ if (receivedTimeLimit)
1459
+ throw new Error("--time-limit may be provided only once");
1460
+ receivedTimeLimit = true;
1461
+ let value;
1462
+ if (argument === "--time-limit") {
1463
+ const next = readOptionValue(arguments_, index, "--time-limit");
1464
+ value = next.value;
1465
+ index = next.index;
1466
+ } else {
1467
+ value = argument.slice("--time-limit=".length);
1468
+ }
1469
+ timeLimitSeconds = parseTimeLimit(value);
1470
+ continue;
1471
+ }
1472
+ if (argument === "--replay" || argument.startsWith("--replay=")) {
1473
+ if (receivedReplay)
1474
+ throw new Error("--replay may be provided only once");
1475
+ receivedReplay = true;
1476
+ if (argument === "--replay") {
1477
+ const next = readOptionValue(arguments_, index, "--replay");
1478
+ replayPath = next.value;
1479
+ index = next.index;
1480
+ } else {
1481
+ replayPath = argument.slice("--replay=".length);
1482
+ }
1483
+ if (replayPath.length === 0)
1484
+ throw new Error("--replay requires a value");
1485
+ continue;
1486
+ }
1487
+ throw new Error(`Unknown argument at position ${String(index + 1)}`);
1488
+ }
1489
+ if (receivedReplay && receivedTimeLimit) {
1490
+ throw new Error("--replay and --time-limit cannot be used together");
1491
+ }
1492
+ return {
1493
+ kind: "run",
1494
+ baseUrl: requireLocalRootHttpOrigin(baseUrl),
1495
+ replayPath,
1496
+ timeLimitSeconds
1497
+ };
1498
+ }
1499
+ function isWithin(root, candidate) {
1500
+ const path = relative(root, candidate);
1501
+ return path === "" || !path.startsWith("..") && !isAbsolute(path);
1502
+ }
1503
+ function validateReadinessPath(value) {
1504
+ if (!value.startsWith("/") || value.startsWith("//")) {
1505
+ throw new Error("server.readinessPath must be an origin-relative path");
1506
+ }
1507
+ const url = new URL(value, "http://127.0.0.1");
1508
+ if (url.origin !== "http://127.0.0.1" || url.hash !== "") {
1509
+ throw new Error("server.readinessPath must stay on the server origin without a fragment");
1510
+ }
1511
+ }
1512
+ function validateEntryPath(value) {
1513
+ if (!value.startsWith("/") || value.startsWith("//")) {
1514
+ throw new Error("entryPath must be an origin-relative path");
1515
+ }
1516
+ const url = new URL(value, "http://127.0.0.1");
1517
+ if (url.origin !== "http://127.0.0.1" || url.hash !== "" || url.search !== "" || url.pathname !== value) {
1518
+ throw new Error("entryPath must be a normalized path without a query or fragment");
1519
+ }
1520
+ }
1521
+ function validateTargetQuery(value) {
1522
+ if (!isRecord2(value)) {
1523
+ throw new Error("targetQuery must be an object of string query parameters");
1524
+ }
1525
+ const entries = Object.entries(value);
1526
+ if (entries.length > 16) {
1527
+ throw new Error("targetQuery may contain at most 16 parameters");
1528
+ }
1529
+ const validated = {};
1530
+ for (const [name, queryValue] of [...entries].sort(([left], [right]) => left.localeCompare(right))) {
1531
+ if (name.length === 0 || name.length > 128 || !QUERY_PARAMETER_NAME_PATTERN.test(name) || PROTOTYPE_PROPERTY_NAMES.has(name) || hasControlCharacters3(name) || name === SCENARIO_QUERY_KEY2 || name === FIXTURE_QUERY_KEY2) {
1532
+ throw new Error("targetQuery contains an invalid or reserved parameter name");
1533
+ }
1534
+ if (typeof queryValue !== "string" || queryValue.length > 2048 || hasControlCharacters3(queryValue)) {
1535
+ throw new Error(`targetQuery ${name} must be a bounded string without control characters`);
1536
+ }
1537
+ validated[name] = queryValue;
1538
+ }
1539
+ return Object.freeze(validated);
1540
+ }
1541
+ function validateDirectBombadilFuzzConfig(config, baseUrlOverride) {
1542
+ const repositoryRoot = resolve(config.repositoryRoot);
1543
+ if (!isAbsolute(config.repositoryRoot) || repositoryRoot !== config.repositoryRoot) {
1544
+ throw new Error("repositoryRoot must be an absolute normalized path");
1545
+ }
1546
+ if (!ARTIFACT_NAME_PATTERN.test(config.artifactName)) {
1547
+ throw new Error("artifactName must be a safe lowercase kebab identifier");
1548
+ }
1549
+ if (config.label.trim().length === 0 || config.label.length > 160 || hasControlCharacters3(config.label)) {
1550
+ throw new Error("label must contain 1-160 visible characters");
1551
+ }
1552
+ if (config.scenario.length > 120 || !SCENARIO_PATTERN.test(config.scenario)) {
1553
+ throw new Error("scenario must be a valid Direct scenario identifier");
1554
+ }
1555
+ if (config.expectedRoute.trim().length === 0 || config.expectedRoute.length > 256 || hasControlCharacters3(config.expectedRoute)) {
1556
+ throw new Error("expectedRoute must contain 1-256 visible characters");
1557
+ }
1558
+ const specificationPath = resolve(config.specificationPath);
1559
+ const serverCwd = resolve(config.server.cwd);
1560
+ if (!isAbsolute(config.specificationPath) || !isWithin(repositoryRoot, specificationPath)) {
1561
+ throw new Error("specificationPath must be an absolute path inside repositoryRoot");
1562
+ }
1563
+ if (!/\.[cm]?[jt]sx?$/u.test(specificationPath)) {
1564
+ throw new Error("specificationPath must name a JavaScript or TypeScript specification");
1565
+ }
1566
+ if (!isAbsolute(config.server.cwd) || !isWithin(repositoryRoot, serverCwd)) {
1567
+ throw new Error("server.cwd must be an absolute path inside repositoryRoot");
1568
+ }
1569
+ if (config.server.command.length === 0) {
1570
+ throw new Error("server.command must contain at least one argument");
1571
+ }
1572
+ if (config.server.command.filter((argument) => argument === "{port}").length !== 1) {
1573
+ throw new Error("server.command must contain exactly one literal {port} token");
1574
+ }
1575
+ for (const argument of config.server.command) {
1576
+ if (argument.length === 0 || argument.includes("\x00")) {
1577
+ throw new Error("server.command arguments must be nonempty strings without null bytes");
1578
+ }
1579
+ }
1580
+ for (const [name, value] of Object.entries(config.server.env ?? {})) {
1581
+ if (!ENVIRONMENT_NAME_PATTERN.test(name)) {
1582
+ throw new Error("server.env contains an invalid environment variable name");
1583
+ }
1584
+ if (value !== undefined && (typeof value !== "string" || value.includes("\x00"))) {
1585
+ throw new Error(`server.env ${name} must be a string without null bytes`);
1586
+ }
1587
+ }
1588
+ const readinessPath = config.server.readinessPath ?? "/";
1589
+ validateReadinessPath(readinessPath);
1590
+ const entryPath = config.entryPath ?? "/";
1591
+ validateEntryPath(entryPath);
1592
+ const targetQuery = validateTargetQuery(config.targetQuery ?? {});
1593
+ const startupTimeoutMs = config.server.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
1594
+ if (!Number.isSafeInteger(startupTimeoutMs) || startupTimeoutMs < 1000 || startupTimeoutMs > MAX_STARTUP_TIMEOUT_MS) {
1595
+ throw new Error(`server.startupTimeoutMs must be an integer between 1000 and ${String(MAX_STARTUP_TIMEOUT_MS)}`);
1596
+ }
1597
+ const baseUrl = requireLocalRootHttpOrigin(baseUrlOverride ?? config.baseUrl);
1598
+ const port = new URL(baseUrl).port;
1599
+ return {
1600
+ ...config,
1601
+ repositoryRoot,
1602
+ specificationPath,
1603
+ baseUrl,
1604
+ artifactRoot: join2(repositoryRoot, "artifacts", "direct-bombadil", config.artifactName),
1605
+ bombadilExecutable: bombadilNativeBinary(repositoryRoot),
1606
+ entryPath,
1607
+ port,
1608
+ targetQuery,
1609
+ server: {
1610
+ ...config.server,
1611
+ cwd: serverCwd,
1612
+ readinessPath,
1613
+ startupTimeoutMs
1614
+ }
1615
+ };
1616
+ }
1617
+ function resolveReplayPath(repositoryRoot, replayPath) {
1618
+ if (replayPath === null)
1619
+ return null;
1620
+ const resolved = resolve(repositoryRoot, replayPath);
1621
+ if (!isWithin(repositoryRoot, resolved) || !resolved.endsWith(".jsonl")) {
1622
+ throw new Error("--replay must name a .jsonl trace inside repositoryRoot");
1623
+ }
1624
+ return resolved;
1625
+ }
1626
+ function createDirectBombadilInvocation(options) {
1627
+ const target = new URL(options.entryPath ?? "/", `${options.baseUrl}/`);
1628
+ target.searchParams.set(SCENARIO_QUERY_KEY2, options.scenario);
1629
+ for (const [name, value] of Object.entries(options.targetQuery ?? {})) {
1630
+ target.searchParams.set(name, value);
1631
+ }
1632
+ const command = [
1633
+ options.bombadilExecutable,
1634
+ "browser",
1635
+ "test",
1636
+ target.href,
1637
+ options.specificationPath,
1638
+ "--output-path",
1639
+ options.outputPath,
1640
+ "--headless",
1641
+ "--instrument-javascript="
1642
+ ];
1643
+ if (options.replayPath === null) {
1644
+ command.push("--exit-on-violation", "--time-limit", `${String(options.timeLimitSeconds)}s`);
1645
+ } else {
1646
+ command.push("--reproduce", options.replayPath);
1647
+ }
1648
+ return {
1649
+ command,
1650
+ cwd: options.repositoryRoot,
1651
+ outputPath: options.outputPath,
1652
+ targetUrl: target.href,
1653
+ wallClockTimeoutMs: options.replayPath === null ? options.timeLimitSeconds * 1000 + RANDOM_RUN_OVERHEAD_MS : REPLAY_WALL_CLOCK_TIMEOUT_MS
1654
+ };
1655
+ }
1656
+ function captureStream(stream, maximumLength = LOG_LIMIT) {
1657
+ let stopCapture;
1658
+ const stopped = new Promise((resolveStopped) => {
1659
+ stopCapture = resolveStopped;
1660
+ });
1661
+ const result = (async () => {
1662
+ const reader = stream.getReader();
1663
+ const decoder = new TextDecoder;
1664
+ let output = "";
1665
+ for (;; ) {
1666
+ const next = await Promise.race([
1667
+ reader.read().then((chunk) => ({ kind: "chunk", chunk }), (error) => ({ kind: "error", error })),
1668
+ stopped.then(() => ({ kind: "stopped" }))
1669
+ ]);
1670
+ if (next.kind === "stopped") {
1671
+ reader.cancel().catch(() => {
1672
+ return;
1673
+ });
1674
+ return tail(`${output}${decoder.decode()}`, maximumLength);
1675
+ }
1676
+ if (next.kind === "error")
1677
+ throw next.error;
1678
+ if (next.chunk.done)
1679
+ return tail(`${output}${decoder.decode()}`, maximumLength);
1680
+ output = tail(`${output}${decoder.decode(next.chunk.value, { stream: true })}`, maximumLength);
1681
+ }
1682
+ })();
1683
+ return { result, stop: stopCapture };
1684
+ }
1685
+ function signalProcessGroup(process_, signal) {
1686
+ try {
1687
+ process2.kill(-process_.pid, signal);
1688
+ } catch {
1689
+ if (process_.exitCode === null)
1690
+ process_.kill(signal);
1691
+ }
1692
+ }
1693
+ async function terminateProcessGroup(process_, graceMs) {
1694
+ signalProcessGroup(process_, "SIGTERM");
1695
+ await Bun.sleep(graceMs);
1696
+ signalProcessGroup(process_, "SIGKILL");
1697
+ await Promise.race([process_.exited.then(() => {
1698
+ return;
1699
+ }), Bun.sleep(graceMs)]);
1700
+ }
1701
+ async function runBombadilNativeProcess(invocation) {
1702
+ const process_ = Bun.spawn([...invocation.command], {
1703
+ cwd: invocation.cwd,
1704
+ detached: true,
1705
+ env: { ...process2.env, NO_COLOR: "1" },
1706
+ stdin: "ignore",
1707
+ stdout: "pipe",
1708
+ stderr: "pipe"
1709
+ });
1710
+ let timeout;
1711
+ let abortListener;
1712
+ const timeoutPromise = new Promise((resolveTimeout) => {
1713
+ timeout = setTimeout(() => resolveTimeout("timeout"), invocation.wallClockTimeoutMs);
1714
+ });
1715
+ const abortPromise = new Promise((resolveAbort) => {
1716
+ if (invocation.abortSignal === undefined)
1717
+ return;
1718
+ if (invocation.abortSignal.aborted) {
1719
+ resolveAbort("aborted");
1720
+ return;
1721
+ }
1722
+ abortListener = () => resolveAbort("aborted");
1723
+ invocation.abortSignal.addEventListener("abort", abortListener, { once: true });
1724
+ });
1725
+ try {
1726
+ const stdoutCapture = captureStream(process_.stdout);
1727
+ const stderrCapture = captureStream(process_.stderr);
1728
+ const outputPromise = Promise.all([stdoutCapture.result, stderrCapture.result]);
1729
+ const outcome = await Promise.race([
1730
+ process_.exited.then((exitCode) => ({ kind: "exited", exitCode })),
1731
+ timeoutPromise.then(() => ({ kind: "timeout" })),
1732
+ abortPromise.then(() => ({ kind: "aborted" }))
1733
+ ]);
1734
+ const terminationGraceMs = invocation.terminationGraceMs ?? PROCESS_TERMINATION_GRACE_MS;
1735
+ if (outcome.kind === "exited") {
1736
+ signalProcessGroup(process_, "SIGKILL");
1737
+ } else {
1738
+ await terminateProcessGroup(process_, terminationGraceMs);
1739
+ }
1740
+ const outputSettled = await Promise.race([
1741
+ outputPromise.then(() => true, () => true),
1742
+ Bun.sleep(Math.max(terminationGraceMs, MIN_PROCESS_OUTPUT_DRAIN_MS)).then(() => false)
1743
+ ]);
1744
+ if (!outputSettled) {
1745
+ stdoutCapture.stop();
1746
+ stderrCapture.stop();
1747
+ }
1748
+ const [stdout, stderr] = await outputPromise;
1749
+ return {
1750
+ exitCode: outcome.kind === "exited" ? outcome.exitCode : process_.exitCode ?? 137,
1751
+ stderr,
1752
+ stdout,
1753
+ termination: outcome.kind === "exited" ? null : outcome.kind
1754
+ };
1755
+ } finally {
1756
+ if (timeout !== undefined)
1757
+ clearTimeout(timeout);
1758
+ if (abortListener !== undefined) {
1759
+ invocation.abortSignal?.removeEventListener("abort", abortListener);
1760
+ }
1761
+ }
1762
+ }
1763
+ var defaultDependencies = {
1764
+ acquireServer: acquireVerificationServer,
1765
+ now: () => new Date,
1766
+ runBombadil: runBombadilNativeProcess,
1767
+ serverOutputTimeoutMs: SERVER_OUTPUT_TIMEOUT_MS,
1768
+ spawnServer: spawnVerificationServer,
1769
+ stopServer: stopVerificationServer
1770
+ };
1771
+ async function readServerOutputBounded(server, timeoutMs) {
1772
+ let timeout;
1773
+ const timeoutPromise = new Promise((resolveTimeout) => {
1774
+ timeout = setTimeout(() => resolveTimeout({ kind: "timeout" }), timeoutMs);
1775
+ });
1776
+ try {
1777
+ const outcome = await Promise.race([
1778
+ server.output.then((output) => ({ kind: "output", output }), (error) => ({ kind: "error", error })),
1779
+ timeoutPromise
1780
+ ]);
1781
+ if (outcome.kind === "timeout") {
1782
+ throw new Error(`Verification server output did not settle within ${String(timeoutMs)}ms after cleanup`);
1783
+ }
1784
+ if (outcome.kind === "error") {
1785
+ throw outcome.error instanceof Error ? outcome.error : new Error(renderUnknown(outcome.error));
1786
+ }
1787
+ return tail(outcome.output, LOG_LIMIT);
1788
+ } finally {
1789
+ if (timeout !== undefined)
1790
+ clearTimeout(timeout);
1791
+ }
1792
+ }
1793
+ async function requireRegularFile(path, label) {
1794
+ let metadata;
1795
+ try {
1796
+ metadata = await stat(path);
1797
+ } catch {
1798
+ throw new Error(`${label} does not exist at its configured path`);
1799
+ }
1800
+ if (!metadata.isFile())
1801
+ throw new Error(`${label} must be a regular file`);
1802
+ }
1803
+ async function requireDirectory(path, label) {
1804
+ let metadata;
1805
+ try {
1806
+ metadata = await stat(path);
1807
+ } catch {
1808
+ throw new Error(`${label} does not exist at its configured path`);
1809
+ }
1810
+ if (!metadata.isDirectory())
1811
+ throw new Error(`${label} must be a directory`);
1812
+ }
1813
+ async function resolveExistingRealPath(path, label) {
1814
+ try {
1815
+ return await realpath(path);
1816
+ } catch {
1817
+ throw new Error(`${label} does not exist at its configured path`);
1818
+ }
1819
+ }
1820
+ async function resolveConfinedRealPath(options) {
1821
+ const resolved = await resolveExistingRealPath(options.candidate, options.label);
1822
+ if (!isWithin(options.repositoryRoot, resolved)) {
1823
+ throw new Error(`${options.label} resolves outside repositoryRoot`);
1824
+ }
1825
+ if (options.kind === "directory") {
1826
+ await requireDirectory(resolved, options.label);
1827
+ } else {
1828
+ await requireRegularFile(resolved, options.label);
1829
+ }
1830
+ return resolved;
1831
+ }
1832
+ async function resolveDirectBombadilRealPaths(config, replayPath) {
1833
+ const repositoryRoot = await resolveExistingRealPath(config.repositoryRoot, "repositoryRoot");
1834
+ await requireDirectory(repositoryRoot, "repositoryRoot");
1835
+ const specificationPath = await resolveConfinedRealPath({
1836
+ candidate: config.specificationPath,
1837
+ kind: "file",
1838
+ label: "specificationPath",
1839
+ repositoryRoot
1840
+ });
1841
+ if (!/\.[cm]?[jt]sx?$/u.test(specificationPath)) {
1842
+ throw new Error("specificationPath must resolve to a JavaScript or TypeScript file");
1843
+ }
1844
+ const serverCwd = await resolveConfinedRealPath({
1845
+ candidate: config.server.cwd,
1846
+ kind: "directory",
1847
+ label: "server.cwd",
1848
+ repositoryRoot
1849
+ });
1850
+ const resolvedReplayPath = replayPath === null ? null : await resolveConfinedRealPath({
1851
+ candidate: replayPath,
1852
+ kind: "file",
1853
+ label: "--replay",
1854
+ repositoryRoot
1855
+ });
1856
+ if (resolvedReplayPath !== null && !resolvedReplayPath.endsWith(".jsonl")) {
1857
+ throw new Error("--replay must resolve to a .jsonl trace inside repositoryRoot");
1858
+ }
1859
+ return {
1860
+ config: {
1861
+ ...config,
1862
+ repositoryRoot,
1863
+ specificationPath,
1864
+ artifactRoot: join2(repositoryRoot, "artifacts", "direct-bombadil", config.artifactName),
1865
+ bombadilExecutable: bombadilNativeBinary(repositoryRoot),
1866
+ server: { ...config.server, cwd: serverCwd }
1867
+ },
1868
+ replayPath: resolvedReplayPath
1869
+ };
1870
+ }
1871
+ async function readExactBombadilVersion(repositoryRoot) {
1872
+ const packagePath = join2(repositoryRoot, "node_modules", "@antithesishq", "bombadil", "package.json");
1873
+ let input;
1874
+ try {
1875
+ input = JSON.parse(await readFile(packagePath, "utf8"));
1876
+ } catch {
1877
+ throw new Error("The root Bombadil package metadata is missing or malformed");
1878
+ }
1879
+ if (typeof input !== "object" || input === null || Array.isArray(input) || Reflect.get(input, "version") !== EXPECTED_BOMBADIL_VERSION) {
1880
+ throw new Error(`The root Bombadil package must be exactly ${EXPECTED_BOMBADIL_VERSION}`);
1881
+ }
1882
+ return EXPECTED_BOMBADIL_VERSION;
1883
+ }
1884
+ function helpText(defaultBaseUrl) {
1885
+ return [
1886
+ "Usage: bun fuzz-browser.ts [options]",
1887
+ "",
1888
+ ` --base-url <url> Local server root (default: ${defaultBaseUrl})`,
1889
+ ` --time-limit <Ns> Random exploration limit, 12-300s (default: ${String(DEFAULT_TIME_LIMIT_SECONDS)}s)`,
1890
+ " --replay <trace> Reproduce a repository-local trace.jsonl",
1891
+ " -h, --help Show this help"
1892
+ ].join(`
1893
+ `);
1894
+ }
1895
+ async function runDirectBombadilFuzz(config, arguments_ = process2.argv.slice(2), dependencyOverrides = {}) {
1896
+ const parsed = parseDirectBombadilFuzzArguments(arguments_, config.baseUrl);
1897
+ if (parsed.kind === "help") {
1898
+ process2.stdout.write(`${helpText(config.baseUrl)}
1899
+ `);
1900
+ return { kind: "help" };
1901
+ }
1902
+ const lexicalConfig = validateDirectBombadilFuzzConfig(config, parsed.baseUrl);
1903
+ const lexicalReplayPath = resolveReplayPath(lexicalConfig.repositoryRoot, parsed.replayPath);
1904
+ const resolvedPaths = await resolveDirectBombadilRealPaths(lexicalConfig, lexicalReplayPath);
1905
+ const validated = resolvedPaths.config;
1906
+ const replayPath = resolvedPaths.replayPath;
1907
+ const dependencies = { ...defaultDependencies, ...dependencyOverrides };
1908
+ const generatedAt = dependencies.now();
1909
+ const artifactRun = await createArtifactRun({
1910
+ artifactRoot: validated.artifactRoot,
1911
+ generatedAt: generatedAt.toISOString()
1912
+ });
1913
+ const outputPath = join2(artifactRun.runDirectory, "bombadil");
1914
+ const tracePath = join2(outputPath, "trace.jsonl");
1915
+ const abortController = new AbortController;
1916
+ const invocation = createDirectBombadilInvocation({
1917
+ baseUrl: validated.baseUrl,
1918
+ bombadilExecutable: validated.bombadilExecutable,
1919
+ entryPath: validated.entryPath,
1920
+ outputPath,
1921
+ replayPath,
1922
+ repositoryRoot: validated.repositoryRoot,
1923
+ scenario: validated.scenario,
1924
+ specificationPath: validated.specificationPath,
1925
+ targetQuery: validated.targetQuery,
1926
+ timeLimitSeconds: parsed.timeLimitSeconds
1927
+ });
1928
+ const abortableInvocation = { ...invocation, abortSignal: abortController.signal };
1929
+ const serverCommand = validated.server.command.map((argument) => argument === "{port}" ? validated.port : argument);
1930
+ let bombadilVersion = null;
1931
+ let lease = null;
1932
+ let ownedServer = null;
1933
+ let processResult = null;
1934
+ let attestation = null;
1935
+ let attestationFailure = null;
1936
+ let rawTracePath = null;
1937
+ let serverOutput = "";
1938
+ let serverOutputFailure = null;
1939
+ let failure = null;
1940
+ let interruptedSignal = null;
1941
+ const interrupt = (signal) => {
1942
+ interruptedSignal ??= signal;
1943
+ abortController.abort();
1944
+ if (ownedServer?.exitCode() === null)
1945
+ ownedServer.terminate();
1946
+ };
1947
+ const interruptSignals = ["SIGINT", "SIGTERM"];
1948
+ const processSignals = process2;
1949
+ for (const signal of interruptSignals)
1950
+ processSignals.once(signal, interrupt);
1951
+ try {
1952
+ try {
1953
+ await requireRegularFile(validated.bombadilExecutable, "The root Bombadil executable");
1954
+ bombadilVersion = await readExactBombadilVersion(validated.repositoryRoot);
1955
+ if (abortController.signal.aborted)
1956
+ throw new Error("Bombadil fuzzing was interrupted");
1957
+ lease = await dependencies.acquireServer({
1958
+ baseUrl: validated.baseUrl,
1959
+ label: validated.label,
1960
+ readinessPath: validated.server.readinessPath,
1961
+ reuseExistingLocalServer: false,
1962
+ startupTimeoutMs: validated.server.startupTimeoutMs,
1963
+ startServer: () => {
1964
+ ownedServer = dependencies.spawnServer({
1965
+ command: serverCommand,
1966
+ cwd: validated.server.cwd,
1967
+ ...validated.server.env === undefined ? {} : { env: validated.server.env }
1968
+ });
1969
+ return ownedServer;
1970
+ }
1971
+ });
1972
+ let processFailure = null;
1973
+ try {
1974
+ processResult = await dependencies.runBombadil(abortableInvocation);
1975
+ } catch (error) {
1976
+ processFailure = error;
1977
+ }
1978
+ const traceMetadata = await stat(tracePath).catch(() => null);
1979
+ if (traceMetadata?.isFile() === true && traceMetadata.size > 0) {
1980
+ rawTracePath = tracePath;
1981
+ }
1982
+ try {
1983
+ attestation = await attestDirectBombadilTrace({
1984
+ expectedRoute: validated.expectedRoute,
1985
+ expectedScenario: validated.scenario,
1986
+ tracePath
1987
+ });
1988
+ } catch (error) {
1989
+ attestationFailure = error;
1990
+ }
1991
+ if (processFailure !== null) {
1992
+ throw processFailure instanceof Error ? processFailure : new Error(renderUnknown(processFailure));
1993
+ }
1994
+ if (processResult === null)
1995
+ throw new Error("Bombadil did not return a process result");
1996
+ if (processResult.termination === "timeout") {
1997
+ throw new Error(`Bombadil exceeded its ${String(invocation.wallClockTimeoutMs)}ms wall-clock limit`);
1998
+ }
1999
+ if (processResult.termination === "aborted") {
2000
+ throw new Error("Bombadil process was interrupted");
2001
+ }
2002
+ if (processResult.exitCode !== 0) {
2003
+ throw new Error(`Bombadil exited with status ${String(processResult.exitCode)}`);
2004
+ }
2005
+ if (attestationFailure !== null) {
2006
+ throw attestationFailure instanceof Error ? attestationFailure : new Error(renderUnknown(attestationFailure));
2007
+ }
2008
+ } catch (error) {
2009
+ failure = error;
2010
+ }
2011
+ const serverToStop = lease?.source === "started" ? lease.server : ownedServer;
2012
+ if (serverToStop !== null) {
2013
+ try {
2014
+ await dependencies.stopServer(serverToStop);
2015
+ } catch (error) {
2016
+ failure ??= error;
2017
+ }
2018
+ }
2019
+ const serverAfterRun = ownedServer;
2020
+ if (serverAfterRun !== null) {
2021
+ try {
2022
+ serverOutput = await readServerOutputBounded(serverAfterRun, dependencies.serverOutputTimeoutMs);
2023
+ } catch (error) {
2024
+ serverOutputFailure = error;
2025
+ failure ??= error;
2026
+ }
2027
+ }
2028
+ } finally {
2029
+ for (const signal of interruptSignals) {
2030
+ processSignals.removeListener(signal, interrupt);
2031
+ }
2032
+ }
2033
+ const capturedSignal = interruptedSignal;
2034
+ if (capturedSignal !== null && failure === null) {
2035
+ failure = new Error(`Bombadil fuzzing was interrupted by ${capturedSignal}`);
2036
+ }
2037
+ const completedAt = dependencies.now();
2038
+ const status = failure === null ? "passed" : "failed";
2039
+ const logPath = join2(artifactRun.runDirectory, "bombadil.log");
2040
+ const serverLogPath = join2(artifactRun.runDirectory, "server.log");
2041
+ const record = {
2042
+ schema: ARTIFACT_SCHEMA,
2043
+ evidenceClass: "diagnostic-fuzz",
2044
+ artifactName: validated.artifactName,
2045
+ label: validated.label,
2046
+ status,
2047
+ generatedAt: generatedAt.toISOString(),
2048
+ completedAt: completedAt.toISOString(),
2049
+ durationMs: Math.max(0, completedAt.getTime() - generatedAt.getTime()),
2050
+ scenario: validated.scenario,
2051
+ expectedRoute: validated.expectedRoute,
2052
+ baseUrl: validated.baseUrl,
2053
+ entryPath: validated.entryPath,
2054
+ targetQuery: validated.targetQuery,
2055
+ targetUrl: invocation.targetUrl,
2056
+ specificationPath: validated.specificationPath,
2057
+ replayPath,
2058
+ timeLimitSeconds: replayPath === null ? parsed.timeLimitSeconds : null,
2059
+ serverSource: lease?.source ?? null,
2060
+ bombadil: {
2061
+ version: bombadilVersion,
2062
+ executable: validated.bombadilExecutable,
2063
+ exitCode: processResult?.exitCode ?? null,
2064
+ termination: processResult?.termination ?? null,
2065
+ outputPath,
2066
+ rawTracePath,
2067
+ tracePath: attestation === null ? null : tracePath,
2068
+ logPath
2069
+ },
2070
+ server: {
2071
+ logPath: serverLogPath,
2072
+ logPresent: serverOutput.length > 0,
2073
+ outputFailure: serverOutputFailure === null ? null : renderUnknown(serverOutputFailure)
2074
+ },
2075
+ attestation,
2076
+ attestationFailure: attestationFailure === null ? null : renderUnknown(attestationFailure),
2077
+ initialDirect: attestation?.initial ?? null,
2078
+ interruptedSignal: capturedSignal,
2079
+ failure: failure === null ? null : renderUnknown(failure)
2080
+ };
2081
+ const log = [processResult?.stdout ?? "", processResult?.stderr ?? ""].filter((part) => part.length > 0).join(`
2082
+ `);
2083
+ try {
2084
+ await writeFile2(logPath, `${log}${log.length > 0 ? `
2085
+ ` : ""}`, "utf8");
2086
+ await writeFile2(serverLogPath, `${serverOutput}${serverOutput.length > 0 ? `
2087
+ ` : ""}`, "utf8");
2088
+ await writeJsonAtomically(join2(artifactRun.runDirectory, "run.json"), record);
2089
+ await writeJsonAtomically(artifactRun.manifestPath, record);
2090
+ const summary = `${status === "passed" ? "PASS" : "FAIL"} ${validated.label}; artifacts: ${artifactRun.runDirectory}; log: ${logPath}`;
2091
+ (status === "passed" ? process2.stdout : process2.stderr).write(`${summary}
2092
+ `);
2093
+ if (failure !== null) {
2094
+ throw failure instanceof Error ? failure : new Error(renderUnknown(failure));
2095
+ }
2096
+ return {
2097
+ kind: "run",
2098
+ artifactDirectory: artifactRun.runDirectory,
2099
+ manifestPath: artifactRun.manifestPath,
2100
+ status: "passed"
2101
+ };
2102
+ } finally {
2103
+ if (capturedSignal !== null) {
2104
+ process2.kill(process2.pid, capturedSignal);
2105
+ }
2106
+ }
2107
+ }
2108
+
2109
+ // src/tooling/bombadil.ts
2110
+ var attestDirectBombadilTrace2 = attestDirectBombadilTrace;
2111
+ function runDirectBombadilFuzz2(config, arguments_) {
2112
+ return arguments_ === undefined ? runDirectBombadilFuzz(config) : runDirectBombadilFuzz(config, arguments_);
2113
+ }
2114
+ export {
2115
+ runDirectBombadilFuzz2 as runDirectBombadilFuzz,
2116
+ attestDirectBombadilTrace2 as attestDirectBombadilTrace
2117
+ };