@actuarial-ts/core 0.6.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 (103) hide show
  1. package/README.md +3 -3
  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/diagnosticAggregation.d.ts +3 -0
  6. package/dist/diagnosticAggregation.d.ts.map +1 -1
  7. package/dist/diagnosticAggregation.js +30 -7
  8. package/dist/diagnosticAggregation.js.map +1 -1
  9. package/dist/diagnosticDefinitions.d.ts +3 -0
  10. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  11. package/dist/diagnosticDefinitions.js +844 -182
  12. package/dist/diagnosticDefinitions.js.map +1 -1
  13. package/dist/diagnosticDerivations.d.ts +23 -0
  14. package/dist/diagnosticDerivations.d.ts.map +1 -1
  15. package/dist/diagnosticDerivations.js +187 -43
  16. package/dist/diagnosticDerivations.js.map +1 -1
  17. package/dist/diagnosticExposure.d.ts +2 -2
  18. package/dist/diagnosticExposure.d.ts.map +1 -1
  19. package/dist/diagnosticExposure.js +228 -20
  20. package/dist/diagnosticExposure.js.map +1 -1
  21. package/dist/diagnosticExpressions.d.ts +1 -1
  22. package/dist/diagnosticExpressions.d.ts.map +1 -1
  23. package/dist/diagnosticExpressions.js +35 -15
  24. package/dist/diagnosticExpressions.js.map +1 -1
  25. package/dist/diagnosticFormulas.d.ts +3 -0
  26. package/dist/diagnosticFormulas.d.ts.map +1 -1
  27. package/dist/diagnosticFormulas.js +160 -26
  28. package/dist/diagnosticFormulas.js.map +1 -1
  29. package/dist/diagnosticIdentity.d.ts +15 -1
  30. package/dist/diagnosticIdentity.d.ts.map +1 -1
  31. package/dist/diagnosticIdentity.js +318 -51
  32. package/dist/diagnosticIdentity.js.map +1 -1
  33. package/dist/diagnosticOrdering.d.ts +8 -0
  34. package/dist/diagnosticOrdering.d.ts.map +1 -0
  35. package/dist/diagnosticOrdering.js +73 -0
  36. package/dist/diagnosticOrdering.js.map +1 -0
  37. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  38. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  39. package/dist/diagnosticPeriodAxis.js +69 -0
  40. package/dist/diagnosticPeriodAxis.js.map +1 -0
  41. package/dist/diagnosticPeriods.d.ts +2 -3
  42. package/dist/diagnosticPeriods.d.ts.map +1 -1
  43. package/dist/diagnosticPeriods.js +20 -68
  44. package/dist/diagnosticPeriods.js.map +1 -1
  45. package/dist/diagnosticPreparation.d.ts +47 -14
  46. package/dist/diagnosticPreparation.d.ts.map +1 -1
  47. package/dist/diagnosticPreparation.js +1482 -112
  48. package/dist/diagnosticPreparation.js.map +1 -1
  49. package/dist/diagnosticReview.d.ts +36 -14
  50. package/dist/diagnosticReview.d.ts.map +1 -1
  51. package/dist/diagnosticReview.js +448 -42
  52. package/dist/diagnosticReview.js.map +1 -1
  53. package/dist/diagnosticRunner.d.ts +2 -1
  54. package/dist/diagnosticRunner.d.ts.map +1 -1
  55. package/dist/diagnosticRunner.js +656 -70
  56. package/dist/diagnosticRunner.js.map +1 -1
  57. package/dist/diagnosticRuntime.d.ts +26 -0
  58. package/dist/diagnosticRuntime.d.ts.map +1 -0
  59. package/dist/diagnosticRuntime.js +243 -0
  60. package/dist/diagnosticRuntime.js.map +1 -0
  61. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  62. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  63. package/dist/diagnosticSourceOrdering.js +38 -0
  64. package/dist/diagnosticSourceOrdering.js.map +1 -0
  65. package/dist/index.d.ts +8 -4
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +5 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/mack.d.ts +4 -0
  70. package/dist/mack.d.ts.map +1 -1
  71. package/dist/mack.js +32 -4
  72. package/dist/mack.js.map +1 -1
  73. package/dist/triangle.d.ts.map +1 -1
  74. package/dist/triangle.js +59 -9
  75. package/dist/triangle.js.map +1 -1
  76. package/dist/types.d.ts +4 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js +22 -8
  79. package/dist/types.js.map +1 -1
  80. package/dist/version.d.ts +1 -1
  81. package/dist/version.js +1 -1
  82. package/package.json +3 -2
  83. package/src/capping.ts +109 -18
  84. package/src/diagnosticAggregation.ts +69 -20
  85. package/src/diagnosticDefinitions.ts +2675 -426
  86. package/src/diagnosticDerivations.ts +281 -49
  87. package/src/diagnosticExposure.ts +441 -50
  88. package/src/diagnosticExpressions.ts +99 -17
  89. package/src/diagnosticFormulas.ts +239 -39
  90. package/src/diagnosticIdentity.ts +691 -191
  91. package/src/diagnosticOrdering.ts +106 -0
  92. package/src/diagnosticPeriodAxis.ts +92 -0
  93. package/src/diagnosticPeriods.ts +67 -82
  94. package/src/diagnosticPreparation.ts +2369 -127
  95. package/src/diagnosticReview.ts +864 -58
  96. package/src/diagnosticRunner.ts +1076 -72
  97. package/src/diagnosticRuntime.ts +264 -0
  98. package/src/diagnosticSourceOrdering.ts +50 -0
  99. package/src/index.ts +31 -3
  100. package/src/mack.ts +72 -9
  101. package/src/triangle.ts +103 -13
  102. package/src/types.ts +55 -20
  103. package/src/version.ts +1 -1
@@ -1,4 +1,24 @@
1
- import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
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";
2
22
 
3
23
  export interface DiagnosticExposureObservation {
4
24
  readonly key: string;
@@ -14,7 +34,11 @@ export interface DiagnosticExposureObservation {
14
34
  export type DiagnosticAuditedNumericValue =
15
35
  | { readonly status: "observed"; readonly value: number }
16
36
  | { readonly status: "missing"; readonly value: null }
17
- | { readonly status: "non-finite"; readonly value: null; readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity" };
37
+ | {
38
+ readonly status: "non-finite";
39
+ readonly value: null;
40
+ readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity";
41
+ };
18
42
 
19
43
  export interface DiagnosticExposureAuditObservation {
20
44
  readonly sourceGroup: string;
@@ -26,66 +50,433 @@ export interface DiagnosticExposureAuditObservation {
26
50
  }
27
51
 
28
52
  export type ReconciledDiagnosticExposure =
29
- | { readonly measureId: string; readonly key: string; readonly status: "valid"; readonly sourceGroup: string; readonly origin: string; readonly valuation?: string; readonly value: number; readonly deduplicated: number; readonly sources: readonly DiagnosticSourceLocation[] }
30
- | { readonly measureId: string; readonly key: string; readonly status: "invalid"; readonly issues: readonly ("missing" | "incomplete" | "non-finite" | "duplicate" | "conflict")[]; readonly value: null; readonly observations: readonly DiagnosticExposureAuditObservation[] };
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
+ };
31
74
 
32
- export function auditDiagnosticNumber(value: number | null): DiagnosticAuditedNumericValue {
33
- if (value === null) return { status: "missing", value: null };
34
- if (!Number.isFinite(value)) return { status: "non-finite", value: null, nonFiniteKind: Number.isNaN(value) ? "nan" : value > 0 ? "positive-infinity" : "negative-infinity" };
35
- return { status: "observed", value: Object.is(value, -0) ? 0 : value };
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);
36
322
  }
37
323
 
38
- function equalAudit(left: DiagnosticExposureAuditObservation, right: DiagnosticExposureAuditObservation): boolean {
39
- return left.sourceGroup === right.sourceGroup && left.origin === right.origin && left.complete === right.complete &&
40
- JSON.stringify(left.value) === JSON.stringify(right.value);
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
+ );
41
334
  }
42
335
 
43
336
  export function reconcileDiagnosticExposures(
44
337
  observations: readonly DiagnosticExposureObservation[],
45
- timingByMeasure: Readonly<Record<string, "origin-static" | "valuation-specific">>,
338
+ timingByMeasure: Readonly<
339
+ Record<string, "origin-static" | "valuation-specific">
340
+ >,
341
+ periodAxis?: DiagnosticPeriodAxis,
46
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));
47
381
  const cohorts = new Map<string, DiagnosticExposureObservation[]>();
48
382
  for (const observation of observations) {
49
- const timing = timingByMeasure[observation.measureId];
50
- const identity = timing === "valuation-specific"
51
- ? `${observation.measureId}\u0000${observation.key}\u0000${observation.valuation ?? ""}`
52
- : `${observation.measureId}\u0000${observation.key}`;
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
+ );
53
395
  const cohort = cohorts.get(identity) ?? [];
54
396
  cohort.push(observation);
55
397
  cohorts.set(identity, cohort);
56
398
  }
57
- return Object.freeze([...cohorts.values()].map((cohort): ReconciledDiagnosticExposure => {
58
- const first = cohort[0]!;
59
- const timing = timingByMeasure[first.measureId];
60
- const audited = cohort.map((item): DiagnosticExposureAuditObservation => ({
61
- sourceGroup: item.sourceGroup,
62
- origin: item.origin,
63
- ...(item.valuation === undefined ? {} : { valuation: item.valuation }),
64
- value: auditDiagnosticNumber(item.value),
65
- complete: item.complete,
66
- ...(item.source === undefined ? {} : { source: item.source }),
67
- }));
68
- const issues: ("missing" | "incomplete" | "non-finite" | "duplicate" | "conflict")[] = [];
69
- if (audited.some((item) => item.value.status === "missing")) issues.push("missing");
70
- if (audited.some((item) => !item.complete)) issues.push("incomplete");
71
- if (audited.some((item) => item.value.status === "non-finite")) issues.push("non-finite");
72
- if (timing === "valuation-specific" && audited.length > 1) issues.push("duplicate");
73
- if (audited.slice(1).some((item) => !equalAudit(audited[0]!, item))) issues.push("conflict");
74
- const validStaticCopies = timing === "origin-static" && issues.length === 0;
75
- if (issues.length > 0) return Object.freeze({ measureId: first.measureId, key: first.key, status: "invalid", issues: Object.freeze(issues), value: null, observations: Object.freeze(audited) });
76
- const value = audited[0]!.value;
77
- if (value.status !== "observed") throw new Error("unreachable invalid exposure state");
78
- const sources = audited.flatMap((item) => item.source ? [item.source] : []).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)));
79
- return Object.freeze({
80
- measureId: first.measureId,
81
- key: first.key,
82
- status: "valid",
83
- sourceGroup: first.sourceGroup,
84
- origin: first.origin,
85
- ...(timing === "valuation-specific" ? { valuation: first.valuation! } : {}),
86
- value: value.value,
87
- deduplicated: validStaticCopies ? audited.length - 1 : 0,
88
- sources: Object.freeze(sources),
89
- });
90
- }).sort((left, right) => `${left.measureId}\u0000${left.key}` < `${right.measureId}\u0000${right.key}` ? -1 : 1));
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
+ );
91
482
  }