@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,77 @@
1
+ import { err, ok, type Result } from "./result.js";
2
+
3
+ declare const scenarioIdBrand: unique symbol;
4
+ declare const operationIdBrand: unique symbol;
5
+ declare const coverageKeyBrand: unique symbol;
6
+
7
+ export type ScenarioId = string & { readonly [scenarioIdBrand]: "ScenarioId" };
8
+ export type OperationId = string & { readonly [operationIdBrand]: "OperationId" };
9
+ export type CoverageKey = string & { readonly [coverageKeyBrand]: "CoverageKey" };
10
+
11
+ export type IdentifierKind = "scenario" | "operation" | "coverage";
12
+
13
+ export interface IdentifierError {
14
+ readonly code: "invalid-identifier";
15
+ readonly kind: IdentifierKind;
16
+ readonly value: unknown;
17
+ readonly message: string;
18
+ }
19
+
20
+ const IDENTIFIER_PATTERN = /^[a-z][a-z0-9]*(?:[._/-][a-z0-9]+)*$/u;
21
+ const MAX_IDENTIFIER_LENGTH = 120;
22
+
23
+ function parseIdentifier(input: unknown, kind: IdentifierKind): Result<string, IdentifierError> {
24
+ if (
25
+ typeof input !== "string"
26
+ || input.length === 0
27
+ || input.length > MAX_IDENTIFIER_LENGTH
28
+ || !IDENTIFIER_PATTERN.test(input)
29
+ ) {
30
+ return err({
31
+ code: "invalid-identifier",
32
+ kind,
33
+ value: input,
34
+ message: `${kind} identifiers must be 1-${MAX_IDENTIFIER_LENGTH} lowercase ASCII characters with separated alphanumeric segments`,
35
+ });
36
+ }
37
+ return ok(input);
38
+ }
39
+
40
+ export function parseScenarioId(input: unknown): Result<ScenarioId, IdentifierError> {
41
+ const parsed = parseIdentifier(input, "scenario");
42
+ return parsed.ok ? ok(parsed.value as ScenarioId) : parsed;
43
+ }
44
+
45
+ export function parseOperationId(input: unknown): Result<OperationId, IdentifierError> {
46
+ const parsed = parseIdentifier(input, "operation");
47
+ return parsed.ok ? ok(parsed.value as OperationId) : parsed;
48
+ }
49
+
50
+ export function parseCoverageKey(input: unknown): Result<CoverageKey, IdentifierError> {
51
+ const parsed = parseIdentifier(input, "coverage");
52
+ return parsed.ok ? ok(parsed.value as CoverageKey) : parsed;
53
+ }
54
+
55
+ export function scenarioId(input: string): ScenarioId {
56
+ const parsed = parseScenarioId(input);
57
+ if (!parsed.ok) {
58
+ throw new Error(parsed.error.message);
59
+ }
60
+ return parsed.value;
61
+ }
62
+
63
+ export function operationId(input: string): OperationId {
64
+ const parsed = parseOperationId(input);
65
+ if (!parsed.ok) {
66
+ throw new Error(parsed.error.message);
67
+ }
68
+ return parsed.value;
69
+ }
70
+
71
+ export function coverageKey(input: string): CoverageKey {
72
+ const parsed = parseCoverageKey(input);
73
+ if (!parsed.ok) {
74
+ throw new Error(parsed.error.message);
75
+ }
76
+ return parsed.value;
77
+ }
@@ -0,0 +1,13 @@
1
+ export * from "./coverage.js";
2
+ export * from "./effects.js";
3
+ export * from "./fixture.js";
4
+ export * from "./ids.js";
5
+ export * from "./json.js";
6
+ export * from "./json-value.js";
7
+ export * from "./query.js";
8
+ export * from "./reason.js";
9
+ export * from "./result.js";
10
+ export * from "./resource.js";
11
+ export * from "./runtime.js";
12
+ export * from "./scenario.js";
13
+ export * from "./store.js";
@@ -0,0 +1,7 @@
1
+ export type JsonPrimitive = boolean | null | number | string;
2
+
3
+ export type JsonArray = readonly JsonValue[];
4
+
5
+ export type JsonObject = { [key: string]: JsonValue };
6
+
7
+ export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
@@ -0,0 +1,593 @@
1
+ import type { JsonObject, JsonValue } from "./json-value.js";
2
+ import { renderUnknownReason } from "./reason.js";
3
+ import { err, ok, type Result } from "./result.js";
4
+
5
+ export interface JsonLimits {
6
+ readonly maxDepth: number;
7
+ readonly maxNodes: number;
8
+ readonly maxStringBytes: number;
9
+ }
10
+
11
+ export const DEFAULT_JSON_LIMITS = Object.freeze({
12
+ maxDepth: 64,
13
+ maxNodes: 100_000,
14
+ maxStringBytes: 1_048_576,
15
+ }) satisfies JsonLimits;
16
+
17
+ export type JsonBoundaryErrorCode =
18
+ | "accessor-property"
19
+ | "cycle"
20
+ | "depth-exceeded"
21
+ | "invalid-number"
22
+ | "invalid-object"
23
+ | "invalid-type"
24
+ | "node-limit-exceeded"
25
+ | "string-limit-exceeded"
26
+ | "symbol-key";
27
+
28
+ export interface JsonBoundaryError {
29
+ readonly code: JsonBoundaryErrorCode;
30
+ readonly path: string;
31
+ readonly message: string;
32
+ }
33
+
34
+ export type ExactJsonSourceErrorCode = "duplicate-key" | "invalid-json";
35
+
36
+ export interface ExactJsonSourceError {
37
+ readonly code: ExactJsonSourceErrorCode;
38
+ readonly path: string;
39
+ readonly message: string;
40
+ }
41
+
42
+ interface JsonBudget {
43
+ nodes: number;
44
+ stringBytes: number;
45
+ }
46
+
47
+ interface JsonCloneOptions {
48
+ readonly freeze: boolean;
49
+ readonly normalizeNegativeZero: boolean;
50
+ readonly objectPrototype: "null" | "ordinary";
51
+ readonly sortObjectKeys: boolean;
52
+ }
53
+
54
+ const PARSED_JSON_OPTIONS = Object.freeze({
55
+ freeze: false,
56
+ normalizeNegativeZero: false,
57
+ objectPrototype: "null",
58
+ sortObjectKeys: false,
59
+ }) satisfies JsonCloneOptions;
60
+
61
+ const CLONED_JSON_OPTIONS = Object.freeze({
62
+ freeze: false,
63
+ normalizeNegativeZero: true,
64
+ objectPrototype: "ordinary",
65
+ sortObjectKeys: true,
66
+ }) satisfies JsonCloneOptions;
67
+
68
+ const FROZEN_CLONED_JSON_OPTIONS = Object.freeze({
69
+ freeze: true,
70
+ normalizeNegativeZero: true,
71
+ objectPrototype: "ordinary",
72
+ sortObjectKeys: true,
73
+ }) satisfies JsonCloneOptions;
74
+
75
+ function jsonError(code: JsonBoundaryErrorCode, path: string, message: string): JsonBoundaryError {
76
+ return { code, path, message };
77
+ }
78
+
79
+ function exactJsonSourceError(
80
+ code: ExactJsonSourceErrorCode,
81
+ path: string,
82
+ message: string,
83
+ ): ExactJsonSourceError {
84
+ return { code, path, message };
85
+ }
86
+
87
+ export function utf8ByteLength(value: string): number {
88
+ let bytes = 0;
89
+ for (let index = 0; index < value.length; index += 1) {
90
+ const code = value.charCodeAt(index);
91
+ if (code <= 0x7f) {
92
+ bytes += 1;
93
+ } else if (code <= 0x7ff) {
94
+ bytes += 2;
95
+ } else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
96
+ const next = value.charCodeAt(index + 1);
97
+ if (next >= 0xdc00 && next <= 0xdfff) {
98
+ bytes += 4;
99
+ index += 1;
100
+ } else {
101
+ bytes += 3;
102
+ }
103
+ } else {
104
+ bytes += 3;
105
+ }
106
+ }
107
+ return bytes;
108
+ }
109
+
110
+ interface DuplicateJsonKey {
111
+ readonly key: string;
112
+ readonly path: string;
113
+ }
114
+
115
+ function childJsonPath(path: string, key: string): string {
116
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key)
117
+ ? `${path}.${key}`
118
+ : `${path}[${JSON.stringify(key)}]`;
119
+ }
120
+
121
+ /** Scan syntactically valid JSON while retaining object-key occurrences. */
122
+ function findDuplicateJsonKey(source: string): DuplicateJsonKey | null {
123
+ let index = 0;
124
+ let duplicate: DuplicateJsonKey | null = null;
125
+
126
+ const skipWhitespace = (): void => {
127
+ while (
128
+ source[index] === " "
129
+ || source[index] === "\n"
130
+ || source[index] === "\r"
131
+ || source[index] === "\t"
132
+ ) {
133
+ index += 1;
134
+ }
135
+ };
136
+
137
+ const readString = (): string => {
138
+ const start = index;
139
+ index += 1;
140
+ while (index < source.length) {
141
+ const character = source[index];
142
+ if (character === "\\") {
143
+ index += 2;
144
+ continue;
145
+ }
146
+ index += 1;
147
+ if (character === "\"") {
148
+ return JSON.parse(source.slice(start, index)) as string;
149
+ }
150
+ }
151
+ throw new Error("Unterminated JSON string");
152
+ };
153
+
154
+ const scanValue = (path: string): void => {
155
+ skipWhitespace();
156
+ const character = source[index];
157
+ if (character === "{") {
158
+ index += 1;
159
+ skipWhitespace();
160
+ if (source[index] === "}") {
161
+ index += 1;
162
+ return;
163
+ }
164
+ const keys = new Set<string>();
165
+ while (index < source.length) {
166
+ skipWhitespace();
167
+ const key = readString();
168
+ const keyPath = childJsonPath(path, key);
169
+ if (keys.has(key) && duplicate === null) {
170
+ duplicate = { key, path: keyPath };
171
+ }
172
+ keys.add(key);
173
+ skipWhitespace();
174
+ index += 1; // Colon. JSON.parse has already validated the grammar.
175
+ scanValue(keyPath);
176
+ skipWhitespace();
177
+ if (source[index] === "}") {
178
+ index += 1;
179
+ return;
180
+ }
181
+ index += 1; // Comma.
182
+ }
183
+ return;
184
+ }
185
+ if (character === "[") {
186
+ index += 1;
187
+ skipWhitespace();
188
+ if (source[index] === "]") {
189
+ index += 1;
190
+ return;
191
+ }
192
+ let itemIndex = 0;
193
+ while (index < source.length) {
194
+ scanValue(`${path}[${String(itemIndex)}]`);
195
+ itemIndex += 1;
196
+ skipWhitespace();
197
+ if (source[index] === "]") {
198
+ index += 1;
199
+ return;
200
+ }
201
+ index += 1; // Comma.
202
+ }
203
+ return;
204
+ }
205
+ if (character === "\"") {
206
+ readString();
207
+ return;
208
+ }
209
+ while (index < source.length) {
210
+ const next = source[index];
211
+ if (next === "," || next === "]" || next === "}" || /\s/u.test(next ?? "")) return;
212
+ index += 1;
213
+ }
214
+ };
215
+
216
+ skipWhitespace();
217
+ scanValue("$");
218
+ return duplicate;
219
+ }
220
+
221
+ /** Decode JSON text without allowing duplicate object keys to collapse. */
222
+ export function parseExactJsonSource(
223
+ source: unknown,
224
+ ): Result<unknown, ExactJsonSourceError> {
225
+ if (typeof source !== "string") {
226
+ return err(exactJsonSourceError("invalid-json", "$", "JSON source must be a string"));
227
+ }
228
+ let parsed: unknown;
229
+ try {
230
+ parsed = JSON.parse(source) as unknown;
231
+ } catch {
232
+ return err(exactJsonSourceError("invalid-json", "$", "Source is not valid JSON"));
233
+ }
234
+ try {
235
+ const duplicate = findDuplicateJsonKey(source);
236
+ return duplicate === null
237
+ ? ok(parsed)
238
+ : err(exactJsonSourceError(
239
+ "duplicate-key",
240
+ duplicate.path,
241
+ `Duplicate JSON object key at ${duplicate.path}: ${duplicate.key}`,
242
+ ));
243
+ } catch (reason) {
244
+ return err(exactJsonSourceError(
245
+ "invalid-json",
246
+ "$",
247
+ renderUnknownReason(reason, "JSON source inspection failed"),
248
+ ));
249
+ }
250
+ }
251
+
252
+ function parseJsonAt(
253
+ input: unknown,
254
+ path: string,
255
+ depth: number,
256
+ limits: JsonLimits,
257
+ budget: JsonBudget,
258
+ ancestors: ReadonlySet<object>,
259
+ options: JsonCloneOptions,
260
+ ): Result<JsonValue, JsonBoundaryError> {
261
+ budget.nodes += 1;
262
+ if (budget.nodes > limits.maxNodes) {
263
+ return err(jsonError("node-limit-exceeded", path, `JSON value exceeds ${limits.maxNodes} nodes`));
264
+ }
265
+ if (depth > limits.maxDepth) {
266
+ return err(jsonError("depth-exceeded", path, `JSON value exceeds depth ${limits.maxDepth}`));
267
+ }
268
+ if (input === null || typeof input === "boolean") {
269
+ return ok(input);
270
+ }
271
+ if (typeof input === "string") {
272
+ budget.stringBytes += utf8ByteLength(input);
273
+ if (budget.stringBytes > limits.maxStringBytes) {
274
+ return err(jsonError("string-limit-exceeded", path, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
275
+ }
276
+ return ok(input);
277
+ }
278
+ if (typeof input === "number") {
279
+ return Number.isFinite(input)
280
+ ? ok(options.normalizeNegativeZero && Object.is(input, -0) ? 0 : input)
281
+ : err(jsonError("invalid-number", path, "JSON numbers must be finite"));
282
+ }
283
+ if (typeof input !== "object") {
284
+ return err(jsonError("invalid-type", path, `${typeof input} is not a JSON value`));
285
+ }
286
+ if (ancestors.has(input)) {
287
+ return err(jsonError("cycle", path, "JSON values cannot contain cycles"));
288
+ }
289
+
290
+ const nextAncestors = new Set(ancestors);
291
+ nextAncestors.add(input);
292
+
293
+ if (Array.isArray(input)) {
294
+ if (Object.getPrototypeOf(input) !== Array.prototype) {
295
+ return err(jsonError("invalid-object", path, "JSON arrays must have the standard Array prototype"));
296
+ }
297
+ const lengthDescriptor = Object.getOwnPropertyDescriptor(input, "length");
298
+ if (
299
+ lengthDescriptor === undefined
300
+ || lengthDescriptor.get !== undefined
301
+ || lengthDescriptor.set !== undefined
302
+ || !Number.isSafeInteger(lengthDescriptor.value)
303
+ || (lengthDescriptor.value as number) < 0
304
+ ) {
305
+ return err(jsonError("invalid-object", path, "JSON arrays must have a valid data length"));
306
+ }
307
+ const length = lengthDescriptor.value as number;
308
+ for (const key of Reflect.ownKeys(input)) {
309
+ if (typeof key === "symbol") {
310
+ return err(jsonError("symbol-key", path, "JSON arrays cannot have symbol keys"));
311
+ }
312
+ if (key === "length") continue;
313
+ const index = Number(key);
314
+ if (!Number.isSafeInteger(index) || index < 0 || index >= length || String(index) !== key) {
315
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON arrays cannot have extra properties"));
316
+ }
317
+ }
318
+ const output: JsonValue[] = [];
319
+ for (let index = 0; index < length; index += 1) {
320
+ const descriptor = Object.getOwnPropertyDescriptor(input, index);
321
+ if (descriptor === undefined) {
322
+ return err(jsonError("invalid-object", `${path}[${index}]`, "Sparse arrays are not exact JSON values"));
323
+ }
324
+ if (descriptor.get !== undefined || descriptor.set !== undefined) {
325
+ return err(jsonError("accessor-property", `${path}[${index}]`, "JSON arrays must use data elements"));
326
+ }
327
+ if (!descriptor.enumerable) {
328
+ return err(jsonError("invalid-object", `${path}[${index}]`, "JSON array elements must be enumerable"));
329
+ }
330
+ const item = parseJsonAt(
331
+ descriptor.value,
332
+ `${path}[${index}]`,
333
+ depth + 1,
334
+ limits,
335
+ budget,
336
+ nextAncestors,
337
+ options,
338
+ );
339
+ if (!item.ok) {
340
+ return item;
341
+ }
342
+ output.push(item.value);
343
+ }
344
+ return ok(options.freeze ? Object.freeze(output) : output);
345
+ }
346
+
347
+ const prototype = Object.getPrototypeOf(input) as unknown;
348
+ if (prototype !== Object.prototype && prototype !== null) {
349
+ return err(jsonError("invalid-object", path, "JSON objects must have Object or null prototypes"));
350
+ }
351
+
352
+ const output = (
353
+ options.objectPrototype === "ordinary" ? {} : Object.create(null)
354
+ ) as JsonObject;
355
+ const entries: Array<readonly [string, JsonValue]> | null =
356
+ options.sortObjectKeys ? [] : null;
357
+ for (const key of Reflect.ownKeys(input)) {
358
+ if (typeof key === "symbol") {
359
+ return err(jsonError("symbol-key", path, "JSON objects cannot have symbol keys"));
360
+ }
361
+ const descriptor = Object.getOwnPropertyDescriptor(input, key);
362
+ if (descriptor === undefined || descriptor.get !== undefined || descriptor.set !== undefined) {
363
+ return err(jsonError("accessor-property", `${path}.${key}`, "JSON objects must use data properties"));
364
+ }
365
+ if (!descriptor.enumerable) {
366
+ return err(jsonError("invalid-object", `${path}.${key}`, "JSON object properties must be enumerable"));
367
+ }
368
+ budget.stringBytes += utf8ByteLength(key);
369
+ if (budget.stringBytes > limits.maxStringBytes) {
370
+ return err(jsonError("string-limit-exceeded", `${path}.${key}`, `JSON strings exceed ${limits.maxStringBytes} UTF-8 bytes`));
371
+ }
372
+ const child = parseJsonAt(
373
+ descriptor.value,
374
+ `${path}.${key}`,
375
+ depth + 1,
376
+ limits,
377
+ budget,
378
+ nextAncestors,
379
+ options,
380
+ );
381
+ if (!child.ok) {
382
+ return child;
383
+ }
384
+ if (entries === null) {
385
+ output[key] = child.value;
386
+ } else {
387
+ entries.push([key, child.value]);
388
+ }
389
+ }
390
+ if (entries !== null) {
391
+ entries.sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
392
+ for (const [key, value] of entries) {
393
+ Object.defineProperty(output, key, {
394
+ configurable: true,
395
+ enumerable: true,
396
+ value,
397
+ writable: true,
398
+ });
399
+ }
400
+ }
401
+ return ok(options.freeze ? Object.freeze(output) : output);
402
+ }
403
+
404
+ function validateAndCloneJson(
405
+ input: unknown,
406
+ limits: JsonLimits,
407
+ options: JsonCloneOptions,
408
+ ): Result<JsonValue, JsonBoundaryError> {
409
+ if (
410
+ !Number.isSafeInteger(limits.maxDepth)
411
+ || limits.maxDepth < 0
412
+ || !Number.isSafeInteger(limits.maxNodes)
413
+ || limits.maxNodes < 1
414
+ || !Number.isSafeInteger(limits.maxStringBytes)
415
+ || limits.maxStringBytes < 0
416
+ ) {
417
+ throw new Error("JSON limits must be non-negative safe integers and allow at least one node");
418
+ }
419
+ try {
420
+ return parseJsonAt(
421
+ input,
422
+ "$",
423
+ 0,
424
+ limits,
425
+ { nodes: 0, stringBytes: 0 },
426
+ new Set(),
427
+ options,
428
+ );
429
+ } catch (reason) {
430
+ return err(jsonError(
431
+ "invalid-object",
432
+ "$",
433
+ renderUnknownReason(reason, "JSON object inspection failed"),
434
+ ));
435
+ }
436
+ }
437
+
438
+ export function parseJsonValue(
439
+ input: unknown,
440
+ limits: JsonLimits = DEFAULT_JSON_LIMITS,
441
+ ): Result<JsonValue, JsonBoundaryError> {
442
+ return validateAndCloneJson(input, limits, PARSED_JSON_OPTIONS);
443
+ }
444
+
445
+ function canonicalize(value: JsonValue): string {
446
+ if (value === null || typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
447
+ return JSON.stringify(value);
448
+ }
449
+ if (Array.isArray(value)) {
450
+ return `[${value.map(canonicalize).join(",")}]`;
451
+ }
452
+ const entries = Object.entries(value)
453
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
454
+ .map(([key, child]) => `${JSON.stringify(key)}:${canonicalize(child)}`);
455
+ return `{${entries.join(",")}}`;
456
+ }
457
+
458
+ export function canonicalJson(
459
+ input: unknown,
460
+ limits: JsonLimits = DEFAULT_JSON_LIMITS,
461
+ ): Result<string, JsonBoundaryError> {
462
+ const parsed = parseJsonValue(input, limits);
463
+ return parsed.ok ? ok(canonicalize(parsed.value)) : parsed;
464
+ }
465
+
466
+ export function cloneJson(
467
+ input: unknown,
468
+ limits: JsonLimits = DEFAULT_JSON_LIMITS,
469
+ ): Result<JsonValue, JsonBoundaryError> {
470
+ return validateAndCloneJson(input, limits, CLONED_JSON_OPTIONS);
471
+ }
472
+
473
+ export function freezeJson<Value extends JsonValue>(value: Value): Value {
474
+ if (value !== null && typeof value === "object") {
475
+ for (const child of Array.isArray(value) ? value : Object.values(value)) {
476
+ freezeJson(child);
477
+ }
478
+ Object.freeze(value);
479
+ }
480
+ return value;
481
+ }
482
+
483
+ export const STABLE_HASH_ALGORITHM = "fnv1a-64" as const;
484
+ const TAGGED_STABLE_HASH_PATTERN = /^fnv1a-64:[0-9a-f]{16}$/u;
485
+ declare const stableHashValueBrand: unique symbol;
486
+
487
+ type StableHashValue = string & {
488
+ readonly [stableHashValueBrand]: "StableHashValue";
489
+ };
490
+
491
+ export interface StableHash {
492
+ readonly algorithm: typeof STABLE_HASH_ALGORITHM;
493
+ readonly value: StableHashValue;
494
+ }
495
+
496
+ export type TaggedStableHash = `${typeof STABLE_HASH_ALGORITHM}:${string}`;
497
+
498
+ export interface TaggedStableHashError {
499
+ readonly code: "invalid-stable-hash";
500
+ readonly message: string;
501
+ }
502
+
503
+ export function tagStableHash(hash: StableHash): TaggedStableHash {
504
+ return `${hash.algorithm}:${hash.value}`;
505
+ }
506
+
507
+ export function parseTaggedStableHash(
508
+ input: unknown,
509
+ ): Result<TaggedStableHash, TaggedStableHashError> {
510
+ return typeof input === "string" && TAGGED_STABLE_HASH_PATTERN.test(input)
511
+ ? ok(input as TaggedStableHash)
512
+ : err({
513
+ code: "invalid-stable-hash",
514
+ message: `Stable hashes must use ${STABLE_HASH_ALGORITHM} with 16 lowercase hexadecimal digits`,
515
+ });
516
+ }
517
+
518
+ function updateFnvByte(hash: bigint, byte: number): bigint {
519
+ return BigInt.asUintN(64, (hash ^ BigInt(byte)) * 0x100000001b3n);
520
+ }
521
+
522
+ export function stableHash(
523
+ input: unknown,
524
+ limits: JsonLimits = DEFAULT_JSON_LIMITS,
525
+ ): Result<StableHash, JsonBoundaryError> {
526
+ const serialized = canonicalJson(input, limits);
527
+ if (!serialized.ok) {
528
+ return serialized;
529
+ }
530
+ let hash = 0xcbf29ce484222325n;
531
+ for (let index = 0; index < serialized.value.length; index += 1) {
532
+ const code = serialized.value.charCodeAt(index);
533
+ if (code <= 0x7f) {
534
+ hash = updateFnvByte(hash, code);
535
+ } else if (code <= 0x7ff) {
536
+ hash = updateFnvByte(hash, 0xc0 | (code >> 6));
537
+ hash = updateFnvByte(hash, 0x80 | (code & 0x3f));
538
+ } else if (code >= 0xd800 && code <= 0xdbff && index + 1 < serialized.value.length) {
539
+ const next = serialized.value.charCodeAt(index + 1);
540
+ if (next >= 0xdc00 && next <= 0xdfff) {
541
+ const point = 0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00);
542
+ hash = updateFnvByte(hash, 0xf0 | (point >> 18));
543
+ hash = updateFnvByte(hash, 0x80 | ((point >> 12) & 0x3f));
544
+ hash = updateFnvByte(hash, 0x80 | ((point >> 6) & 0x3f));
545
+ hash = updateFnvByte(hash, 0x80 | (point & 0x3f));
546
+ index += 1;
547
+ } else {
548
+ hash = updateFnvByte(hash, 0xef);
549
+ hash = updateFnvByte(hash, 0xbf);
550
+ hash = updateFnvByte(hash, 0xbd);
551
+ }
552
+ } else {
553
+ hash = updateFnvByte(hash, 0xe0 | (code >> 12));
554
+ hash = updateFnvByte(hash, 0x80 | ((code >> 6) & 0x3f));
555
+ hash = updateFnvByte(hash, 0x80 | (code & 0x3f));
556
+ }
557
+ }
558
+ return ok({
559
+ algorithm: STABLE_HASH_ALGORITHM,
560
+ value: hash.toString(16).padStart(16, "0") as StableHashValue,
561
+ });
562
+ }
563
+
564
+ export type WorldParser<World extends JsonValue> = (input: unknown) => World;
565
+
566
+ export interface WorldParseError {
567
+ readonly code: "invalid-world";
568
+ readonly message: string;
569
+ }
570
+
571
+ export function parseAndCloneWorld<World extends JsonValue>(
572
+ input: unknown,
573
+ parseWorld: WorldParser<World>,
574
+ ): Result<World, JsonBoundaryError | WorldParseError> {
575
+ const cloned = cloneJson(input);
576
+ if (!cloned.ok) {
577
+ return cloned;
578
+ }
579
+ try {
580
+ const world = parseWorld(cloned.value);
581
+ const verified = validateAndCloneJson(
582
+ world,
583
+ DEFAULT_JSON_LIMITS,
584
+ FROZEN_CLONED_JSON_OPTIONS,
585
+ );
586
+ if (!verified.ok) {
587
+ return verified;
588
+ }
589
+ return ok(verified.value as World);
590
+ } catch (reason) {
591
+ return err({ code: "invalid-world", message: renderUnknownReason(reason) });
592
+ }
593
+ }