@actuarial-ts/core 0.4.0 → 0.6.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 (87) hide show
  1. package/README.md +65 -215
  2. package/dist/casualtyDiagnostics.d.ts +19 -49
  3. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  4. package/dist/casualtyDiagnostics.js +84 -114
  5. package/dist/casualtyDiagnostics.js.map +1 -1
  6. package/dist/diagnosticAggregation.d.ts +29 -0
  7. package/dist/diagnosticAggregation.d.ts.map +1 -0
  8. package/dist/diagnosticAggregation.js +50 -0
  9. package/dist/diagnosticAggregation.js.map +1 -0
  10. package/dist/diagnosticDefinitions.d.ts +286 -0
  11. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  12. package/dist/diagnosticDefinitions.js +1026 -0
  13. package/dist/diagnosticDefinitions.js.map +1 -0
  14. package/dist/diagnosticDerivations.d.ts +12 -0
  15. package/dist/diagnosticDerivations.d.ts.map +1 -0
  16. package/dist/diagnosticDerivations.js +80 -0
  17. package/dist/diagnosticDerivations.js.map +1 -0
  18. package/dist/diagnosticExposure.d.ts +51 -0
  19. package/dist/diagnosticExposure.d.ts.map +1 -0
  20. package/dist/diagnosticExposure.js +65 -0
  21. package/dist/diagnosticExposure.js.map +1 -0
  22. package/dist/diagnosticExpressions.d.ts +56 -0
  23. package/dist/diagnosticExpressions.d.ts.map +1 -0
  24. package/dist/diagnosticExpressions.js +104 -0
  25. package/dist/diagnosticExpressions.js.map +1 -0
  26. package/dist/diagnosticFormulas.d.ts +195 -0
  27. package/dist/diagnosticFormulas.d.ts.map +1 -0
  28. package/dist/diagnosticFormulas.js +85 -0
  29. package/dist/diagnosticFormulas.js.map +1 -0
  30. package/dist/diagnosticIdentity.d.ts +237 -0
  31. package/dist/diagnosticIdentity.d.ts.map +1 -0
  32. package/dist/diagnosticIdentity.js +454 -0
  33. package/dist/diagnosticIdentity.js.map +1 -0
  34. package/dist/diagnosticPeriods.d.ts +16 -0
  35. package/dist/diagnosticPeriods.d.ts.map +1 -0
  36. package/dist/diagnosticPeriods.js +98 -0
  37. package/dist/diagnosticPeriods.js.map +1 -0
  38. package/dist/diagnosticPreparation.d.ts +98 -0
  39. package/dist/diagnosticPreparation.d.ts.map +1 -0
  40. package/dist/diagnosticPreparation.js +169 -0
  41. package/dist/diagnosticPreparation.js.map +1 -0
  42. package/dist/diagnosticReview.d.ts +44 -0
  43. package/dist/diagnosticReview.d.ts.map +1 -0
  44. package/dist/diagnosticReview.js +84 -0
  45. package/dist/diagnosticReview.js.map +1 -0
  46. package/dist/diagnosticRules.d.ts +32 -0
  47. package/dist/diagnosticRules.d.ts.map +1 -0
  48. package/dist/diagnosticRules.js +32 -0
  49. package/dist/diagnosticRules.js.map +1 -0
  50. package/dist/diagnosticRunner.d.ts +85 -0
  51. package/dist/diagnosticRunner.d.ts.map +1 -0
  52. package/dist/diagnosticRunner.js +117 -0
  53. package/dist/diagnosticRunner.js.map +1 -0
  54. package/dist/index.d.ts +24 -1
  55. package/dist/index.d.ts.map +1 -1
  56. package/dist/index.js +12 -1
  57. package/dist/index.js.map +1 -1
  58. package/dist/types.d.ts +17 -0
  59. package/dist/types.d.ts.map +1 -1
  60. package/dist/types.js +114 -0
  61. package/dist/types.js.map +1 -1
  62. package/dist/version.d.ts +3 -0
  63. package/dist/version.d.ts.map +1 -0
  64. package/dist/version.js +3 -0
  65. package/dist/version.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/casualtyDiagnostics.ts +79 -187
  68. package/src/diagnosticAggregation.ts +79 -0
  69. package/src/diagnosticDefinitions.ts +1375 -0
  70. package/src/diagnosticDerivations.ts +89 -0
  71. package/src/diagnosticExposure.ts +91 -0
  72. package/src/diagnosticExpressions.ts +173 -0
  73. package/src/diagnosticFormulas.ts +150 -0
  74. package/src/diagnosticIdentity.ts +767 -0
  75. package/src/diagnosticPeriods.ts +117 -0
  76. package/src/diagnosticPreparation.ts +233 -0
  77. package/src/diagnosticReview.ts +91 -0
  78. package/src/diagnosticRules.ts +69 -0
  79. package/src/diagnosticRunner.ts +110 -0
  80. package/src/index.ts +156 -1
  81. package/src/types.ts +161 -0
  82. package/src/version.ts +2 -0
  83. package/dist/metricDiagnostics.d.ts +0 -212
  84. package/dist/metricDiagnostics.d.ts.map +0 -1
  85. package/dist/metricDiagnostics.js +0 -627
  86. package/dist/metricDiagnostics.js.map +0 -1
  87. package/src/metricDiagnostics.ts +0 -876
@@ -0,0 +1,767 @@
1
+ import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import type {
3
+ AmountBasisDefinition,
4
+ AmountLimitation,
5
+ AmountPerspective,
6
+ DiagnosticComparisonPredicate,
7
+ DiagnosticControlTotalProjection,
8
+ DiagnosticCountPopulationDefinition,
9
+ DiagnosticDeepReadonly,
10
+ DiagnosticDefinition,
11
+ DiagnosticDerivedMeasureDefinition,
12
+ DiagnosticDevelopmentSemantics,
13
+ DiagnosticExposureBasisDefinition,
14
+ DiagnosticExposureTiming,
15
+ DiagnosticMeasureDefinition,
16
+ DiagnosticMeasureKind,
17
+ DiagnosticMeasureSource,
18
+ DiagnosticMetricPresentation,
19
+ DiagnosticMissingPolicy,
20
+ DiagnosticPeriodAxis,
21
+ DiagnosticReviewFilter,
22
+ DiagnosticReviewOperand,
23
+ DiagnosticReviewPredicate,
24
+ DiagnosticReviewRule,
25
+ DiagnosticRuleOperand,
26
+ DiagnosticSourceLocation,
27
+ } from "./diagnosticDefinitions.js";
28
+ import type {
29
+ DiagnosticClaimExpression,
30
+ DiagnosticMeasureExpression,
31
+ DiagnosticRoleExpression,
32
+ } from "./diagnosticExpressions.js";
33
+
34
+ function compareCodeUnits(left: string, right: string): number {
35
+ return left === right ? 0 : left < right ? -1 : 1;
36
+ }
37
+
38
+ function sortedUnique(values: readonly string[]): readonly string[] {
39
+ return [...new Set(values)].sort(compareCodeUnits);
40
+ }
41
+
42
+ function sortedRecord<T>(entries: readonly (readonly [string, T])[]): Readonly<Record<string, T>> {
43
+ const result = Object.create(null) as Record<string, T>;
44
+ for (const [key, value] of [...entries].sort(([left], [right]) => compareCodeUnits(left, right))) {
45
+ result[key] = value;
46
+ }
47
+ return result;
48
+ }
49
+
50
+ function normalizeJsonValue<T>(value: T): T {
51
+ if (typeof value === "number" && Object.is(value, -0)) return 0 as T;
52
+ if (Array.isArray(value)) return value.map((item) => normalizeJsonValue(item)) as T;
53
+ if (value !== null && typeof value === "object") {
54
+ return sortedRecord(Object.entries(value as Record<string, unknown>).map(([key, item]) =>
55
+ [key, normalizeJsonValue(item)] as const,
56
+ )) as T;
57
+ }
58
+ return value;
59
+ }
60
+
61
+ function deepFreeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
62
+ if (value === null || typeof value !== "object") return value as DiagnosticDeepReadonly<T>;
63
+ if (seen.has(value)) return value as DiagnosticDeepReadonly<T>;
64
+ seen.add(value);
65
+ for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child, seen);
66
+ return Object.freeze(value) as DiagnosticDeepReadonly<T>;
67
+ }
68
+
69
+ function attributes(
70
+ value: Readonly<Record<string, string | number | boolean | null>> | undefined,
71
+ ): Readonly<Record<string, string | number | boolean | null>> {
72
+ return sortedRecord(Object.entries(value ?? {}).map(([key, item]) =>
73
+ [key, normalizeJsonValue(item)] as const,
74
+ ));
75
+ }
76
+
77
+ export type NormalizedAmountLimitationIdentity =
78
+ | { readonly kind: "unlimited" }
79
+ | {
80
+ readonly kind: "layer" | "pre-limited";
81
+ readonly attachment: number;
82
+ readonly limit: number | null;
83
+ readonly application: "claim" | "occurrence" | "policy" | "source-defined";
84
+ readonly derivation:
85
+ | { readonly kind: "sdk" }
86
+ | {
87
+ readonly kind: "external";
88
+ readonly actor: "caller" | "source";
89
+ readonly transformationRef: string;
90
+ };
91
+ }
92
+ | { readonly kind: "unknown"; readonly description: string | null };
93
+
94
+ export interface NormalizedDiagnosticToleranceIdentity {
95
+ readonly absolute: number;
96
+ readonly relative: number;
97
+ }
98
+
99
+ export interface NormalizedDiagnosticReviewFilterIdentity {
100
+ readonly sourceGroups: readonly string[] | null;
101
+ readonly origins: readonly string[] | null;
102
+ readonly originFrom: string | null;
103
+ readonly originThrough: string | null;
104
+ readonly valuations: readonly string[] | null;
105
+ readonly valuationFrom: string | null;
106
+ readonly valuationThrough: string | null;
107
+ readonly minDevelopmentAge: number | null;
108
+ readonly maxDevelopmentAge: number | null;
109
+ }
110
+
111
+ export type NormalizedDiagnosticPeriodAxisIdentity =
112
+ | DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "calendar" }>>
113
+ | (Omit<
114
+ DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "ordered" }>>,
115
+ "origins" | "valuations"
116
+ > & {
117
+ readonly origins: readonly {
118
+ readonly label: string;
119
+ readonly aliases: readonly string[];
120
+ readonly coordinate: number;
121
+ }[];
122
+ readonly valuations: readonly {
123
+ readonly label: string;
124
+ readonly aliases: readonly string[];
125
+ readonly coordinate: number;
126
+ }[];
127
+ });
128
+
129
+ export interface NormalizedDiagnosticFormulaIdentity {
130
+ readonly id: string;
131
+ readonly version: string;
132
+ readonly roles: Readonly<Record<string, {
133
+ readonly kind: DiagnosticMeasureKind;
134
+ readonly compatibilityGroup: string | null;
135
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics | null;
136
+ }>>;
137
+ readonly numerator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
138
+ readonly denominator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
139
+ readonly denominatorPolicy: "positive-or-null";
140
+ }
141
+
142
+ interface NormalizedDiagnosticReviewRuleBase {
143
+ readonly id: string;
144
+ readonly code: string;
145
+ readonly description: string;
146
+ readonly severity: "warning" | "fail";
147
+ readonly missingInput: "not-evaluated" | "finding";
148
+ readonly tolerance: NormalizedDiagnosticToleranceIdentity;
149
+ }
150
+
151
+ export type NormalizedDiagnosticReviewRuleIdentity =
152
+ | (NormalizedDiagnosticReviewRuleBase & {
153
+ readonly kind: "compare";
154
+ readonly when: DiagnosticDeepReadonly<DiagnosticReviewPredicate>;
155
+ })
156
+ | (NormalizedDiagnosticReviewRuleBase & {
157
+ readonly kind: "reconcile";
158
+ readonly actual: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
159
+ readonly expected: DiagnosticDeepReadonly<DiagnosticReviewOperand>;
160
+ })
161
+ | (NormalizedDiagnosticReviewRuleBase & {
162
+ readonly kind: "monotonic";
163
+ readonly expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
164
+ readonly direction: "nondecreasing" | "nonincreasing";
165
+ })
166
+ | (NormalizedDiagnosticReviewRuleBase & {
167
+ readonly kind: "layer-order";
168
+ readonly narrower: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
169
+ readonly broader: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
170
+ readonly comparability: DiagnosticDeepReadonly<
171
+ Extract<DiagnosticReviewRule, { kind: "layer-order" }>["comparability"]
172
+ >;
173
+ })
174
+ | (NormalizedDiagnosticReviewRuleBase & {
175
+ readonly kind: "control-total";
176
+ readonly expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
177
+ readonly expected: number;
178
+ readonly filter: NormalizedDiagnosticReviewFilterIdentity | null;
179
+ readonly projection: DiagnosticDeepReadonly<DiagnosticControlTotalProjection>;
180
+ });
181
+
182
+ export interface NormalizedDiagnosticDefinitionIdentity {
183
+ readonly diagnosticDefinitionVersion: "1.0.0";
184
+ readonly id: string;
185
+ readonly version: string;
186
+ readonly lossRowGrain: "claim" | "aggregate";
187
+ readonly measures: readonly {
188
+ readonly id: string;
189
+ readonly displayName: string;
190
+ readonly description: string;
191
+ readonly source: DiagnosticMeasureSource;
192
+ readonly kind: DiagnosticMeasureKind;
193
+ readonly unit: string;
194
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics;
195
+ readonly aggregation: "sum";
196
+ readonly missing: DiagnosticMissingPolicy;
197
+ readonly basisId: string | null;
198
+ readonly countPopulationId: string | null;
199
+ readonly exposureBasisId: string | null;
200
+ readonly exposureTiming: DiagnosticExposureTiming | null;
201
+ }[];
202
+ readonly countPopulations: readonly {
203
+ readonly id: string;
204
+ readonly displayName: string;
205
+ readonly subject: DiagnosticCountPopulationDefinition["subject"];
206
+ readonly unit: string;
207
+ readonly description: string;
208
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
209
+ }[];
210
+ readonly exposureBases: readonly {
211
+ readonly id: string;
212
+ readonly displayName: string;
213
+ readonly basis: DiagnosticExposureBasisDefinition["basis"];
214
+ readonly unit: string;
215
+ readonly description: string;
216
+ readonly sourceDescription: string | null;
217
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
218
+ }[];
219
+ readonly amountBases: readonly {
220
+ readonly id: string;
221
+ readonly displayName: string;
222
+ readonly currency: string;
223
+ readonly perspective: AmountPerspective;
224
+ readonly components: readonly {
225
+ readonly id: string;
226
+ readonly treatment: "included" | "excluded" | "unknown";
227
+ readonly limitation: NormalizedAmountLimitationIdentity;
228
+ }[];
229
+ readonly sourceDescription: string | null;
230
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
231
+ }[];
232
+ readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
233
+ readonly formulas: readonly NormalizedDiagnosticFormulaIdentity[];
234
+ readonly instances: readonly {
235
+ readonly id: string;
236
+ readonly version: string;
237
+ readonly formulaId: string;
238
+ readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
239
+ readonly presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>;
240
+ readonly rules: readonly {
241
+ readonly id: string;
242
+ readonly code: string;
243
+ readonly message: string;
244
+ readonly severity: "warning" | "fail";
245
+ readonly when: {
246
+ readonly left: DiagnosticDeepReadonly<DiagnosticRuleOperand>;
247
+ readonly operator: DiagnosticComparisonPredicate["operator"];
248
+ readonly right: DiagnosticDeepReadonly<DiagnosticRuleOperand>;
249
+ readonly tolerance: NormalizedDiagnosticToleranceIdentity;
250
+ };
251
+ }[];
252
+ }[];
253
+ readonly reviewRules: readonly NormalizedDiagnosticReviewRuleIdentity[];
254
+ readonly periodAxis: NormalizedDiagnosticPeriodAxisIdentity;
255
+ }
256
+
257
+ export interface NormalizedDiagnosticCalculationScope {
258
+ readonly formulaFingerprint: string;
259
+ readonly instance: {
260
+ readonly id: string;
261
+ readonly version: string;
262
+ readonly formulaId: string;
263
+ readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
264
+ };
265
+ readonly lossRowGrain: "claim" | "aggregate";
266
+ readonly measures: readonly {
267
+ readonly id: string;
268
+ readonly source: DiagnosticMeasureSource;
269
+ readonly kind: DiagnosticMeasureKind;
270
+ readonly unit: string;
271
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics;
272
+ readonly aggregation: "sum";
273
+ readonly missing: DiagnosticMissingPolicy;
274
+ readonly basisId: string | null;
275
+ readonly countPopulationId: string | null;
276
+ readonly exposureBasisId: string | null;
277
+ readonly exposureTiming: DiagnosticExposureTiming | null;
278
+ }[];
279
+ readonly countPopulations: readonly {
280
+ readonly id: string;
281
+ readonly subject: DiagnosticCountPopulationDefinition["subject"];
282
+ readonly unit: string;
283
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
284
+ }[];
285
+ readonly exposureBases: readonly {
286
+ readonly id: string;
287
+ readonly basis: DiagnosticExposureBasisDefinition["basis"];
288
+ readonly unit: string;
289
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
290
+ }[];
291
+ readonly amountBases: readonly {
292
+ readonly id: string;
293
+ readonly currency: string;
294
+ readonly perspective: AmountPerspective;
295
+ readonly components: readonly {
296
+ readonly id: string;
297
+ readonly treatment: "included" | "excluded" | "unknown";
298
+ readonly limitation: NormalizedAmountLimitationIdentity;
299
+ }[];
300
+ readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
301
+ }[];
302
+ readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
303
+ }
304
+
305
+ export interface NormalizedDiagnosticSourceLocationIdentity {
306
+ readonly artifactId: string;
307
+ readonly sourceFile: string | null;
308
+ readonly sourceSheet: string | null;
309
+ readonly sourceRow: number | null;
310
+ readonly sourceCell: string | null;
311
+ }
312
+
313
+ function normalizeLimitation(limitation: AmountLimitation): NormalizedAmountLimitationIdentity {
314
+ if (limitation.kind === "unlimited") return { kind: "unlimited" };
315
+ if (limitation.kind === "unknown") {
316
+ return { kind: "unknown", description: limitation.description ?? null };
317
+ }
318
+ return {
319
+ kind: limitation.kind,
320
+ attachment: Object.is(limitation.attachment, -0) ? 0 : limitation.attachment,
321
+ limit: limitation.limit !== null && Object.is(limitation.limit, -0) ? 0 : limitation.limit,
322
+ application: limitation.application,
323
+ derivation: limitation.derivation.kind === "sdk"
324
+ ? { kind: "sdk" }
325
+ : {
326
+ kind: "external",
327
+ actor: limitation.derivation.actor,
328
+ transformationRef: limitation.derivation.transformationRef,
329
+ },
330
+ };
331
+ }
332
+
333
+ function normalizeMeasureExpression<T extends DiagnosticMeasureExpression | DiagnosticClaimExpression>(
334
+ expression: T,
335
+ ): DiagnosticDeepReadonly<T> {
336
+ if (expression.op === "measure") return { op: "measure", measureId: expression.measureId } as DiagnosticDeepReadonly<T>;
337
+ if (expression.op === "claim-layer") {
338
+ return {
339
+ op: "claim-layer",
340
+ measureId: expression.measureId,
341
+ attachment: Object.is(expression.attachment, -0) ? 0 : expression.attachment,
342
+ limit: expression.limit !== null && Object.is(expression.limit, -0) ? 0 : expression.limit,
343
+ } as DiagnosticDeepReadonly<T>;
344
+ }
345
+ if (expression.op === "subtract") {
346
+ return {
347
+ op: "subtract",
348
+ left: normalizeMeasureExpression(expression.left),
349
+ right: normalizeMeasureExpression(expression.right),
350
+ } as DiagnosticDeepReadonly<T>;
351
+ }
352
+ return {
353
+ op: "add",
354
+ terms: expression.terms.map((term) => normalizeMeasureExpression(term)),
355
+ } as unknown as DiagnosticDeepReadonly<T>;
356
+ }
357
+
358
+ function normalizeRoleExpression(expression: DiagnosticRoleExpression): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
359
+ if (expression.op === "role") return { op: "role", role: expression.role };
360
+ if (expression.op === "subtract") {
361
+ return {
362
+ op: "subtract",
363
+ left: normalizeRoleExpression(expression.left),
364
+ right: normalizeRoleExpression(expression.right),
365
+ };
366
+ }
367
+ return { op: "add", terms: expression.terms.map(normalizeRoleExpression) };
368
+ }
369
+
370
+ function normalizeTolerance(
371
+ tolerance: { absolute?: number; relative?: number } | undefined,
372
+ ): NormalizedDiagnosticToleranceIdentity {
373
+ return {
374
+ absolute: Object.is(tolerance?.absolute ?? 0, -0) ? 0 : (tolerance?.absolute ?? 0),
375
+ relative: Object.is(tolerance?.relative ?? 0, -0) ? 0 : (tolerance?.relative ?? 0),
376
+ };
377
+ }
378
+
379
+ function normalizeRuleOperand(operand: DiagnosticRuleOperand): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
380
+ if (operand.source === "measure") {
381
+ return { source: "measure", expression: normalizeMeasureExpression(operand.expression) };
382
+ }
383
+ if (operand.source === "calculation") return { source: "calculation", field: operand.field };
384
+ return { source: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
385
+ }
386
+
387
+ function normalizeReviewOperand(operand: DiagnosticReviewOperand): DiagnosticDeepReadonly<DiagnosticReviewOperand> {
388
+ if (operand.op === "constant") return { op: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
389
+ return normalizeMeasureExpression(operand);
390
+ }
391
+
392
+ function normalizeReviewFilter(filter: DiagnosticReviewFilter): NormalizedDiagnosticReviewFilterIdentity {
393
+ return {
394
+ sourceGroups: filter.sourceGroups === undefined ? null : sortedUnique(filter.sourceGroups),
395
+ origins: filter.origins === undefined ? null : sortedUnique(filter.origins),
396
+ originFrom: filter.originFrom ?? null,
397
+ originThrough: filter.originThrough ?? null,
398
+ valuations: filter.valuations === undefined ? null : sortedUnique(filter.valuations),
399
+ valuationFrom: filter.valuationFrom ?? null,
400
+ valuationThrough: filter.valuationThrough ?? null,
401
+ minDevelopmentAge: filter.minDevelopmentAge === undefined
402
+ ? null
403
+ : Object.is(filter.minDevelopmentAge, -0) ? 0 : filter.minDevelopmentAge,
404
+ maxDevelopmentAge: filter.maxDevelopmentAge === undefined
405
+ ? null
406
+ : Object.is(filter.maxDevelopmentAge, -0) ? 0 : filter.maxDevelopmentAge,
407
+ };
408
+ }
409
+
410
+ function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPeriodAxisIdentity {
411
+ if (axis.kind === "calendar") {
412
+ return {
413
+ kind: "calendar",
414
+ originCadence: axis.originCadence,
415
+ valuationCadence: axis.valuationCadence,
416
+ originAnchor: axis.originAnchor,
417
+ valuationAnchor: axis.valuationAnchor,
418
+ ageUnit: "month",
419
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
420
+ };
421
+ }
422
+ const coordinates = (values: typeof axis.origins) => values.map((coordinate) => ({
423
+ label: coordinate.label,
424
+ aliases: sortedUnique(coordinate.aliases ?? []),
425
+ coordinate: Object.is(coordinate.coordinate, -0) ? 0 : coordinate.coordinate,
426
+ })).sort((left, right) => left.coordinate - right.coordinate || compareCodeUnits(left.label, right.label));
427
+ return {
428
+ kind: "ordered",
429
+ id: axis.id,
430
+ version: axis.version,
431
+ ageUnit: axis.ageUnit,
432
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
433
+ origins: coordinates(axis.origins),
434
+ valuations: coordinates(axis.valuations),
435
+ };
436
+ }
437
+
438
+ function topologicalDerivations(
439
+ derivations: readonly DiagnosticDerivedMeasureDefinition[],
440
+ ): readonly DiagnosticDerivedMeasureDefinition[] {
441
+ const byOutput = new Map(derivations.map((derivation) => [derivation.outputMeasureId, derivation]));
442
+ const remaining = new Map(derivations.map((derivation) => [derivation.id, derivation]));
443
+ const emittedOutputs = new Set<string>();
444
+ const ordered: DiagnosticDerivedMeasureDefinition[] = [];
445
+ while (remaining.size > 0) {
446
+ const ready = [...remaining.values()].filter((derivation) =>
447
+ expressionDependencies(derivation.expression)
448
+ .filter((id) => byOutput.has(id))
449
+ .every((id) => emittedOutputs.has(id)),
450
+ ).sort((left, right) => compareCodeUnits(left.id, right.id));
451
+ if (ready.length === 0) return [...derivations].sort((left, right) => compareCodeUnits(left.id, right.id));
452
+ for (const derivation of ready) {
453
+ ordered.push(derivation);
454
+ emittedOutputs.add(derivation.outputMeasureId);
455
+ remaining.delete(derivation.id);
456
+ }
457
+ }
458
+ return ordered;
459
+ }
460
+
461
+ function normalizeReviewRule(rule: DiagnosticReviewRule): NormalizedDiagnosticReviewRuleIdentity {
462
+ const base: NormalizedDiagnosticReviewRuleBase = {
463
+ id: rule.id,
464
+ code: rule.code,
465
+ description: rule.description,
466
+ severity: rule.severity,
467
+ missingInput: rule.missingInput,
468
+ tolerance: normalizeTolerance(rule.tolerance),
469
+ };
470
+ if (rule.kind === "compare") {
471
+ return {
472
+ ...base,
473
+ kind: "compare",
474
+ when: {
475
+ left: normalizeReviewOperand(rule.when.left),
476
+ operator: rule.when.operator,
477
+ right: normalizeReviewOperand(rule.when.right),
478
+ },
479
+ };
480
+ }
481
+ if (rule.kind === "reconcile") {
482
+ return {
483
+ ...base,
484
+ kind: "reconcile",
485
+ actual: normalizeMeasureExpression(rule.actual),
486
+ expected: normalizeReviewOperand(rule.expected),
487
+ };
488
+ }
489
+ if (rule.kind === "monotonic") {
490
+ return {
491
+ ...base,
492
+ kind: "monotonic",
493
+ expression: normalizeMeasureExpression(rule.expression),
494
+ direction: rule.direction,
495
+ };
496
+ }
497
+ if (rule.kind === "layer-order") {
498
+ return {
499
+ ...base,
500
+ kind: "layer-order",
501
+ narrower: normalizeMeasureExpression(rule.narrower),
502
+ broader: normalizeMeasureExpression(rule.broader),
503
+ comparability: normalizeJsonValue(rule.comparability),
504
+ };
505
+ }
506
+ return {
507
+ ...base,
508
+ kind: "control-total",
509
+ expression: normalizeMeasureExpression(rule.expression),
510
+ expected: Object.is(rule.expected, -0) ? 0 : rule.expected,
511
+ filter: rule.filter === undefined ? null : normalizeReviewFilter(rule.filter),
512
+ projection: normalizeJsonValue(rule.projection),
513
+ };
514
+ }
515
+
516
+ export function normalizeDiagnosticDefinition(
517
+ definition: DiagnosticDefinition,
518
+ ): DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity> {
519
+ const normalized: NormalizedDiagnosticDefinitionIdentity = {
520
+ diagnosticDefinitionVersion: "1.0.0",
521
+ id: definition.id,
522
+ version: definition.version,
523
+ lossRowGrain: definition.lossRowGrain,
524
+ measures: definition.measures.map((measure) => ({
525
+ id: measure.id,
526
+ displayName: measure.displayName,
527
+ description: measure.description,
528
+ source: measure.source,
529
+ kind: measure.kind,
530
+ unit: measure.unit,
531
+ developmentSemantics: measure.developmentSemantics,
532
+ aggregation: "sum" as const,
533
+ missing: measure.missing,
534
+ basisId: measure.basisId ?? null,
535
+ countPopulationId: measure.countPopulationId ?? null,
536
+ exposureBasisId: measure.exposureBasisId ?? null,
537
+ exposureTiming: measure.exposureTiming ?? null,
538
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
539
+ countPopulations: definition.countPopulations.map((population) => ({
540
+ id: population.id,
541
+ displayName: population.displayName,
542
+ subject: population.subject,
543
+ unit: population.unit,
544
+ description: population.description,
545
+ attributes: attributes(population.attributes),
546
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
547
+ exposureBases: definition.exposureBases.map((basis) => ({
548
+ id: basis.id,
549
+ displayName: basis.displayName,
550
+ basis: basis.basis,
551
+ unit: basis.unit,
552
+ description: basis.description,
553
+ sourceDescription: basis.sourceDescription ?? null,
554
+ attributes: attributes(basis.attributes),
555
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
556
+ amountBases: definition.amountBases.map((basis) => ({
557
+ id: basis.id,
558
+ displayName: basis.displayName,
559
+ currency: basis.currency,
560
+ perspective: basis.perspective,
561
+ components: basis.components.map((component) => ({
562
+ id: component.id,
563
+ treatment: component.treatment,
564
+ limitation: normalizeLimitation(component.limitation),
565
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
566
+ sourceDescription: basis.sourceDescription ?? null,
567
+ attributes: attributes(basis.attributes),
568
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
569
+ derivedMeasures: topologicalDerivations(definition.derivedMeasures).map((derivation) => ({
570
+ id: derivation.id,
571
+ outputMeasureId: derivation.outputMeasureId,
572
+ expression: normalizeMeasureExpression(derivation.expression),
573
+ })),
574
+ formulas: definition.formulas.map((formula) => ({
575
+ id: formula.id,
576
+ version: formula.version,
577
+ roles: sortedRecord(Object.entries(formula.roles).map(([name, role]) => [name, {
578
+ kind: role.kind,
579
+ compatibilityGroup: role.compatibilityGroup ?? null,
580
+ developmentSemantics: role.developmentSemantics ?? null,
581
+ }] as const)),
582
+ numerator: normalizeRoleExpression(formula.numerator),
583
+ denominator: normalizeRoleExpression(formula.denominator),
584
+ denominatorPolicy: "positive-or-null" as const,
585
+ })).sort((left, right) => compareCodeUnits(left.id, right.id)),
586
+ instances: definition.instances.map((instance) => ({
587
+ id: instance.id,
588
+ version: instance.version,
589
+ formulaId: instance.formulaId,
590
+ bindings: sortedRecord(Object.entries(instance.bindings).map(([role, expression]) =>
591
+ [role, normalizeMeasureExpression(expression)] as const,
592
+ )),
593
+ presentation: {
594
+ displayName: instance.presentation.displayName,
595
+ description: instance.presentation.description,
596
+ displayUnit: instance.presentation.displayUnit,
597
+ scale: Object.is(instance.presentation.scale, -0) ? 0 : instance.presentation.scale,
598
+ numeratorLabel: instance.presentation.numeratorLabel,
599
+ denominatorLabel: instance.presentation.denominatorLabel,
600
+ },
601
+ rules: instance.rules.map((rule) => ({
602
+ id: rule.id,
603
+ code: rule.code,
604
+ message: rule.message,
605
+ severity: rule.severity,
606
+ when: {
607
+ left: normalizeRuleOperand(rule.when.left),
608
+ operator: rule.when.operator,
609
+ right: normalizeRuleOperand(rule.when.right),
610
+ tolerance: normalizeTolerance(rule.when.tolerance),
611
+ },
612
+ })),
613
+ })),
614
+ reviewRules: definition.reviewRules.map(normalizeReviewRule),
615
+ periodAxis: normalizePeriodAxis(definition.periodAxis),
616
+ };
617
+ return deepFreeze(normalized);
618
+ }
619
+
620
+ export function normalizeDiagnosticSourceLocation(
621
+ source: DiagnosticSourceLocation,
622
+ ): NormalizedDiagnosticSourceLocationIdentity {
623
+ return deepFreeze({
624
+ artifactId: source.artifactId,
625
+ sourceFile: source.sourceFile ?? null,
626
+ sourceSheet: source.sourceSheet ?? null,
627
+ sourceRow: source.sourceRow === undefined ? null : Object.is(source.sourceRow, -0) ? 0 : source.sourceRow,
628
+ sourceCell: source.sourceCell ?? null,
629
+ });
630
+ }
631
+
632
+ function expressionDependencies(expression: DiagnosticMeasureExpression | DiagnosticClaimExpression): readonly string[] {
633
+ if (expression.op === "measure" || expression.op === "claim-layer") return [expression.measureId];
634
+ if (expression.op === "subtract") return sortedUnique([
635
+ ...expressionDependencies(expression.left),
636
+ ...expressionDependencies(expression.right),
637
+ ]);
638
+ return sortedUnique(expression.terms.flatMap(expressionDependencies));
639
+ }
640
+
641
+ function ruleMeasureDependencies(operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>): readonly string[] {
642
+ return operand.source === "measure" ? expressionDependencies(operand.expression) : [];
643
+ }
644
+
645
+ function tag<K extends string, T>(kind: K, property: string, value: T): string {
646
+ return `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({
647
+ identityVersion: 1,
648
+ kind,
649
+ [property]: value,
650
+ }))}`;
651
+ }
652
+
653
+ export interface BuiltDiagnosticIdentities {
654
+ readonly formulaFingerprints: Readonly<Record<string, string>>;
655
+ readonly calculationFingerprints: Readonly<Record<string, string>>;
656
+ readonly definitionIntegrity: string;
657
+ readonly calculationScopesByInstanceId: ReadonlyMap<string, NormalizedDiagnosticCalculationScope>;
658
+ readonly calculationDependenciesByInstanceId: ReadonlyMap<string, readonly string[]>;
659
+ readonly evaluationDependenciesByInstanceId: ReadonlyMap<string, readonly string[]>;
660
+ }
661
+
662
+ export function buildDiagnosticIdentities(
663
+ definition: DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity>,
664
+ ): BuiltDiagnosticIdentities {
665
+ const formulas = new Map(definition.formulas.map((formula) => [formula.id, formula]));
666
+ const measures = new Map(definition.measures.map((measure) => [measure.id, measure]));
667
+ const derivations = new Map(definition.derivedMeasures.map((derivation) => [derivation.outputMeasureId, derivation]));
668
+ const populations = new Map(definition.countPopulations.map((population) => [population.id, population]));
669
+ const exposureBases = new Map(definition.exposureBases.map((basis) => [basis.id, basis]));
670
+ const amountBases = new Map(definition.amountBases.map((basis) => [basis.id, basis]));
671
+
672
+ const formulaFingerprints = sortedRecord(definition.formulas.map((formula) =>
673
+ [formula.id, tag("diagnostic-formula", "formula", formula)] as const,
674
+ ));
675
+ const calculationFingerprints = Object.create(null) as Record<string, string>;
676
+ const calculationScopesByInstanceId = new Map<string, NormalizedDiagnosticCalculationScope>();
677
+ const calculationDependenciesByInstanceId = new Map<string, readonly string[]>();
678
+ const evaluationDependenciesByInstanceId = new Map<string, readonly string[]>();
679
+
680
+ const transitiveDependencies = (roots: readonly string[]): readonly string[] => {
681
+ const found = new Set<string>();
682
+ const stack = [...roots];
683
+ while (stack.length > 0) {
684
+ const id = stack.pop()!;
685
+ if (found.has(id)) continue;
686
+ found.add(id);
687
+ const derivation = derivations.get(id);
688
+ if (derivation) stack.push(...expressionDependencies(derivation.expression));
689
+ }
690
+ return [...found].sort(compareCodeUnits);
691
+ };
692
+
693
+ for (const instance of definition.instances) {
694
+ const calculationDependencies = transitiveDependencies(
695
+ Object.values(instance.bindings).flatMap(expressionDependencies),
696
+ );
697
+ const evaluationDependencies = transitiveDependencies([
698
+ ...calculationDependencies,
699
+ ...instance.rules.flatMap((rule) => [
700
+ ...ruleMeasureDependencies(rule.when.left),
701
+ ...ruleMeasureDependencies(rule.when.right),
702
+ ]),
703
+ ]);
704
+ calculationDependenciesByInstanceId.set(instance.id, calculationDependencies);
705
+ evaluationDependenciesByInstanceId.set(instance.id, evaluationDependencies);
706
+
707
+ const selectedMeasures = calculationDependencies.map((id) => measures.get(id)!).filter(Boolean);
708
+ const selectedDerivations = definition.derivedMeasures.filter((derivation) => calculationDependencies.includes(derivation.outputMeasureId));
709
+ const countPopulationIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.countPopulationId === null ? [] : [measure.countPopulationId]));
710
+ const exposureBasisIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.exposureBasisId === null ? [] : [measure.exposureBasisId]));
711
+ const amountBasisIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.basisId === null ? [] : [measure.basisId]));
712
+ const scope: NormalizedDiagnosticCalculationScope = {
713
+ formulaFingerprint: formulaFingerprints[instance.formulaId]!,
714
+ instance: {
715
+ id: instance.id,
716
+ version: instance.version,
717
+ formulaId: instance.formulaId,
718
+ bindings: instance.bindings,
719
+ },
720
+ lossRowGrain: definition.lossRowGrain,
721
+ measures: selectedMeasures.map((measure) => ({
722
+ id: measure.id,
723
+ source: measure.source,
724
+ kind: measure.kind,
725
+ unit: measure.unit,
726
+ developmentSemantics: measure.developmentSemantics,
727
+ aggregation: "sum",
728
+ missing: measure.missing,
729
+ basisId: measure.basisId,
730
+ countPopulationId: measure.countPopulationId,
731
+ exposureBasisId: measure.exposureBasisId,
732
+ exposureTiming: measure.exposureTiming,
733
+ })),
734
+ countPopulations: countPopulationIds.map((id) => {
735
+ const population = populations.get(id)!;
736
+ return { id, subject: population.subject, unit: population.unit, attributes: population.attributes };
737
+ }),
738
+ exposureBases: exposureBasisIds.map((id) => {
739
+ const basis = exposureBases.get(id)!;
740
+ return { id, basis: basis.basis, unit: basis.unit, attributes: basis.attributes };
741
+ }),
742
+ amountBases: amountBasisIds.map((id) => {
743
+ const basis = amountBases.get(id)!;
744
+ return {
745
+ id,
746
+ currency: basis.currency,
747
+ perspective: basis.perspective,
748
+ components: basis.components,
749
+ attributes: basis.attributes,
750
+ };
751
+ }),
752
+ derivedMeasures: selectedDerivations,
753
+ };
754
+ const frozenScope = deepFreeze(scope);
755
+ calculationScopesByInstanceId.set(instance.id, frozenScope);
756
+ calculationFingerprints[instance.id] = tag("diagnostic-calculation", "calculation", frozenScope);
757
+ }
758
+
759
+ return {
760
+ formulaFingerprints: deepFreeze(formulaFingerprints),
761
+ calculationFingerprints: deepFreeze(sortedRecord(Object.entries(calculationFingerprints))),
762
+ definitionIntegrity: tag("diagnostic-definition", "definition", definition),
763
+ calculationScopesByInstanceId,
764
+ calculationDependenciesByInstanceId,
765
+ evaluationDependenciesByInstanceId,
766
+ };
767
+ }