@actuarial-ts/compliance 0.5.0 → 0.6.1
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.
- package/README.md +51 -81
- package/dist/bundle.d.ts +7 -10
- package/dist/bundle.d.ts.map +1 -1
- package/dist/bundle.js +185 -32
- package/dist/bundle.js.map +1 -1
- package/dist/diagnosticRun.d.ts +106 -0
- package/dist/diagnosticRun.d.ts.map +1 -0
- package/dist/diagnosticRun.js +798 -0
- package/dist/diagnosticRun.js.map +1 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +16 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/ledger.d.ts.map +1 -1
- package/dist/ledger.js +47 -6
- package/dist/ledger.js.map +1 -1
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +27 -3
- package/dist/metadata.js.map +1 -1
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +6 -4
- package/src/bundle.ts +289 -42
- package/src/diagnosticRun.ts +1451 -0
- package/src/errors.ts +12 -0
- package/src/index.ts +3 -1
- package/src/ledger.ts +59 -8
- package/src/metadata.ts +48 -7
- package/src/version.ts +1 -0
- package/dist/diagnostics.d.ts +0 -75
- package/dist/diagnostics.d.ts.map +0 -1
- package/dist/diagnostics.js +0 -44
- package/dist/diagnostics.js.map +0 -1
- package/src/diagnostics.ts +0 -104
package/src/bundle.ts
CHANGED
|
@@ -26,14 +26,39 @@
|
|
|
26
26
|
* responsibility for compliance remains with the credentialed actuary.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import {
|
|
30
|
-
|
|
29
|
+
import {
|
|
30
|
+
canonicalJson,
|
|
31
|
+
fnv1a64,
|
|
32
|
+
isDiagnosticPlainRecord,
|
|
33
|
+
} from "@actuarial-ts/core";
|
|
34
|
+
import {
|
|
35
|
+
INTERCHANGE_SPEC_VERSION,
|
|
36
|
+
diagnosticDefinitionToDoc,
|
|
37
|
+
docToDiagnosticDefinition,
|
|
38
|
+
type DiagnosticDefinitionDoc,
|
|
31
39
|
BundleDoc,
|
|
32
40
|
MethodResultDoc,
|
|
33
41
|
SelectionDoc,
|
|
34
42
|
StochasticResultDoc,
|
|
35
43
|
TriangleDoc,
|
|
36
44
|
} from "@actuarial-ts/interchange";
|
|
45
|
+
import type {
|
|
46
|
+
DiagnosticRunProvenance,
|
|
47
|
+
VerifiedDiagnosticRunProvenance,
|
|
48
|
+
} from "./diagnosticRun.js";
|
|
49
|
+
import {
|
|
50
|
+
assertVerifiedDiagnosticRunProvenance,
|
|
51
|
+
serializedDiagnosticRunMismatch,
|
|
52
|
+
verifiedDefinitionForBundle,
|
|
53
|
+
} from "./diagnosticRun.js";
|
|
54
|
+
import { ComplianceError } from "./errors.js";
|
|
55
|
+
import { COMPLIANCE_PACKAGE_VERSION } from "./version.js";
|
|
56
|
+
export {
|
|
57
|
+
ComplianceError,
|
|
58
|
+
COMPLIANCE_ERROR_CODES,
|
|
59
|
+
type ComplianceErrorCode,
|
|
60
|
+
} from "./errors.js";
|
|
61
|
+
export { COMPLIANCE_PACKAGE_VERSION } from "./version.js";
|
|
37
62
|
|
|
38
63
|
export { canonicalJson, fnv1a64 };
|
|
39
64
|
|
|
@@ -54,25 +79,6 @@ export type WrappedBundleDoc = BundleDoc;
|
|
|
54
79
|
* of this package. Add the code here when introducing a new throw.
|
|
55
80
|
* (UNSUPPORTED_VALUE moved to core's registry with canonicalJson in 0.2.0.)
|
|
56
81
|
*/
|
|
57
|
-
export const COMPLIANCE_ERROR_CODES = [
|
|
58
|
-
"BAD_BUNDLE",
|
|
59
|
-
"BAD_CDF",
|
|
60
|
-
"MISSING_RATIONALE",
|
|
61
|
-
] as const;
|
|
62
|
-
|
|
63
|
-
export type ComplianceErrorCode = (typeof COMPLIANCE_ERROR_CODES)[number];
|
|
64
|
-
|
|
65
|
-
/** Thrown for invalid compliance input (bad bundles, judgment without rationale, non-positive CDFs). */
|
|
66
|
-
export class ComplianceError extends Error {
|
|
67
|
-
readonly code: ComplianceErrorCode;
|
|
68
|
-
constructor(code: ComplianceErrorCode, message: string) {
|
|
69
|
-
super(message);
|
|
70
|
-
this.name = "ComplianceError";
|
|
71
|
-
this.code = code;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
82
|
|
|
77
83
|
/**
|
|
78
84
|
* The interchange version the wrapped form is written under. Kept in literal
|
|
@@ -81,7 +87,7 @@ export class ComplianceError extends Error {
|
|
|
81
87
|
* test parses the emitted doc with the real interchange parser, which
|
|
82
88
|
* refuses a wrong-major version — drift fails loudly.
|
|
83
89
|
*/
|
|
84
|
-
export const WRAPPED_BUNDLE_INTERCHANGE_VERSION =
|
|
90
|
+
export const WRAPPED_BUNDLE_INTERCHANGE_VERSION = INTERCHANGE_SPEC_VERSION;
|
|
85
91
|
|
|
86
92
|
/**
|
|
87
93
|
* This package's version, stamped into a wrapped bundle's `generator`
|
|
@@ -89,7 +95,6 @@ export const WRAPPED_BUNDLE_INTERCHANGE_VERSION = "1.0.0";
|
|
|
89
95
|
* silently drift (mirroring interchange's INTERCHANGE_PACKAGE_VERSION
|
|
90
96
|
* discipline).
|
|
91
97
|
*/
|
|
92
|
-
export const COMPLIANCE_PACKAGE_VERSION = "0.5.0";
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
100
|
* The interchange mirror for a wrapped bundle (spec 3.2): the triangles the
|
|
@@ -136,6 +141,8 @@ export interface CreateBundleInput {
|
|
|
136
141
|
* Wrapped-mode only; the inner payload never carries it.
|
|
137
142
|
*/
|
|
138
143
|
generator?: { name: string; version: string };
|
|
144
|
+
/** Authenticated diagnostic runs included in the canonical body and, when wrapped, as typed definition documents. */
|
|
145
|
+
diagnosticRuns?: readonly VerifiedDiagnosticRunProvenance[];
|
|
139
146
|
}
|
|
140
147
|
|
|
141
148
|
export interface ReproducibilityBundle {
|
|
@@ -165,9 +172,13 @@ export interface WrappedBundleResult extends ReproducibilityBundle {
|
|
|
165
172
|
* ALSO returned as a wrapped BundleDoc; the unwrapped `{ payload, hash }` is
|
|
166
173
|
* byte-identical either way (the v0.1.x compat fixture pins this).
|
|
167
174
|
*/
|
|
168
|
-
export function createBundle(
|
|
175
|
+
export function createBundle(
|
|
176
|
+
input: CreateBundleInput & { wrap: BundleWrapInput },
|
|
177
|
+
): WrappedBundleResult;
|
|
169
178
|
export function createBundle(input: CreateBundleInput): ReproducibilityBundle;
|
|
170
|
-
export function createBundle(
|
|
179
|
+
export function createBundle(
|
|
180
|
+
input: CreateBundleInput,
|
|
181
|
+
): ReproducibilityBundle | WrappedBundleResult {
|
|
171
182
|
const body: Record<string, unknown> = {
|
|
172
183
|
createdAt: input.createdAt,
|
|
173
184
|
inputs: input.inputs,
|
|
@@ -176,6 +187,63 @@ export function createBundle(input: CreateBundleInput): ReproducibilityBundle |
|
|
|
176
187
|
sdkVersions: input.sdkVersions,
|
|
177
188
|
};
|
|
178
189
|
if (input.seeds !== undefined) body["seeds"] = input.seeds;
|
|
190
|
+
if (input.diagnosticRuns !== undefined) {
|
|
191
|
+
for (const run of input.diagnosticRuns)
|
|
192
|
+
assertVerifiedDiagnosticRunProvenance(run);
|
|
193
|
+
const seenBindings = new Set<string>();
|
|
194
|
+
for (const [runIndex, run] of input.diagnosticRuns.entries()) {
|
|
195
|
+
if (seenBindings.has(run.runResultFingerprint))
|
|
196
|
+
throw new ComplianceError(
|
|
197
|
+
"BAD_DIAGNOSTIC_RUN",
|
|
198
|
+
"Duplicate diagnostic run-result fingerprint",
|
|
199
|
+
`$.diagnosticRuns[${runIndex}].runResultFingerprint`,
|
|
200
|
+
);
|
|
201
|
+
seenBindings.add(run.runResultFingerprint);
|
|
202
|
+
}
|
|
203
|
+
if (input.diagnosticRuns.length > 0) {
|
|
204
|
+
if (!isDiagnosticPlainRecord(input.sdkVersions))
|
|
205
|
+
throw new ComplianceError(
|
|
206
|
+
"BAD_DIAGNOSTIC_RUN",
|
|
207
|
+
"Diagnostic bundles require a plain SDK-version map",
|
|
208
|
+
"$.sdkVersions",
|
|
209
|
+
);
|
|
210
|
+
const expected = input.diagnosticRuns[0]!.manifest.engine.packages;
|
|
211
|
+
for (const [runIndex, run] of input.diagnosticRuns.entries()) {
|
|
212
|
+
if (
|
|
213
|
+
canonicalJson(run.manifest.engine.packages) !==
|
|
214
|
+
canonicalJson(expected)
|
|
215
|
+
)
|
|
216
|
+
throw new ComplianceError(
|
|
217
|
+
"BAD_DIAGNOSTIC_RUN",
|
|
218
|
+
"Diagnostic runs disagree on SDK package versions",
|
|
219
|
+
`$.diagnosticRuns[${runIndex}].manifest.engine.packages`,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
for (const [name, version] of Object.entries({
|
|
223
|
+
"@actuarial-ts/core": expected.core,
|
|
224
|
+
"@actuarial-ts/data": expected.data,
|
|
225
|
+
"@actuarial-ts/compliance": expected.compliance,
|
|
226
|
+
}))
|
|
227
|
+
if (input.sdkVersions[name] !== version)
|
|
228
|
+
throw new ComplianceError(
|
|
229
|
+
"BAD_DIAGNOSTIC_RUN",
|
|
230
|
+
`Outer sdkVersions must record ${name} at ${version}`,
|
|
231
|
+
`$.sdkVersions.${name}`,
|
|
232
|
+
);
|
|
233
|
+
if (
|
|
234
|
+
input.wrap !== undefined &&
|
|
235
|
+
input.generator !== undefined &&
|
|
236
|
+
(input.generator.name !== "@actuarial-ts/compliance" ||
|
|
237
|
+
input.generator.version !== COMPLIANCE_PACKAGE_VERSION)
|
|
238
|
+
)
|
|
239
|
+
throw new ComplianceError(
|
|
240
|
+
"BAD_DIAGNOSTIC_RUN",
|
|
241
|
+
"Wrapped diagnostic bundles require the actual compliance generator stamp",
|
|
242
|
+
"$.generator",
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
body["diagnosticRuns"] = input.diagnosticRuns;
|
|
246
|
+
}
|
|
179
247
|
const payload = canonicalJson(body);
|
|
180
248
|
const inner: ReproducibilityBundle = { payload, hash: fnv1a64(payload) };
|
|
181
249
|
if (input.wrap === undefined) return inner;
|
|
@@ -183,18 +251,48 @@ export function createBundle(input: CreateBundleInput): ReproducibilityBundle |
|
|
|
183
251
|
// The inner record is carried opaquely as the `bundle` segment; the outer
|
|
184
252
|
// tag is fnv1a64(canonicalJson({ bundle, interchange })) — exactly
|
|
185
253
|
// interchange's semanticBodyOf for kind "bundle" (spec 3.2).
|
|
186
|
-
const bundleSegment: Record<string, unknown> = {
|
|
254
|
+
const bundleSegment: Record<string, unknown> = {
|
|
255
|
+
hash: inner.hash,
|
|
256
|
+
payload: inner.payload,
|
|
257
|
+
};
|
|
258
|
+
const definitions = new Map<string, DiagnosticDefinitionDoc>();
|
|
259
|
+
for (const run of input.diagnosticRuns ?? []) {
|
|
260
|
+
const compiled = verifiedDefinitionForBundle(run);
|
|
261
|
+
definitions.set(
|
|
262
|
+
run.definition.identities.definition,
|
|
263
|
+
diagnosticDefinitionToDoc(compiled, {
|
|
264
|
+
createdAt: input.createdAt,
|
|
265
|
+
generator: {
|
|
266
|
+
name: "@actuarial-ts/compliance",
|
|
267
|
+
version: COMPLIANCE_PACKAGE_VERSION,
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
}
|
|
187
272
|
const interchange = {
|
|
188
273
|
triangles: [...input.wrap.triangles],
|
|
189
274
|
selections: [...input.wrap.selections],
|
|
190
275
|
results: [...input.wrap.results],
|
|
276
|
+
...(definitions.size === 0
|
|
277
|
+
? {}
|
|
278
|
+
: {
|
|
279
|
+
diagnosticDefinitions: [...definitions.values()].sort((a, b) =>
|
|
280
|
+
a.diagnosticDefinition.identities.definition <
|
|
281
|
+
b.diagnosticDefinition.identities.definition
|
|
282
|
+
? -1
|
|
283
|
+
: 1,
|
|
284
|
+
),
|
|
285
|
+
}),
|
|
191
286
|
};
|
|
192
287
|
const wrapped: WrappedBundleDoc = {
|
|
193
288
|
interchangeVersion: WRAPPED_BUNDLE_INTERCHANGE_VERSION,
|
|
194
289
|
kind: "bundle",
|
|
195
290
|
generator: input.generator
|
|
196
291
|
? { ...input.generator }
|
|
197
|
-
: {
|
|
292
|
+
: {
|
|
293
|
+
name: "@actuarial-ts/compliance",
|
|
294
|
+
version: COMPLIANCE_PACKAGE_VERSION,
|
|
295
|
+
},
|
|
198
296
|
createdAt: input.createdAt,
|
|
199
297
|
bundle: bundleSegment,
|
|
200
298
|
interchange,
|
|
@@ -229,18 +327,29 @@ export interface VerifyBundleResult {
|
|
|
229
327
|
}
|
|
230
328
|
|
|
231
329
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
232
|
-
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
330
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
331
|
+
return false;
|
|
233
332
|
const proto: unknown = Object.getPrototypeOf(value);
|
|
234
333
|
return proto === Object.prototype || proto === null;
|
|
235
334
|
}
|
|
236
335
|
|
|
237
336
|
/** First differing path between two canonicalizable structures, or null when equal. */
|
|
238
|
-
function firstDifference(
|
|
337
|
+
function firstDifference(
|
|
338
|
+
stored: unknown,
|
|
339
|
+
rerun: unknown,
|
|
340
|
+
path: string,
|
|
341
|
+
): string | null {
|
|
239
342
|
if (isPlainObject(stored) && isPlainObject(rerun)) {
|
|
240
|
-
const keys = [
|
|
343
|
+
const keys = [
|
|
344
|
+
...new Set([...Object.keys(stored), ...Object.keys(rerun)]),
|
|
345
|
+
].sort();
|
|
241
346
|
for (const key of keys) {
|
|
242
347
|
const keyPath = `${path}.${key}`;
|
|
243
|
-
if (
|
|
348
|
+
if (
|
|
349
|
+
!Object.prototype.hasOwnProperty.call(stored, key) ||
|
|
350
|
+
!Object.prototype.hasOwnProperty.call(rerun, key)
|
|
351
|
+
)
|
|
352
|
+
return keyPath;
|
|
244
353
|
const diff = firstDifference(stored[key], rerun[key], keyPath);
|
|
245
354
|
if (diff !== null) return diff;
|
|
246
355
|
}
|
|
@@ -258,7 +367,10 @@ function firstDifference(stored: unknown, rerun: unknown, path: string): string
|
|
|
258
367
|
}
|
|
259
368
|
|
|
260
369
|
/** Inner verification — the pre-wrapped behavior, unchanged byte for byte. */
|
|
261
|
-
function verifyUnwrapped(
|
|
370
|
+
function verifyUnwrapped(
|
|
371
|
+
bundle: ReproducibilityBundle,
|
|
372
|
+
rerunResults: unknown,
|
|
373
|
+
): VerifyBundleResult {
|
|
262
374
|
// The bundle's own hash first. Without this, the attestation ("these results
|
|
263
375
|
// came from exactly these inputs, parameters, and SDK versions") checked the
|
|
264
376
|
// results segment only — a bundle with rewritten inputs and hash "deadbeef"
|
|
@@ -274,21 +386,68 @@ function verifyUnwrapped(bundle: ReproducibilityBundle, rerunResults: unknown):
|
|
|
274
386
|
throw new ComplianceError("BAD_BUNDLE", "bundle payload is not valid JSON");
|
|
275
387
|
}
|
|
276
388
|
if (!isPlainObject(stored) || !("results" in stored)) {
|
|
277
|
-
throw new ComplianceError(
|
|
389
|
+
throw new ComplianceError(
|
|
390
|
+
"BAD_BUNDLE",
|
|
391
|
+
'bundle payload has no "results" segment',
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
if (Object.prototype.hasOwnProperty.call(stored, "diagnosticRuns")) {
|
|
395
|
+
if (!Array.isArray(stored["diagnosticRuns"]))
|
|
396
|
+
throw new ComplianceError(
|
|
397
|
+
"BAD_BUNDLE",
|
|
398
|
+
'bundle payload "diagnosticRuns" must be an array',
|
|
399
|
+
);
|
|
400
|
+
const seen = new Set<string>();
|
|
401
|
+
for (const [index, run] of stored["diagnosticRuns"].entries()) {
|
|
402
|
+
const path = `$.diagnosticRuns[${index}]`;
|
|
403
|
+
const mismatch = serializedDiagnosticRunMismatch(run, path);
|
|
404
|
+
if (mismatch !== null)
|
|
405
|
+
return { reproduced: false, mismatchPath: mismatch };
|
|
406
|
+
const fingerprint = (run as Record<string, unknown>)[
|
|
407
|
+
"runResultFingerprint"
|
|
408
|
+
] as string;
|
|
409
|
+
if (seen.has(fingerprint))
|
|
410
|
+
return {
|
|
411
|
+
reproduced: false,
|
|
412
|
+
mismatchPath: `${path}.runResultFingerprint`,
|
|
413
|
+
};
|
|
414
|
+
seen.add(fingerprint);
|
|
415
|
+
const packages = (run as DiagnosticRunProvenance).manifest.engine
|
|
416
|
+
.packages;
|
|
417
|
+
for (const [name, shortName] of [
|
|
418
|
+
["@actuarial-ts/core", "core"],
|
|
419
|
+
["@actuarial-ts/data", "data"],
|
|
420
|
+
["@actuarial-ts/compliance", "compliance"],
|
|
421
|
+
] as const) {
|
|
422
|
+
if (
|
|
423
|
+
!isPlainObject(stored["sdkVersions"]) ||
|
|
424
|
+
stored["sdkVersions"][name] !== packages?.[shortName]
|
|
425
|
+
)
|
|
426
|
+
return { reproduced: false, mismatchPath: `$.sdkVersions.${name}` };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
278
429
|
}
|
|
279
430
|
const storedResults = stored["results"];
|
|
280
431
|
const rerunCanonical = canonicalJson(rerunResults);
|
|
281
432
|
const storedCanonical = canonicalJson(storedResults);
|
|
282
433
|
if (rerunCanonical === storedCanonical) return { reproduced: true };
|
|
283
|
-
return {
|
|
434
|
+
return {
|
|
435
|
+
reproduced: false,
|
|
436
|
+
mismatchPath: firstDifference(storedResults, rerunResults, "$") ?? "$",
|
|
437
|
+
};
|
|
284
438
|
}
|
|
285
439
|
|
|
286
|
-
function isWrappedBundleDoc(
|
|
440
|
+
function isWrappedBundleDoc(
|
|
441
|
+
value: ReproducibilityBundle | WrappedBundleDoc,
|
|
442
|
+
): value is WrappedBundleDoc {
|
|
287
443
|
return (value as { kind?: unknown }).kind === "bundle";
|
|
288
444
|
}
|
|
289
445
|
|
|
290
446
|
/** Wrapped mode: the outer tag (spec 3.2) AND the inner bundle exactly as today. */
|
|
291
|
-
function verifyWrapped(
|
|
447
|
+
function verifyWrapped(
|
|
448
|
+
doc: WrappedBundleDoc,
|
|
449
|
+
rerunResults: unknown,
|
|
450
|
+
): VerifyBundleResult {
|
|
292
451
|
const innerRaw: unknown = doc.bundle;
|
|
293
452
|
if (
|
|
294
453
|
!isPlainObject(innerRaw) ||
|
|
@@ -301,18 +460,104 @@ function verifyWrapped(doc: WrappedBundleDoc, rerunResults: unknown): VerifyBund
|
|
|
301
460
|
);
|
|
302
461
|
}
|
|
303
462
|
if (doc.interchange === undefined || doc.interchange === null) {
|
|
304
|
-
throw new ComplianceError(
|
|
463
|
+
throw new ComplianceError(
|
|
464
|
+
"BAD_BUNDLE",
|
|
465
|
+
'wrapped bundle is missing its "interchange" mirror',
|
|
466
|
+
);
|
|
305
467
|
}
|
|
306
468
|
// OUTER tag over the two-field semantic body { bundle, interchange } —
|
|
307
469
|
// exactly interchange's semanticBodyOf for kind "bundle" (spec 3.2).
|
|
308
|
-
const expected = fnv1a64(
|
|
470
|
+
const expected = fnv1a64(
|
|
471
|
+
canonicalJson({ bundle: doc.bundle, interchange: doc.interchange }),
|
|
472
|
+
);
|
|
309
473
|
const actual = typeof doc.integrity === "string" ? doc.integrity : null;
|
|
310
|
-
const outerIntegrity: OuterIntegrityCheck = {
|
|
474
|
+
const outerIntegrity: OuterIntegrityCheck = {
|
|
475
|
+
ok: expected === actual,
|
|
476
|
+
expected,
|
|
477
|
+
actual,
|
|
478
|
+
};
|
|
311
479
|
if (!outerIntegrity.ok) {
|
|
312
480
|
return { reproduced: false, mismatchPath: "$.integrity", outerIntegrity };
|
|
313
481
|
}
|
|
314
|
-
const inner: ReproducibilityBundle = {
|
|
315
|
-
|
|
482
|
+
const inner: ReproducibilityBundle = {
|
|
483
|
+
payload: innerRaw["payload"],
|
|
484
|
+
hash: innerRaw["hash"],
|
|
485
|
+
};
|
|
486
|
+
const innerResult = verifyUnwrapped(inner, rerunResults);
|
|
487
|
+
if (!innerResult.reproduced) return { ...innerResult, outerIntegrity };
|
|
488
|
+
const body = JSON.parse(inner.payload) as Record<string, unknown>;
|
|
489
|
+
const runs = Array.isArray(body["diagnosticRuns"])
|
|
490
|
+
? body["diagnosticRuns"]
|
|
491
|
+
: [];
|
|
492
|
+
const docs = (doc.interchange as { diagnosticDefinitions?: unknown })
|
|
493
|
+
.diagnosticDefinitions;
|
|
494
|
+
if (runs.length > 0) {
|
|
495
|
+
const packages = (runs[0] as DiagnosticRunProvenance).manifest.engine
|
|
496
|
+
.packages;
|
|
497
|
+
if (doc.generator?.name !== "@actuarial-ts/compliance")
|
|
498
|
+
return {
|
|
499
|
+
reproduced: false,
|
|
500
|
+
mismatchPath: "$.generator.name",
|
|
501
|
+
outerIntegrity,
|
|
502
|
+
};
|
|
503
|
+
if (doc.generator.version !== packages.compliance)
|
|
504
|
+
return {
|
|
505
|
+
reproduced: false,
|
|
506
|
+
mismatchPath: "$.generator.version",
|
|
507
|
+
outerIntegrity,
|
|
508
|
+
};
|
|
509
|
+
if (!Array.isArray(docs))
|
|
510
|
+
return {
|
|
511
|
+
reproduced: false,
|
|
512
|
+
mismatchPath: "$.interchange.diagnosticDefinitions",
|
|
513
|
+
outerIntegrity,
|
|
514
|
+
};
|
|
515
|
+
const expected = new Set(
|
|
516
|
+
runs.map(
|
|
517
|
+
(run) =>
|
|
518
|
+
(run as DiagnosticRunProvenance).definition.identities.definition,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
const actual = new Set<string>();
|
|
522
|
+
for (const [index, raw] of docs.entries()) {
|
|
523
|
+
try {
|
|
524
|
+
const document = raw as DiagnosticDefinitionDoc;
|
|
525
|
+
const integrity =
|
|
526
|
+
docToDiagnosticDefinition(document).definition.definitionIntegrity;
|
|
527
|
+
if (actual.has(integrity))
|
|
528
|
+
return {
|
|
529
|
+
reproduced: false,
|
|
530
|
+
mismatchPath: `$.interchange.diagnosticDefinitions[${index}]`,
|
|
531
|
+
outerIntegrity,
|
|
532
|
+
};
|
|
533
|
+
if (
|
|
534
|
+
document.generator.name !== doc.generator.name ||
|
|
535
|
+
document.generator.version !== doc.generator.version
|
|
536
|
+
)
|
|
537
|
+
return {
|
|
538
|
+
reproduced: false,
|
|
539
|
+
mismatchPath: `$.interchange.diagnosticDefinitions[${index}].generator`,
|
|
540
|
+
outerIntegrity,
|
|
541
|
+
};
|
|
542
|
+
actual.add(integrity);
|
|
543
|
+
} catch {
|
|
544
|
+
return {
|
|
545
|
+
reproduced: false,
|
|
546
|
+
mismatchPath: `$.interchange.diagnosticDefinitions[${index}]`,
|
|
547
|
+
outerIntegrity,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (
|
|
552
|
+
canonicalJson([...actual].sort()) !== canonicalJson([...expected].sort())
|
|
553
|
+
)
|
|
554
|
+
return {
|
|
555
|
+
reproduced: false,
|
|
556
|
+
mismatchPath: "$.interchange.diagnosticDefinitions",
|
|
557
|
+
outerIntegrity,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return { ...innerResult, outerIntegrity };
|
|
316
561
|
}
|
|
317
562
|
|
|
318
563
|
/**
|
|
@@ -337,5 +582,7 @@ export function verifyBundle(
|
|
|
337
582
|
bundle: ReproducibilityBundle | WrappedBundleDoc,
|
|
338
583
|
rerunResults: unknown,
|
|
339
584
|
): VerifyBundleResult {
|
|
340
|
-
return isWrappedBundleDoc(bundle)
|
|
585
|
+
return isWrappedBundleDoc(bundle)
|
|
586
|
+
? verifyWrapped(bundle, rerunResults)
|
|
587
|
+
: verifyUnwrapped(bundle, rerunResults);
|
|
341
588
|
}
|