@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,1267 @@
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
+ DiagnosticsFilter,
28
+ } from "./diagnosticDefinitions.js";
29
+ import type {
30
+ DiagnosticClaimExpression,
31
+ DiagnosticMeasureExpression,
32
+ DiagnosticRoleExpression,
33
+ } from "./diagnosticExpressions.js";
34
+ import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
35
+ import {
36
+ diagnosticJsonPreflight,
37
+ isDiagnosticPlainRecord,
38
+ isDiagnosticToken,
39
+ normalizeDiagnosticNumber,
40
+ } from "./diagnosticRuntime.js";
41
+ import {
42
+ DiagnosticValidationError,
43
+ type DiagnosticValidationIssue,
44
+ } from "./types.js";
45
+
46
+ export type DiagnosticIdentityProjection<T> = T extends DiagnosticSourceLocation
47
+ ? NormalizedDiagnosticSourceLocationIdentity
48
+ : T extends readonly (infer U)[]
49
+ ? readonly DiagnosticIdentityProjection<U>[]
50
+ : T extends object
51
+ ? { readonly [K in keyof T]: DiagnosticIdentityProjection<T[K]> }
52
+ : T;
53
+
54
+ export interface NormalizedDiagnosticsFilterIdentity
55
+ extends NormalizedDiagnosticReviewFilterIdentity {
56
+ readonly outputGroups: readonly string[] | null;
57
+ readonly instanceIds: readonly string[] | null;
58
+ }
59
+
60
+ /** Validates selector shapes without interpreting periods against a definition. */
61
+ function validateFilterIdentityInput(filter: DiagnosticsFilter): void {
62
+ const issues: DiagnosticValidationIssue[] = [
63
+ ...diagnosticJsonPreflight(filter, "configuration"),
64
+ ];
65
+ if (issues.length) throw new DiagnosticValidationError(issues);
66
+ if (!isDiagnosticPlainRecord(filter))
67
+ throw new DiagnosticValidationError([
68
+ {
69
+ domain: "configuration",
70
+ code: "invalid-type",
71
+ path: "$",
72
+ message: "Filter must be a plain object or null",
73
+ },
74
+ ]);
75
+
76
+ const lists = [
77
+ "sourceGroups", "outputGroups", "origins", "valuations", "instanceIds",
78
+ ];
79
+ const labels = [
80
+ "originFrom", "originThrough", "valuationFrom", "valuationThrough",
81
+ ];
82
+ const bounds = ["minDevelopmentAge", "maxDevelopmentAge"];
83
+ const checkToken = (value: unknown, path: string) => {
84
+ if (!isDiagnosticToken(value))
85
+ issues.push({
86
+ domain: "configuration",
87
+ code: "invalid-string",
88
+ path,
89
+ message: "Filter selectors must use nonempty token strings",
90
+ });
91
+ };
92
+ for (const key of Object.keys(filter).sort()) {
93
+ const value: unknown = filter[key];
94
+ const path = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
95
+ ? `$.${key}`
96
+ : `$[${JSON.stringify(key)}]`;
97
+ if (lists.includes(key)) {
98
+ if (!Array.isArray(value))
99
+ issues.push({
100
+ domain: "configuration",
101
+ code: "invalid-type",
102
+ path,
103
+ message: "Filter selector must be an array",
104
+ });
105
+ else
106
+ value.forEach((item, index) => checkToken(item, `${path}[${index}]`));
107
+ } else if (labels.includes(key)) checkToken(value, path);
108
+ else if (bounds.includes(key)) {
109
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
110
+ issues.push({
111
+ domain: "configuration",
112
+ code: "invalid-number",
113
+ path,
114
+ message: "Development-age bounds must be nonnegative safe integers",
115
+ });
116
+ } else
117
+ issues.push({
118
+ domain: "configuration",
119
+ code: "unknown-key",
120
+ path,
121
+ message: `Unknown filter key ${key}`,
122
+ });
123
+ }
124
+ if (
125
+ typeof filter.minDevelopmentAge === "number" &&
126
+ typeof filter.maxDevelopmentAge === "number" &&
127
+ filter.minDevelopmentAge > filter.maxDevelopmentAge
128
+ )
129
+ issues.push({
130
+ domain: "configuration",
131
+ code: "invalid-configuration",
132
+ path: "$",
133
+ message: "Minimum development age exceeds maximum",
134
+ });
135
+ if (issues.length) throw new DiagnosticValidationError(issues);
136
+ }
137
+
138
+ /** Materializes optional selectors; period aliases must already be canonical. */
139
+ export function normalizeDiagnosticsFilterIdentity(
140
+ filter: DiagnosticsFilter | null,
141
+ ): NormalizedDiagnosticsFilterIdentity | null {
142
+ if (filter === null) return null;
143
+ validateFilterIdentityInput(filter);
144
+ return deepFreeze({
145
+ sourceGroups:
146
+ filter.sourceGroups === undefined
147
+ ? null
148
+ : sortedUnique(filter.sourceGroups),
149
+ outputGroups:
150
+ filter.outputGroups === undefined
151
+ ? null
152
+ : sortedUnique(filter.outputGroups),
153
+ origins: filter.origins === undefined ? null : sortedUnique(filter.origins),
154
+ originFrom: filter.originFrom ?? null,
155
+ originThrough: filter.originThrough ?? null,
156
+ valuations:
157
+ filter.valuations === undefined ? null : sortedUnique(filter.valuations),
158
+ valuationFrom: filter.valuationFrom ?? null,
159
+ valuationThrough: filter.valuationThrough ?? null,
160
+ minDevelopmentAge:
161
+ filter.minDevelopmentAge === undefined
162
+ ? null
163
+ : normalizeDiagnosticNumber(filter.minDevelopmentAge),
164
+ maxDevelopmentAge:
165
+ filter.maxDevelopmentAge === undefined
166
+ ? null
167
+ : normalizeDiagnosticNumber(filter.maxDevelopmentAge),
168
+ instanceIds:
169
+ filter.instanceIds === undefined ? null : sortedUnique(filter.instanceIds),
170
+ });
171
+ }
172
+
173
+ /**
174
+ * Owned identity copy. Source slots are schema-defined; free JSON dimensions
175
+ * must not be interpreted as source evidence just because a key looks similar.
176
+ */
177
+ export function projectDiagnosticIdentity<T>(
178
+ value: T,
179
+ ): DiagnosticIdentityProjection<T> {
180
+ const issues = diagnosticJsonPreflight(value, "input", {
181
+ maxNodes: Number.MAX_SAFE_INTEGER,
182
+ });
183
+ if (issues.length) throw new DiagnosticValidationError(issues);
184
+ const clone = (
185
+ item: unknown,
186
+ sourceSlot = false,
187
+ freeJson = false,
188
+ path = "$",
189
+ ): unknown => {
190
+ if (item === null || typeof item !== "object")
191
+ return typeof item === "number" && Object.is(item, -0) ? 0 : item;
192
+ if (Array.isArray(item))
193
+ return Object.freeze(
194
+ item.map((child, index) =>
195
+ clone(child, sourceSlot, freeJson, `${path}[${index}]`),
196
+ ),
197
+ );
198
+ const record = item as Record<string, unknown>;
199
+ if (sourceSlot && typeof record.artifactId === "string") {
200
+ const sourceIssues = [];
201
+ for (const key of Object.keys(record)) {
202
+ const fieldPath = `${path}.${key}`;
203
+ if (
204
+ ![
205
+ "artifactId",
206
+ "sourceFile",
207
+ "sourceSheet",
208
+ "sourceRow",
209
+ "sourceCell",
210
+ ].includes(key)
211
+ )
212
+ sourceIssues.push({
213
+ domain: "input" as const,
214
+ code: "unknown-key" as const,
215
+ path: fieldPath,
216
+ message: `Unknown source-location key ${key}`,
217
+ });
218
+ else if (key === "sourceRow") {
219
+ if (
220
+ record[key] !== null &&
221
+ !(
222
+ typeof record[key] === "number" &&
223
+ Number.isSafeInteger(record[key]) &&
224
+ record[key] >= 0
225
+ )
226
+ )
227
+ sourceIssues.push({
228
+ domain: "input" as const,
229
+ code: "invalid-number" as const,
230
+ path: fieldPath,
231
+ message: "Source row must be a nonnegative safe integer or null",
232
+ });
233
+ } else if (
234
+ !(key !== "artifactId" && record[key] === null) &&
235
+ !isDiagnosticToken(record[key])
236
+ )
237
+ sourceIssues.push({
238
+ domain: "input" as const,
239
+ code: "invalid-string" as const,
240
+ path: fieldPath,
241
+ message: "Source location must use nonempty token strings",
242
+ });
243
+ }
244
+ if (sourceIssues.length)
245
+ throw new DiagnosticValidationError(sourceIssues);
246
+ return normalizeDiagnosticSourceLocation(
247
+ record as unknown as DiagnosticSourceLocation,
248
+ );
249
+ }
250
+ const result = Object.create(null) as Record<string, unknown>;
251
+ for (const key of Object.keys(record).sort()) {
252
+ const opaque =
253
+ freeJson || key === "groupDimensions" || key === "dimensions";
254
+ result[key] = clone(
255
+ record[key],
256
+ !opaque && (key === "source" || key === "sources"),
257
+ opaque,
258
+ `${path}.${key}`,
259
+ );
260
+ }
261
+ return Object.freeze(result);
262
+ };
263
+ return clone(value) as DiagnosticIdentityProjection<T>;
264
+ }
265
+
266
+ function compareCodeUnits(left: string, right: string): number {
267
+ return left === right ? 0 : left < right ? -1 : 1;
268
+ }
269
+
270
+ function sortedUnique(values: readonly string[]): readonly string[] {
271
+ return [...new Set(values)].sort(compareCodeUnits);
272
+ }
273
+
274
+ function sortedRecord<T>(
275
+ entries: readonly (readonly [string, T])[],
276
+ ): Readonly<Record<string, T>> {
277
+ const result = Object.create(null) as Record<string, T>;
278
+ for (const [key, value] of [...entries].sort(([left], [right]) =>
279
+ compareCodeUnits(left, right),
280
+ )) {
281
+ result[key] = value;
282
+ }
283
+ return result;
284
+ }
285
+
286
+ function normalizeJsonValue<T>(value: T): T {
287
+ if (typeof value === "number" && Object.is(value, -0)) return 0 as T;
288
+ if (Array.isArray(value))
289
+ return value.map((item) => normalizeJsonValue(item)) as T;
290
+ if (value !== null && typeof value === "object") {
291
+ return sortedRecord(
292
+ Object.entries(value as Record<string, unknown>).map(
293
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
294
+ ),
295
+ ) as T;
296
+ }
297
+ return value;
298
+ }
299
+
300
+ function deepFreeze<T>(
301
+ value: T,
302
+ seen = new WeakSet<object>(),
303
+ ): DiagnosticDeepReadonly<T> {
304
+ if (value === null || typeof value !== "object")
305
+ return value as DiagnosticDeepReadonly<T>;
306
+ if (seen.has(value)) return value as DiagnosticDeepReadonly<T>;
307
+ seen.add(value);
308
+ for (const child of Object.values(value as Record<string, unknown>))
309
+ deepFreeze(child, seen);
310
+ return Object.freeze(value) as DiagnosticDeepReadonly<T>;
311
+ }
312
+
313
+ function attributes(
314
+ value: Readonly<Record<string, string | number | boolean | null>> | undefined,
315
+ ): Readonly<Record<string, string | number | boolean | null>> {
316
+ return sortedRecord(
317
+ Object.entries(value ?? {}).map(
318
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
319
+ ),
320
+ );
321
+ }
322
+
323
+ export type NormalizedAmountLimitationIdentity =
324
+ | { readonly kind: "unlimited" }
325
+ | {
326
+ readonly kind: "layer" | "pre-limited";
327
+ readonly attachment: number;
328
+ readonly limit: number | null;
329
+ readonly application:
330
+ | "claim"
331
+ | "occurrence"
332
+ | "policy"
333
+ | "source-defined";
334
+ readonly derivation:
335
+ | { readonly kind: "sdk" }
336
+ | {
337
+ readonly kind: "external";
338
+ readonly actor: "caller" | "source";
339
+ readonly transformationRef: string;
340
+ };
341
+ }
342
+ | { readonly kind: "unknown"; readonly description: string | null };
343
+
344
+ export interface NormalizedDiagnosticToleranceIdentity {
345
+ readonly absolute: number;
346
+ readonly relative: number;
347
+ }
348
+
349
+ export interface NormalizedDiagnosticReviewFilterIdentity {
350
+ readonly sourceGroups: readonly string[] | null;
351
+ readonly origins: readonly string[] | null;
352
+ readonly originFrom: string | null;
353
+ readonly originThrough: string | null;
354
+ readonly valuations: readonly string[] | null;
355
+ readonly valuationFrom: string | null;
356
+ readonly valuationThrough: string | null;
357
+ readonly minDevelopmentAge: number | null;
358
+ readonly maxDevelopmentAge: number | null;
359
+ }
360
+
361
+ export type NormalizedDiagnosticPeriodAxisIdentity =
362
+ | DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "calendar" }>>
363
+ | (Omit<
364
+ DiagnosticDeepReadonly<
365
+ Extract<DiagnosticPeriodAxis, { kind: "ordered" }>
366
+ >,
367
+ "origins" | "valuations"
368
+ > & {
369
+ readonly origins: readonly {
370
+ readonly label: string;
371
+ readonly aliases: readonly string[];
372
+ readonly coordinate: number;
373
+ }[];
374
+ readonly valuations: readonly {
375
+ readonly label: string;
376
+ readonly aliases: readonly string[];
377
+ readonly coordinate: number;
378
+ }[];
379
+ });
380
+
381
+ export interface NormalizedDiagnosticFormulaIdentity {
382
+ readonly id: string;
383
+ readonly version: string;
384
+ readonly roles: Readonly<
385
+ Record<
386
+ string,
387
+ {
388
+ readonly kind: DiagnosticMeasureKind;
389
+ readonly compatibilityGroup: string | null;
390
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics | null;
391
+ }
392
+ >
393
+ >;
394
+ readonly numerator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
395
+ readonly denominator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
396
+ readonly denominatorPolicy: "positive-or-null";
397
+ }
398
+
399
+ interface NormalizedDiagnosticReviewRuleBase {
400
+ readonly id: string;
401
+ readonly code: string;
402
+ readonly description: string;
403
+ readonly severity: "warning" | "fail";
404
+ readonly missingInput: "not-evaluated" | "finding";
405
+ readonly tolerance: NormalizedDiagnosticToleranceIdentity;
406
+ }
407
+
408
+ export type NormalizedDiagnosticReviewRuleIdentity =
409
+ | (NormalizedDiagnosticReviewRuleBase & {
410
+ readonly kind: "compare";
411
+ readonly when: DiagnosticDeepReadonly<DiagnosticReviewPredicate>;
412
+ })
413
+ | (NormalizedDiagnosticReviewRuleBase & {
414
+ readonly kind: "reconcile";
415
+ readonly actual: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
416
+ readonly expected: DiagnosticDeepReadonly<DiagnosticReviewOperand>;
417
+ })
418
+ | (NormalizedDiagnosticReviewRuleBase & {
419
+ readonly kind: "monotonic";
420
+ readonly expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
421
+ readonly direction: "nondecreasing" | "nonincreasing";
422
+ })
423
+ | (NormalizedDiagnosticReviewRuleBase & {
424
+ readonly kind: "layer-order";
425
+ readonly narrower: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
426
+ readonly broader: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
427
+ readonly comparability: DiagnosticDeepReadonly<
428
+ Extract<DiagnosticReviewRule, { kind: "layer-order" }>["comparability"]
429
+ >;
430
+ })
431
+ | (NormalizedDiagnosticReviewRuleBase & {
432
+ readonly kind: "control-total";
433
+ readonly expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>;
434
+ readonly expected: number;
435
+ readonly filter: NormalizedDiagnosticReviewFilterIdentity | null;
436
+ readonly projection: DiagnosticDeepReadonly<DiagnosticControlTotalProjection>;
437
+ });
438
+
439
+ export interface NormalizedDiagnosticDefinitionIdentity {
440
+ readonly diagnosticDefinitionVersion: "1.0.0";
441
+ readonly id: string;
442
+ readonly version: string;
443
+ readonly lossRowGrain: "claim" | "aggregate";
444
+ readonly measures: readonly {
445
+ readonly id: string;
446
+ readonly displayName: string;
447
+ readonly description: string;
448
+ readonly source: DiagnosticMeasureSource;
449
+ readonly kind: DiagnosticMeasureKind;
450
+ readonly unit: string;
451
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics;
452
+ readonly aggregation: "sum";
453
+ readonly missing: DiagnosticMissingPolicy;
454
+ readonly basisId: string | null;
455
+ readonly countPopulationId: string | null;
456
+ readonly exposureBasisId: string | null;
457
+ readonly exposureTiming: DiagnosticExposureTiming | null;
458
+ }[];
459
+ readonly countPopulations: readonly {
460
+ readonly id: string;
461
+ readonly displayName: string;
462
+ readonly subject: DiagnosticCountPopulationDefinition["subject"];
463
+ readonly unit: string;
464
+ readonly description: string;
465
+ readonly attributes: Readonly<
466
+ Record<string, string | number | boolean | null>
467
+ >;
468
+ }[];
469
+ readonly exposureBases: readonly {
470
+ readonly id: string;
471
+ readonly displayName: string;
472
+ readonly basis: DiagnosticExposureBasisDefinition["basis"];
473
+ readonly unit: string;
474
+ readonly description: string;
475
+ readonly sourceDescription: string | null;
476
+ readonly attributes: Readonly<
477
+ Record<string, string | number | boolean | null>
478
+ >;
479
+ }[];
480
+ readonly amountBases: readonly {
481
+ readonly id: string;
482
+ readonly displayName: string;
483
+ readonly currency: string;
484
+ readonly perspective: AmountPerspective;
485
+ readonly components: readonly {
486
+ readonly id: string;
487
+ readonly treatment: "included" | "excluded" | "unknown";
488
+ readonly limitation: NormalizedAmountLimitationIdentity;
489
+ }[];
490
+ readonly sourceDescription: string | null;
491
+ readonly attributes: Readonly<
492
+ Record<string, string | number | boolean | null>
493
+ >;
494
+ }[];
495
+ readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
496
+ readonly formulas: readonly NormalizedDiagnosticFormulaIdentity[];
497
+ readonly instances: readonly {
498
+ readonly id: string;
499
+ readonly version: string;
500
+ readonly formulaId: string;
501
+ readonly bindings: Readonly<
502
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
503
+ >;
504
+ readonly presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>;
505
+ readonly rules: readonly {
506
+ readonly id: string;
507
+ readonly code: string;
508
+ readonly message: string;
509
+ readonly severity: "warning" | "fail";
510
+ readonly when: {
511
+ readonly left: DiagnosticDeepReadonly<DiagnosticRuleOperand>;
512
+ readonly operator: DiagnosticComparisonPredicate["operator"];
513
+ readonly right: DiagnosticDeepReadonly<DiagnosticRuleOperand>;
514
+ readonly tolerance: NormalizedDiagnosticToleranceIdentity;
515
+ };
516
+ }[];
517
+ }[];
518
+ readonly reviewRules: readonly NormalizedDiagnosticReviewRuleIdentity[];
519
+ readonly periodAxis: NormalizedDiagnosticPeriodAxisIdentity;
520
+ }
521
+
522
+ export interface NormalizedDiagnosticCalculationScope {
523
+ readonly formulaFingerprint: string;
524
+ readonly instance: {
525
+ readonly id: string;
526
+ readonly version: string;
527
+ readonly formulaId: string;
528
+ readonly bindings: Readonly<
529
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
530
+ >;
531
+ };
532
+ readonly lossRowGrain: "claim" | "aggregate";
533
+ readonly measures: readonly {
534
+ readonly id: string;
535
+ readonly source: DiagnosticMeasureSource;
536
+ readonly kind: DiagnosticMeasureKind;
537
+ readonly unit: string;
538
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics;
539
+ readonly aggregation: "sum";
540
+ readonly missing: DiagnosticMissingPolicy;
541
+ readonly basisId: string | null;
542
+ readonly countPopulationId: string | null;
543
+ readonly exposureBasisId: string | null;
544
+ readonly exposureTiming: DiagnosticExposureTiming | null;
545
+ }[];
546
+ readonly countPopulations: readonly {
547
+ readonly id: string;
548
+ readonly subject: DiagnosticCountPopulationDefinition["subject"];
549
+ readonly unit: string;
550
+ readonly attributes: Readonly<
551
+ Record<string, string | number | boolean | null>
552
+ >;
553
+ }[];
554
+ readonly exposureBases: readonly {
555
+ readonly id: string;
556
+ readonly basis: DiagnosticExposureBasisDefinition["basis"];
557
+ readonly unit: string;
558
+ readonly attributes: Readonly<
559
+ Record<string, string | number | boolean | null>
560
+ >;
561
+ }[];
562
+ readonly amountBases: readonly {
563
+ readonly id: string;
564
+ readonly currency: string;
565
+ readonly perspective: AmountPerspective;
566
+ readonly components: readonly {
567
+ readonly id: string;
568
+ readonly treatment: "included" | "excluded" | "unknown";
569
+ readonly limitation: NormalizedAmountLimitationIdentity;
570
+ }[];
571
+ readonly attributes: Readonly<
572
+ Record<string, string | number | boolean | null>
573
+ >;
574
+ }[];
575
+ readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
576
+ }
577
+
578
+ export interface NormalizedDiagnosticSourceLocationIdentity {
579
+ readonly artifactId: string;
580
+ readonly sourceFile: string | null;
581
+ readonly sourceSheet: string | null;
582
+ readonly sourceRow: number | null;
583
+ readonly sourceCell: string | null;
584
+ }
585
+
586
+ function normalizeLimitation(
587
+ limitation: AmountLimitation,
588
+ ): NormalizedAmountLimitationIdentity {
589
+ if (limitation.kind === "unlimited") return { kind: "unlimited" };
590
+ if (limitation.kind === "unknown") {
591
+ return { kind: "unknown", description: limitation.description ?? null };
592
+ }
593
+ return {
594
+ kind: limitation.kind,
595
+ attachment: Object.is(limitation.attachment, -0)
596
+ ? 0
597
+ : limitation.attachment,
598
+ limit:
599
+ limitation.limit !== null && Object.is(limitation.limit, -0)
600
+ ? 0
601
+ : limitation.limit,
602
+ application: limitation.application,
603
+ derivation:
604
+ limitation.derivation.kind === "sdk"
605
+ ? { kind: "sdk" }
606
+ : {
607
+ kind: "external",
608
+ actor: limitation.derivation.actor,
609
+ transformationRef: limitation.derivation.transformationRef,
610
+ },
611
+ };
612
+ }
613
+
614
+ function normalizeMeasureExpression<
615
+ T extends DiagnosticMeasureExpression | DiagnosticClaimExpression,
616
+ >(expression: T): DiagnosticDeepReadonly<T> {
617
+ if (expression.op === "measure")
618
+ return {
619
+ op: "measure",
620
+ measureId: expression.measureId,
621
+ } as DiagnosticDeepReadonly<T>;
622
+ if (expression.op === "claim-layer") {
623
+ return {
624
+ op: "claim-layer",
625
+ measureId: expression.measureId,
626
+ attachment: Object.is(expression.attachment, -0)
627
+ ? 0
628
+ : expression.attachment,
629
+ limit:
630
+ expression.limit !== null && Object.is(expression.limit, -0)
631
+ ? 0
632
+ : expression.limit,
633
+ } as DiagnosticDeepReadonly<T>;
634
+ }
635
+ if (expression.op === "subtract") {
636
+ return {
637
+ op: "subtract",
638
+ left: normalizeMeasureExpression(expression.left),
639
+ right: normalizeMeasureExpression(expression.right),
640
+ } as DiagnosticDeepReadonly<T>;
641
+ }
642
+ return {
643
+ op: "add",
644
+ terms: expression.terms.map((term) => normalizeMeasureExpression(term)),
645
+ } as unknown as DiagnosticDeepReadonly<T>;
646
+ }
647
+
648
+ function normalizeRoleExpression(
649
+ expression: DiagnosticRoleExpression,
650
+ ): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
651
+ if (expression.op === "role") return { op: "role", role: expression.role };
652
+ if (expression.op === "subtract") {
653
+ return {
654
+ op: "subtract",
655
+ left: normalizeRoleExpression(expression.left),
656
+ right: normalizeRoleExpression(expression.right),
657
+ };
658
+ }
659
+ return { op: "add", terms: expression.terms.map(normalizeRoleExpression) };
660
+ }
661
+
662
+ function normalizeTolerance(
663
+ tolerance: { absolute?: number; relative?: number } | undefined,
664
+ ): NormalizedDiagnosticToleranceIdentity {
665
+ return {
666
+ absolute: Object.is(tolerance?.absolute ?? 0, -0)
667
+ ? 0
668
+ : (tolerance?.absolute ?? 0),
669
+ relative: Object.is(tolerance?.relative ?? 0, -0)
670
+ ? 0
671
+ : (tolerance?.relative ?? 0),
672
+ };
673
+ }
674
+
675
+ function normalizeRuleOperand(
676
+ operand: DiagnosticRuleOperand,
677
+ ): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
678
+ if (operand.source === "measure") {
679
+ return {
680
+ source: "measure",
681
+ expression: normalizeMeasureExpression(operand.expression),
682
+ };
683
+ }
684
+ if (operand.source === "calculation")
685
+ return { source: "calculation", field: operand.field };
686
+ return {
687
+ source: "constant",
688
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
689
+ };
690
+ }
691
+
692
+ function normalizeReviewOperand(
693
+ operand: DiagnosticReviewOperand,
694
+ ): DiagnosticDeepReadonly<DiagnosticReviewOperand> {
695
+ if (operand.op === "constant")
696
+ return {
697
+ op: "constant",
698
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
699
+ };
700
+ return normalizeMeasureExpression(operand);
701
+ }
702
+
703
+ function normalizeReviewFilter(
704
+ filter: DiagnosticReviewFilter,
705
+ axis: DiagnosticPeriodAxis,
706
+ ): NormalizedDiagnosticReviewFilterIdentity {
707
+ const normalizeMany = (
708
+ side: "origin" | "valuation",
709
+ values: readonly string[] | undefined,
710
+ ) =>
711
+ values === undefined
712
+ ? null
713
+ : sortedUnique(
714
+ values.map(
715
+ (value) =>
716
+ normalizeDiagnosticPeriodWithAxis(axis, side, value)!.label,
717
+ ),
718
+ );
719
+ const normalizeOne = (
720
+ side: "origin" | "valuation",
721
+ value: string | undefined,
722
+ ) =>
723
+ value === undefined
724
+ ? null
725
+ : normalizeDiagnosticPeriodWithAxis(axis, side, value)!.label;
726
+ return {
727
+ sourceGroups:
728
+ filter.sourceGroups === undefined
729
+ ? null
730
+ : sortedUnique(filter.sourceGroups),
731
+ origins: normalizeMany("origin", filter.origins),
732
+ originFrom: normalizeOne("origin", filter.originFrom),
733
+ originThrough: normalizeOne("origin", filter.originThrough),
734
+ valuations: normalizeMany("valuation", filter.valuations),
735
+ valuationFrom: normalizeOne("valuation", filter.valuationFrom),
736
+ valuationThrough: normalizeOne("valuation", filter.valuationThrough),
737
+ minDevelopmentAge:
738
+ filter.minDevelopmentAge === undefined
739
+ ? null
740
+ : Object.is(filter.minDevelopmentAge, -0)
741
+ ? 0
742
+ : filter.minDevelopmentAge,
743
+ maxDevelopmentAge:
744
+ filter.maxDevelopmentAge === undefined
745
+ ? null
746
+ : Object.is(filter.maxDevelopmentAge, -0)
747
+ ? 0
748
+ : filter.maxDevelopmentAge,
749
+ };
750
+ }
751
+
752
+ function normalizePeriodAxis(
753
+ axis: DiagnosticPeriodAxis,
754
+ ): NormalizedDiagnosticPeriodAxisIdentity {
755
+ if (axis.kind === "calendar") {
756
+ return {
757
+ kind: "calendar",
758
+ originCadence: axis.originCadence,
759
+ valuationCadence: axis.valuationCadence,
760
+ originAnchor: axis.originAnchor,
761
+ valuationAnchor: axis.valuationAnchor,
762
+ ageUnit: "month",
763
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
764
+ };
765
+ }
766
+ const coordinates = (values: typeof axis.origins) =>
767
+ values
768
+ .map((coordinate) => ({
769
+ label: coordinate.label,
770
+ aliases: sortedUnique(coordinate.aliases ?? []),
771
+ coordinate: Object.is(coordinate.coordinate, -0)
772
+ ? 0
773
+ : coordinate.coordinate,
774
+ }))
775
+ .sort(
776
+ (left, right) =>
777
+ left.coordinate - right.coordinate ||
778
+ compareCodeUnits(left.label, right.label),
779
+ );
780
+ return {
781
+ kind: "ordered",
782
+ id: axis.id,
783
+ version: axis.version,
784
+ ageUnit: axis.ageUnit,
785
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
786
+ origins: coordinates(axis.origins),
787
+ valuations: coordinates(axis.valuations),
788
+ };
789
+ }
790
+
791
+ function topologicalDerivations(
792
+ derivations: readonly DiagnosticDerivedMeasureDefinition[],
793
+ ): readonly DiagnosticDerivedMeasureDefinition[] {
794
+ const byOutput = new Map(
795
+ derivations.map((derivation) => [derivation.outputMeasureId, derivation]),
796
+ );
797
+ const remaining = new Map(
798
+ derivations.map((derivation) => [derivation.id, derivation]),
799
+ );
800
+ const emittedOutputs = new Set<string>();
801
+ const ordered: DiagnosticDerivedMeasureDefinition[] = [];
802
+ while (remaining.size > 0) {
803
+ const ready = [...remaining.values()]
804
+ .filter((derivation) =>
805
+ expressionDependencies(derivation.expression)
806
+ .filter((id) => byOutput.has(id))
807
+ .every((id) => emittedOutputs.has(id)),
808
+ )
809
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
810
+ if (ready.length === 0)
811
+ return [...derivations].sort((left, right) =>
812
+ compareCodeUnits(left.id, right.id),
813
+ );
814
+ for (const derivation of ready) {
815
+ ordered.push(derivation);
816
+ emittedOutputs.add(derivation.outputMeasureId);
817
+ remaining.delete(derivation.id);
818
+ }
819
+ }
820
+ return ordered;
821
+ }
822
+
823
+ function normalizeReviewRule(
824
+ rule: DiagnosticReviewRule,
825
+ axis: DiagnosticPeriodAxis,
826
+ ): NormalizedDiagnosticReviewRuleIdentity {
827
+ const base: NormalizedDiagnosticReviewRuleBase = {
828
+ id: rule.id,
829
+ code: rule.code,
830
+ description: rule.description,
831
+ severity: rule.severity,
832
+ missingInput: rule.missingInput,
833
+ tolerance: normalizeTolerance(rule.tolerance),
834
+ };
835
+ if (rule.kind === "compare") {
836
+ return {
837
+ ...base,
838
+ kind: "compare",
839
+ when: {
840
+ left: normalizeReviewOperand(rule.when.left),
841
+ operator: rule.when.operator,
842
+ right: normalizeReviewOperand(rule.when.right),
843
+ },
844
+ };
845
+ }
846
+ if (rule.kind === "reconcile") {
847
+ return {
848
+ ...base,
849
+ kind: "reconcile",
850
+ actual: normalizeMeasureExpression(rule.actual),
851
+ expected: normalizeReviewOperand(rule.expected),
852
+ };
853
+ }
854
+ if (rule.kind === "monotonic") {
855
+ return {
856
+ ...base,
857
+ kind: "monotonic",
858
+ expression: normalizeMeasureExpression(rule.expression),
859
+ direction: rule.direction,
860
+ };
861
+ }
862
+ if (rule.kind === "layer-order") {
863
+ return {
864
+ ...base,
865
+ kind: "layer-order",
866
+ narrower: normalizeMeasureExpression(rule.narrower),
867
+ broader: normalizeMeasureExpression(rule.broader),
868
+ comparability: normalizeJsonValue(rule.comparability),
869
+ };
870
+ }
871
+ return {
872
+ ...base,
873
+ kind: "control-total",
874
+ expression: normalizeMeasureExpression(rule.expression),
875
+ expected: Object.is(rule.expected, -0) ? 0 : rule.expected,
876
+ filter:
877
+ rule.filter === undefined
878
+ ? null
879
+ : normalizeReviewFilter(rule.filter, axis),
880
+ projection:
881
+ rule.projection.kind === "valuation"
882
+ ? {
883
+ kind: "valuation",
884
+ valuation: normalizeDiagnosticPeriodWithAxis(
885
+ axis,
886
+ "valuation",
887
+ rule.projection.valuation,
888
+ )!.label,
889
+ }
890
+ : normalizeJsonValue(rule.projection),
891
+ };
892
+ }
893
+
894
+ export function normalizeDiagnosticDefinition(
895
+ definition: DiagnosticDefinition,
896
+ ): DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity> {
897
+ const normalized: NormalizedDiagnosticDefinitionIdentity = {
898
+ diagnosticDefinitionVersion: "1.0.0",
899
+ id: definition.id,
900
+ version: definition.version,
901
+ lossRowGrain: definition.lossRowGrain,
902
+ measures: definition.measures
903
+ .map((measure) => ({
904
+ id: measure.id,
905
+ displayName: measure.displayName,
906
+ description: measure.description,
907
+ source: measure.source,
908
+ kind: measure.kind,
909
+ unit: measure.unit,
910
+ developmentSemantics: measure.developmentSemantics,
911
+ aggregation: "sum" as const,
912
+ missing: measure.missing,
913
+ basisId: measure.basisId ?? null,
914
+ countPopulationId: measure.countPopulationId ?? null,
915
+ exposureBasisId: measure.exposureBasisId ?? null,
916
+ exposureTiming: measure.exposureTiming ?? null,
917
+ }))
918
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
919
+ countPopulations: definition.countPopulations
920
+ .map((population) => ({
921
+ id: population.id,
922
+ displayName: population.displayName,
923
+ subject: population.subject,
924
+ unit: population.unit,
925
+ description: population.description,
926
+ attributes: attributes(population.attributes),
927
+ }))
928
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
929
+ exposureBases: definition.exposureBases
930
+ .map((basis) => ({
931
+ id: basis.id,
932
+ displayName: basis.displayName,
933
+ basis: basis.basis,
934
+ unit: basis.unit,
935
+ description: basis.description,
936
+ sourceDescription: basis.sourceDescription ?? null,
937
+ attributes: attributes(basis.attributes),
938
+ }))
939
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
940
+ amountBases: definition.amountBases
941
+ .map((basis) => ({
942
+ id: basis.id,
943
+ displayName: basis.displayName,
944
+ currency: basis.currency,
945
+ perspective: basis.perspective,
946
+ components: basis.components
947
+ .map((component) => ({
948
+ id: component.id,
949
+ treatment: component.treatment,
950
+ limitation: normalizeLimitation(component.limitation),
951
+ }))
952
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
953
+ sourceDescription: basis.sourceDescription ?? null,
954
+ attributes: attributes(basis.attributes),
955
+ }))
956
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
957
+ derivedMeasures: topologicalDerivations(definition.derivedMeasures).map(
958
+ (derivation) => ({
959
+ id: derivation.id,
960
+ outputMeasureId: derivation.outputMeasureId,
961
+ expression: normalizeMeasureExpression(derivation.expression),
962
+ }),
963
+ ),
964
+ formulas: definition.formulas
965
+ .map((formula) => ({
966
+ id: formula.id,
967
+ version: formula.version,
968
+ roles: sortedRecord(
969
+ Object.entries(formula.roles).map(
970
+ ([name, role]) =>
971
+ [
972
+ name,
973
+ {
974
+ kind: role.kind,
975
+ compatibilityGroup: role.compatibilityGroup ?? null,
976
+ developmentSemantics: role.developmentSemantics ?? null,
977
+ },
978
+ ] as const,
979
+ ),
980
+ ),
981
+ numerator: normalizeRoleExpression(formula.numerator),
982
+ denominator: normalizeRoleExpression(formula.denominator),
983
+ denominatorPolicy: "positive-or-null" as const,
984
+ }))
985
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
986
+ instances: definition.instances.map((instance) => ({
987
+ id: instance.id,
988
+ version: instance.version,
989
+ formulaId: instance.formulaId,
990
+ bindings: sortedRecord(
991
+ Object.entries(instance.bindings).map(
992
+ ([role, expression]) =>
993
+ [role, normalizeMeasureExpression(expression)] as const,
994
+ ),
995
+ ),
996
+ presentation: {
997
+ displayName: instance.presentation.displayName,
998
+ description: instance.presentation.description,
999
+ displayUnit: instance.presentation.displayUnit,
1000
+ scale: Object.is(instance.presentation.scale, -0)
1001
+ ? 0
1002
+ : instance.presentation.scale,
1003
+ numeratorLabel: instance.presentation.numeratorLabel,
1004
+ denominatorLabel: instance.presentation.denominatorLabel,
1005
+ },
1006
+ rules: instance.rules.map((rule) => ({
1007
+ id: rule.id,
1008
+ code: rule.code,
1009
+ message: rule.message,
1010
+ severity: rule.severity,
1011
+ when: {
1012
+ left: normalizeRuleOperand(rule.when.left),
1013
+ operator: rule.when.operator,
1014
+ right: normalizeRuleOperand(rule.when.right),
1015
+ tolerance: normalizeTolerance(rule.when.tolerance),
1016
+ },
1017
+ })),
1018
+ })),
1019
+ reviewRules: definition.reviewRules.map((rule) =>
1020
+ normalizeReviewRule(rule, definition.periodAxis),
1021
+ ),
1022
+ periodAxis: normalizePeriodAxis(definition.periodAxis),
1023
+ };
1024
+ return deepFreeze(normalized);
1025
+ }
1026
+
1027
+ export function normalizeDiagnosticSourceLocation(
1028
+ source: DiagnosticSourceLocation,
1029
+ ): NormalizedDiagnosticSourceLocationIdentity {
1030
+ return deepFreeze({
1031
+ artifactId: source.artifactId,
1032
+ sourceFile: source.sourceFile ?? null,
1033
+ sourceSheet: source.sourceSheet ?? null,
1034
+ sourceRow:
1035
+ source.sourceRow === undefined
1036
+ ? null
1037
+ : Object.is(source.sourceRow, -0)
1038
+ ? 0
1039
+ : source.sourceRow,
1040
+ sourceCell: source.sourceCell ?? null,
1041
+ });
1042
+ }
1043
+
1044
+ function expressionDependencies(
1045
+ expression: DiagnosticMeasureExpression | DiagnosticClaimExpression,
1046
+ ): readonly string[] {
1047
+ if (expression.op === "measure" || expression.op === "claim-layer")
1048
+ return [expression.measureId];
1049
+ if (expression.op === "subtract")
1050
+ return sortedUnique([
1051
+ ...expressionDependencies(expression.left),
1052
+ ...expressionDependencies(expression.right),
1053
+ ]);
1054
+ return sortedUnique(expression.terms.flatMap(expressionDependencies));
1055
+ }
1056
+
1057
+ function ruleMeasureDependencies(
1058
+ operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>,
1059
+ ): readonly string[] {
1060
+ return operand.source === "measure"
1061
+ ? expressionDependencies(operand.expression)
1062
+ : [];
1063
+ }
1064
+
1065
+ function tag<K extends string, T>(kind: K, property: string, value: T): string {
1066
+ return `fnv1a64-jcs-v1:${fnv1a64(
1067
+ canonicalJson({
1068
+ identityVersion: 1,
1069
+ kind,
1070
+ [property]: value,
1071
+ }),
1072
+ )}`;
1073
+ }
1074
+
1075
+ export interface BuiltDiagnosticIdentities {
1076
+ readonly formulaFingerprints: Readonly<Record<string, string>>;
1077
+ readonly calculationFingerprints: Readonly<Record<string, string>>;
1078
+ readonly definitionIntegrity: string;
1079
+ readonly calculationScopesByInstanceId: ReadonlyMap<
1080
+ string,
1081
+ NormalizedDiagnosticCalculationScope
1082
+ >;
1083
+ readonly calculationDependenciesByInstanceId: ReadonlyMap<
1084
+ string,
1085
+ readonly string[]
1086
+ >;
1087
+ readonly evaluationDependenciesByInstanceId: ReadonlyMap<
1088
+ string,
1089
+ readonly string[]
1090
+ >;
1091
+ }
1092
+
1093
+ export function buildDiagnosticIdentities(
1094
+ definition: DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity>,
1095
+ ): BuiltDiagnosticIdentities {
1096
+ const formulas = new Map(
1097
+ definition.formulas.map((formula) => [formula.id, formula]),
1098
+ );
1099
+ const measures = new Map(
1100
+ definition.measures.map((measure) => [measure.id, measure]),
1101
+ );
1102
+ const derivations = new Map(
1103
+ definition.derivedMeasures.map((derivation) => [
1104
+ derivation.outputMeasureId,
1105
+ derivation,
1106
+ ]),
1107
+ );
1108
+ const populations = new Map(
1109
+ definition.countPopulations.map((population) => [
1110
+ population.id,
1111
+ population,
1112
+ ]),
1113
+ );
1114
+ const exposureBases = new Map(
1115
+ definition.exposureBases.map((basis) => [basis.id, basis]),
1116
+ );
1117
+ const amountBases = new Map(
1118
+ definition.amountBases.map((basis) => [basis.id, basis]),
1119
+ );
1120
+
1121
+ const formulaFingerprints = sortedRecord(
1122
+ definition.formulas.map(
1123
+ (formula) =>
1124
+ [formula.id, tag("diagnostic-formula", "formula", formula)] as const,
1125
+ ),
1126
+ );
1127
+ const calculationFingerprints = Object.create(null) as Record<string, string>;
1128
+ const calculationScopesByInstanceId = new Map<
1129
+ string,
1130
+ NormalizedDiagnosticCalculationScope
1131
+ >();
1132
+ const calculationDependenciesByInstanceId = new Map<
1133
+ string,
1134
+ readonly string[]
1135
+ >();
1136
+ const evaluationDependenciesByInstanceId = new Map<
1137
+ string,
1138
+ readonly string[]
1139
+ >();
1140
+
1141
+ const transitiveDependencies = (
1142
+ roots: readonly string[],
1143
+ ): readonly string[] => {
1144
+ const found = new Set<string>();
1145
+ const stack = [...roots];
1146
+ while (stack.length > 0) {
1147
+ const id = stack.pop()!;
1148
+ if (found.has(id)) continue;
1149
+ found.add(id);
1150
+ const derivation = derivations.get(id);
1151
+ if (derivation)
1152
+ stack.push(...expressionDependencies(derivation.expression));
1153
+ }
1154
+ return [...found].sort(compareCodeUnits);
1155
+ };
1156
+
1157
+ for (const instance of definition.instances) {
1158
+ const calculationDependencies = transitiveDependencies(
1159
+ Object.values(instance.bindings).flatMap(expressionDependencies),
1160
+ );
1161
+ const evaluationDependencies = transitiveDependencies([
1162
+ ...calculationDependencies,
1163
+ ...instance.rules.flatMap((rule) => [
1164
+ ...ruleMeasureDependencies(rule.when.left),
1165
+ ...ruleMeasureDependencies(rule.when.right),
1166
+ ]),
1167
+ ]);
1168
+ calculationDependenciesByInstanceId.set(
1169
+ instance.id,
1170
+ calculationDependencies,
1171
+ );
1172
+ evaluationDependenciesByInstanceId.set(instance.id, evaluationDependencies);
1173
+
1174
+ const selectedMeasures = calculationDependencies
1175
+ .map((id) => measures.get(id)!)
1176
+ .filter(Boolean);
1177
+ const selectedDerivations = definition.derivedMeasures.filter(
1178
+ (derivation) =>
1179
+ calculationDependencies.includes(derivation.outputMeasureId),
1180
+ );
1181
+ const countPopulationIds = sortedUnique(
1182
+ selectedMeasures.flatMap((measure) =>
1183
+ measure.countPopulationId === null ? [] : [measure.countPopulationId],
1184
+ ),
1185
+ );
1186
+ const exposureBasisIds = sortedUnique(
1187
+ selectedMeasures.flatMap((measure) =>
1188
+ measure.exposureBasisId === null ? [] : [measure.exposureBasisId],
1189
+ ),
1190
+ );
1191
+ const amountBasisIds = sortedUnique(
1192
+ selectedMeasures.flatMap((measure) =>
1193
+ measure.basisId === null ? [] : [measure.basisId],
1194
+ ),
1195
+ );
1196
+ const scope: NormalizedDiagnosticCalculationScope = {
1197
+ formulaFingerprint: formulaFingerprints[instance.formulaId]!,
1198
+ instance: {
1199
+ id: instance.id,
1200
+ version: instance.version,
1201
+ formulaId: instance.formulaId,
1202
+ bindings: instance.bindings,
1203
+ },
1204
+ lossRowGrain: definition.lossRowGrain,
1205
+ measures: selectedMeasures.map((measure) => ({
1206
+ id: measure.id,
1207
+ source: measure.source,
1208
+ kind: measure.kind,
1209
+ unit: measure.unit,
1210
+ developmentSemantics: measure.developmentSemantics,
1211
+ aggregation: "sum",
1212
+ missing: measure.missing,
1213
+ basisId: measure.basisId,
1214
+ countPopulationId: measure.countPopulationId,
1215
+ exposureBasisId: measure.exposureBasisId,
1216
+ exposureTiming: measure.exposureTiming,
1217
+ })),
1218
+ countPopulations: countPopulationIds.map((id) => {
1219
+ const population = populations.get(id)!;
1220
+ return {
1221
+ id,
1222
+ subject: population.subject,
1223
+ unit: population.unit,
1224
+ attributes: population.attributes,
1225
+ };
1226
+ }),
1227
+ exposureBases: exposureBasisIds.map((id) => {
1228
+ const basis = exposureBases.get(id)!;
1229
+ return {
1230
+ id,
1231
+ basis: basis.basis,
1232
+ unit: basis.unit,
1233
+ attributes: basis.attributes,
1234
+ };
1235
+ }),
1236
+ amountBases: amountBasisIds.map((id) => {
1237
+ const basis = amountBases.get(id)!;
1238
+ return {
1239
+ id,
1240
+ currency: basis.currency,
1241
+ perspective: basis.perspective,
1242
+ components: basis.components,
1243
+ attributes: basis.attributes,
1244
+ };
1245
+ }),
1246
+ derivedMeasures: selectedDerivations,
1247
+ };
1248
+ const frozenScope = deepFreeze(scope);
1249
+ calculationScopesByInstanceId.set(instance.id, frozenScope);
1250
+ calculationFingerprints[instance.id] = tag(
1251
+ "diagnostic-calculation",
1252
+ "calculation",
1253
+ frozenScope,
1254
+ );
1255
+ }
1256
+
1257
+ return {
1258
+ formulaFingerprints: deepFreeze(formulaFingerprints),
1259
+ calculationFingerprints: deepFreeze(
1260
+ sortedRecord(Object.entries(calculationFingerprints)),
1261
+ ),
1262
+ definitionIntegrity: tag("diagnostic-definition", "definition", definition),
1263
+ calculationScopesByInstanceId,
1264
+ calculationDependenciesByInstanceId,
1265
+ evaluationDependenciesByInstanceId,
1266
+ };
1267
+ }