@actuarial-ts/compliance 0.6.1 → 0.7.0

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 (69) hide show
  1. package/README.md +6 -2
  2. package/dist/diagnosticArtifactStream.d.ts +30 -0
  3. package/dist/diagnosticArtifactStream.d.ts.map +1 -0
  4. package/dist/diagnosticArtifactStream.js +128 -0
  5. package/dist/diagnosticArtifactStream.js.map +1 -0
  6. package/dist/diagnosticByteSource.d.ts +10 -0
  7. package/dist/diagnosticByteSource.d.ts.map +1 -0
  8. package/dist/diagnosticByteSource.js +70 -0
  9. package/dist/diagnosticByteSource.js.map +1 -0
  10. package/dist/diagnosticByteView.d.ts +3 -0
  11. package/dist/diagnosticByteView.d.ts.map +1 -0
  12. package/dist/diagnosticByteView.js +27 -0
  13. package/dist/diagnosticByteView.js.map +1 -0
  14. package/dist/diagnosticCompactRun.d.ts +43 -0
  15. package/dist/diagnosticCompactRun.d.ts.map +1 -0
  16. package/dist/diagnosticCompactRun.js +253 -0
  17. package/dist/diagnosticCompactRun.js.map +1 -0
  18. package/dist/diagnosticReplayFrames.d.ts +10 -0
  19. package/dist/diagnosticReplayFrames.d.ts.map +1 -0
  20. package/dist/diagnosticReplayFrames.js +99 -0
  21. package/dist/diagnosticReplayFrames.js.map +1 -0
  22. package/dist/diagnosticReplayProtocol.d.ts +17 -0
  23. package/dist/diagnosticReplayProtocol.d.ts.map +1 -0
  24. package/dist/diagnosticReplayProtocol.js +98 -0
  25. package/dist/diagnosticReplayProtocol.js.map +1 -0
  26. package/dist/diagnosticReplayReader.d.ts +41 -0
  27. package/dist/diagnosticReplayReader.d.ts.map +1 -0
  28. package/dist/diagnosticReplayReader.js +346 -0
  29. package/dist/diagnosticReplayReader.js.map +1 -0
  30. package/dist/diagnosticReplayValue.d.ts +34 -0
  31. package/dist/diagnosticReplayValue.d.ts.map +1 -0
  32. package/dist/diagnosticReplayValue.js +269 -0
  33. package/dist/diagnosticReplayValue.js.map +1 -0
  34. package/dist/diagnosticReplayWriter.d.ts +25 -0
  35. package/dist/diagnosticReplayWriter.d.ts.map +1 -0
  36. package/dist/diagnosticReplayWriter.js +148 -0
  37. package/dist/diagnosticReplayWriter.js.map +1 -0
  38. package/dist/diagnosticRun.d.ts.map +1 -1
  39. package/dist/diagnosticRun.js +1 -193
  40. package/dist/diagnosticRun.js.map +1 -1
  41. package/dist/diagnosticRunEvidence.d.ts +18 -0
  42. package/dist/diagnosticRunEvidence.d.ts.map +1 -0
  43. package/dist/diagnosticRunEvidence.js +195 -0
  44. package/dist/diagnosticRunEvidence.js.map +1 -0
  45. package/dist/index.d.ts +4 -0
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +4 -0
  48. package/dist/index.js.map +1 -1
  49. package/dist/sha256Stream.d.ts +6 -0
  50. package/dist/sha256Stream.d.ts.map +1 -0
  51. package/dist/sha256Stream.js +103 -0
  52. package/dist/sha256Stream.js.map +1 -0
  53. package/dist/version.d.ts +1 -1
  54. package/dist/version.js +1 -1
  55. package/package.json +4 -4
  56. package/src/diagnosticArtifactStream.ts +148 -0
  57. package/src/diagnosticByteSource.ts +82 -0
  58. package/src/diagnosticByteView.ts +27 -0
  59. package/src/diagnosticCompactRun.ts +462 -0
  60. package/src/diagnosticReplayFrames.ts +99 -0
  61. package/src/diagnosticReplayProtocol.ts +99 -0
  62. package/src/diagnosticReplayReader.ts +428 -0
  63. package/src/diagnosticReplayValue.ts +269 -0
  64. package/src/diagnosticReplayWriter.ts +190 -0
  65. package/src/diagnosticRun.ts +7 -373
  66. package/src/diagnosticRunEvidence.ts +391 -0
  67. package/src/index.ts +10 -0
  68. package/src/sha256Stream.ts +108 -0
  69. package/src/version.ts +1 -1
@@ -0,0 +1,428 @@
1
+ import { iterateDiagnosticIdentityJson, type DiagnosticIdentityDocument } from "@actuarial-ts/core";
2
+ import {
3
+ runValidatedMetricDiagnosticsCompact,
4
+ validateDiagnosticRunInputCompact,
5
+ } from "@actuarial-ts/data";
6
+ import { digestDiagnosticArtifactChunks } from "./diagnosticArtifactStream.js";
7
+ import {
8
+ createCompactDiagnosticRunIdentity,
9
+ type CompactDiagnosticArtifactEvidence,
10
+ } from "./diagnosticCompactRun.js";
11
+ import { diagnosticByteView } from "./diagnosticByteView.js";
12
+ import { consumeDiagnosticByteSource } from "./diagnosticByteSource.js";
13
+ import {
14
+ decodeReplayFrames,
15
+ MAX_REPLAY_READ_BYTES,
16
+ REPLAY_TEXT_UNITS,
17
+ type ReplayFrame,
18
+ } from "./diagnosticReplayFrames.js";
19
+ import { ReplayValueBuilder, type ReplayValueLimits } from "./diagnosticReplayValue.js";
20
+ import type {
21
+ DiagnosticArtifactDigest,
22
+ DiagnosticPreparationLineage,
23
+ DiagnosticRunIdentity,
24
+ } from "./diagnosticRun.js";
25
+ import type { DiagnosticReplayByteSource } from "./diagnosticReplayWriter.js";
26
+ import { createSha256 } from "./sha256Stream.js";
27
+ import {
28
+ REPLAY_FORMAT,
29
+ REPLAY_VERSION,
30
+ badReplay,
31
+ mismatch,
32
+ replayArray,
33
+ replayCancellation,
34
+ replayFrameBytes,
35
+ replayLimit,
36
+ replayRecord,
37
+ replayToken,
38
+ decodeArtifactChunk,
39
+ } from "./diagnosticReplayProtocol.js";
40
+
41
+ /** Host resource policy, not a representation of the SDK's tested capacity. */
42
+ export interface DiagnosticReplayReadLimits {
43
+ readonly maximumEncodedBytes: number;
44
+ readonly maximumArtifacts: number;
45
+ readonly maximumRuns: number;
46
+ readonly maximumArtifactBytes: number;
47
+ readonly maximumInputDepth: number;
48
+ readonly maximumInputNodes: number;
49
+ readonly maximumInputStringUnits: number;
50
+ readonly maximumInputTotalStringUnits: number;
51
+ }
52
+ export interface VerifyDiagnosticReplayStreamOptions {
53
+ readonly limits: DiagnosticReplayReadLimits;
54
+ readonly signal?: AbortSignal;
55
+ }
56
+ declare const verifiedReplayBrand: unique symbol;
57
+ export interface VerifiedDiagnosticReplayStream {
58
+ readonly [verifiedReplayBrand]: true;
59
+ readonly format: "diagnostic-replay";
60
+ readonly version: 1;
61
+ readonly artifacts: readonly DiagnosticArtifactDigest[];
62
+ readonly runs: readonly (DiagnosticRunIdentity & { readonly id: string })[];
63
+ /** SHA-256 of canonical numbered frames before the final trailer; not a signature. */
64
+ readonly frameDigest: string;
65
+ }
66
+ const verifiedStreams = new WeakSet<object>();
67
+
68
+ class ReplayCursor {
69
+ private sequence = 0;
70
+ private readonly hash = createSha256();
71
+ constructor(
72
+ private readonly iterator: AsyncGenerator<ReplayFrame>,
73
+ private readonly checkCancellation: () => void,
74
+ ) {}
75
+ async next(): Promise<ReplayFrame> {
76
+ this.checkCancellation();
77
+ const item = await this.iterator.next();
78
+ this.checkCancellation();
79
+ if (item.done) badReplay("Replay ended before its required trailer or section boundary");
80
+ const frame = item.value;
81
+ if (
82
+ frame.length < 2 ||
83
+ frame[0] !== this.sequence ||
84
+ !Number.isSafeInteger(frame[0]) ||
85
+ typeof frame[1] !== "string"
86
+ )
87
+ badReplay("Replay frame has an invalid sequence or event");
88
+ this.sequence++;
89
+ if (frame[1] !== "end") this.hash.update(replayFrameBytes(frame));
90
+ return frame.slice(1);
91
+ }
92
+ digest() {
93
+ return this.hash.digest();
94
+ }
95
+ async requireEof(): Promise<void> {
96
+ this.checkCancellation();
97
+ if (!(await this.iterator.next()).done) badReplay("Unexpected frames after the replay trailer");
98
+ this.checkCancellation();
99
+ }
100
+ async close() {
101
+ await this.iterator.return(undefined);
102
+ }
103
+ }
104
+ function event(frame: ReplayFrame, name: string, arity: number): void {
105
+ if (frame[0] !== name || frame.length !== arity)
106
+ badReplay(`Expected ${name} with exact arity ${arity}`);
107
+ }
108
+ async function readValue(
109
+ cursor: ReplayCursor,
110
+ ending: string,
111
+ limits: ReplayValueLimits,
112
+ ): Promise<unknown> {
113
+ const builder = new ReplayValueBuilder(limits);
114
+ for (;;) {
115
+ const frame = await cursor.next();
116
+ if (frame[0] === ending) {
117
+ event(frame, ending, 1);
118
+ return builder.finish();
119
+ }
120
+ builder.push(frame);
121
+ }
122
+ }
123
+
124
+ /** Compare complete expected canonical text with bounded candidate fragments. */
125
+ async function compareEvidence(
126
+ cursor: ReplayCursor,
127
+ channel: "manifest" | "result",
128
+ document: DiagnosticIdentityDocument,
129
+ ): Promise<void> {
130
+ const expected = iterateDiagnosticIdentityJson(document);
131
+ let chunk = "",
132
+ index = 0,
133
+ position = 0,
134
+ done = false;
135
+ function advance(): void {
136
+ while (!done && index === chunk.length) {
137
+ const item = expected.next();
138
+ done = Boolean(item.done);
139
+ chunk = item.done ? "" : item.value;
140
+ index = 0;
141
+ }
142
+ }
143
+ try {
144
+ for (;;) {
145
+ const frame = await cursor.next();
146
+ if (frame[0] === `${channel}-end`) {
147
+ event(frame, `${channel}-end`, 1);
148
+ advance();
149
+ if (!done) mismatch(`${channel} evidence is truncated at code-unit offset ${position}`);
150
+ return;
151
+ }
152
+ event(frame, channel, 2);
153
+ const text = frame[1];
154
+ if (typeof text !== "string" || text.length === 0 || text.length > REPLAY_TEXT_UNITS)
155
+ badReplay("Invalid bounded canonical evidence fragment");
156
+ let start = 0;
157
+ while (start < text.length) {
158
+ advance();
159
+ if (done) mismatch(`${channel} evidence has extra content at code-unit offset ${position}`);
160
+ const length = Math.min(text.length - start, chunk.length - index);
161
+ for (let offset = 0; offset < length; offset++)
162
+ if (text.charCodeAt(start + offset) !== chunk.charCodeAt(index + offset))
163
+ mismatch(`${channel} evidence differs at code-unit offset ${position + offset}`);
164
+ start += length;
165
+ index += length;
166
+ position += length;
167
+ }
168
+ }
169
+ } finally {
170
+ expected.return(undefined);
171
+ }
172
+ }
173
+
174
+ function artifactMetadata(value: unknown): DiagnosticArtifactDigest {
175
+ const item = replayRecord(value, [
176
+ "id",
177
+ "scope",
178
+ "assurance",
179
+ "algorithm",
180
+ "value",
181
+ "byteLength",
182
+ ]);
183
+ const id = replayToken(item.id);
184
+ if (item.scope !== "input" && item.scope !== "preparation") badReplay("Invalid artifact scope");
185
+ const scope = item.scope;
186
+ if (item.assurance === "caller-declared") {
187
+ if (Object.hasOwn(item, "byteLength"))
188
+ badReplay("Declared artifact cannot claim a computed byte length");
189
+ return Object.freeze({
190
+ id,
191
+ scope,
192
+ assurance: "caller-declared",
193
+ algorithm: replayToken(item.algorithm),
194
+ value: replayToken(item.value),
195
+ });
196
+ }
197
+ if (
198
+ item.assurance !== "sdk-computed" ||
199
+ item.algorithm !== "sha256" ||
200
+ typeof item.value !== "string" ||
201
+ !/^[a-f0-9]{64}$/.test(item.value) ||
202
+ typeof item.byteLength !== "number" ||
203
+ !Number.isSafeInteger(item.byteLength) ||
204
+ item.byteLength < 0
205
+ )
206
+ badReplay("Invalid computed artifact digest metadata");
207
+ return Object.freeze({
208
+ id,
209
+ scope,
210
+ assurance: "sdk-computed",
211
+ algorithm: "sha256",
212
+ value: item.value,
213
+ byteLength: item.byteLength,
214
+ });
215
+ }
216
+
217
+ async function readArtifact(
218
+ cursor: ReplayCursor,
219
+ inputLimits: ReplayValueLimits,
220
+ maximumArtifactBytes: number,
221
+ ): Promise<CompactDiagnosticArtifactEvidence> {
222
+ const artifact = artifactMetadata(await readValue(cursor, "artifact-data", inputLimits));
223
+ if (artifact.assurance === "caller-declared") {
224
+ event(await cursor.next(), "artifact-end", 1);
225
+ return artifact;
226
+ }
227
+ async function* bytes() {
228
+ for (;;) {
229
+ const frame = await cursor.next();
230
+ if (frame[0] === "artifact-end") {
231
+ event(frame, "artifact-end", 1);
232
+ return;
233
+ }
234
+ event(frame, "bytes", 2);
235
+ yield decodeArtifactChunk(frame[1]);
236
+ }
237
+ }
238
+ const computed = await digestDiagnosticArtifactChunks(
239
+ {
240
+ id: artifact.id,
241
+ scope: artifact.scope,
242
+ expectedByteLength: artifact.byteLength,
243
+ maximumByteLength: maximumArtifactBytes,
244
+ },
245
+ bytes(),
246
+ );
247
+ if (computed.value !== artifact.value)
248
+ mismatch(`Artifact ${artifact.id} bytes do not match its digest`);
249
+ return computed;
250
+ }
251
+
252
+ async function readRun(
253
+ cursor: ReplayCursor,
254
+ artifacts: ReadonlyMap<string, CompactDiagnosticArtifactEvidence>,
255
+ used: Set<string>,
256
+ limits: ReplayValueLimits,
257
+ ) {
258
+ const item = replayRecord(await readValue(cursor, "input-end", limits), [
259
+ "id",
260
+ "input",
261
+ "inputArtifactIds",
262
+ "preparationArtifactIds",
263
+ "preparationLineage",
264
+ ]);
265
+ const id = replayToken(item.id);
266
+ function resolve(value: unknown, scope: "input" | "preparation") {
267
+ const seen = new Set<string>();
268
+ return replayArray(value).map((key) => {
269
+ const token = replayToken(key);
270
+ if (seen.has(token)) badReplay("Duplicate artifact reference in replay run");
271
+ seen.add(token);
272
+ const artifact = artifacts.get(token);
273
+ if (!artifact || artifact.scope !== scope)
274
+ badReplay("Replay references an absent artifact or the wrong scope");
275
+ used.add(token);
276
+ return artifact;
277
+ });
278
+ }
279
+ const inputArtifacts = resolve(item.inputArtifactIds, "input");
280
+ const preparationArtifacts = resolve(item.preparationArtifactIds, "preparation");
281
+ // Candidate raw input always crosses the complete SDK schema, ownership,
282
+ // preparation and execution-policy boundaries. Parsing does not grant trust.
283
+ const completedRun = runValidatedMetricDiagnosticsCompact(
284
+ validateDiagnosticRunInputCompact(item.input),
285
+ );
286
+ if (completedRun.status !== "completed")
287
+ mismatch(`Replay run ${id} is blocked at ${completedRun.stage}`);
288
+ const provenance = createCompactDiagnosticRunIdentity({
289
+ completedRun,
290
+ inputArtifacts,
291
+ preparationArtifacts,
292
+ preparationLineage: item.preparationLineage as readonly DiagnosticPreparationLineage[],
293
+ });
294
+ await compareEvidence(cursor, "manifest", provenance.manifestIdentityDocument);
295
+ await compareEvidence(cursor, "result", provenance.resultIdentityDocument);
296
+ const end = await cursor.next();
297
+ event(end, "run-end", 4);
298
+ if (
299
+ end[1] !== provenance.runFingerprint ||
300
+ end[2] !== provenance.resultFingerprint ||
301
+ end[3] !== provenance.runResultFingerprint
302
+ )
303
+ mismatch("Replayed fingerprint tags do not match the full verified evidence");
304
+ return Object.freeze({
305
+ id,
306
+ runFingerprint: provenance.runFingerprint,
307
+ resultFingerprint: provenance.resultFingerprint,
308
+ runResultFingerprint: provenance.runResultFingerprint,
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Verify all bytes/evidence and independently replay every run, sequentially.
314
+ * Reads must be <=128 KiB. Required resource limits apply before retaining raw
315
+ * inputs; results/audits are compared as text, never reconstructed from a file.
316
+ * Only a final receipt is returned after trailer integrity AND EOF. Hosts must
317
+ * not treat a partially consumed stream as a valid archive. Cancellation is
318
+ * cooperative between chunks/phases; use the signal for host I/O as well.
319
+ */
320
+ export function verifyDiagnosticReplayStream(
321
+ chunks: DiagnosticReplayByteSource,
322
+ supplied: VerifyDiagnosticReplayStreamOptions,
323
+ ): Promise<VerifiedDiagnosticReplayStream> {
324
+ const options = replayRecord(supplied, ["limits", "signal"]);
325
+ const fields = [
326
+ "maximumEncodedBytes",
327
+ "maximumArtifacts",
328
+ "maximumRuns",
329
+ "maximumArtifactBytes",
330
+ "maximumInputDepth",
331
+ "maximumInputNodes",
332
+ "maximumInputStringUnits",
333
+ "maximumInputTotalStringUnits",
334
+ ] as const;
335
+ const raw = replayRecord(options.limits, fields);
336
+ const limits = Object.fromEntries(
337
+ fields.map((key) => [key, replayLimit(raw[key], key)]),
338
+ ) as unknown as DiagnosticReplayReadLimits;
339
+ const inputLimits: ReplayValueLimits = {
340
+ maximumDepth: limits.maximumInputDepth,
341
+ maximumNodes: limits.maximumInputNodes,
342
+ maximumStringUnits: limits.maximumInputStringUnits,
343
+ maximumTotalStringUnits: limits.maximumInputTotalStringUnits,
344
+ };
345
+ const checkCancellation = replayCancellation(options.signal);
346
+ checkCancellation();
347
+ async function* bounded() {
348
+ let size = 0;
349
+ for await (const chunk of consumeDiagnosticByteSource(chunks, checkCancellation)) {
350
+ checkCancellation();
351
+ const view = diagnosticByteView(chunk);
352
+ if (view.length > MAX_REPLAY_READ_BYTES) badReplay("Replay reads must be at most 128 KiB");
353
+ if (view.length > limits.maximumEncodedBytes - size)
354
+ badReplay("Replay exceeds the encoded byte limit");
355
+ size += view.length;
356
+ yield new Uint8Array(view);
357
+ }
358
+ }
359
+ async function verify(): Promise<VerifiedDiagnosticReplayStream> {
360
+ const cursor = new ReplayCursor(decodeReplayFrames(bounded()), checkCancellation);
361
+ let failed = false;
362
+ try {
363
+ const header = await cursor.next();
364
+ event(header, REPLAY_FORMAT, 2);
365
+ if (header[1] !== REPLAY_VERSION) badReplay("Unsupported diagnostic replay version");
366
+ const artifacts = new Map<string, CompactDiagnosticArtifactEvidence>();
367
+ let frame = await cursor.next();
368
+ while (frame[0] === "artifact") {
369
+ event(frame, "artifact", 1);
370
+ if (artifacts.size >= limits.maximumArtifacts)
371
+ badReplay("Replay exceeds its artifact count limit");
372
+ const artifact = await readArtifact(cursor, inputLimits, limits.maximumArtifactBytes);
373
+ if (artifacts.has(artifact.id)) badReplay("Duplicate artifact ID in replay archive");
374
+ artifacts.set(artifact.id, artifact);
375
+ frame = await cursor.next();
376
+ }
377
+ const runs: (DiagnosticRunIdentity & { readonly id: string })[] = [];
378
+ const runIds = new Set<string>(),
379
+ used = new Set<string>();
380
+ while (frame[0] === "run") {
381
+ event(frame, "run", 1);
382
+ if (runs.length >= limits.maximumRuns) badReplay("Replay exceeds its run count limit");
383
+ const run = await readRun(cursor, artifacts, used, inputLimits);
384
+ if (runIds.has(run.id)) badReplay("Duplicate run ID in replay archive");
385
+ runIds.add(run.id);
386
+ runs.push(run);
387
+ frame = await cursor.next();
388
+ }
389
+ event(frame, "end", 4);
390
+ if (
391
+ !runs.length ||
392
+ frame[1] !== artifacts.size ||
393
+ frame[2] !== runs.length ||
394
+ used.size !== artifacts.size
395
+ )
396
+ badReplay("Replay trailer counts, run presence or artifact ownership are invalid");
397
+ const frameDigest = cursor.digest();
398
+ if (frame[3] !== frameDigest) mismatch("Replay frame integrity digest does not match");
399
+ await cursor.requireEof();
400
+ const receipt = Object.freeze({
401
+ format: REPLAY_FORMAT,
402
+ version: REPLAY_VERSION,
403
+ artifacts: Object.freeze([...artifacts.values()]),
404
+ runs: Object.freeze(runs),
405
+ frameDigest,
406
+ }) as VerifiedDiagnosticReplayStream;
407
+ verifiedStreams.add(receipt);
408
+ return receipt;
409
+ } catch (error) {
410
+ failed = true;
411
+ throw error;
412
+ } finally {
413
+ try {
414
+ await cursor.close();
415
+ } catch (cleanupError) {
416
+ if (!failed) throw cleanupError;
417
+ }
418
+ }
419
+ }
420
+ return verify();
421
+ }
422
+
423
+ export function assertVerifiedDiagnosticReplayStream(
424
+ value: unknown,
425
+ ): asserts value is VerifiedDiagnosticReplayStream {
426
+ if (value === null || typeof value !== "object" || !verifiedStreams.has(value))
427
+ badReplay("Value is not an authentic fully verified replay stream receipt");
428
+ }
@@ -0,0 +1,269 @@
1
+ import { ComplianceError } from "./errors.js";
2
+ import { REPLAY_TEXT_UNITS, type ReplayFrame } from "./diagnosticReplayFrames.js";
3
+
4
+ export interface ReplayValueLimits {
5
+ readonly maximumDepth: number;
6
+ readonly maximumNodes: number;
7
+ /** Maximum UTF-16 code units in one key or string value. */
8
+ readonly maximumStringUnits: number;
9
+ /** Maximum UTF-16 code units across every key and string value in this input. */
10
+ readonly maximumTotalStringUnits: number;
11
+ }
12
+ function invalid(message: string): never {
13
+ throw new ComplianceError("BAD_DIAGNOSTIC_RUN", message, "$.replayInput");
14
+ }
15
+
16
+ function snapshotLimits(limits: ReplayValueLimits): ReplayValueLimits {
17
+ if (
18
+ limits === null ||
19
+ typeof limits !== "object" ||
20
+ (Object.getPrototypeOf(limits) !== Object.prototype && Object.getPrototypeOf(limits) !== null)
21
+ )
22
+ invalid("Replay value limits must be a plain data object");
23
+ const fields = [
24
+ "maximumDepth",
25
+ "maximumNodes",
26
+ "maximumStringUnits",
27
+ "maximumTotalStringUnits",
28
+ ] as const;
29
+ const descriptors = Object.getOwnPropertyDescriptors(limits);
30
+ for (const key of Reflect.ownKeys(descriptors))
31
+ if (typeof key !== "string" || !fields.some((field) => field === key))
32
+ invalid("Unknown replay value limit");
33
+ const owned = Object.create(null) as Record<(typeof fields)[number], number>;
34
+ for (const key of fields) {
35
+ const field = descriptors[key];
36
+ if (!field || !field.enumerable || !("value" in field))
37
+ invalid("Replay value limits require own enumerable data properties");
38
+ const value: unknown = field.value;
39
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1)
40
+ invalid("Replay value limits must be positive safe integers");
41
+ owned[key] = value;
42
+ }
43
+ return Object.freeze(owned);
44
+ }
45
+ type Container =
46
+ | { kind: "array"; value: unknown[] }
47
+ | { kind: "object"; value: Record<string, unknown>; lastKey: string | null; key: string | null };
48
+
49
+ /**
50
+ * Reconstruct input only, within explicit host resource bounds. Reconstructed
51
+ * values still require full SDK validation. Never use this for candidate
52
+ * result/audit identity channels: those must be compared as text streams.
53
+ */
54
+ export class ReplayValueBuilder {
55
+ private readonly limits: ReplayValueLimits;
56
+ private readonly stack: Container[] = [];
57
+ private nodes = 0;
58
+ private assigned = false;
59
+ private root: unknown;
60
+ private stringParts: string[] | null = null;
61
+ private stringUnits = 0;
62
+ private totalStringUnits = 0;
63
+ private stringKind: "key" | "value" | null = null;
64
+ private sealed = false;
65
+
66
+ constructor(limits: ReplayValueLimits) {
67
+ this.limits = snapshotLimits(limits);
68
+ }
69
+ private attach(value: unknown) {
70
+ if (++this.nodes > this.limits.maximumNodes) invalid("Replay input exceeds its node limit");
71
+ const parent = this.stack.at(-1);
72
+ if (!parent) {
73
+ if (this.assigned) invalid("Multiple roots in replay input");
74
+ this.root = value;
75
+ this.assigned = true;
76
+ } else if (parent.kind === "array") parent.value.push(value);
77
+ else {
78
+ if (parent.key === null) invalid("Object value is missing its key");
79
+ Object.defineProperty(parent.value, parent.key, {
80
+ value,
81
+ enumerable: true,
82
+ writable: true,
83
+ configurable: true,
84
+ });
85
+ parent.key = null;
86
+ }
87
+ }
88
+ push(frame: ReplayFrame): void {
89
+ if (this.sealed) invalid("Replay input builder is finalized");
90
+ try {
91
+ this.consume(frame);
92
+ } catch (error) {
93
+ this.sealed = true;
94
+ throw error;
95
+ }
96
+ }
97
+ private consume(frame: ReplayFrame): void {
98
+ if (!Array.isArray(frame)) invalid("Replay value event must be an array");
99
+ const [event, payload] = frame;
100
+ const arity = (expected: number) => {
101
+ if (frame.length !== expected) invalid("Invalid replay value event arity");
102
+ };
103
+ if (this.stringParts !== null) {
104
+ if (event === "text") {
105
+ arity(2);
106
+ if (
107
+ typeof payload !== "string" ||
108
+ payload.length === 0 ||
109
+ payload.length > REPLAY_TEXT_UNITS
110
+ )
111
+ invalid("Invalid replay text fragment");
112
+ if (payload.length > this.limits.maximumStringUnits - this.stringUnits)
113
+ invalid("Replay input exceeds its string limit");
114
+ if (payload.length > this.limits.maximumTotalStringUnits - this.totalStringUnits)
115
+ invalid("Replay input exceeds its total string limit");
116
+ // Check both budgets before retaining this fragment. Key text counts,
117
+ // and starting another string never resets the whole-input budget.
118
+ this.stringUnits += payload.length;
119
+ this.totalStringUnits += payload.length;
120
+ this.stringParts.push(payload);
121
+ return;
122
+ }
123
+ if (event !== "string-end") invalid("Replay string was not terminated");
124
+ arity(1);
125
+ const text = this.stringParts.join("");
126
+ const kind = this.stringKind;
127
+ this.stringParts = null;
128
+ this.stringKind = null;
129
+ if (kind === "value") this.attach(text);
130
+ else {
131
+ const parent = this.stack.at(-1);
132
+ if (!parent || parent.kind !== "object" || parent.key !== null)
133
+ invalid("Unexpected replay object key");
134
+ if (parent.lastKey !== null && text <= parent.lastKey)
135
+ invalid("Replay object keys must be strictly increasing and unique");
136
+ parent.lastKey = text;
137
+ parent.key = text;
138
+ }
139
+ return;
140
+ }
141
+ if (event === "string-start") {
142
+ arity(2);
143
+ if (payload !== "key" && payload !== "value") invalid("Invalid replay string role");
144
+ const parent = this.stack.at(-1);
145
+ if (payload === "key" && (!parent || parent.kind !== "object" || parent.key !== null))
146
+ invalid("Unexpected replay object key");
147
+ this.stringParts = [];
148
+ this.stringKind = payload;
149
+ this.stringUnits = 0;
150
+ } else if (event === "object" || event === "array") {
151
+ arity(1);
152
+ if (this.stack.length >= this.limits.maximumDepth)
153
+ invalid("Replay input exceeds its depth limit");
154
+ const container: Container =
155
+ event === "array"
156
+ ? { kind: "array", value: [] }
157
+ : { kind: "object", value: Object.create(null), lastKey: null, key: null };
158
+ this.attach(container.value);
159
+ this.stack.push(container);
160
+ } else if (event === "end-object" || event === "end-array") {
161
+ arity(1);
162
+ const parent = this.stack.at(-1);
163
+ if (
164
+ !parent ||
165
+ event !== `end-${parent.kind}` ||
166
+ (parent.kind === "object" && parent.key !== null)
167
+ )
168
+ invalid("Mismatched or incomplete replay container");
169
+ this.stack.pop();
170
+ } else if (event === "scalar") {
171
+ arity(2);
172
+ if (
173
+ payload !== null &&
174
+ typeof payload !== "boolean" &&
175
+ !(typeof payload === "number" && Number.isFinite(payload))
176
+ )
177
+ invalid("Invalid replay scalar");
178
+ this.attach(payload);
179
+ } else if (event === "special-number") {
180
+ arity(2);
181
+ // Preserve raw non-finite observations for SDK audit/blocking, not null.
182
+ if (payload === "NaN") this.attach(NaN);
183
+ else if (payload === "+Infinity") this.attach(Infinity);
184
+ else if (payload === "-Infinity") this.attach(-Infinity);
185
+ else if (payload === "-0") this.attach(-0);
186
+ else invalid("Invalid special replay number");
187
+ } else invalid("Unknown replay value event");
188
+ }
189
+ finish(): unknown {
190
+ if (this.sealed) invalid("Replay input builder is finalized");
191
+ this.sealed = true;
192
+ if (!this.assigned || this.stack.length || this.stringParts !== null)
193
+ invalid("Incomplete replay input value");
194
+ return this.root;
195
+ }
196
+ }
197
+
198
+ /** Live read-only input events. The enclosing genuine run owns its snapshots. */
199
+ export function* replayValueFrames(value: unknown): Generator<ReplayFrame> {
200
+ const active = new Set<object>();
201
+ function* string(text: string, role: "key" | "value"): Generator<ReplayFrame> {
202
+ yield ["string-start", role];
203
+ for (let index = 0; index < text.length; index += REPLAY_TEXT_UNITS)
204
+ yield ["text", text.slice(index, index + REPLAY_TEXT_UNITS)];
205
+ yield ["string-end"];
206
+ }
207
+ function* visit(value: unknown): Generator<ReplayFrame> {
208
+ if (typeof value === "string") {
209
+ yield* string(value, "value");
210
+ return;
211
+ }
212
+ if (typeof value === "number" && (!Number.isFinite(value) || Object.is(value, -0))) {
213
+ yield [
214
+ "special-number",
215
+ Number.isNaN(value)
216
+ ? "NaN"
217
+ : value === Infinity
218
+ ? "+Infinity"
219
+ : value === -Infinity
220
+ ? "-Infinity"
221
+ : "-0",
222
+ ];
223
+ return;
224
+ }
225
+ if (value === null || typeof value === "number" || typeof value === "boolean") {
226
+ yield ["scalar", value];
227
+ return;
228
+ }
229
+ if (typeof value !== "object") invalid("Unsupported replay input value");
230
+ const array = Array.isArray(value);
231
+ const prototype = Object.getPrototypeOf(value);
232
+ if (
233
+ array ? prototype !== Array.prototype : prototype !== null && prototype !== Object.prototype
234
+ )
235
+ invalid("Replay input must contain plain data");
236
+ if (active.has(value)) invalid("Replay input contains a cycle");
237
+ active.add(value);
238
+ try {
239
+ yield [array ? "array" : "object"];
240
+ if (array) {
241
+ for (const key of Reflect.ownKeys(value))
242
+ if (
243
+ key !== "length" &&
244
+ (typeof key !== "string" ||
245
+ !/^(0|[1-9][0-9]*)$/.test(key) ||
246
+ Number(key) >= value.length)
247
+ )
248
+ invalid("Replay input arrays must contain indexed data only");
249
+ for (let index = 0; index < value.length; index++) {
250
+ const field = Object.getOwnPropertyDescriptor(value, String(index));
251
+ if (!field || !("value" in field))
252
+ invalid("Replay input arrays must contain indexed data");
253
+ yield* visit(field.value);
254
+ }
255
+ } else
256
+ for (const key of Object.keys(value).sort()) {
257
+ const field = Object.getOwnPropertyDescriptor(value, key);
258
+ if (!field || !("value" in field))
259
+ invalid("Replay input objects must contain data properties");
260
+ yield* string(key, "key");
261
+ yield* visit(field.value);
262
+ }
263
+ yield [array ? "end-array" : "end-object"];
264
+ } finally {
265
+ active.delete(value);
266
+ }
267
+ }
268
+ yield* visit(value);
269
+ }