@actuarial-ts/core 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.
Files changed (120) hide show
  1. package/README.md +65 -226
  2. package/dist/capping.d.ts.map +1 -1
  3. package/dist/capping.js +39 -6
  4. package/dist/capping.js.map +1 -1
  5. package/dist/casualtyDiagnostics.d.ts +19 -49
  6. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  7. package/dist/casualtyDiagnostics.js +84 -116
  8. package/dist/casualtyDiagnostics.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +32 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -0
  11. package/dist/diagnosticAggregation.js +73 -0
  12. package/dist/diagnosticAggregation.js.map +1 -0
  13. package/dist/diagnosticDefinitions.d.ts +289 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  15. package/dist/diagnosticDefinitions.js +1688 -0
  16. package/dist/diagnosticDefinitions.js.map +1 -0
  17. package/dist/diagnosticDerivations.d.ts +35 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -0
  19. package/dist/diagnosticDerivations.js +224 -0
  20. package/dist/diagnosticDerivations.js.map +1 -0
  21. package/dist/diagnosticExposure.d.ts +51 -0
  22. package/dist/diagnosticExposure.d.ts.map +1 -0
  23. package/dist/diagnosticExposure.js +273 -0
  24. package/dist/diagnosticExposure.js.map +1 -0
  25. package/dist/diagnosticExpressions.d.ts +56 -0
  26. package/dist/diagnosticExpressions.d.ts.map +1 -0
  27. package/dist/diagnosticExpressions.js +124 -0
  28. package/dist/diagnosticExpressions.js.map +1 -0
  29. package/dist/diagnosticFormulas.d.ts +198 -0
  30. package/dist/diagnosticFormulas.d.ts.map +1 -0
  31. package/dist/diagnosticFormulas.js +219 -0
  32. package/dist/diagnosticFormulas.js.map +1 -0
  33. package/dist/diagnosticIdentity.d.ts +251 -0
  34. package/dist/diagnosticIdentity.d.ts.map +1 -0
  35. package/dist/diagnosticIdentity.js +721 -0
  36. package/dist/diagnosticIdentity.js.map +1 -0
  37. package/dist/diagnosticOrdering.d.ts +8 -0
  38. package/dist/diagnosticOrdering.d.ts.map +1 -0
  39. package/dist/diagnosticOrdering.js +73 -0
  40. package/dist/diagnosticOrdering.js.map +1 -0
  41. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  42. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  43. package/dist/diagnosticPeriodAxis.js +69 -0
  44. package/dist/diagnosticPeriodAxis.js.map +1 -0
  45. package/dist/diagnosticPeriods.d.ts +15 -0
  46. package/dist/diagnosticPeriods.d.ts.map +1 -0
  47. package/dist/diagnosticPeriods.js +50 -0
  48. package/dist/diagnosticPeriods.js.map +1 -0
  49. package/dist/diagnosticPreparation.d.ts +131 -0
  50. package/dist/diagnosticPreparation.d.ts.map +1 -0
  51. package/dist/diagnosticPreparation.js +1539 -0
  52. package/dist/diagnosticPreparation.js.map +1 -0
  53. package/dist/diagnosticReview.d.ts +66 -0
  54. package/dist/diagnosticReview.d.ts.map +1 -0
  55. package/dist/diagnosticReview.js +490 -0
  56. package/dist/diagnosticReview.js.map +1 -0
  57. package/dist/diagnosticRules.d.ts +32 -0
  58. package/dist/diagnosticRules.d.ts.map +1 -0
  59. package/dist/diagnosticRules.js +32 -0
  60. package/dist/diagnosticRules.js.map +1 -0
  61. package/dist/diagnosticRunner.d.ts +86 -0
  62. package/dist/diagnosticRunner.d.ts.map +1 -0
  63. package/dist/diagnosticRunner.js +703 -0
  64. package/dist/diagnosticRunner.js.map +1 -0
  65. package/dist/diagnosticRuntime.d.ts +26 -0
  66. package/dist/diagnosticRuntime.d.ts.map +1 -0
  67. package/dist/diagnosticRuntime.js +243 -0
  68. package/dist/diagnosticRuntime.js.map +1 -0
  69. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  70. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  71. package/dist/diagnosticSourceOrdering.js +38 -0
  72. package/dist/diagnosticSourceOrdering.js.map +1 -0
  73. package/dist/index.d.ts +28 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +16 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/mack.d.ts +4 -0
  78. package/dist/mack.d.ts.map +1 -1
  79. package/dist/mack.js +32 -4
  80. package/dist/mack.js.map +1 -1
  81. package/dist/triangle.d.ts.map +1 -1
  82. package/dist/triangle.js +59 -9
  83. package/dist/triangle.js.map +1 -1
  84. package/dist/types.d.ts +21 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +128 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/version.d.ts +3 -0
  89. package/dist/version.d.ts.map +1 -0
  90. package/dist/version.js +3 -0
  91. package/dist/version.js.map +1 -0
  92. package/package.json +4 -3
  93. package/src/capping.ts +109 -18
  94. package/src/casualtyDiagnostics.ts +79 -189
  95. package/src/diagnosticAggregation.ts +128 -0
  96. package/src/diagnosticDefinitions.ts +3624 -0
  97. package/src/diagnosticDerivations.ts +321 -0
  98. package/src/diagnosticExposure.ts +482 -0
  99. package/src/diagnosticExpressions.ts +255 -0
  100. package/src/diagnosticFormulas.ts +350 -0
  101. package/src/diagnosticIdentity.ts +1267 -0
  102. package/src/diagnosticOrdering.ts +106 -0
  103. package/src/diagnosticPeriodAxis.ts +92 -0
  104. package/src/diagnosticPeriods.ts +102 -0
  105. package/src/diagnosticPreparation.ts +2475 -0
  106. package/src/diagnosticReview.ts +897 -0
  107. package/src/diagnosticRules.ts +69 -0
  108. package/src/diagnosticRunner.ts +1114 -0
  109. package/src/diagnosticRuntime.ts +264 -0
  110. package/src/diagnosticSourceOrdering.ts +50 -0
  111. package/src/index.ts +184 -1
  112. package/src/mack.ts +72 -9
  113. package/src/triangle.ts +103 -13
  114. package/src/types.ts +196 -0
  115. package/src/version.ts +2 -0
  116. package/dist/metricDiagnostics.d.ts +0 -212
  117. package/dist/metricDiagnostics.d.ts.map +0 -1
  118. package/dist/metricDiagnostics.js +0 -627
  119. package/dist/metricDiagnostics.js.map +0 -1
  120. package/src/metricDiagnostics.ts +0 -876
@@ -0,0 +1,482 @@
1
+ import type {
2
+ DiagnosticPeriodAxis,
3
+ DiagnosticSourceLocation,
4
+ } from "./diagnosticDefinitions.js";
5
+ import { canonicalJson } from "./canonical.js";
6
+ import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
7
+ import { compareDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
8
+ import { compareDiagnosticIdentityValues } from "./diagnosticOrdering.js";
9
+ import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
10
+ import { validateDiagnosticPeriodAxisInput } from "./diagnosticDefinitions.js";
11
+ import {
12
+ DiagnosticValidationError,
13
+ type DiagnosticValidationIssue,
14
+ } from "./types.js";
15
+ import {
16
+ diagnosticJsonPreflight,
17
+ hasDiagnosticOwn,
18
+ isDiagnosticPlainRecord,
19
+ isDiagnosticToken,
20
+ snapshotDiagnosticJson,
21
+ } from "./diagnosticRuntime.js";
22
+
23
+ export interface DiagnosticExposureObservation {
24
+ readonly key: string;
25
+ readonly sourceGroup: string;
26
+ readonly origin: string;
27
+ readonly valuation?: string;
28
+ readonly measureId: string;
29
+ readonly value: number | null;
30
+ readonly complete: boolean;
31
+ readonly source?: DiagnosticSourceLocation;
32
+ }
33
+
34
+ export type DiagnosticAuditedNumericValue =
35
+ | { readonly status: "observed"; readonly value: number }
36
+ | { readonly status: "missing"; readonly value: null }
37
+ | {
38
+ readonly status: "non-finite";
39
+ readonly value: null;
40
+ readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity";
41
+ };
42
+
43
+ export interface DiagnosticExposureAuditObservation {
44
+ readonly sourceGroup: string;
45
+ readonly origin: string;
46
+ readonly valuation?: string;
47
+ readonly value: DiagnosticAuditedNumericValue;
48
+ readonly complete: boolean;
49
+ readonly source?: DiagnosticSourceLocation;
50
+ }
51
+
52
+ export type ReconciledDiagnosticExposure =
53
+ | {
54
+ readonly measureId: string;
55
+ readonly key: string;
56
+ readonly status: "valid";
57
+ readonly sourceGroup: string;
58
+ readonly origin: string;
59
+ readonly valuation?: string;
60
+ readonly value: number;
61
+ readonly deduplicated: number;
62
+ readonly sources: readonly DiagnosticSourceLocation[];
63
+ }
64
+ | {
65
+ readonly measureId: string;
66
+ readonly key: string;
67
+ readonly status: "invalid";
68
+ readonly issues: readonly (
69
+ "missing" | "incomplete" | "non-finite" | "duplicate" | "conflict"
70
+ )[];
71
+ readonly value: null;
72
+ readonly observations: readonly DiagnosticExposureAuditObservation[];
73
+ };
74
+
75
+ export function auditDiagnosticNumber(
76
+ value: number | null,
77
+ ): DiagnosticAuditedNumericValue {
78
+ if (value !== null && typeof value !== "number")
79
+ throw new DiagnosticValidationError([
80
+ {
81
+ domain: "input",
82
+ code: "invalid-type",
83
+ path: "$",
84
+ message: "Numeric input must be a number or null",
85
+ },
86
+ ]);
87
+ if (value === null) return Object.freeze({ status: "missing", value: null });
88
+ if (!Number.isFinite(value))
89
+ return Object.freeze({
90
+ status: "non-finite",
91
+ value: null,
92
+ nonFiniteKind: Number.isNaN(value)
93
+ ? "nan"
94
+ : value > 0
95
+ ? "positive-infinity"
96
+ : "negative-infinity",
97
+ });
98
+ return Object.freeze({
99
+ status: "observed",
100
+ value: Object.is(value, -0) ? 0 : value,
101
+ });
102
+ }
103
+
104
+ function propertyPath(path: string, key: string): string {
105
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
106
+ ? `${path}.${key}`
107
+ : `${path}[${JSON.stringify(key)}]`;
108
+ }
109
+
110
+ function validateExposureArguments(
111
+ observations: unknown,
112
+ timingByMeasure: unknown,
113
+ periodAxis: DiagnosticPeriodAxis | undefined,
114
+ ): void {
115
+ // Nonfinite exposure amounts are supported audited inputs. They are the
116
+ // only non-JSON numbers permitted here; metadata still must be finite.
117
+ const issues: DiagnosticValidationIssue[] = [
118
+ ...diagnosticJsonPreflight(observations, "input")
119
+ .filter(
120
+ (issue) =>
121
+ !(
122
+ issue.code === "invalid-json-value" &&
123
+ issue.message === "JSON numeric value must be finite" &&
124
+ /^\$\[\d+\]\.value$/.test(issue.path)
125
+ ),
126
+ )
127
+ .map((issue) => ({
128
+ ...issue,
129
+ path: `$.observations${issue.path.slice(1)}`,
130
+ })),
131
+ ...diagnosticJsonPreflight(timingByMeasure, "configuration").map(
132
+ (issue) => ({
133
+ ...issue,
134
+ path: `$.timingByMeasure${issue.path.slice(1)}`,
135
+ }),
136
+ ),
137
+ ...(periodAxis === undefined
138
+ ? []
139
+ : validateDiagnosticPeriodAxisInput(periodAxis)),
140
+ ];
141
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
142
+ const issue = (
143
+ domain: DiagnosticValidationIssue["domain"],
144
+ code: DiagnosticValidationIssue["code"],
145
+ path: string,
146
+ message: string,
147
+ ) => issues.push({ domain, code, path, message });
148
+ const token = (
149
+ value: unknown,
150
+ path: string,
151
+ domain: DiagnosticValidationIssue["domain"] = "input",
152
+ ) => {
153
+ if (!isDiagnosticToken(value))
154
+ issue(
155
+ domain,
156
+ typeof value === "string" ? "invalid-string" : "invalid-type",
157
+ path,
158
+ "Expected a nonempty token with valid Unicode and no U+0000",
159
+ );
160
+ };
161
+ const exactKeys = (
162
+ value: Record<string, unknown>,
163
+ allowed: readonly string[],
164
+ path: string,
165
+ ) => {
166
+ for (const key of Object.keys(value))
167
+ if (!allowed.includes(key))
168
+ issue(
169
+ "input",
170
+ "unknown-key",
171
+ propertyPath(path, key),
172
+ `Unknown key ${key}`,
173
+ );
174
+ };
175
+ if (!Array.isArray(observations))
176
+ issue(
177
+ "input",
178
+ "invalid-type",
179
+ "$.observations",
180
+ "Exposure observations must be an array",
181
+ );
182
+ if (!isDiagnosticPlainRecord(timingByMeasure))
183
+ issue(
184
+ "configuration",
185
+ "invalid-type",
186
+ "$.timingByMeasure",
187
+ "Exposure timings must be a plain record",
188
+ );
189
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
190
+ const timings = timingByMeasure as Record<string, unknown>;
191
+ for (const [measureId, timing] of Object.entries(timings)) {
192
+ const path = propertyPath("$.timingByMeasure", measureId);
193
+ token(measureId, path, "configuration");
194
+ if (timing !== "origin-static" && timing !== "valuation-specific")
195
+ issue(
196
+ "configuration",
197
+ "invalid-type",
198
+ path,
199
+ "Exposure timing must be origin-static or valuation-specific",
200
+ );
201
+ }
202
+ for (const [index, observation] of (observations as unknown[]).entries()) {
203
+ const path = `$.observations[${index}]`;
204
+ if (!isDiagnosticPlainRecord(observation)) {
205
+ issue(
206
+ "input",
207
+ "invalid-type",
208
+ path,
209
+ "Exposure observation must be an object",
210
+ );
211
+ continue;
212
+ }
213
+ exactKeys(
214
+ observation,
215
+ [
216
+ "key",
217
+ "sourceGroup",
218
+ "origin",
219
+ "valuation",
220
+ "measureId",
221
+ "value",
222
+ "complete",
223
+ "source",
224
+ ],
225
+ path,
226
+ );
227
+ for (const key of ["key", "sourceGroup", "origin", "measureId"])
228
+ token(observation[key], `${path}.${key}`);
229
+ if (hasDiagnosticOwn(observation, "valuation"))
230
+ token(observation.valuation, `${path}.valuation`);
231
+ if (typeof observation.complete !== "boolean")
232
+ issue(
233
+ "input",
234
+ "invalid-type",
235
+ `${path}.complete`,
236
+ "Exposure completeness must be boolean",
237
+ );
238
+ if (observation.value !== null && typeof observation.value !== "number")
239
+ issue(
240
+ "input",
241
+ "invalid-type",
242
+ `${path}.value`,
243
+ "Exposure value must be a number or null",
244
+ );
245
+ if (isDiagnosticToken(observation.measureId)) {
246
+ if (!hasDiagnosticOwn(timings, observation.measureId))
247
+ issue(
248
+ "configuration",
249
+ "unknown-reference",
250
+ `${path}.measureId`,
251
+ "Exposure measure has no declared timing",
252
+ );
253
+ else if (
254
+ timings[observation.measureId] === "valuation-specific" &&
255
+ !hasDiagnosticOwn(observation, "valuation")
256
+ )
257
+ issue(
258
+ "input",
259
+ "missing-required",
260
+ `${path}.valuation`,
261
+ "Valuation-specific exposure requires a valuation",
262
+ );
263
+ }
264
+ if (hasDiagnosticOwn(observation, "source")) {
265
+ const source = observation.source;
266
+ if (!isDiagnosticPlainRecord(source))
267
+ issue(
268
+ "input",
269
+ "invalid-type",
270
+ `${path}.source`,
271
+ "Source location must be an object",
272
+ );
273
+ else {
274
+ exactKeys(
275
+ source,
276
+ [
277
+ "artifactId",
278
+ "sourceFile",
279
+ "sourceSheet",
280
+ "sourceRow",
281
+ "sourceCell",
282
+ ],
283
+ `${path}.source`,
284
+ );
285
+ token(source.artifactId, `${path}.source.artifactId`);
286
+ for (const key of ["sourceFile", "sourceSheet", "sourceCell"])
287
+ if (hasDiagnosticOwn(source, key))
288
+ token(source[key], `${path}.source.${key}`);
289
+ if (
290
+ hasDiagnosticOwn(source, "sourceRow") &&
291
+ (typeof source.sourceRow !== "number" ||
292
+ !Number.isSafeInteger(source.sourceRow) ||
293
+ source.sourceRow < 0)
294
+ )
295
+ issue(
296
+ "input",
297
+ "invalid-number",
298
+ `${path}.source.sourceRow`,
299
+ "Source row must be a nonnegative safe integer",
300
+ );
301
+ }
302
+ }
303
+ if (periodAxis !== undefined)
304
+ for (const side of ["origin", "valuation"] as const) {
305
+ if (
306
+ isDiagnosticToken(observation[side]) &&
307
+ normalizeDiagnosticPeriodWithAxis(
308
+ periodAxis,
309
+ side,
310
+ observation[side],
311
+ ) === null
312
+ )
313
+ issue(
314
+ "input",
315
+ "invalid-period",
316
+ `${path}.${side}`,
317
+ `Unknown ${side} period ${JSON.stringify(observation[side])}`,
318
+ );
319
+ }
320
+ }
321
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
322
+ }
323
+
324
+ function equalAudit(
325
+ left: DiagnosticExposureAuditObservation,
326
+ right: DiagnosticExposureAuditObservation,
327
+ ): boolean {
328
+ return (
329
+ left.sourceGroup === right.sourceGroup &&
330
+ left.origin === right.origin &&
331
+ left.complete === right.complete &&
332
+ JSON.stringify(left.value) === JSON.stringify(right.value)
333
+ );
334
+ }
335
+
336
+ export function reconcileDiagnosticExposures(
337
+ observations: readonly DiagnosticExposureObservation[],
338
+ timingByMeasure: Readonly<
339
+ Record<string, "origin-static" | "valuation-specific">
340
+ >,
341
+ periodAxis?: DiagnosticPeriodAxis,
342
+ ): readonly ReconciledDiagnosticExposure[] {
343
+ validateExposureArguments(observations, timingByMeasure, periodAxis);
344
+ const periodValue = (
345
+ role: "origin" | "valuation",
346
+ label: string | undefined,
347
+ ) =>
348
+ label === undefined || periodAxis === undefined
349
+ ? label
350
+ : (normalizeDiagnosticPeriodWithAxis(periodAxis, role, label)
351
+ ?.coordinate ?? label);
352
+ const observationOrder = (
353
+ left: DiagnosticExposureAuditObservation,
354
+ right: DiagnosticExposureAuditObservation,
355
+ ) =>
356
+ compareDiagnosticIdentityValues(
357
+ [
358
+ left.sourceGroup,
359
+ periodValue("origin", left.origin),
360
+ periodValue("valuation", left.valuation),
361
+ left.value.status,
362
+ left.value.status === "non-finite"
363
+ ? left.value.nonFiniteKind
364
+ : left.value.value,
365
+ left.complete,
366
+ ],
367
+ [
368
+ right.sourceGroup,
369
+ periodValue("origin", right.origin),
370
+ periodValue("valuation", right.valuation),
371
+ right.value.status,
372
+ right.value.status === "non-finite"
373
+ ? right.value.nonFiniteKind
374
+ : right.value.value,
375
+ right.complete,
376
+ ],
377
+ ) ||
378
+ (left.source === undefined || right.source === undefined
379
+ ? compareDiagnosticIdentityValues(left.source, right.source)
380
+ : compareDiagnosticSourceLocations(left.source, right.source));
381
+ const cohorts = new Map<string, DiagnosticExposureObservation[]>();
382
+ for (const observation of observations) {
383
+ const timing = hasDiagnosticOwn(timingByMeasure, observation.measureId)
384
+ ? timingByMeasure[observation.measureId]
385
+ : undefined;
386
+ const identity = canonicalJson(
387
+ timing === "valuation-specific"
388
+ ? [
389
+ observation.measureId,
390
+ observation.key,
391
+ observation.valuation ?? null,
392
+ ]
393
+ : [observation.measureId, observation.key],
394
+ );
395
+ const cohort = cohorts.get(identity) ?? [];
396
+ cohort.push(observation);
397
+ cohorts.set(identity, cohort);
398
+ }
399
+ return snapshotDiagnosticJson(
400
+ [...cohorts.values()]
401
+ .map((cohort): ReconciledDiagnosticExposure => {
402
+ const first = cohort[0]!;
403
+ const timing = hasDiagnosticOwn(timingByMeasure, first.measureId)
404
+ ? timingByMeasure[first.measureId]
405
+ : undefined;
406
+ const audited = cohort
407
+ .map((item): DiagnosticExposureAuditObservation => ({
408
+ sourceGroup: item.sourceGroup,
409
+ origin: item.origin,
410
+ ...(item.valuation === undefined
411
+ ? {}
412
+ : { valuation: item.valuation }),
413
+ value: auditDiagnosticNumber(item.value),
414
+ complete: item.complete,
415
+ ...(item.source === undefined
416
+ ? {}
417
+ : { source: Object.freeze({ ...item.source }) }),
418
+ }))
419
+ .sort(observationOrder);
420
+ const issues: (
421
+ "missing" | "incomplete" | "non-finite" | "duplicate" | "conflict"
422
+ )[] = [];
423
+ if (audited.some((item) => item.value.status === "missing"))
424
+ issues.push("missing");
425
+ if (audited.some((item) => !item.complete)) issues.push("incomplete");
426
+ if (audited.some((item) => item.value.status === "non-finite"))
427
+ issues.push("non-finite");
428
+ if (timing === "valuation-specific" && audited.length > 1)
429
+ issues.push("duplicate");
430
+ if (audited.slice(1).some((item) => !equalAudit(audited[0]!, item)))
431
+ issues.push("conflict");
432
+ const validStaticCopies =
433
+ timing === "origin-static" && issues.length === 0;
434
+ if (issues.length > 0)
435
+ return Object.freeze({
436
+ measureId: first.measureId,
437
+ key: first.key,
438
+ status: "invalid",
439
+ issues: Object.freeze(issues),
440
+ value: null,
441
+ observations: Object.freeze(audited),
442
+ });
443
+ const value = audited[0]!.value;
444
+ if (value.status !== "observed")
445
+ throw new Error("unreachable invalid exposure state");
446
+ const sources = normalizeDiagnosticSourceLocations(
447
+ audited.map((item) => item.source),
448
+ );
449
+ return Object.freeze({
450
+ measureId: first.measureId,
451
+ key: first.key,
452
+ status: "valid",
453
+ sourceGroup: first.sourceGroup,
454
+ origin: first.origin,
455
+ ...(timing === "valuation-specific"
456
+ ? { valuation: first.valuation! }
457
+ : {}),
458
+ value: value.value,
459
+ deduplicated: validStaticCopies ? audited.length - 1 : 0,
460
+ sources: Object.freeze(sources),
461
+ });
462
+ })
463
+ .sort((left, right) => {
464
+ return (
465
+ compareDiagnosticIdentityValues(
466
+ [
467
+ left.measureId,
468
+ left.key,
469
+ left.status,
470
+ left.status === "invalid" ? left.issues : undefined,
471
+ ],
472
+ [
473
+ right.measureId,
474
+ right.key,
475
+ right.status,
476
+ right.status === "invalid" ? right.issues : undefined,
477
+ ],
478
+ ) || compareDiagnosticIdentityValues(left, right)
479
+ );
480
+ }),
481
+ );
482
+ }