@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,616 @@
1
+ import {
2
+ FIXTURE_QUERY_KEY,
3
+ MAX_DIRECT_SCENARIOS,
4
+ SCENARIO_QUERY_KEY,
5
+ STABLE_HASH_ALGORITHM,
6
+ cloneJson,
7
+ createCoverageCatalogSnapshot,
8
+ err,
9
+ freezeJson,
10
+ isRecord,
11
+ ok,
12
+ parseCoverageCatalogSnapshot,
13
+ parseJsonValue,
14
+ parseScenarioId,
15
+ parseTaggedStableHash,
16
+ renderUnknownReason,
17
+ stableHash,
18
+ tagStableHash
19
+ } from "./index-1csg00w4.js";
20
+
21
+ // src/testing/manifest.ts
22
+ var DIRECT_SESSION_MANIFEST_SCHEMA = "direct.session-manifest/v1";
23
+ var DIRECT_CATALOG_HASH_ALGORITHM = STABLE_HASH_ALGORITHM;
24
+ var DIRECT_SESSION_MANIFEST_JSON_LIMITS = Object.freeze({
25
+ maxDepth: 64,
26
+ maxNodes: 1e5,
27
+ maxStringBytes: 16777216
28
+ });
29
+ var MANIFEST_KEYS = new Set([
30
+ "active",
31
+ "catalogHash",
32
+ "coverage",
33
+ "defaultScenario",
34
+ "queries",
35
+ "scenarios",
36
+ "schema"
37
+ ]);
38
+ var QUERY_KEYS = new Set(["fixture", "scenario"]);
39
+ var ACTIVE_KEYS = new Set([
40
+ "activationHash",
41
+ "route",
42
+ "scenario",
43
+ "selectionHash",
44
+ "source"
45
+ ]);
46
+ var SCENARIO_KEYS = new Set([
47
+ "description",
48
+ "id",
49
+ "route",
50
+ "title"
51
+ ]);
52
+ function manifestError(code, message) {
53
+ return Object.freeze({ code, message });
54
+ }
55
+ function exactKeys(input, expected, label) {
56
+ for (const key of Object.keys(input)) {
57
+ if (!expected.has(key))
58
+ throw new Error(`Unknown ${label} key: ${key}`);
59
+ }
60
+ for (const key of expected) {
61
+ if (!Object.hasOwn(input, key))
62
+ throw new Error(`Missing ${label} key: ${key}`);
63
+ }
64
+ }
65
+ function hasControlCharacters(value) {
66
+ for (const character of value) {
67
+ const code = character.charCodeAt(0);
68
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) {
69
+ return true;
70
+ }
71
+ }
72
+ return false;
73
+ }
74
+ function validText(value, maximum) {
75
+ return value.trim().length > 0 && value.length <= maximum && !hasControlCharacters(value);
76
+ }
77
+ function validRoute(value) {
78
+ if (value.trim().length === 0 || value.length > 256)
79
+ return false;
80
+ for (const character of value) {
81
+ const code = character.charCodeAt(0);
82
+ if (code < 32 || code === 127)
83
+ return false;
84
+ }
85
+ return true;
86
+ }
87
+ function parseTaggedHash(value, label) {
88
+ const parsed = parseTaggedStableHash(value);
89
+ if (!parsed.ok)
90
+ throw new Error(`${label}: ${parsed.error.message}`);
91
+ return parsed.value;
92
+ }
93
+ function selectionHash(payload) {
94
+ const hashed = stableHash(payload, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
95
+ if (!hashed.ok) {
96
+ return err(manifestError("invalid-manifest", hashed.error.message));
97
+ }
98
+ return ok(tagStableHash(hashed.value));
99
+ }
100
+ function catalogHash(payload) {
101
+ const hashed = stableHash(payload, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
102
+ if (!hashed.ok) {
103
+ return err(manifestError("invalid-manifest", hashed.error.message));
104
+ }
105
+ return ok(tagStableHash(hashed.value));
106
+ }
107
+ function parseManifestUnchecked(input) {
108
+ const parsedJson = parseJsonValue(input, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
109
+ if (!parsedJson.ok || !isRecord(parsedJson.value)) {
110
+ return err(manifestError("invalid-manifest", parsedJson.ok ? "Direct session manifest must be an object" : parsedJson.error.message));
111
+ }
112
+ const candidate = parsedJson.value;
113
+ exactKeys(candidate, MANIFEST_KEYS, "Direct session manifest");
114
+ if (candidate.schema !== DIRECT_SESSION_MANIFEST_SCHEMA) {
115
+ throw new Error(`Direct session manifest schema must be ${DIRECT_SESSION_MANIFEST_SCHEMA}`);
116
+ }
117
+ if (!isRecord(candidate.queries)) {
118
+ throw new Error("Direct session manifest queries must be an object");
119
+ }
120
+ exactKeys(candidate.queries, QUERY_KEYS, "Direct session manifest queries");
121
+ if (candidate.queries.scenario !== SCENARIO_QUERY_KEY || candidate.queries.fixture !== FIXTURE_QUERY_KEY) {
122
+ throw new Error("Direct session manifest query keys do not match Direct");
123
+ }
124
+ const queries = Object.freeze({
125
+ scenario: SCENARIO_QUERY_KEY,
126
+ fixture: FIXTURE_QUERY_KEY
127
+ });
128
+ const defaultScenario = parseScenarioId(candidate.defaultScenario);
129
+ if (!defaultScenario.ok) {
130
+ throw new Error(`Invalid default scenario: ${defaultScenario.error.message}`);
131
+ }
132
+ if (!Array.isArray(candidate.scenarios)) {
133
+ throw new Error("Direct session manifest scenarios must be an array");
134
+ }
135
+ if (candidate.scenarios.length > MAX_DIRECT_SCENARIOS) {
136
+ throw new Error(`Direct session manifests support at most ${String(MAX_DIRECT_SCENARIOS)} scenarios`);
137
+ }
138
+ const scenarios = [];
139
+ const byId = new Map;
140
+ for (const [index, rawScenario] of candidate.scenarios.entries()) {
141
+ if (!isRecord(rawScenario)) {
142
+ throw new Error(`Direct session manifest scenario ${String(index)} must be an object`);
143
+ }
144
+ exactKeys(rawScenario, SCENARIO_KEYS, `Direct session manifest scenario ${String(index)}`);
145
+ const id = parseScenarioId(rawScenario.id);
146
+ if (!id.ok) {
147
+ throw new Error(`Invalid Direct session manifest scenario ${String(index)}: ${id.error.message}`);
148
+ }
149
+ if (byId.has(id.value)) {
150
+ return err(manifestError("duplicate-scenario", `Duplicate Direct session manifest scenario: ${id.value}`));
151
+ }
152
+ if (typeof rawScenario.title !== "string" || !validText(rawScenario.title, 160)) {
153
+ throw new Error(`Direct session manifest scenario ${id.value} title must contain 1-160 visible characters`);
154
+ }
155
+ if (rawScenario.description !== null && (typeof rawScenario.description !== "string" || !validText(rawScenario.description, 2000))) {
156
+ throw new Error(`Direct session manifest scenario ${id.value} description must be null or contain 1-2000 visible characters`);
157
+ }
158
+ if (typeof rawScenario.route !== "string" || !validRoute(rawScenario.route)) {
159
+ throw new Error(`Direct session manifest scenario ${id.value} route must contain 1-256 visible characters`);
160
+ }
161
+ const scenario = Object.freeze({
162
+ id: id.value,
163
+ title: rawScenario.title,
164
+ description: rawScenario.description,
165
+ route: rawScenario.route
166
+ });
167
+ scenarios.push(scenario);
168
+ byId.set(id.value, scenario);
169
+ }
170
+ const frozenScenarios = Object.freeze(scenarios);
171
+ if (!byId.has(defaultScenario.value)) {
172
+ return err(manifestError("unknown-scenario", `Direct session manifest default scenario is missing: ${defaultScenario.value}`));
173
+ }
174
+ if (!isRecord(candidate.active)) {
175
+ throw new Error("Direct session manifest active selection must be an object");
176
+ }
177
+ exactKeys(candidate.active, ACTIVE_KEYS, "Direct session manifest active selection");
178
+ if (candidate.active.source !== "scenario" && candidate.active.source !== "fixture") {
179
+ throw new Error("Direct session manifest active source must be scenario or fixture");
180
+ }
181
+ const activeScenario = parseScenarioId(candidate.active.scenario);
182
+ if (!activeScenario.ok) {
183
+ throw new Error(`Invalid active scenario: ${activeScenario.error.message}`);
184
+ }
185
+ const activeDefinition = byId.get(activeScenario.value);
186
+ if (activeDefinition === undefined) {
187
+ return err(manifestError("unknown-scenario", `Direct session manifest active scenario is missing: ${activeScenario.value}`));
188
+ }
189
+ if (typeof candidate.active.route !== "string" || !validRoute(candidate.active.route)) {
190
+ throw new Error("Direct session manifest active route is invalid");
191
+ }
192
+ if (candidate.active.route !== activeDefinition.route) {
193
+ return err(manifestError("route-mismatch", `Direct session manifest active route does not match scenario ${activeScenario.value}`));
194
+ }
195
+ const activationHash = parseTaggedHash(candidate.active.activationHash, "Direct session manifest activationHash");
196
+ let suppliedSelectionHash;
197
+ try {
198
+ suppliedSelectionHash = parseTaggedHash(candidate.active.selectionHash, "Direct session manifest selectionHash");
199
+ } catch (reason) {
200
+ return err(manifestError("invalid-selection-hash", renderUnknownReason(reason, "Direct session manifest selectionHash is invalid")));
201
+ }
202
+ const expectedSelectionHash = selectionHash({
203
+ source: candidate.active.source,
204
+ scenario: activeScenario.value,
205
+ route: activeDefinition.route,
206
+ activationHash
207
+ });
208
+ if (!expectedSelectionHash.ok)
209
+ return expectedSelectionHash;
210
+ if (suppliedSelectionHash !== expectedSelectionHash.value) {
211
+ return err(manifestError("selection-hash-mismatch", "Direct session manifest selectionHash does not match its active selection"));
212
+ }
213
+ const active = Object.freeze({
214
+ source: candidate.active.source,
215
+ scenario: activeScenario.value,
216
+ route: activeDefinition.route,
217
+ activationHash,
218
+ selectionHash: expectedSelectionHash.value
219
+ });
220
+ const coverage = parseCoverageCatalogSnapshot(candidate.coverage, DIRECT_SESSION_MANIFEST_JSON_LIMITS);
221
+ if (!coverage.ok) {
222
+ throw new Error(coverage.error.message);
223
+ }
224
+ for (const entry of coverage.value.entries) {
225
+ for (const scenario of entry.scenarios) {
226
+ if (!byId.has(scenario)) {
227
+ return err(manifestError("unknown-coverage-scenario", `Coverage ${entry.key} cites unknown Direct session manifest scenario ${scenario}`));
228
+ }
229
+ }
230
+ }
231
+ let suppliedCatalogHash;
232
+ try {
233
+ const parsedHash = parseTaggedHash(candidate.catalogHash, "Direct session manifest catalogHash");
234
+ const separator = parsedHash.indexOf(":");
235
+ suppliedCatalogHash = `${DIRECT_CATALOG_HASH_ALGORITHM}:${parsedHash.slice(separator + 1)}`;
236
+ } catch (reason) {
237
+ return err(manifestError("invalid-catalog-hash", renderUnknownReason(reason, "Direct session manifest catalogHash is invalid")));
238
+ }
239
+ const expectedCatalogHash = catalogHash({
240
+ queries,
241
+ defaultScenario: defaultScenario.value,
242
+ scenarios: frozenScenarios,
243
+ coverage: coverage.value
244
+ });
245
+ if (!expectedCatalogHash.ok)
246
+ return expectedCatalogHash;
247
+ if (suppliedCatalogHash !== expectedCatalogHash.value) {
248
+ return err(manifestError("catalog-hash-mismatch", "Direct session manifest catalogHash does not match its public catalog"));
249
+ }
250
+ return ok(Object.freeze({
251
+ schema: DIRECT_SESSION_MANIFEST_SCHEMA,
252
+ catalogHash: expectedCatalogHash.value,
253
+ queries,
254
+ defaultScenario: defaultScenario.value,
255
+ active,
256
+ scenarios: frozenScenarios,
257
+ coverage: coverage.value
258
+ }));
259
+ }
260
+ function parseDirectSessionManifest(input) {
261
+ try {
262
+ return parseManifestUnchecked(input);
263
+ } catch (reason) {
264
+ return err(manifestError("invalid-manifest", renderUnknownReason(reason, "Direct session manifest is invalid")));
265
+ }
266
+ }
267
+ function createDirectSessionManifest(definition, activation) {
268
+ try {
269
+ const activeScenario = definition.scenarios.get(activation.scenario);
270
+ if (activeScenario === undefined) {
271
+ return err(manifestError("unknown-scenario", `Direct session activation is missing from its definition: ${activation.scenario}`));
272
+ }
273
+ if (activeScenario.route !== activation.route) {
274
+ return err(manifestError("route-mismatch", `Direct session activation route does not match scenario ${activation.scenario}`));
275
+ }
276
+ const activeIdentity = {
277
+ source: activation.source,
278
+ scenario: activation.scenario,
279
+ route: activation.route,
280
+ world: activation.world,
281
+ runtime: activation.runtime
282
+ };
283
+ const hashedActivation = stableHash(activeIdentity);
284
+ if (!hashedActivation.ok) {
285
+ return err(manifestError("invalid-manifest", hashedActivation.error.message));
286
+ }
287
+ const expectedActivationHash = tagStableHash(hashedActivation.value);
288
+ if (activation.activationHash !== expectedActivationHash) {
289
+ return err(manifestError("activation-hash-mismatch", "Direct session activationHash does not identify its active state"));
290
+ }
291
+ if (activation.source === "scenario") {
292
+ const authoredHash = stableHash({
293
+ source: activation.source,
294
+ scenario: activeScenario.id,
295
+ route: activeScenario.route,
296
+ world: activeScenario.world,
297
+ runtime: activeScenario.runtime
298
+ });
299
+ if (!authoredHash.ok) {
300
+ return err(manifestError("invalid-manifest", authoredHash.error.message));
301
+ }
302
+ if (tagStableHash(authoredHash.value) !== expectedActivationHash) {
303
+ return err(manifestError("activation-hash-mismatch", `Direct scenario activation does not match authored scenario ${activation.scenario}`));
304
+ }
305
+ }
306
+ const scenarios = Object.freeze(definition.scenarios.list().map((scenario) => Object.freeze({
307
+ id: scenario.id,
308
+ title: scenario.title,
309
+ description: scenario.description,
310
+ route: scenario.route
311
+ })));
312
+ const queries = Object.freeze({
313
+ scenario: SCENARIO_QUERY_KEY,
314
+ fixture: FIXTURE_QUERY_KEY
315
+ });
316
+ const coverage = createCoverageCatalogSnapshot(definition.coverage);
317
+ const hash = catalogHash({
318
+ queries,
319
+ defaultScenario: definition.defaultScenario.id,
320
+ scenarios,
321
+ coverage
322
+ });
323
+ if (!hash.ok)
324
+ return hash;
325
+ const activeSelectionHash = selectionHash({
326
+ source: activation.source,
327
+ scenario: activation.scenario,
328
+ route: activation.route,
329
+ activationHash: expectedActivationHash
330
+ });
331
+ if (!activeSelectionHash.ok)
332
+ return activeSelectionHash;
333
+ return parseDirectSessionManifest({
334
+ schema: DIRECT_SESSION_MANIFEST_SCHEMA,
335
+ catalogHash: hash.value,
336
+ queries,
337
+ defaultScenario: definition.defaultScenario.id,
338
+ active: {
339
+ source: activation.source,
340
+ scenario: activation.scenario,
341
+ route: activation.route,
342
+ activationHash: expectedActivationHash,
343
+ selectionHash: activeSelectionHash.value
344
+ },
345
+ scenarios,
346
+ coverage
347
+ });
348
+ } catch (reason) {
349
+ return err(manifestError("invalid-manifest", renderUnknownReason(reason, "Direct session manifest could not be created")));
350
+ }
351
+ }
352
+
353
+ // src/testing/probe.ts
354
+ var DIRECT_PROBE_SCHEMA = "direct.probe/v1";
355
+ var MAX_DIRECT_PROBE_COUNTERS = 128;
356
+ function freezeCounterSources(sources) {
357
+ return Object.freeze([...sources]);
358
+ }
359
+ var COUNTER_NAME_PATTERN = /^[a-z][A-Za-z0-9]*(?:[.-][A-Za-z0-9]+)*$/u;
360
+ var SNAPSHOT_KEYS = new Set([
361
+ "schema",
362
+ "activationHash",
363
+ "generation",
364
+ "revision",
365
+ "activity",
366
+ "pending",
367
+ "violations",
368
+ "remainingWork",
369
+ "isQuiescent"
370
+ ]);
371
+ var ACTIVITY_KEYS = new Set(["active", "started", "settled"]);
372
+ function probeError(code, message, counter = null) {
373
+ return Object.freeze({ code, message, counter });
374
+ }
375
+ function readNonNegativeInteger(input) {
376
+ return typeof input === "number" && Number.isSafeInteger(input) && input >= 0 ? input : null;
377
+ }
378
+ function isPromiseLike(value) {
379
+ return (typeof value === "object" && value !== null || typeof value === "function") && typeof Reflect.get(value, "then") === "function";
380
+ }
381
+ function containPromiseLike(value) {
382
+ if (!isPromiseLike(value))
383
+ return false;
384
+ Promise.resolve(value).catch(() => {
385
+ return;
386
+ });
387
+ return true;
388
+ }
389
+ function parseSnapshotCounters(input, category) {
390
+ if (!isRecord(input)) {
391
+ return err(probeError("invalid-snapshot", `Probe ${category} counters must be an object`));
392
+ }
393
+ const output = Object.create(null);
394
+ for (const [name, candidate] of Object.entries(input)) {
395
+ if (name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
396
+ return err(probeError("invalid-counter-name", "Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens", name));
397
+ }
398
+ const value = readNonNegativeInteger(candidate);
399
+ if (value === null) {
400
+ return err(probeError("invalid-counter", `Counter ${name} must be a non-negative safe integer`, name));
401
+ }
402
+ output[name] = value;
403
+ }
404
+ return ok(Object.freeze(output));
405
+ }
406
+ function parseDirectProbeSnapshot(input) {
407
+ const parsed = parseJsonValue(input);
408
+ if (!parsed.ok || !isRecord(parsed.value)) {
409
+ return err(probeError("invalid-snapshot", parsed.ok ? "Direct probe snapshot must be an object" : parsed.error.message));
410
+ }
411
+ const record = parsed.value;
412
+ for (const key of Object.keys(record)) {
413
+ if (!SNAPSHOT_KEYS.has(key)) {
414
+ return err(probeError("invalid-snapshot", `Unknown Direct probe snapshot key: ${key}`));
415
+ }
416
+ }
417
+ if (record.schema !== DIRECT_PROBE_SCHEMA) {
418
+ return err(probeError("invalid-snapshot", `Direct probe schema must be ${DIRECT_PROBE_SCHEMA}`));
419
+ }
420
+ const activationHash = parseTaggedStableHash(record.activationHash);
421
+ if (!activationHash.ok) {
422
+ return err(probeError("invalid-activation-hash", "Direct probe activation hash is invalid"));
423
+ }
424
+ const generation = readNonNegativeInteger(record.generation);
425
+ const revision = readNonNegativeInteger(record.revision);
426
+ if (generation === null || generation < 1 || revision === null) {
427
+ return err(probeError("invalid-snapshot", "Direct probe generation must be positive and revision must be non-negative"));
428
+ }
429
+ if (generation - 1 > revision) {
430
+ return err(probeError("invalid-snapshot", "Direct probe generation cannot exceed revision plus one"));
431
+ }
432
+ if (!isRecord(record.activity)) {
433
+ return err(probeError("invalid-snapshot", "Direct probe activity must be an object"));
434
+ }
435
+ for (const key of Object.keys(record.activity)) {
436
+ if (!ACTIVITY_KEYS.has(key)) {
437
+ return err(probeError("invalid-snapshot", `Unknown Direct activity key: ${key}`));
438
+ }
439
+ }
440
+ const active = readNonNegativeInteger(record.activity.active);
441
+ const started = readNonNegativeInteger(record.activity.started);
442
+ const settled = readNonNegativeInteger(record.activity.settled);
443
+ if (active === null || started === null || settled === null || settled > started || active !== started - settled) {
444
+ return err(probeError("invalid-snapshot", "Direct activity counters must be non-negative and conserve started work"));
445
+ }
446
+ if (started > revision || settled > revision - started) {
447
+ return err(probeError("invalid-snapshot", "Direct activity transitions cannot exceed the store revision"));
448
+ }
449
+ const pending = parseSnapshotCounters(record.pending, "pending");
450
+ if (!pending.ok)
451
+ return pending;
452
+ const violations = parseSnapshotCounters(record.violations, "violation");
453
+ if (!violations.ok)
454
+ return violations;
455
+ if (Object.keys(pending.value).length + Object.keys(violations.value).length > MAX_DIRECT_PROBE_COUNTERS) {
456
+ return err(probeError("too-many-counters", `A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`));
457
+ }
458
+ if (record.remainingWork === undefined) {
459
+ return err(probeError("invalid-snapshot", "Direct probe snapshot requires remainingWork"));
460
+ }
461
+ if (typeof record.isQuiescent !== "boolean") {
462
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent must be boolean"));
463
+ }
464
+ const expectedQuiescence = active === 0 && Object.values(pending.value).every((value) => value === 0);
465
+ if (record.isQuiescent !== expectedQuiescence) {
466
+ return err(probeError("invalid-snapshot", "Direct probe isQuiescent does not match its activity and pending counters"));
467
+ }
468
+ return ok(Object.freeze({
469
+ schema: DIRECT_PROBE_SCHEMA,
470
+ activationHash: activationHash.value,
471
+ generation,
472
+ revision,
473
+ activity: Object.freeze({ active, started, settled }),
474
+ pending: pending.value,
475
+ violations: violations.value,
476
+ remainingWork: freezeJson(record.remainingWork),
477
+ isQuiescent: record.isQuiescent
478
+ }));
479
+ }
480
+ function prepareCountersUnchecked(pending, violations) {
481
+ if (pending.length + violations.length > MAX_DIRECT_PROBE_COUNTERS) {
482
+ return err(probeError("too-many-counters", `A probe supports at most ${String(MAX_DIRECT_PROBE_COUNTERS)} counters`));
483
+ }
484
+ const prepared = [];
485
+ const seen = new Set;
486
+ for (const [category, sources] of [
487
+ ["pending", pending],
488
+ ["violation", violations]
489
+ ]) {
490
+ for (const source of sources) {
491
+ const name = source.name;
492
+ const read = source.read;
493
+ if (typeof name !== "string" || name.length > 80 || !COUNTER_NAME_PATTERN.test(name)) {
494
+ return err(probeError("invalid-counter-name", "Counter names must be 1-80 ASCII alphanumeric characters with optional dots or hyphens", typeof name === "string" ? name : null));
495
+ }
496
+ if (typeof read !== "function") {
497
+ return err(probeError("invalid-counter-source", `Counter ${name} must provide a synchronous read function`, name));
498
+ }
499
+ const key = `${category}:${name}`;
500
+ if (seen.has(key)) {
501
+ return err(probeError("duplicate-counter", `Duplicate ${category} counter: ${name}`, name));
502
+ }
503
+ seen.add(key);
504
+ prepared.push(Object.freeze({ name, read, category }));
505
+ }
506
+ }
507
+ prepared.sort((left, right) => {
508
+ const leftKey = `${left.category}:${left.name}`;
509
+ const rightKey = `${right.category}:${right.name}`;
510
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
511
+ });
512
+ return ok(Object.freeze(prepared));
513
+ }
514
+ function prepareCounters(pending, violations) {
515
+ try {
516
+ return prepareCountersUnchecked(pending, violations);
517
+ } catch (reason) {
518
+ return err(probeError("invalid-counter-source", renderUnknownReason(reason, "Direct counter inspection failed")));
519
+ }
520
+ }
521
+ function readCounters(sources) {
522
+ const pending = Object.create(null);
523
+ const violations = Object.create(null);
524
+ for (const source of sources) {
525
+ let value;
526
+ try {
527
+ value = source.read();
528
+ if (containPromiseLike(value)) {
529
+ return err(probeError("asynchronous-read", `Counter ${source.name} must be read synchronously`, source.name));
530
+ }
531
+ } catch (reason) {
532
+ return err(probeError("probe-read-failed", renderUnknownReason(reason, `Failed to read ${source.name}`), source.name));
533
+ }
534
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
535
+ return err(probeError("invalid-counter", `Counter ${source.name} must be a non-negative safe integer`, source.name));
536
+ }
537
+ (source.category === "pending" ? pending : violations)[source.name] = value;
538
+ }
539
+ return ok({ pending: Object.freeze(pending), violations: Object.freeze(violations) });
540
+ }
541
+ function readRemaining(read) {
542
+ let candidate;
543
+ try {
544
+ candidate = read();
545
+ if (containPromiseLike(candidate)) {
546
+ return err(probeError("asynchronous-read", "Remaining work must be read synchronously"));
547
+ }
548
+ } catch (reason) {
549
+ return err(probeError("probe-read-failed", renderUnknownReason(reason, "Failed to read remaining work")));
550
+ }
551
+ const cloned = cloneJson(candidate);
552
+ return cloned.ok ? ok(freezeJson(cloned.value)) : err(probeError("invalid-remaining-work", cloned.error.message));
553
+ }
554
+ function createDirectProbe(options) {
555
+ let store;
556
+ let activationHash;
557
+ let pending;
558
+ let violations;
559
+ let readRemainingWork;
560
+ try {
561
+ store = options.store;
562
+ activationHash = options.activationHash;
563
+ const pendingInput = options.pending ?? [];
564
+ const violationsInput = options.violations ?? [];
565
+ readRemainingWork = options.readRemainingWork ?? (() => Object.freeze({}));
566
+ if (!Array.isArray(pendingInput) || !Array.isArray(violationsInput)) {
567
+ return err(probeError("invalid-options", "Direct probe counters must be arrays"));
568
+ }
569
+ if (typeof readRemainingWork !== "function") {
570
+ return err(probeError("invalid-options", "Direct remaining-work reader must be a function"));
571
+ }
572
+ pending = freezeCounterSources(pendingInput);
573
+ violations = freezeCounterSources(violationsInput);
574
+ } catch (reason) {
575
+ return err(probeError("invalid-options", renderUnknownReason(reason, "Direct probe options could not be inspected")));
576
+ }
577
+ const parsedActivationHash = parseTaggedStableHash(activationHash);
578
+ if (!parsedActivationHash.ok) {
579
+ return err(probeError("invalid-activation-hash", parsedActivationHash.error.message));
580
+ }
581
+ const counters = prepareCounters(pending, violations);
582
+ if (!counters.ok)
583
+ return counters;
584
+ const snapshot = () => {
585
+ const read = readCounters(counters.value);
586
+ if (!read.ok)
587
+ return read;
588
+ const remaining = readRemaining(readRemainingWork);
589
+ if (!remaining.ok)
590
+ return remaining;
591
+ const storeSnapshot = store.getSnapshot();
592
+ const isQuiescent = storeSnapshot.activity.active === 0 && Object.values(read.value.pending).every((value2) => value2 === 0);
593
+ const value = {
594
+ schema: DIRECT_PROBE_SCHEMA,
595
+ activationHash: parsedActivationHash.value,
596
+ generation: Number(storeSnapshot.generation),
597
+ revision: storeSnapshot.revision,
598
+ activity: storeSnapshot.activity,
599
+ pending: read.value.pending,
600
+ violations: read.value.violations,
601
+ remainingWork: remaining.value,
602
+ isQuiescent
603
+ };
604
+ return ok(Object.freeze(value));
605
+ };
606
+ const probe = {
607
+ snapshot,
608
+ isQuiescent: () => {
609
+ const current = snapshot();
610
+ return current.ok ? ok(current.value.isQuiescent) : current;
611
+ }
612
+ };
613
+ return ok(Object.freeze(probe));
614
+ }
615
+
616
+ export { DIRECT_SESSION_MANIFEST_SCHEMA, DIRECT_CATALOG_HASH_ALGORITHM, parseDirectSessionManifest, createDirectSessionManifest, DIRECT_PROBE_SCHEMA, MAX_DIRECT_PROBE_COUNTERS, parseDirectProbeSnapshot, createDirectProbe };