@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,1499 @@
1
+ // @bun
2
+ // src/core/result.ts
3
+ function ok(value) {
4
+ return { ok: true, value };
5
+ }
6
+ function err(error) {
7
+ return { ok: false, error };
8
+ }
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+
13
+ // src/core/ids.ts
14
+ var IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[._/-][a-z0-9]+)*$/u;
15
+ var MAX_IDENTIFIER_LENGTH = 120;
16
+ function parseIdentifier(input, kind) {
17
+ if (typeof input !== "string" || input.length === 0 || input.length > MAX_IDENTIFIER_LENGTH || !IDENTIFIER_PATTERN.test(input)) {
18
+ return err({
19
+ code: "invalid-identifier",
20
+ kind,
21
+ value: input,
22
+ message: `${kind} identifiers must be 1-${MAX_IDENTIFIER_LENGTH} lowercase ASCII characters with separated alphanumeric segments`
23
+ });
24
+ }
25
+ return ok(input);
26
+ }
27
+ function parseScenarioId(input) {
28
+ const parsed = parseIdentifier(input, "scenario");
29
+ return parsed.ok ? ok(parsed.value) : parsed;
30
+ }
31
+ function parseCoverageKey(input) {
32
+ const parsed = parseIdentifier(input, "coverage");
33
+ return parsed.ok ? ok(parsed.value) : parsed;
34
+ }
35
+
36
+ // src/core/reason.ts
37
+ function renderUnknownReason(reason, fallback = "Unknown failure") {
38
+ try {
39
+ if (typeof reason === "object" && reason !== null || typeof reason === "function") {
40
+ const message = Reflect.get(reason, "message");
41
+ if (typeof message === "string")
42
+ return message;
43
+ }
44
+ } catch {}
45
+ try {
46
+ return String(reason);
47
+ } catch {
48
+ return fallback;
49
+ }
50
+ }
51
+
52
+ // src/core/json.ts
53
+ var DEFAULT_JSON_LIMITS = Object.freeze({
54
+ maxDepth: 64,
55
+ maxNodes: 1e5,
56
+ maxStringBytes: 1048576
57
+ });
58
+ var PARSED_JSON_OPTIONS = Object.freeze({
59
+ freeze: false,
60
+ normalizeNegativeZero: false,
61
+ objectPrototype: "null",
62
+ sortObjectKeys: false
63
+ });
64
+ var CLONED_JSON_OPTIONS = Object.freeze({
65
+ freeze: false,
66
+ normalizeNegativeZero: true,
67
+ objectPrototype: "ordinary",
68
+ sortObjectKeys: true
69
+ });
70
+ var FROZEN_CLONED_JSON_OPTIONS = Object.freeze({
71
+ freeze: true,
72
+ normalizeNegativeZero: true,
73
+ objectPrototype: "ordinary",
74
+ sortObjectKeys: true
75
+ });
76
+ function jsonError(code, path, message) {
77
+ return { code, path, message };
78
+ }
79
+ function utf8ByteLength(value) {
80
+ let bytes = 0;
81
+ for (let index = 0;index < value.length; index += 1) {
82
+ const code = value.charCodeAt(index);
83
+ if (code <= 127) {
84
+ bytes += 1;
85
+ } else if (code <= 2047) {
86
+ bytes += 2;
87
+ } else if (code >= 55296 && code <= 56319 && index + 1 < value.length) {
88
+ const next = value.charCodeAt(index + 1);
89
+ if (next >= 56320 && next <= 57343) {
90
+ bytes += 4;
91
+ index += 1;
92
+ } else {
93
+ bytes += 3;
94
+ }
95
+ } else {
96
+ bytes += 3;
97
+ }
98
+ }
99
+ return bytes;
100
+ }
101
+ function parseJsonAt(input, path, depth, limits, budget, ancestors, options) {
102
+ budget.nodes += 1;
103
+ if (budget.nodes > limits.maxNodes) {
104
+ return err(jsonError("node-limit-exceeded", path, `JSON value exceeds ${limits.maxNodes} nodes`));
105
+ }
106
+ if (depth > limits.maxDepth) {
107
+ return err(jsonError("depth-exceeded", path, `JSON value exceeds depth ${limits.maxDepth}`));
108
+ }
109
+ if (input === null || typeof input === "boolean") {
110
+ return ok(input);
111
+ }
112
+ if (typeof input === "string") {
113
+ budget.stringBytes += utf8ByteLength(input);
114
+ if (budget.stringBytes > limits.maxStringBytes) {
115
+ return err(jsonError("string-limit-exceeded", path, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
116
+ }
117
+ return ok(input);
118
+ }
119
+ if (typeof input === "number") {
120
+ return Number.isFinite(input) ? ok(options.normalizeNegativeZero && Object.is(input, -0) ? 0 : input) : err(jsonError("invalid-number", path, "JSON numbers must be finite"));
121
+ }
122
+ if (typeof input !== "object") {
123
+ return err(jsonError("invalid-type", path, `${typeof input} is not a JSON value`));
124
+ }
125
+ if (ancestors.has(input)) {
126
+ return err(jsonError("cycle", path, "JSON values cannot contain cycles"));
127
+ }
128
+ const nextAncestors = new Set(ancestors);
129
+ nextAncestors.add(input);
130
+ if (Array.isArray(input)) {
131
+ if (Object.getPrototypeOf(input) !== Array.prototype) {
132
+ return err(jsonError("invalid-object", path, "JSON arrays must have the standard Array prototype"));
133
+ }
134
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(input, "length");
135
+ if (lengthDescriptor === undefined || lengthDescriptor.get !== undefined || lengthDescriptor.set !== undefined || !Number.isSafeInteger(lengthDescriptor.value) || lengthDescriptor.value < 0) {
136
+ return err(jsonError("invalid-object", path, "JSON arrays must have a valid data length"));
137
+ }
138
+ const length = lengthDescriptor.value;
139
+ for (const key of Reflect.ownKeys(input)) {
140
+ if (typeof key === "symbol") {
141
+ return err(jsonError("symbol-key", path, "JSON arrays cannot have symbol keys"));
142
+ }
143
+ if (key === "length")
144
+ continue;
145
+ const index = Number(key);
146
+ if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== key) {
147
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON arrays cannot have extra properties"));
148
+ }
149
+ }
150
+ const output2 = [];
151
+ for (let index = 0;index < length; index += 1) {
152
+ const descriptor = Object.getOwnPropertyDescriptor(input, index);
153
+ if (descriptor === undefined) {
154
+ return err(jsonError("invalid-object", `${path}[${index}]`, "Sparse arrays are not exact JSON values"));
155
+ }
156
+ if (descriptor.get !== undefined || descriptor.set !== undefined) {
157
+ return err(jsonError("accessor-property", `${path}[${index}]`, "JSON arrays must use data elements"));
158
+ }
159
+ if (!descriptor.enumerable) {
160
+ return err(jsonError("invalid-object", `${path}[${index}]`, "JSON array elements must be enumerable"));
161
+ }
162
+ const item = parseJsonAt(descriptor.value, `${path}[${index}]`, depth + 1, limits, budget, nextAncestors, options);
163
+ if (!item.ok) {
164
+ return item;
165
+ }
166
+ output2.push(item.value);
167
+ }
168
+ return ok(options.freeze ? Object.freeze(output2) : output2);
169
+ }
170
+ const prototype = Object.getPrototypeOf(input);
171
+ if (prototype !== Object.prototype && prototype !== null) {
172
+ return err(jsonError("invalid-object", path, "JSON objects must have Object or null prototypes"));
173
+ }
174
+ const output = options.objectPrototype === "ordinary" ? {} : Object.create(null);
175
+ const entries = options.sortObjectKeys ? [] : null;
176
+ for (const key of Reflect.ownKeys(input)) {
177
+ if (typeof key === "symbol") {
178
+ return err(jsonError("symbol-key", path, "JSON objects cannot have symbol keys"));
179
+ }
180
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
181
+ if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined) {
182
+ return err(jsonError("accessor-property", `${path}.${key}`, "JSON objects must use data properties"));
183
+ }
184
+ if (!descriptor.enumerable) {
185
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON object properties must be enumerable"));
186
+ }
187
+ budget.stringBytes += utf8ByteLength(key);
188
+ if (budget.stringBytes > limits.maxStringBytes) {
189
+ return err(jsonError("string-limit-exceeded", `${path}.${key}`, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
190
+ }
191
+ const child = parseJsonAt(descriptor.value, `${path}.${key}`, depth + 1, limits, budget, nextAncestors, options);
192
+ if (!child.ok) {
193
+ return child;
194
+ }
195
+ if (entries === null) {
196
+ output[key] = child.value;
197
+ } else {
198
+ entries.push([key, child.value]);
199
+ }
200
+ }
201
+ if (entries !== null) {
202
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
203
+ for (const [key, value] of entries) {
204
+ Object.defineProperty(output, key, {
205
+ configurable: true,
206
+ enumerable: true,
207
+ value,
208
+ writable: true
209
+ });
210
+ }
211
+ }
212
+ return ok(options.freeze ? Object.freeze(output) : output);
213
+ }
214
+ function validateAndCloneJson(input, limits, options) {
215
+ if (!Number.isSafeInteger(limits.maxDepth) || limits.maxDepth < 0 || !Number.isSafeInteger(limits.maxNodes) || limits.maxNodes < 1 || !Number.isSafeInteger(limits.maxStringBytes) || limits.maxStringBytes < 0) {
216
+ throw new Error("JSON limits must be non-negative safe integers and allow at least one node");
217
+ }
218
+ try {
219
+ return parseJsonAt(input, "$", 0, limits, { nodes: 0, stringBytes: 0 }, new Set, options);
220
+ } catch (reason) {
221
+ return err(jsonError("invalid-object", "$", renderUnknownReason(reason, "JSON object inspection failed")));
222
+ }
223
+ }
224
+ function parseJsonValue(input, limits = DEFAULT_JSON_LIMITS) {
225
+ return validateAndCloneJson(input, limits, PARSED_JSON_OPTIONS);
226
+ }
227
+ function canonicalize(value) {
228
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
229
+ return JSON.stringify(value);
230
+ }
231
+ if (Array.isArray(value)) {
232
+ return `[${value.map(canonicalize).join(",")}]`;
233
+ }
234
+ const entries = Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => `${JSON.stringify(key)}:${canonicalize(child)}`);
235
+ return `{${entries.join(",")}}`;
236
+ }
237
+ function canonicalJson(input, limits = DEFAULT_JSON_LIMITS) {
238
+ const parsed = parseJsonValue(input, limits);
239
+ return parsed.ok ? ok(canonicalize(parsed.value)) : parsed;
240
+ }
241
+ function freezeJson(value) {
242
+ if (value !== null && typeof value === "object") {
243
+ for (const child of Array.isArray(value) ? value : Object.values(value)) {
244
+ freezeJson(child);
245
+ }
246
+ Object.freeze(value);
247
+ }
248
+ return value;
249
+ }
250
+ var STABLE_HASH_ALGORITHM = "fnv1a-64";
251
+ var TAGGED_STABLE_HASH_PATTERN = /^fnv1a-64:[0-9a-f]{16}$/u;
252
+ function tagStableHash(hash) {
253
+ return `${hash.algorithm}:${hash.value}`;
254
+ }
255
+ function parseTaggedStableHash(input) {
256
+ return typeof input === "string" && TAGGED_STABLE_HASH_PATTERN.test(input) ? ok(input) : err({
257
+ code: "invalid-stable-hash",
258
+ message: `Stable hashes must use ${STABLE_HASH_ALGORITHM} with 16 lowercase hexadecimal digits`
259
+ });
260
+ }
261
+ function updateFnvByte(hash, byte) {
262
+ return BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
263
+ }
264
+ function stableHash(input, limits = DEFAULT_JSON_LIMITS) {
265
+ const serialized = canonicalJson(input, limits);
266
+ if (!serialized.ok) {
267
+ return serialized;
268
+ }
269
+ let hash = 0xcbf29ce484222325n;
270
+ for (let index = 0;index < serialized.value.length; index += 1) {
271
+ const code = serialized.value.charCodeAt(index);
272
+ if (code <= 127) {
273
+ hash = updateFnvByte(hash, code);
274
+ } else if (code <= 2047) {
275
+ hash = updateFnvByte(hash, 192 | code >> 6);
276
+ hash = updateFnvByte(hash, 128 | code & 63);
277
+ } else if (code >= 55296 && code <= 56319 && index + 1 < serialized.value.length) {
278
+ const next = serialized.value.charCodeAt(index + 1);
279
+ if (next >= 56320 && next <= 57343) {
280
+ const point = 65536 + (code - 55296 << 10) + (next - 56320);
281
+ hash = updateFnvByte(hash, 240 | point >> 18);
282
+ hash = updateFnvByte(hash, 128 | point >> 12 & 63);
283
+ hash = updateFnvByte(hash, 128 | point >> 6 & 63);
284
+ hash = updateFnvByte(hash, 128 | point & 63);
285
+ index += 1;
286
+ } else {
287
+ hash = updateFnvByte(hash, 239);
288
+ hash = updateFnvByte(hash, 191);
289
+ hash = updateFnvByte(hash, 189);
290
+ }
291
+ } else {
292
+ hash = updateFnvByte(hash, 224 | code >> 12);
293
+ hash = updateFnvByte(hash, 128 | code >> 6 & 63);
294
+ hash = updateFnvByte(hash, 128 | code & 63);
295
+ }
296
+ }
297
+ return ok({
298
+ algorithm: STABLE_HASH_ALGORITHM,
299
+ value: hash.toString(16).padStart(16, "0")
300
+ });
301
+ }
302
+
303
+ // src/core/coverage.ts
304
+ var DIRECT_COVERAGE_SCHEMA = "direct.coverage/v2";
305
+ var MAX_DIRECT_COVERAGE_ENTRIES = 256;
306
+ var DIRECT_COVERAGE_JSON_LIMITS = Object.freeze({
307
+ ...DEFAULT_JSON_LIMITS,
308
+ maxStringBytes: 16777216
309
+ });
310
+ var EMPTY_COVERAGE_CATALOG_SNAPSHOT = Object.freeze({
311
+ schema: DIRECT_COVERAGE_SCHEMA,
312
+ entries: Object.freeze([])
313
+ });
314
+ function coverageError(code, message, keys = []) {
315
+ return { code, message, keys };
316
+ }
317
+ function hasControlCharacters(value) {
318
+ for (const character of value) {
319
+ const code = character.charCodeAt(0);
320
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) {
321
+ return true;
322
+ }
323
+ }
324
+ return false;
325
+ }
326
+ var COVERAGE_ENTRY_KEYS = new Set(["key", "mode", "claim", "scenarios"]);
327
+ var COVERAGE_SNAPSHOT_KEYS = new Set(["schema", "entries"]);
328
+ function isStringArray(value) {
329
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string");
330
+ }
331
+ function createCoverageCatalogSnapshot(catalog) {
332
+ return Object.freeze({
333
+ schema: DIRECT_COVERAGE_SCHEMA,
334
+ entries: catalog.list()
335
+ });
336
+ }
337
+ function parseCoverageCatalogSnapshot(input, limits = DIRECT_COVERAGE_JSON_LIMITS) {
338
+ const parsed = parseJsonValue(input, limits);
339
+ if (!parsed.ok || !isRecord(parsed.value)) {
340
+ return err(coverageError("invalid-coverage", parsed.ok ? "Coverage snapshot must be an object" : parsed.error.message));
341
+ }
342
+ for (const key of Object.keys(parsed.value)) {
343
+ if (!COVERAGE_SNAPSHOT_KEYS.has(key)) {
344
+ return err(coverageError("invalid-coverage", `Unknown coverage snapshot key: ${key}`));
345
+ }
346
+ }
347
+ if (parsed.value.schema !== DIRECT_COVERAGE_SCHEMA) {
348
+ return err(coverageError("invalid-coverage", `Coverage snapshot schema must be ${DIRECT_COVERAGE_SCHEMA}`));
349
+ }
350
+ if (!Array.isArray(parsed.value.entries)) {
351
+ return err(coverageError("invalid-coverage", "Coverage snapshot entries must be an array"));
352
+ }
353
+ const entries = [];
354
+ for (const [index, candidate] of parsed.value.entries.entries()) {
355
+ if (!isRecord(candidate)) {
356
+ return err(coverageError("invalid-coverage", `Coverage entry ${String(index)} must be an object`));
357
+ }
358
+ for (const key of Object.keys(candidate)) {
359
+ if (!COVERAGE_ENTRY_KEYS.has(key)) {
360
+ return err(coverageError("invalid-coverage", `Unknown coverage entry key at ${String(index)}: ${key}`));
361
+ }
362
+ }
363
+ if (typeof candidate.key !== "string" || typeof candidate.claim !== "string" || candidate.mode !== "fixture" && candidate.mode !== "mixed" && candidate.mode !== "direct" || !isStringArray(candidate.scenarios)) {
364
+ return err(coverageError("invalid-coverage", `Coverage entry ${String(index)} has an invalid wire shape`));
365
+ }
366
+ if (candidate.mode === "direct") {
367
+ if (candidate.scenarios.length > 0) {
368
+ return err(coverageError("invalid-mode", `Direct coverage ${candidate.key} cannot cite fixture scenarios`, [candidate.key]));
369
+ }
370
+ entries.push({
371
+ key: candidate.key,
372
+ mode: candidate.mode,
373
+ claim: candidate.claim,
374
+ scenarios: []
375
+ });
376
+ } else {
377
+ const firstScenario = candidate.scenarios[0];
378
+ if (typeof firstScenario !== "string") {
379
+ return err(coverageError("invalid-mode", `${candidate.mode} coverage ${candidate.key} must cite at least one scenario`, [candidate.key]));
380
+ }
381
+ entries.push({
382
+ key: candidate.key,
383
+ mode: candidate.mode,
384
+ claim: candidate.claim,
385
+ scenarios: [firstScenario, ...candidate.scenarios.slice(1)]
386
+ });
387
+ }
388
+ }
389
+ const catalog = createCoverageCatalog(entries);
390
+ return catalog.ok ? ok(createCoverageCatalogSnapshot(catalog.value)) : catalog;
391
+ }
392
+ function createCoverageCatalog(inputs, scenarios) {
393
+ if (inputs.length > MAX_DIRECT_COVERAGE_ENTRIES) {
394
+ return err(coverageError("too-many-coverage-entries", `Direct definitions support at most ${String(MAX_DIRECT_COVERAGE_ENTRIES)} coverage entries`));
395
+ }
396
+ const entries = [];
397
+ const byKey = new Map;
398
+ for (const input of inputs) {
399
+ const key = parseCoverageKey(input.key);
400
+ if (!key.ok) {
401
+ return err(coverageError("invalid-coverage", key.error.message, [String(input.key)]));
402
+ }
403
+ if (byKey.has(key.value)) {
404
+ return err(coverageError("duplicate-coverage", `Duplicate coverage key: ${key.value}`, [key.value]));
405
+ }
406
+ if (input.claim.trim().length === 0 || input.claim.length > 1000 || hasControlCharacters(input.claim)) {
407
+ return err(coverageError("invalid-claim", `Coverage ${key.value} needs a 1-1000 character claim`, [key.value]));
408
+ }
409
+ if (input.mode !== "fixture" && input.mode !== "mixed" && input.mode !== "direct") {
410
+ return err(coverageError("invalid-mode", `Coverage ${key.value} has an unknown proof mode`, [key.value]));
411
+ }
412
+ if (input.mode === "direct" && input.scenarios.length > 0) {
413
+ return err(coverageError("invalid-mode", `Direct coverage ${key.value} cannot cite fixture scenarios`, [key.value]));
414
+ }
415
+ if (input.mode !== "direct" && input.scenarios.length === 0) {
416
+ return err(coverageError("invalid-mode", `${input.mode} coverage ${key.value} must cite at least one scenario`, [key.value]));
417
+ }
418
+ const scenarioIds = [];
419
+ const seenScenarios = new Set;
420
+ for (const candidate of input.scenarios) {
421
+ const id = parseScenarioId(candidate);
422
+ if (!id.ok) {
423
+ return err(coverageError("invalid-scenario", id.error.message, [String(candidate)]));
424
+ }
425
+ if (seenScenarios.has(id.value)) {
426
+ return err(coverageError("invalid-scenario", `Coverage ${key.value} repeats scenario ${id.value}`, [id.value]));
427
+ }
428
+ if (scenarios !== undefined && scenarios.get(id.value) === undefined) {
429
+ return err(coverageError("unknown-scenario", `Coverage ${key.value} cites unknown scenario ${id.value}`, [id.value]));
430
+ }
431
+ seenScenarios.add(id.value);
432
+ scenarioIds.push(id.value);
433
+ }
434
+ let entry;
435
+ if (input.mode === "direct") {
436
+ const scenarios2 = Object.freeze([]);
437
+ entry = Object.freeze({
438
+ key: key.value,
439
+ mode: input.mode,
440
+ claim: input.claim,
441
+ scenarios: scenarios2
442
+ });
443
+ } else {
444
+ const firstScenarioId = scenarioIds[0];
445
+ if (firstScenarioId === undefined) {
446
+ return err(coverageError("invalid-mode", `${input.mode} coverage ${key.value} must cite at least one scenario`, [key.value]));
447
+ }
448
+ const scenarios2 = Object.freeze([
449
+ firstScenarioId,
450
+ ...scenarioIds.slice(1)
451
+ ]);
452
+ entry = Object.freeze({
453
+ key: key.value,
454
+ mode: input.mode,
455
+ claim: input.claim,
456
+ scenarios: scenarios2
457
+ });
458
+ }
459
+ entries.push(entry);
460
+ byKey.set(key.value, entry);
461
+ }
462
+ const frozenEntries = Object.freeze(entries);
463
+ const keys = Object.freeze(frozenEntries.map((entry) => entry.key));
464
+ const catalog = {
465
+ size: frozenEntries.length,
466
+ keys: () => keys,
467
+ list: () => frozenEntries,
468
+ get: (key) => byKey.get(key),
469
+ resolve: (input) => {
470
+ const key = parseCoverageKey(input);
471
+ if (!key.ok) {
472
+ return err(coverageError("invalid-coverage", key.error.message, [String(input)]));
473
+ }
474
+ const entry = byKey.get(key.value);
475
+ return entry === undefined ? err(coverageError("unknown-coverage", `Unknown coverage key: ${key.value}`, [key.value])) : ok(entry);
476
+ },
477
+ requireExactKeys: (expected) => {
478
+ const expectedKeys = [];
479
+ const seen = new Set;
480
+ for (const candidate of expected) {
481
+ const parsed = parseCoverageKey(candidate);
482
+ if (!parsed.ok) {
483
+ return err(coverageError("invalid-coverage", parsed.error.message, [String(candidate)]));
484
+ }
485
+ if (seen.has(parsed.value)) {
486
+ return err(coverageError("duplicate-expected-key", `Expected coverage repeats ${parsed.value}`, [parsed.value]));
487
+ }
488
+ seen.add(parsed.value);
489
+ expectedKeys.push(parsed.value);
490
+ }
491
+ const missing = expectedKeys.filter((key) => !byKey.has(key));
492
+ if (missing.length > 0) {
493
+ return err(coverageError("missing-coverage", `Missing coverage keys: ${missing.join(", ")}`, missing));
494
+ }
495
+ const unexpected = keys.filter((key) => !seen.has(key));
496
+ if (unexpected.length > 0) {
497
+ return err(coverageError("unexpected-coverage", `Unexpected coverage keys: ${unexpected.join(", ")}`, unexpected));
498
+ }
499
+ return ok(true);
500
+ }
501
+ };
502
+ return ok(Object.freeze(catalog));
503
+ }
504
+ // src/core/fixture.ts
505
+ var DEFAULT_MAX_FIXTURE_BYTES = 65536;
506
+ var FIXTURE_KEYS = new Set(["schema", "scenario", "route", "world", "runtime"]);
507
+
508
+ // src/core/query.ts
509
+ var SCENARIO_QUERY_KEY = "__direct_scenario";
510
+ var FIXTURE_QUERY_KEY = "__direct_fixture";
511
+ var FIXTURE_QUERY_PREFIX_BYTES = utf8ByteLength(`?${FIXTURE_QUERY_KEY}=`);
512
+ function maximumFixtureQueryBytes(maxFixtureBytes) {
513
+ return maxFixtureBytes * 3 + FIXTURE_QUERY_PREFIX_BYTES;
514
+ }
515
+ var DEFAULT_MAX_QUERY_BYTES = maximumFixtureQueryBytes(DEFAULT_MAX_FIXTURE_BYTES);
516
+
517
+ // src/core/scenario.ts
518
+ var MAX_DIRECT_SCENARIOS = 256;
519
+
520
+ // src/testing/manifest.ts
521
+ var DIRECT_SESSION_MANIFEST_SCHEMA = "direct.session-manifest/v1";
522
+ var DIRECT_CATALOG_HASH_ALGORITHM = STABLE_HASH_ALGORITHM;
523
+ var DIRECT_SESSION_MANIFEST_JSON_LIMITS = Object.freeze({
524
+ maxDepth: 64,
525
+ maxNodes: 1e5,
526
+ maxStringBytes: 16777216
527
+ });
528
+ var MANIFEST_KEYS = new Set([
529
+ "active",
530
+ "catalogHash",
531
+ "coverage",
532
+ "defaultScenario",
533
+ "queries",
534
+ "scenarios",
535
+ "schema"
536
+ ]);
537
+ var QUERY_KEYS = new Set(["fixture", "scenario"]);
538
+ var ACTIVE_KEYS = new Set([
539
+ "activationHash",
540
+ "route",
541
+ "scenario",
542
+ "selectionHash",
543
+ "source"
544
+ ]);
545
+ var SCENARIO_KEYS = new Set([
546
+ "description",
547
+ "id",
548
+ "route",
549
+ "title"
550
+ ]);
551
+ function manifestError(code, message) {
552
+ return Object.freeze({ code, message });
553
+ }
554
+ function exactKeys(input, expected, label) {
555
+ for (const key of Object.keys(input)) {
556
+ if (!expected.has(key))
557
+ throw new Error(`Unknown ${label} key: ${key}`);
558
+ }
559
+ for (const key of expected) {
560
+ if (!Object.hasOwn(input, key))
561
+ throw new Error(`Missing ${label} key: ${key}`);
562
+ }
563
+ }
564
+ function hasControlCharacters2(value) {
565
+ for (const character of value) {
566
+ const code = character.charCodeAt(0);
567
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) {
568
+ return true;
569
+ }
570
+ }
571
+ return false;
572
+ }
573
+ function validText(value, maximum) {
574
+ return value.trim().length > 0 && value.length <= maximum && !hasControlCharacters2(value);
575
+ }
576
+ function validRoute(value) {
577
+ if (value.trim().length === 0 || value.length > 256)
578
+ return false;
579
+ for (const character of value) {
580
+ const code = character.charCodeAt(0);
581
+ if (code < 32 || code === 127)
582
+ return false;
583
+ }
584
+ return true;
585
+ }
586
+ function parseTaggedHash(value, label) {
587
+ const parsed = parseTaggedStableHash(value);
588
+ if (!parsed.ok)
589
+ throw new Error(`${label}: ${parsed.error.message}`);
590
+ return parsed.value;
591
+ }
592
+ function selectionHash(payload) {
593
+ const hashed = stableHash(payload, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
594
+ if (!hashed.ok) {
595
+ return err(manifestError("invalid-manifest", hashed.error.message));
596
+ }
597
+ return ok(tagStableHash(hashed.value));
598
+ }
599
+ function catalogHash(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 parseManifestUnchecked(input) {
607
+ const parsedJson = parseJsonValue(input, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
608
+ if (!parsedJson.ok || !isRecord(parsedJson.value)) {
609
+ return err(manifestError("invalid-manifest", parsedJson.ok ? "Direct session manifest must be an object" : parsedJson.error.message));
610
+ }
611
+ const candidate = parsedJson.value;
612
+ exactKeys(candidate, MANIFEST_KEYS, "Direct session manifest");
613
+ if (candidate.schema !== DIRECT_SESSION_MANIFEST_SCHEMA) {
614
+ throw new Error(`Direct session manifest schema must be ${DIRECT_SESSION_MANIFEST_SCHEMA}`);
615
+ }
616
+ if (!isRecord(candidate.queries)) {
617
+ throw new Error("Direct session manifest queries must be an object");
618
+ }
619
+ exactKeys(candidate.queries, QUERY_KEYS, "Direct session manifest queries");
620
+ if (candidate.queries.scenario !== SCENARIO_QUERY_KEY || candidate.queries.fixture !== FIXTURE_QUERY_KEY) {
621
+ throw new Error("Direct session manifest query keys do not match Direct");
622
+ }
623
+ const queries = Object.freeze({
624
+ scenario: SCENARIO_QUERY_KEY,
625
+ fixture: FIXTURE_QUERY_KEY
626
+ });
627
+ const defaultScenario = parseScenarioId(candidate.defaultScenario);
628
+ if (!defaultScenario.ok) {
629
+ throw new Error(`Invalid default scenario: ${defaultScenario.error.message}`);
630
+ }
631
+ if (!Array.isArray(candidate.scenarios)) {
632
+ throw new Error("Direct session manifest scenarios must be an array");
633
+ }
634
+ if (candidate.scenarios.length > MAX_DIRECT_SCENARIOS) {
635
+ throw new Error(`Direct session manifests support at most ${String(MAX_DIRECT_SCENARIOS)} scenarios`);
636
+ }
637
+ const scenarios = [];
638
+ const byId = new Map;
639
+ for (const [index, rawScenario] of candidate.scenarios.entries()) {
640
+ if (!isRecord(rawScenario)) {
641
+ throw new Error(`Direct session manifest scenario ${String(index)} must be an object`);
642
+ }
643
+ exactKeys(rawScenario, SCENARIO_KEYS, `Direct session manifest scenario ${String(index)}`);
644
+ const id = parseScenarioId(rawScenario.id);
645
+ if (!id.ok) {
646
+ throw new Error(`Invalid Direct session manifest scenario ${String(index)}: ${id.error.message}`);
647
+ }
648
+ if (byId.has(id.value)) {
649
+ return err(manifestError("duplicate-scenario", `Duplicate Direct session manifest scenario: ${id.value}`));
650
+ }
651
+ if (typeof rawScenario.title !== "string" || !validText(rawScenario.title, 160)) {
652
+ throw new Error(`Direct session manifest scenario ${id.value} title must contain 1-160 visible characters`);
653
+ }
654
+ if (rawScenario.description !== null && (typeof rawScenario.description !== "string" || !validText(rawScenario.description, 2000))) {
655
+ throw new Error(`Direct session manifest scenario ${id.value} description must be null or contain 1-2000 visible characters`);
656
+ }
657
+ if (typeof rawScenario.route !== "string" || !validRoute(rawScenario.route)) {
658
+ throw new Error(`Direct session manifest scenario ${id.value} route must contain 1-256 visible characters`);
659
+ }
660
+ const scenario = Object.freeze({
661
+ id: id.value,
662
+ title: rawScenario.title,
663
+ description: rawScenario.description,
664
+ route: rawScenario.route
665
+ });
666
+ scenarios.push(scenario);
667
+ byId.set(id.value, scenario);
668
+ }
669
+ const frozenScenarios = Object.freeze(scenarios);
670
+ if (!byId.has(defaultScenario.value)) {
671
+ return err(manifestError("unknown-scenario", `Direct session manifest default scenario is missing: ${defaultScenario.value}`));
672
+ }
673
+ if (!isRecord(candidate.active)) {
674
+ throw new Error("Direct session manifest active selection must be an object");
675
+ }
676
+ exactKeys(candidate.active, ACTIVE_KEYS, "Direct session manifest active selection");
677
+ if (candidate.active.source !== "scenario" && candidate.active.source !== "fixture") {
678
+ throw new Error("Direct session manifest active source must be scenario or fixture");
679
+ }
680
+ const activeScenario = parseScenarioId(candidate.active.scenario);
681
+ if (!activeScenario.ok) {
682
+ throw new Error(`Invalid active scenario: ${activeScenario.error.message}`);
683
+ }
684
+ const activeDefinition = byId.get(activeScenario.value);
685
+ if (activeDefinition === undefined) {
686
+ return err(manifestError("unknown-scenario", `Direct session manifest active scenario is missing: ${activeScenario.value}`));
687
+ }
688
+ if (typeof candidate.active.route !== "string" || !validRoute(candidate.active.route)) {
689
+ throw new Error("Direct session manifest active route is invalid");
690
+ }
691
+ if (candidate.active.route !== activeDefinition.route) {
692
+ return err(manifestError("route-mismatch", `Direct session manifest active route does not match scenario ${activeScenario.value}`));
693
+ }
694
+ const activationHash = parseTaggedHash(candidate.active.activationHash, "Direct session manifest activationHash");
695
+ let suppliedSelectionHash;
696
+ try {
697
+ suppliedSelectionHash = parseTaggedHash(candidate.active.selectionHash, "Direct session manifest selectionHash");
698
+ } catch (reason) {
699
+ return err(manifestError("invalid-selection-hash", renderUnknownReason(reason, "Direct session manifest selectionHash is invalid")));
700
+ }
701
+ const expectedSelectionHash = selectionHash({
702
+ source: candidate.active.source,
703
+ scenario: activeScenario.value,
704
+ route: activeDefinition.route,
705
+ activationHash
706
+ });
707
+ if (!expectedSelectionHash.ok)
708
+ return expectedSelectionHash;
709
+ if (suppliedSelectionHash !== expectedSelectionHash.value) {
710
+ return err(manifestError("selection-hash-mismatch", "Direct session manifest selectionHash does not match its active selection"));
711
+ }
712
+ const active = Object.freeze({
713
+ source: candidate.active.source,
714
+ scenario: activeScenario.value,
715
+ route: activeDefinition.route,
716
+ activationHash,
717
+ selectionHash: expectedSelectionHash.value
718
+ });
719
+ const coverage = parseCoverageCatalogSnapshot(candidate.coverage, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
720
+ if (!coverage.ok) {
721
+ throw new Error(coverage.error.message);
722
+ }
723
+ for (const entry of coverage.value.entries) {
724
+ for (const scenario of entry.scenarios) {
725
+ if (!byId.has(scenario)) {
726
+ return err(manifestError("unknown-coverage-scenario", `Coverage ${entry.key} cites unknown Direct session manifest scenario ${scenario}`));
727
+ }
728
+ }
729
+ }
730
+ let suppliedCatalogHash;
731
+ try {
732
+ const parsedHash = parseTaggedHash(candidate.catalogHash, "Direct session manifest catalogHash");
733
+ const separator = parsedHash.indexOf(":");
734
+ suppliedCatalogHash = `${DIRECT_CATALOG_HASH_ALGORITHM}:${parsedHash.slice(separator + 1)}`;
735
+ } catch (reason) {
736
+ return err(manifestError("invalid-catalog-hash", renderUnknownReason(reason, "Direct session manifest catalogHash is invalid")));
737
+ }
738
+ const expectedCatalogHash = catalogHash({
739
+ queries,
740
+ defaultScenario: defaultScenario.value,
741
+ scenarios: frozenScenarios,
742
+ coverage: coverage.value
743
+ });
744
+ if (!expectedCatalogHash.ok)
745
+ return expectedCatalogHash;
746
+ if (suppliedCatalogHash !== expectedCatalogHash.value) {
747
+ return err(manifestError("catalog-hash-mismatch", "Direct session manifest catalogHash does not match its public catalog"));
748
+ }
749
+ return ok(Object.freeze({
750
+ schema: DIRECT_SESSION_MANIFEST_SCHEMA,
751
+ catalogHash: expectedCatalogHash.value,
752
+ queries,
753
+ defaultScenario: defaultScenario.value,
754
+ active,
755
+ scenarios: frozenScenarios,
756
+ coverage: coverage.value
757
+ }));
758
+ }
759
+ function parseDirectSessionManifest(input) {
760
+ try {
761
+ return parseManifestUnchecked(input);
762
+ } catch (reason) {
763
+ return err(manifestError("invalid-manifest", renderUnknownReason(reason, "Direct session manifest is invalid")));
764
+ }
765
+ }
766
+ // src/testing/probe.ts
767
+ var DIRECT_PROBE_SCHEMA = "direct.probe/v1";
768
+ var MAX_DIRECT_PROBE_COUNTERS = 128;
769
+ var COUNTER_NAME_PATTERN = /^[a-z][A-Za-z0-9]*(?:[.-][A-Za-z0-9]+)*$/u;
770
+ var SNAPSHOT_KEYS = new Set([
771
+ "schema",
772
+ "activationHash",
773
+ "generation",
774
+ "revision",
775
+ "activity",
776
+ "pending",
777
+ "violations",
778
+ "remainingWork",
779
+ "isQuiescent"
780
+ ]);
781
+ var ACTIVITY_KEYS = new Set(["active", "started", "settled"]);
782
+ function probeError(code, message, counter = null) {
783
+ return Object.freeze({ code, message, counter });
784
+ }
785
+ function readNonNegativeInteger(input) {
786
+ return typeof input === "number" && Number.isSafeInteger(input) && input >= 0 ? input : null;
787
+ }
788
+ function parseSnapshotCounters(input, category) {
789
+ if (!isRecord(input)) {
790
+ return err(probeError("invalid-snapshot", `Probe ${category} counters must be an object`));
791
+ }
792
+ const output = Object.create(null);
793
+ for (const [name, candidate] of Object.entries(input)) {
794
+ if (name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
795
+ return err(probeError("invalid-counter-name", "Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens", name));
796
+ }
797
+ const value = readNonNegativeInteger(candidate);
798
+ if (value === null) {
799
+ return err(probeError("invalid-counter", `Counter ${name} must be a non-negative safe integer`, name));
800
+ }
801
+ output[name] = value;
802
+ }
803
+ return ok(Object.freeze(output));
804
+ }
805
+ function parseDirectProbeSnapshot(input) {
806
+ const parsed = parseJsonValue(input);
807
+ if (!parsed.ok || !isRecord(parsed.value)) {
808
+ return err(probeError("invalid-snapshot", parsed.ok ? "Direct probe snapshot must be an object" : parsed.error.message));
809
+ }
810
+ const record = parsed.value;
811
+ for (const key of Object.keys(record)) {
812
+ if (!SNAPSHOT_KEYS.has(key)) {
813
+ return err(probeError("invalid-snapshot", `Unknown Direct probe snapshot key: ${key}`));
814
+ }
815
+ }
816
+ if (record.schema !== DIRECT_PROBE_SCHEMA) {
817
+ return err(probeError("invalid-snapshot", `Direct probe schema must be ${DIRECT_PROBE_SCHEMA}`));
818
+ }
819
+ const activationHash = parseTaggedStableHash(record.activationHash);
820
+ if (!activationHash.ok) {
821
+ return err(probeError("invalid-activation-hash", "Direct probe activation hash is invalid"));
822
+ }
823
+ const generation = readNonNegativeInteger(record.generation);
824
+ const revision = readNonNegativeInteger(record.revision);
825
+ if (generation === null || generation < 1 || revision === null) {
826
+ return err(probeError("invalid-snapshot", "Direct probe generation must be positive and revision must be non-negative"));
827
+ }
828
+ if (generation - 1 > revision) {
829
+ return err(probeError("invalid-snapshot", "Direct probe generation cannot exceed revision plus one"));
830
+ }
831
+ if (!isRecord(record.activity)) {
832
+ return err(probeError("invalid-snapshot", "Direct probe activity must be an object"));
833
+ }
834
+ for (const key of Object.keys(record.activity)) {
835
+ if (!ACTIVITY_KEYS.has(key)) {
836
+ return err(probeError("invalid-snapshot", `Unknown Direct activity key: ${key}`));
837
+ }
838
+ }
839
+ const active = readNonNegativeInteger(record.activity.active);
840
+ const started = readNonNegativeInteger(record.activity.started);
841
+ const settled = readNonNegativeInteger(record.activity.settled);
842
+ if (active === null || started === null || settled === null || settled > started || active !== started - settled) {
843
+ return err(probeError("invalid-snapshot", "Direct activity counters must be non-negative and conserve started work"));
844
+ }
845
+ if (started > revision || settled > revision - started) {
846
+ return err(probeError("invalid-snapshot", "Direct activity transitions cannot exceed the store revision"));
847
+ }
848
+ const pending = parseSnapshotCounters(record.pending, "pending");
849
+ if (!pending.ok)
850
+ return pending;
851
+ const violations = parseSnapshotCounters(record.violations, "violation");
852
+ if (!violations.ok)
853
+ return violations;
854
+ if (Object.keys(pending.value).length + Object.keys(violations.value).length > MAX_DIRECT_PROBE_COUNTERS) {
855
+ return err(probeError("too-many-counters", `A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`));
856
+ }
857
+ if (record.remainingWork === undefined) {
858
+ return err(probeError("invalid-snapshot", "Direct probe snapshot requires remainingWork"));
859
+ }
860
+ if (typeof record.isQuiescent !== "boolean") {
861
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent must be boolean"));
862
+ }
863
+ const expectedQuiescence = active === 0 && Object.values(pending.value).every((value) => value === 0);
864
+ if (record.isQuiescent !== expectedQuiescence) {
865
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent does not match its activity and pending counters"));
866
+ }
867
+ return ok(Object.freeze({
868
+ schema: DIRECT_PROBE_SCHEMA,
869
+ activationHash: activationHash.value,
870
+ generation,
871
+ revision,
872
+ activity: Object.freeze({ active, started, settled }),
873
+ pending: pending.value,
874
+ violations: violations.value,
875
+ remainingWork: freezeJson(record.remainingWork),
876
+ isQuiescent: record.isQuiescent
877
+ }));
878
+ }
879
+ // src/web/browser-bridge.ts
880
+ var DIRECT_BROWSER_BRIDGE_SCHEMA = "direct.browser-bridge/v2";
881
+
882
+ // src/web.ts
883
+ var DIRECT_BROWSER_BRIDGE_SCHEMA2 = DIRECT_BROWSER_BRIDGE_SCHEMA;
884
+
885
+ // src/tooling/browser-verification.ts
886
+ import { randomUUID } from "crypto";
887
+ import { mkdir, rename, rm, writeFile } from "fs/promises";
888
+ import { dirname, join } from "path";
889
+ var DEFAULT_LOG_LIMIT = 12000;
890
+ var DEFAULT_PROBE_TIMEOUT_MS = 1500;
891
+ var DEFAULT_REUSE_PROBE_INTERVAL_MS = 250;
892
+ var DEFAULT_STOP_TIMEOUT_MS = 3000;
893
+ var MAX_RENDERED_ERROR_LENGTH = 4096;
894
+ var MAX_ERROR_CAUSE_DEPTH = 8;
895
+ function parseDirectBrowserContractEnvelope(input) {
896
+ try {
897
+ if (typeof input !== "object" || input === null || Array.isArray(input) || Object.keys(input).length !== 3 || !Object.hasOwn(input, "bridgeSchema") || !Object.hasOwn(input, "manifest") || !Object.hasOwn(input, "probe")) {
898
+ throw new Error("invalid");
899
+ }
900
+ return {
901
+ bridgeSchema: Reflect.get(input, "bridgeSchema"),
902
+ manifest: Reflect.get(input, "manifest"),
903
+ probe: Reflect.get(input, "probe")
904
+ };
905
+ } catch {
906
+ throw new Error("Direct browser contract has an invalid envelope");
907
+ }
908
+ }
909
+ function directCatalogIdentity(manifest2) {
910
+ return JSON.stringify({
911
+ queries: manifest2.queries,
912
+ defaultScenario: manifest2.defaultScenario,
913
+ scenarios: manifest2.scenarios
914
+ });
915
+ }
916
+ function bindDirectScenarioCatalog(manifests) {
917
+ const baseline = manifests[0];
918
+ if (baseline === undefined) {
919
+ throw new Error("Direct scenario verification requires at least one session manifest");
920
+ }
921
+ const baselineCoverage = JSON.stringify(baseline.coverage);
922
+ const baselineCatalog = directCatalogIdentity(baseline);
923
+ for (const [index, manifest2] of manifests.entries()) {
924
+ if (manifest2.catalogHash !== baseline.catalogHash) {
925
+ throw new Error(`Direct scenario ${String(index)} exposed catalog ${manifest2.catalogHash} instead of ${baseline.catalogHash}`);
926
+ }
927
+ if (JSON.stringify(manifest2.coverage) !== baselineCoverage) {
928
+ throw new Error(`Direct scenario ${String(index)} exposed different coverage for catalog ${baseline.catalogHash}`);
929
+ }
930
+ if (directCatalogIdentity(manifest2) !== baselineCatalog) {
931
+ throw new Error(`Direct scenario ${String(index)} exposed different public metadata for catalog ${baseline.catalogHash}`);
932
+ }
933
+ }
934
+ return baseline.coverage;
935
+ }
936
+ function bindDirectBrowserContractEvidence(initial, final, retainedProbe = final.probe) {
937
+ if (directCatalogIdentity(final.manifest) !== directCatalogIdentity(initial.manifest)) {
938
+ throw new Error("Direct public catalog metadata changed during verification");
939
+ }
940
+ if (JSON.stringify(final.manifest.coverage) !== JSON.stringify(initial.manifest.coverage)) {
941
+ throw new Error("Direct coverage changed during verification");
942
+ }
943
+ if (final.manifest.catalogHash !== initial.manifest.catalogHash) {
944
+ throw new Error("Direct catalog hash changed during verification");
945
+ }
946
+ if (JSON.stringify(final.manifest.active) !== JSON.stringify(initial.manifest.active)) {
947
+ throw new Error("Direct activation identity changed during verification");
948
+ }
949
+ if (initial.probe.activationHash !== initial.manifest.active.activationHash || final.probe.activationHash !== final.manifest.active.activationHash || retainedProbe.activationHash !== final.manifest.active.activationHash) {
950
+ throw new Error("Direct probe identity changed during verification");
951
+ }
952
+ return final;
953
+ }
954
+ function createDirectBrowserContractReader(protocol) {
955
+ return async (browser2, expectation) => {
956
+ const envelope = parseDirectBrowserContractEnvelope(await browser2.evaluate(`(() => {
957
+ const bridge = window.__direct;
958
+ return {
959
+ bridgeSchema: bridge?.schema,
960
+ manifest: bridge?.manifest,
961
+ probe: typeof bridge?.snapshot === "function" ? bridge.snapshot() : undefined,
962
+ };
963
+ })()`));
964
+ if (envelope.bridgeSchema !== protocol.bridgeSchema) {
965
+ throw new Error(`Direct browser bridge schema must be ${protocol.bridgeSchema}`);
966
+ }
967
+ const manifest2 = protocol.parseManifest(envelope.manifest);
968
+ if (!manifest2.ok) {
969
+ throw new Error(`Direct session manifest is invalid: ${manifest2.error.message}`);
970
+ }
971
+ const probe2 = protocol.parseProbe(envelope.probe);
972
+ if (!probe2.ok) {
973
+ throw new Error(`Direct probe is invalid: ${probe2.error.message}`);
974
+ }
975
+ if (manifest2.value.active.source !== expectation.source) {
976
+ throw new Error(`Direct activated from ${manifest2.value.active.source} instead of ${expectation.source}`);
977
+ }
978
+ if (String(manifest2.value.active.scenario) !== expectation.scenario) {
979
+ throw new Error(`Direct activated ${String(manifest2.value.active.scenario)} instead of ${expectation.scenario}`);
980
+ }
981
+ if (manifest2.value.active.route !== expectation.route) {
982
+ throw new Error(`Direct scenario ${expectation.scenario} activated route ${manifest2.value.active.route} instead of ${expectation.route}`);
983
+ }
984
+ if (manifest2.value.active.activationHash !== probe2.value.activationHash) {
985
+ throw new Error("Direct session manifest and probe identify different activations");
986
+ }
987
+ return Object.freeze({
988
+ manifest: manifest2.value,
989
+ probe: probe2.value
990
+ });
991
+ };
992
+ }
993
+ function serializeAgentBrowserLaunchArguments(launchArguments) {
994
+ for (const argument of launchArguments) {
995
+ if (!argument.startsWith("--") || argument.includes(`
996
+ `) || argument.includes(",")) {
997
+ throw new Error(`agent-browser launch arguments must be comma-free Chrome flags, received ${JSON.stringify(argument)}`);
998
+ }
999
+ }
1000
+ return launchArguments.join(",");
1001
+ }
1002
+ function isolatedAgentBrowserEnvironment(options) {
1003
+ const environment = { ...options.inheritedEnvironment };
1004
+ for (const variable of Object.keys(environment)) {
1005
+ if (variable.startsWith("AGENT_BROWSER_"))
1006
+ Reflect.deleteProperty(environment, variable);
1007
+ }
1008
+ return {
1009
+ ...environment,
1010
+ AGENT_BROWSER_CONFIG: options.configPath,
1011
+ AGENT_BROWSER_DEFAULT_TIMEOUT: String(options.defaultTimeoutMs),
1012
+ AGENT_BROWSER_IDLE_TIMEOUT_MS: String(options.idleTimeoutMs ?? options.defaultTimeoutMs + 60000),
1013
+ ...options.launchArguments === undefined ? {} : { AGENT_BROWSER_ARGS: serializeAgentBrowserLaunchArguments(options.launchArguments) },
1014
+ AGENT_BROWSER_NAMESPACE: options.session,
1015
+ AGENT_BROWSER_RESTORE_SAVE: "never",
1016
+ AGENT_BROWSER_SESSION: options.session
1017
+ };
1018
+ }
1019
+ function boundedAgentBrowserSessionName(prefix, processId, nonce) {
1020
+ const boundedPrefix = prefix.replaceAll(/[^a-zA-Z0-9_-]+/g, "-").replaceAll(/^-+|-+$/g, "").slice(0, 6) || "verify";
1021
+ const boundedProcessId = Math.max(0, Math.trunc(processId)).toString(36).slice(-6);
1022
+ const boundedNonce = nonce.replaceAll(/[^a-zA-Z0-9]+/g, "").slice(0, 6) || "run";
1023
+ return `${boundedPrefix}-${boundedProcessId}-${boundedNonce}`;
1024
+ }
1025
+ function renderAgentBrowserCommand(arguments_) {
1026
+ const [command, payload] = arguments_;
1027
+ if (command === "eval" && payload !== undefined) {
1028
+ return `${command} (${payload.length} character payload)`;
1029
+ }
1030
+ if (command === "batch") {
1031
+ return `${command} (${arguments_.slice(1).join(`
1032
+ `).length} character payload)`;
1033
+ }
1034
+ return arguments_.join(" ");
1035
+ }
1036
+ var agentBrowserCloseProcessTimeoutMs = 1e4;
1037
+ function agentBrowserProcessTimeoutMs(arguments_, defaultTimeoutMs) {
1038
+ const defaultProcessTimeoutMs = defaultTimeoutMs + 5000;
1039
+ return arguments_[0] === "close" ? Math.min(defaultProcessTimeoutMs, agentBrowserCloseProcessTimeoutMs) : defaultProcessTimeoutMs;
1040
+ }
1041
+ function truncateRenderedError(value) {
1042
+ if (value.length <= MAX_RENDERED_ERROR_LENGTH)
1043
+ return value;
1044
+ return `${value.slice(0, MAX_RENDERED_ERROR_LENGTH - 1)}\u2026`;
1045
+ }
1046
+ function readForeignProperty(value, key) {
1047
+ try {
1048
+ return { ok: true, value: Reflect.get(value, key) };
1049
+ } catch {
1050
+ return { ok: false };
1051
+ }
1052
+ }
1053
+ function isUnknownArray(value) {
1054
+ return Array.isArray(value);
1055
+ }
1056
+ function isNonArrayObject(value) {
1057
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1058
+ }
1059
+ function isNonEmptyStringArray(value) {
1060
+ return isUnknownArray(value) && value.length > 0 && value.every((entry) => typeof entry === "string");
1061
+ }
1062
+ function renderUnknownAtDepth(value, seen, depth) {
1063
+ if (typeof value === "string")
1064
+ return truncateRenderedError(value);
1065
+ if (typeof value === "object" && value !== null || typeof value === "function") {
1066
+ if (seen.has(value))
1067
+ return "[Circular]";
1068
+ if (depth >= MAX_ERROR_CAUSE_DEPTH)
1069
+ return "[Cause depth exceeded]";
1070
+ seen.add(value);
1071
+ const message = readForeignProperty(value, "message");
1072
+ if (message.ok && typeof message.value === "string") {
1073
+ const name = readForeignProperty(value, "name");
1074
+ const label = name.ok && typeof name.value === "string" && name.value.length > 0 ? name.value : "Error";
1075
+ const cause = readForeignProperty(value, "cause");
1076
+ const renderedCause = cause.ok && cause.value !== undefined ? `; caused by ${renderUnknownAtDepth(cause.value, seen, depth + 1)}` : "";
1077
+ return truncateRenderedError(`${label}: ${message.value}${renderedCause}`);
1078
+ }
1079
+ }
1080
+ try {
1081
+ const encoded = JSON.stringify(value);
1082
+ if (encoded !== undefined)
1083
+ return truncateRenderedError(encoded);
1084
+ } catch {}
1085
+ try {
1086
+ return truncateRenderedError(String(value));
1087
+ } catch {
1088
+ return "Unknown failure";
1089
+ }
1090
+ }
1091
+ function renderUnknown(value) {
1092
+ return renderUnknownAtDepth(value, new WeakSet, 0);
1093
+ }
1094
+ function tail(value, maximumLength = DEFAULT_LOG_LIMIT) {
1095
+ return value.length <= maximumLength ? value : value.slice(-maximumLength);
1096
+ }
1097
+ function normalizeRootHttpOrigin(input) {
1098
+ let url;
1099
+ try {
1100
+ url = new URL(input);
1101
+ } catch {
1102
+ throw new Error("--base-url must be an absolute HTTP URL");
1103
+ }
1104
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
1105
+ throw new Error("--base-url must use http: or https:");
1106
+ }
1107
+ if (url.username !== "" || url.password !== "") {
1108
+ throw new Error("--base-url cannot contain credentials");
1109
+ }
1110
+ if (url.pathname !== "/" || url.search !== "" || url.hash !== "") {
1111
+ throw new Error("--base-url must point to the server root without a query string or fragment");
1112
+ }
1113
+ return url.origin;
1114
+ }
1115
+ function parseBaseUrlArguments(arguments_, defaultBaseUrl) {
1116
+ let baseUrl = defaultBaseUrl;
1117
+ let receivedBaseUrl = false;
1118
+ for (let index = 0;index < arguments_.length; index += 1) {
1119
+ const argument = arguments_[index];
1120
+ if (argument === undefined)
1121
+ continue;
1122
+ if (argument === "--help" || argument === "-h")
1123
+ return { kind: "help" };
1124
+ if (argument.startsWith("--base-url=")) {
1125
+ if (receivedBaseUrl)
1126
+ throw new Error("--base-url may be provided only once");
1127
+ receivedBaseUrl = true;
1128
+ baseUrl = argument.slice("--base-url=".length);
1129
+ continue;
1130
+ }
1131
+ if (argument === "--base-url") {
1132
+ if (receivedBaseUrl)
1133
+ throw new Error("--base-url may be provided only once");
1134
+ const value = arguments_[index + 1];
1135
+ if (value === undefined || value.startsWith("-")) {
1136
+ throw new Error("--base-url requires a value");
1137
+ }
1138
+ receivedBaseUrl = true;
1139
+ baseUrl = value;
1140
+ index += 1;
1141
+ continue;
1142
+ }
1143
+ throw new Error(`Unknown argument at position ${String(index + 1)}`);
1144
+ }
1145
+ return { kind: "run", baseUrl: normalizeRootHttpOrigin(baseUrl) };
1146
+ }
1147
+ function canAutomaticallyStartLocalServer(baseUrl, localHosts = new Set(["127.0.0.1", "localhost"])) {
1148
+ const url = new URL(normalizeRootHttpOrigin(baseUrl));
1149
+ return url.protocol === "http:" && localHosts.has(url.hostname);
1150
+ }
1151
+ function parseAgentBrowserEnvelope(source) {
1152
+ let input;
1153
+ try {
1154
+ input = JSON.parse(source);
1155
+ } catch {
1156
+ throw new Error("agent-browser did not return one JSON document");
1157
+ }
1158
+ if (typeof input !== "object" || input === null || Array.isArray(input) || typeof Reflect.get(input, "success") !== "boolean" || !Object.hasOwn(input, "data") || !Object.hasOwn(input, "error")) {
1159
+ throw new Error("agent-browser returned an invalid envelope");
1160
+ }
1161
+ if (!Reflect.get(input, "success")) {
1162
+ throw new Error(`agent-browser reported failure: ${renderUnknown(Reflect.get(input, "error"))}`);
1163
+ }
1164
+ return Reflect.get(input, "data");
1165
+ }
1166
+ function parseAgentBrowserBatchEnvelope(source) {
1167
+ let input;
1168
+ try {
1169
+ input = JSON.parse(source);
1170
+ } catch {
1171
+ throw new Error("agent-browser batch did not return one JSON document");
1172
+ }
1173
+ if (!isUnknownArray(input) || input.length === 0) {
1174
+ throw new Error("agent-browser batch returned an invalid envelope");
1175
+ }
1176
+ return input.map((entry, index) => {
1177
+ if (!isNonArrayObject(entry) || !Object.hasOwn(entry, "command") || !Object.hasOwn(entry, "success") || !Object.hasOwn(entry, "result") || !Object.hasOwn(entry, "error")) {
1178
+ throw new Error(`agent-browser batch returned an invalid envelope at position ${String(index + 1)}`);
1179
+ }
1180
+ const command = readForeignProperty(entry, "command");
1181
+ const success = readForeignProperty(entry, "success");
1182
+ const result = readForeignProperty(entry, "result");
1183
+ const error = readForeignProperty(entry, "error");
1184
+ if (!command.ok || !isNonEmptyStringArray(command.value) || !success.ok || typeof success.value !== "boolean" || !result.ok || !error.ok) {
1185
+ throw new Error(`agent-browser batch returned an invalid envelope at position ${String(index + 1)}`);
1186
+ }
1187
+ if (!success.value) {
1188
+ throw new Error(`agent-browser batch command ${String(index + 1)} (${renderAgentBrowserCommand(command.value)}) reported failure: ${renderUnknown(error.value)}`);
1189
+ }
1190
+ return result.value;
1191
+ });
1192
+ }
1193
+ function createAgentBrowser(options) {
1194
+ const binary = join(options.repositoryRoot, "node_modules/.bin/agent-browser");
1195
+ const createEnvironment = () => {
1196
+ const session2 = boundedAgentBrowserSessionName(options.sessionPrefix, process.pid, randomUUID());
1197
+ return isolatedAgentBrowserEnvironment({
1198
+ configPath: join(options.repositoryRoot, "scripts/direct/agent-browser.verify.json"),
1199
+ defaultTimeoutMs: options.defaultTimeoutMs ?? 35000,
1200
+ ...options.idleTimeoutMs === undefined ? {} : { idleTimeoutMs: options.idleTimeoutMs },
1201
+ inheritedEnvironment: process.env,
1202
+ ...options.launchArguments === undefined ? {} : { launchArguments: options.launchArguments },
1203
+ session: session2
1204
+ });
1205
+ };
1206
+ let environment = createEnvironment();
1207
+ let used = false;
1208
+ async function run(arguments_) {
1209
+ used = true;
1210
+ const defaultTimeoutMs = options.defaultTimeoutMs ?? 35000;
1211
+ const commandArguments = arguments_[0] === "wait" && !arguments_.includes("--timeout") ? [...arguments_, "--timeout", String(defaultTimeoutMs)] : arguments_;
1212
+ const command = Bun.spawn([process.execPath, binary, "--json", ...commandArguments], {
1213
+ cwd: options.repositoryRoot,
1214
+ env: environment,
1215
+ stdin: "ignore",
1216
+ stdout: "pipe",
1217
+ stderr: "pipe"
1218
+ });
1219
+ let timedOut = false;
1220
+ let forceKillTimer;
1221
+ const commandTimeoutMs = agentBrowserProcessTimeoutMs(commandArguments, defaultTimeoutMs);
1222
+ const timeoutTimer = setTimeout(() => {
1223
+ timedOut = true;
1224
+ command.kill();
1225
+ forceKillTimer = setTimeout(() => command.kill(9), 1000);
1226
+ }, commandTimeoutMs);
1227
+ let stdout;
1228
+ let stderr;
1229
+ let exitCode;
1230
+ try {
1231
+ [stdout, stderr, exitCode] = await Promise.all([
1232
+ new Response(command.stdout).text(),
1233
+ new Response(command.stderr).text(),
1234
+ command.exited
1235
+ ]);
1236
+ } finally {
1237
+ clearTimeout(timeoutTimer);
1238
+ if (forceKillTimer !== undefined)
1239
+ clearTimeout(forceKillTimer);
1240
+ }
1241
+ if (timedOut) {
1242
+ throw new Error(`agent-browser ${renderAgentBrowserCommand(commandArguments)} exceeded its ${commandTimeoutMs}ms process deadline`);
1243
+ }
1244
+ if (exitCode !== 0) {
1245
+ throw new Error(`agent-browser ${renderAgentBrowserCommand(commandArguments)} exited with ${exitCode}: ${tail(stderr.trim() || stdout.trim())}`);
1246
+ }
1247
+ return commandArguments[0] === "batch" ? parseAgentBrowserBatchEnvelope(stdout) : parseAgentBrowserEnvelope(stdout);
1248
+ }
1249
+ async function evaluate(expression) {
1250
+ const evaluation = await run(["eval", expression]);
1251
+ if (typeof evaluation !== "object" || evaluation === null || Array.isArray(evaluation) || !Object.hasOwn(evaluation, "result")) {
1252
+ throw new Error("browser evaluation returned invalid data");
1253
+ }
1254
+ return Reflect.get(evaluation, "result");
1255
+ }
1256
+ async function readBodyText() {
1257
+ const result = await evaluate("document.body?.innerText ?? ''");
1258
+ if (typeof result !== "string")
1259
+ throw new Error("body text evaluation did not return a string");
1260
+ return result;
1261
+ }
1262
+ async function close() {
1263
+ if (!used)
1264
+ return;
1265
+ try {
1266
+ await run(["close"]);
1267
+ } catch (error) {
1268
+ if (!renderUnknown(error).includes("Failed to connect: No such file or directory")) {
1269
+ throw error;
1270
+ }
1271
+ } finally {
1272
+ used = false;
1273
+ }
1274
+ }
1275
+ async function restart() {
1276
+ try {
1277
+ await close();
1278
+ } catch {
1279
+ used = false;
1280
+ }
1281
+ environment = createEnvironment();
1282
+ }
1283
+ return { close, evaluate, readBodyText, restart, run };
1284
+ }
1285
+ async function collectStream(stream, logLimit) {
1286
+ const reader = stream.getReader();
1287
+ const decoder = new TextDecoder;
1288
+ let output = "";
1289
+ for (;; ) {
1290
+ const chunk = await reader.read();
1291
+ if (chunk.done)
1292
+ return tail(`${output}${decoder.decode()}`, logLimit);
1293
+ output = tail(`${output}${decoder.decode(chunk.value, { stream: true })}`, logLimit);
1294
+ }
1295
+ }
1296
+ function spawnVerificationServer(options) {
1297
+ const process_ = Bun.spawn([...options.command], {
1298
+ cwd: options.cwd,
1299
+ env: { ...process.env, ...options.env },
1300
+ stdin: "ignore",
1301
+ stdout: "pipe",
1302
+ stderr: "pipe"
1303
+ });
1304
+ const logLimit = options.logLimit ?? DEFAULT_LOG_LIMIT;
1305
+ const output = Promise.all([
1306
+ collectStream(process_.stdout, logLimit),
1307
+ collectStream(process_.stderr, logLimit)
1308
+ ]).then(([stdout, stderr]) => tail(`${stdout}
1309
+ ${stderr}`.trim(), logLimit));
1310
+ return {
1311
+ exited: process_.exited,
1312
+ exitCode: () => process_.exitCode,
1313
+ output,
1314
+ terminate: () => process_.kill("SIGTERM"),
1315
+ kill: () => process_.kill("SIGKILL")
1316
+ };
1317
+ }
1318
+ async function runVerificationCommand(options) {
1319
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
1320
+ throw new Error("verification command timeout must be a finite positive duration");
1321
+ }
1322
+ const command = spawnVerificationServer({
1323
+ command: options.command,
1324
+ cwd: options.cwd,
1325
+ ...options.env === undefined ? {} : { env: options.env }
1326
+ });
1327
+ let timeout;
1328
+ const completed = await Promise.race([
1329
+ command.exited.then(() => true),
1330
+ new Promise((resolve) => {
1331
+ timeout = setTimeout(() => resolve(false), options.timeoutMs);
1332
+ })
1333
+ ]);
1334
+ if (timeout !== undefined)
1335
+ clearTimeout(timeout);
1336
+ if (!completed) {
1337
+ const output2 = tail(await stopVerificationServerWithOutput(command));
1338
+ const message = `${options.label} exceeded its ${options.timeoutMs}ms deadline`;
1339
+ throw new Error(output2 === "" ? message : `${message}:
1340
+ ${output2}`);
1341
+ }
1342
+ const exitCode = command.exitCode();
1343
+ const output = tail(await stopVerificationServerWithOutput(command));
1344
+ if (exitCode !== 0) {
1345
+ throw new Error(`${options.label} exited with ${String(exitCode)}:
1346
+ ${output}`);
1347
+ }
1348
+ return output;
1349
+ }
1350
+ async function settleWithin(promise, timeoutMs) {
1351
+ return await Promise.race([
1352
+ promise.then((value) => ({ settled: true, value })),
1353
+ Bun.sleep(timeoutMs).then(() => ({ settled: false }))
1354
+ ]);
1355
+ }
1356
+ async function serverIsReachable(baseUrl, probeTimeoutMs = DEFAULT_PROBE_TIMEOUT_MS, readinessPath = "/") {
1357
+ if (!readinessPath.startsWith("/") || readinessPath.startsWith("//")) {
1358
+ throw new Error(`readinessPath must be an origin-relative path, received ${JSON.stringify(readinessPath)}`);
1359
+ }
1360
+ const probeUrl = new URL(readinessPath, `${normalizeRootHttpOrigin(baseUrl)}/`);
1361
+ if (probeUrl.hash !== "")
1362
+ throw new Error("readinessPath cannot contain a fragment");
1363
+ try {
1364
+ const response = await fetch(probeUrl, {
1365
+ signal: AbortSignal.timeout(probeTimeoutMs)
1366
+ });
1367
+ await response.body?.cancel();
1368
+ return response.ok;
1369
+ } catch {
1370
+ return false;
1371
+ }
1372
+ }
1373
+ async function stopVerificationServerWithOutput(server, stopTimeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
1374
+ if (!Number.isFinite(stopTimeoutMs) || stopTimeoutMs < 0) {
1375
+ throw new Error("verification server stop timeout must be a finite nonnegative duration");
1376
+ }
1377
+ if (server.exitCode() === null)
1378
+ server.terminate();
1379
+ const stopped = await settleWithin(server.exited, stopTimeoutMs);
1380
+ if (!stopped.settled) {
1381
+ server.kill();
1382
+ const killed = await settleWithin(server.exited, stopTimeoutMs);
1383
+ if (!killed.settled) {
1384
+ throw new Error(`verification server did not exit within ${stopTimeoutMs}ms after SIGKILL`);
1385
+ }
1386
+ }
1387
+ const output = await settleWithin(server.output, stopTimeoutMs);
1388
+ if (!output.settled) {
1389
+ throw new Error(`verification server output did not settle within ${stopTimeoutMs}ms after exit`);
1390
+ }
1391
+ return output.value;
1392
+ }
1393
+ async function stopVerificationServer(server, stopTimeoutMs = DEFAULT_STOP_TIMEOUT_MS) {
1394
+ await stopVerificationServerWithOutput(server, stopTimeoutMs);
1395
+ }
1396
+ async function acquireVerificationServer(options) {
1397
+ const probeTimeoutMs = options.probeTimeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;
1398
+ const readinessPath = options.readinessPath ?? "/";
1399
+ const isReachable = options.isReachable ?? serverIsReachable;
1400
+ const canStartLocally = canAutomaticallyStartLocalServer(options.baseUrl, options.localHosts);
1401
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1402
+ if (canStartLocally && options.reuseExistingLocalServer === false) {
1403
+ throw new Error(`A local server is already reachable at ${options.baseUrl}; ` + "verification will not reuse a server whose worktree ownership is unknown");
1404
+ }
1405
+ await Bun.sleep(options.reuseProbeIntervalMs ?? DEFAULT_REUSE_PROBE_INTERVAL_MS);
1406
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1407
+ return { source: "reused" };
1408
+ }
1409
+ }
1410
+ if (!canStartLocally) {
1411
+ throw new Error(`No server is reachable at ${options.baseUrl}; automatic startup is limited to local HTTP URLs`);
1412
+ }
1413
+ const server = options.startServer();
1414
+ let exitedWithCode = null;
1415
+ try {
1416
+ const deadline = Date.now() + options.startupTimeoutMs;
1417
+ while (Date.now() < deadline) {
1418
+ const exitCode = server.exitCode();
1419
+ if (exitCode !== null) {
1420
+ exitedWithCode = exitCode;
1421
+ break;
1422
+ }
1423
+ if (await isReachable(options.baseUrl, probeTimeoutMs, readinessPath)) {
1424
+ return { source: "started", server };
1425
+ }
1426
+ await Bun.sleep(options.pollIntervalMs ?? 200);
1427
+ }
1428
+ } catch (error) {
1429
+ await stopVerificationServer(server);
1430
+ throw error;
1431
+ }
1432
+ if (exitedWithCode !== null) {
1433
+ const output2 = tail(await stopVerificationServerWithOutput(server));
1434
+ throw new Error(`${options.label} exited with ${exitedWithCode}:
1435
+ ${output2}`);
1436
+ }
1437
+ const timeoutMessage = `${options.label} did not become reachable at ${new URL(readinessPath, `${options.baseUrl}/`).href} within ${options.startupTimeoutMs}ms`;
1438
+ const output = tail(await stopVerificationServerWithOutput(server));
1439
+ throw new Error(output === "" ? timeoutMessage : `${timeoutMessage}:
1440
+ ${output}`);
1441
+ }
1442
+ async function createArtifactRun(options) {
1443
+ const generatedAt = options.generatedAt ?? new Date().toISOString();
1444
+ const processId = options.processId ?? process.pid;
1445
+ const runId = `${generatedAt.replaceAll(/[^0-9A-Za-z]/gu, "-")}-${processId}`;
1446
+ const runDirectory = join(options.artifactRoot, runId);
1447
+ await mkdir(runDirectory, { recursive: true });
1448
+ return {
1449
+ artifactRoot: options.artifactRoot,
1450
+ generatedAt,
1451
+ manifestPath: join(options.artifactRoot, "manifest.json"),
1452
+ runDirectory
1453
+ };
1454
+ }
1455
+ async function writeJsonAtomically(path, value) {
1456
+ const temporaryPath = join(dirname(path), `.${process.pid}-${randomUUID()}.tmp`);
1457
+ try {
1458
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}
1459
+ `, "utf8");
1460
+ await rename(temporaryPath, path);
1461
+ } catch (error) {
1462
+ await rm(temporaryPath, { force: true });
1463
+ throw error;
1464
+ }
1465
+ }
1466
+
1467
+ // src/tooling/browser-verification-entry.ts
1468
+ var readDirectBrowserContract = createDirectBrowserContractReader({
1469
+ bridgeSchema: DIRECT_BROWSER_BRIDGE_SCHEMA2,
1470
+ parseManifest: parseDirectSessionManifest,
1471
+ parseProbe: parseDirectProbeSnapshot
1472
+ });
1473
+ export {
1474
+ writeJsonAtomically,
1475
+ tail,
1476
+ stopVerificationServer,
1477
+ spawnVerificationServer,
1478
+ serverIsReachable,
1479
+ serializeAgentBrowserLaunchArguments,
1480
+ runVerificationCommand,
1481
+ renderUnknown,
1482
+ renderAgentBrowserCommand,
1483
+ readDirectBrowserContract,
1484
+ parseBaseUrlArguments,
1485
+ parseAgentBrowserEnvelope,
1486
+ parseAgentBrowserBatchEnvelope,
1487
+ normalizeRootHttpOrigin,
1488
+ isolatedAgentBrowserEnvironment,
1489
+ createDirectBrowserContractReader,
1490
+ createArtifactRun,
1491
+ createAgentBrowser,
1492
+ canAutomaticallyStartLocalServer,
1493
+ boundedAgentBrowserSessionName,
1494
+ bindDirectScenarioCatalog,
1495
+ bindDirectBrowserContractEvidence,
1496
+ agentBrowserProcessTimeoutMs,
1497
+ agentBrowserCloseProcessTimeoutMs,
1498
+ acquireVerificationServer
1499
+ };