@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
@@ -24,12 +24,244 @@ import type {
24
24
  DiagnosticReviewRule,
25
25
  DiagnosticRuleOperand,
26
26
  DiagnosticSourceLocation,
27
+ DiagnosticsFilter,
27
28
  } from "./diagnosticDefinitions.js";
28
29
  import type {
29
30
  DiagnosticClaimExpression,
30
31
  DiagnosticMeasureExpression,
31
32
  DiagnosticRoleExpression,
32
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
+ }
33
265
 
34
266
  function compareCodeUnits(left: string, right: string): number {
35
267
  return left === right ? 0 : left < right ? -1 : 1;
@@ -39,9 +271,13 @@ function sortedUnique(values: readonly string[]): readonly string[] {
39
271
  return [...new Set(values)].sort(compareCodeUnits);
40
272
  }
41
273
 
42
- function sortedRecord<T>(entries: readonly (readonly [string, T])[]): Readonly<Record<string, T>> {
274
+ function sortedRecord<T>(
275
+ entries: readonly (readonly [string, T])[],
276
+ ): Readonly<Record<string, T>> {
43
277
  const result = Object.create(null) as Record<string, T>;
44
- for (const [key, value] of [...entries].sort(([left], [right]) => compareCodeUnits(left, right))) {
278
+ for (const [key, value] of [...entries].sort(([left], [right]) =>
279
+ compareCodeUnits(left, right),
280
+ )) {
45
281
  result[key] = value;
46
282
  }
47
283
  return result;
@@ -49,29 +285,39 @@ function sortedRecord<T>(entries: readonly (readonly [string, T])[]): Readonly<R
49
285
 
50
286
  function normalizeJsonValue<T>(value: T): T {
51
287
  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;
288
+ if (Array.isArray(value))
289
+ return value.map((item) => normalizeJsonValue(item)) as T;
53
290
  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;
291
+ return sortedRecord(
292
+ Object.entries(value as Record<string, unknown>).map(
293
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
294
+ ),
295
+ ) as T;
57
296
  }
58
297
  return value;
59
298
  }
60
299
 
61
- function deepFreeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
62
- if (value === null || typeof value !== "object") return value as DiagnosticDeepReadonly<T>;
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>;
63
306
  if (seen.has(value)) return value as DiagnosticDeepReadonly<T>;
64
307
  seen.add(value);
65
- for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child, seen);
308
+ for (const child of Object.values(value as Record<string, unknown>))
309
+ deepFreeze(child, seen);
66
310
  return Object.freeze(value) as DiagnosticDeepReadonly<T>;
67
311
  }
68
312
 
69
313
  function attributes(
70
314
  value: Readonly<Record<string, string | number | boolean | null>> | undefined,
71
315
  ): Readonly<Record<string, string | number | boolean | null>> {
72
- return sortedRecord(Object.entries(value ?? {}).map(([key, item]) =>
73
- [key, normalizeJsonValue(item)] as const,
74
- ));
316
+ return sortedRecord(
317
+ Object.entries(value ?? {}).map(
318
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
319
+ ),
320
+ );
75
321
  }
76
322
 
77
323
  export type NormalizedAmountLimitationIdentity =
@@ -80,7 +326,11 @@ export type NormalizedAmountLimitationIdentity =
80
326
  readonly kind: "layer" | "pre-limited";
81
327
  readonly attachment: number;
82
328
  readonly limit: number | null;
83
- readonly application: "claim" | "occurrence" | "policy" | "source-defined";
329
+ readonly application:
330
+ | "claim"
331
+ | "occurrence"
332
+ | "policy"
333
+ | "source-defined";
84
334
  readonly derivation:
85
335
  | { readonly kind: "sdk" }
86
336
  | {
@@ -111,7 +361,9 @@ export interface NormalizedDiagnosticReviewFilterIdentity {
111
361
  export type NormalizedDiagnosticPeriodAxisIdentity =
112
362
  | DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "calendar" }>>
113
363
  | (Omit<
114
- DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "ordered" }>>,
364
+ DiagnosticDeepReadonly<
365
+ Extract<DiagnosticPeriodAxis, { kind: "ordered" }>
366
+ >,
115
367
  "origins" | "valuations"
116
368
  > & {
117
369
  readonly origins: readonly {
@@ -129,11 +381,16 @@ export type NormalizedDiagnosticPeriodAxisIdentity =
129
381
  export interface NormalizedDiagnosticFormulaIdentity {
130
382
  readonly id: string;
131
383
  readonly version: string;
132
- readonly roles: Readonly<Record<string, {
133
- readonly kind: DiagnosticMeasureKind;
134
- readonly compatibilityGroup: string | null;
135
- readonly developmentSemantics: DiagnosticDevelopmentSemantics | null;
136
- }>>;
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
+ >;
137
394
  readonly numerator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
138
395
  readonly denominator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
139
396
  readonly denominatorPolicy: "positive-or-null";
@@ -205,7 +462,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
205
462
  readonly subject: DiagnosticCountPopulationDefinition["subject"];
206
463
  readonly unit: string;
207
464
  readonly description: string;
208
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
465
+ readonly attributes: Readonly<
466
+ Record<string, string | number | boolean | null>
467
+ >;
209
468
  }[];
210
469
  readonly exposureBases: readonly {
211
470
  readonly id: string;
@@ -214,7 +473,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
214
473
  readonly unit: string;
215
474
  readonly description: string;
216
475
  readonly sourceDescription: string | null;
217
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
476
+ readonly attributes: Readonly<
477
+ Record<string, string | number | boolean | null>
478
+ >;
218
479
  }[];
219
480
  readonly amountBases: readonly {
220
481
  readonly id: string;
@@ -227,7 +488,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
227
488
  readonly limitation: NormalizedAmountLimitationIdentity;
228
489
  }[];
229
490
  readonly sourceDescription: string | null;
230
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
491
+ readonly attributes: Readonly<
492
+ Record<string, string | number | boolean | null>
493
+ >;
231
494
  }[];
232
495
  readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
233
496
  readonly formulas: readonly NormalizedDiagnosticFormulaIdentity[];
@@ -235,7 +498,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
235
498
  readonly id: string;
236
499
  readonly version: string;
237
500
  readonly formulaId: string;
238
- readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
501
+ readonly bindings: Readonly<
502
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
503
+ >;
239
504
  readonly presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>;
240
505
  readonly rules: readonly {
241
506
  readonly id: string;
@@ -260,7 +525,9 @@ export interface NormalizedDiagnosticCalculationScope {
260
525
  readonly id: string;
261
526
  readonly version: string;
262
527
  readonly formulaId: string;
263
- readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
528
+ readonly bindings: Readonly<
529
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
530
+ >;
264
531
  };
265
532
  readonly lossRowGrain: "claim" | "aggregate";
266
533
  readonly measures: readonly {
@@ -280,13 +547,17 @@ export interface NormalizedDiagnosticCalculationScope {
280
547
  readonly id: string;
281
548
  readonly subject: DiagnosticCountPopulationDefinition["subject"];
282
549
  readonly unit: string;
283
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
550
+ readonly attributes: Readonly<
551
+ Record<string, string | number | boolean | null>
552
+ >;
284
553
  }[];
285
554
  readonly exposureBases: readonly {
286
555
  readonly id: string;
287
556
  readonly basis: DiagnosticExposureBasisDefinition["basis"];
288
557
  readonly unit: string;
289
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
558
+ readonly attributes: Readonly<
559
+ Record<string, string | number | boolean | null>
560
+ >;
290
561
  }[];
291
562
  readonly amountBases: readonly {
292
563
  readonly id: string;
@@ -297,7 +568,9 @@ export interface NormalizedDiagnosticCalculationScope {
297
568
  readonly treatment: "included" | "excluded" | "unknown";
298
569
  readonly limitation: NormalizedAmountLimitationIdentity;
299
570
  }[];
300
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
571
+ readonly attributes: Readonly<
572
+ Record<string, string | number | boolean | null>
573
+ >;
301
574
  }[];
302
575
  readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
303
576
  }
@@ -310,36 +583,53 @@ export interface NormalizedDiagnosticSourceLocationIdentity {
310
583
  readonly sourceCell: string | null;
311
584
  }
312
585
 
313
- function normalizeLimitation(limitation: AmountLimitation): NormalizedAmountLimitationIdentity {
586
+ function normalizeLimitation(
587
+ limitation: AmountLimitation,
588
+ ): NormalizedAmountLimitationIdentity {
314
589
  if (limitation.kind === "unlimited") return { kind: "unlimited" };
315
590
  if (limitation.kind === "unknown") {
316
591
  return { kind: "unknown", description: limitation.description ?? null };
317
592
  }
318
593
  return {
319
594
  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,
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,
322
602
  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
- },
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
+ },
330
611
  };
331
612
  }
332
613
 
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>;
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>;
337
622
  if (expression.op === "claim-layer") {
338
623
  return {
339
624
  op: "claim-layer",
340
625
  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,
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,
343
633
  } as DiagnosticDeepReadonly<T>;
344
634
  }
345
635
  if (expression.op === "subtract") {
@@ -355,7 +645,9 @@ function normalizeMeasureExpression<T extends DiagnosticMeasureExpression | Diag
355
645
  } as unknown as DiagnosticDeepReadonly<T>;
356
646
  }
357
647
 
358
- function normalizeRoleExpression(expression: DiagnosticRoleExpression): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
648
+ function normalizeRoleExpression(
649
+ expression: DiagnosticRoleExpression,
650
+ ): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
359
651
  if (expression.op === "role") return { op: "role", role: expression.role };
360
652
  if (expression.op === "subtract") {
361
653
  return {
@@ -371,43 +663,95 @@ function normalizeTolerance(
371
663
  tolerance: { absolute?: number; relative?: number } | undefined,
372
664
  ): NormalizedDiagnosticToleranceIdentity {
373
665
  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),
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),
376
672
  };
377
673
  }
378
674
 
379
- function normalizeRuleOperand(operand: DiagnosticRuleOperand): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
675
+ function normalizeRuleOperand(
676
+ operand: DiagnosticRuleOperand,
677
+ ): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
380
678
  if (operand.source === "measure") {
381
- return { source: "measure", expression: normalizeMeasureExpression(operand.expression) };
679
+ return {
680
+ source: "measure",
681
+ expression: normalizeMeasureExpression(operand.expression),
682
+ };
382
683
  }
383
- if (operand.source === "calculation") return { source: "calculation", field: operand.field };
384
- return { source: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
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
+ };
385
690
  }
386
691
 
387
- function normalizeReviewOperand(operand: DiagnosticReviewOperand): DiagnosticDeepReadonly<DiagnosticReviewOperand> {
388
- if (operand.op === "constant") return { op: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
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
+ };
389
700
  return normalizeMeasureExpression(operand);
390
701
  }
391
702
 
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
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
402
712
  ? null
403
- : Object.is(filter.minDevelopmentAge, -0) ? 0 : filter.minDevelopmentAge,
404
- maxDevelopmentAge: filter.maxDevelopmentAge === undefined
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
405
724
  ? null
406
- : Object.is(filter.maxDevelopmentAge, -0) ? 0 : filter.maxDevelopmentAge,
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,
407
749
  };
408
750
  }
409
751
 
410
- function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPeriodAxisIdentity {
752
+ function normalizePeriodAxis(
753
+ axis: DiagnosticPeriodAxis,
754
+ ): NormalizedDiagnosticPeriodAxisIdentity {
411
755
  if (axis.kind === "calendar") {
412
756
  return {
413
757
  kind: "calendar",
@@ -419,11 +763,20 @@ function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPe
419
763
  ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
420
764
  };
421
765
  }
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));
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
+ );
427
780
  return {
428
781
  kind: "ordered",
429
782
  id: axis.id,
@@ -438,17 +791,26 @@ function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPe
438
791
  function topologicalDerivations(
439
792
  derivations: readonly DiagnosticDerivedMeasureDefinition[],
440
793
  ): readonly DiagnosticDerivedMeasureDefinition[] {
441
- const byOutput = new Map(derivations.map((derivation) => [derivation.outputMeasureId, derivation]));
442
- const remaining = new Map(derivations.map((derivation) => [derivation.id, derivation]));
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
+ );
443
800
  const emittedOutputs = new Set<string>();
444
801
  const ordered: DiagnosticDerivedMeasureDefinition[] = [];
445
802
  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));
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
+ );
452
814
  for (const derivation of ready) {
453
815
  ordered.push(derivation);
454
816
  emittedOutputs.add(derivation.outputMeasureId);
@@ -458,7 +820,10 @@ function topologicalDerivations(
458
820
  return ordered;
459
821
  }
460
822
 
461
- function normalizeReviewRule(rule: DiagnosticReviewRule): NormalizedDiagnosticReviewRuleIdentity {
823
+ function normalizeReviewRule(
824
+ rule: DiagnosticReviewRule,
825
+ axis: DiagnosticPeriodAxis,
826
+ ): NormalizedDiagnosticReviewRuleIdentity {
462
827
  const base: NormalizedDiagnosticReviewRuleBase = {
463
828
  id: rule.id,
464
829
  code: rule.code,
@@ -508,8 +873,21 @@ function normalizeReviewRule(rule: DiagnosticReviewRule): NormalizedDiagnosticRe
508
873
  kind: "control-total",
509
874
  expression: normalizeMeasureExpression(rule.expression),
510
875
  expected: Object.is(rule.expected, -0) ? 0 : rule.expected,
511
- filter: rule.filter === undefined ? null : normalizeReviewFilter(rule.filter),
512
- projection: normalizeJsonValue(rule.projection),
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),
513
891
  };
514
892
  }
515
893
 
@@ -521,80 +899,107 @@ export function normalizeDiagnosticDefinition(
521
899
  id: definition.id,
522
900
  version: definition.version,
523
901
  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)),
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)),
586
986
  instances: definition.instances.map((instance) => ({
587
987
  id: instance.id,
588
988
  version: instance.version,
589
989
  formulaId: instance.formulaId,
590
- bindings: sortedRecord(Object.entries(instance.bindings).map(([role, expression]) =>
591
- [role, normalizeMeasureExpression(expression)] as const,
592
- )),
990
+ bindings: sortedRecord(
991
+ Object.entries(instance.bindings).map(
992
+ ([role, expression]) =>
993
+ [role, normalizeMeasureExpression(expression)] as const,
994
+ ),
995
+ ),
593
996
  presentation: {
594
997
  displayName: instance.presentation.displayName,
595
998
  description: instance.presentation.description,
596
999
  displayUnit: instance.presentation.displayUnit,
597
- scale: Object.is(instance.presentation.scale, -0) ? 0 : instance.presentation.scale,
1000
+ scale: Object.is(instance.presentation.scale, -0)
1001
+ ? 0
1002
+ : instance.presentation.scale,
598
1003
  numeratorLabel: instance.presentation.numeratorLabel,
599
1004
  denominatorLabel: instance.presentation.denominatorLabel,
600
1005
  },
@@ -611,7 +1016,9 @@ export function normalizeDiagnosticDefinition(
611
1016
  },
612
1017
  })),
613
1018
  })),
614
- reviewRules: definition.reviewRules.map(normalizeReviewRule),
1019
+ reviewRules: definition.reviewRules.map((rule) =>
1020
+ normalizeReviewRule(rule, definition.periodAxis),
1021
+ ),
615
1022
  periodAxis: normalizePeriodAxis(definition.periodAxis),
616
1023
  };
617
1024
  return deepFreeze(normalized);
@@ -624,60 +1031,116 @@ export function normalizeDiagnosticSourceLocation(
624
1031
  artifactId: source.artifactId,
625
1032
  sourceFile: source.sourceFile ?? null,
626
1033
  sourceSheet: source.sourceSheet ?? null,
627
- sourceRow: source.sourceRow === undefined ? null : Object.is(source.sourceRow, -0) ? 0 : source.sourceRow,
1034
+ sourceRow:
1035
+ source.sourceRow === undefined
1036
+ ? null
1037
+ : Object.is(source.sourceRow, -0)
1038
+ ? 0
1039
+ : source.sourceRow,
628
1040
  sourceCell: source.sourceCell ?? null,
629
1041
  });
630
1042
  }
631
1043
 
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
- ]);
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
+ ]);
638
1054
  return sortedUnique(expression.terms.flatMap(expressionDependencies));
639
1055
  }
640
1056
 
641
- function ruleMeasureDependencies(operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>): readonly string[] {
642
- return operand.source === "measure" ? expressionDependencies(operand.expression) : [];
1057
+ function ruleMeasureDependencies(
1058
+ operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>,
1059
+ ): readonly string[] {
1060
+ return operand.source === "measure"
1061
+ ? expressionDependencies(operand.expression)
1062
+ : [];
643
1063
  }
644
1064
 
645
1065
  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
- }))}`;
1066
+ return `fnv1a64-jcs-v1:${fnv1a64(
1067
+ canonicalJson({
1068
+ identityVersion: 1,
1069
+ kind,
1070
+ [property]: value,
1071
+ }),
1072
+ )}`;
651
1073
  }
652
1074
 
653
1075
  export interface BuiltDiagnosticIdentities {
654
1076
  readonly formulaFingerprints: Readonly<Record<string, string>>;
655
1077
  readonly calculationFingerprints: Readonly<Record<string, string>>;
656
1078
  readonly definitionIntegrity: string;
657
- readonly calculationScopesByInstanceId: ReadonlyMap<string, NormalizedDiagnosticCalculationScope>;
658
- readonly calculationDependenciesByInstanceId: ReadonlyMap<string, readonly string[]>;
659
- readonly evaluationDependenciesByInstanceId: ReadonlyMap<string, readonly 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
+ >;
660
1091
  }
661
1092
 
662
1093
  export function buildDiagnosticIdentities(
663
1094
  definition: DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity>,
664
1095
  ): 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
- ));
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
+ );
675
1127
  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[]>();
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
+ >();
679
1140
 
680
- const transitiveDependencies = (roots: readonly string[]): readonly string[] => {
1141
+ const transitiveDependencies = (
1142
+ roots: readonly string[],
1143
+ ): readonly string[] => {
681
1144
  const found = new Set<string>();
682
1145
  const stack = [...roots];
683
1146
  while (stack.length > 0) {
@@ -685,7 +1148,8 @@ export function buildDiagnosticIdentities(
685
1148
  if (found.has(id)) continue;
686
1149
  found.add(id);
687
1150
  const derivation = derivations.get(id);
688
- if (derivation) stack.push(...expressionDependencies(derivation.expression));
1151
+ if (derivation)
1152
+ stack.push(...expressionDependencies(derivation.expression));
689
1153
  }
690
1154
  return [...found].sort(compareCodeUnits);
691
1155
  };
@@ -701,14 +1165,34 @@ export function buildDiagnosticIdentities(
701
1165
  ...ruleMeasureDependencies(rule.when.right),
702
1166
  ]),
703
1167
  ]);
704
- calculationDependenciesByInstanceId.set(instance.id, calculationDependencies);
1168
+ calculationDependenciesByInstanceId.set(
1169
+ instance.id,
1170
+ calculationDependencies,
1171
+ );
705
1172
  evaluationDependenciesByInstanceId.set(instance.id, evaluationDependencies);
706
1173
 
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]));
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
+ );
712
1196
  const scope: NormalizedDiagnosticCalculationScope = {
713
1197
  formulaFingerprint: formulaFingerprints[instance.formulaId]!,
714
1198
  instance: {
@@ -733,11 +1217,21 @@ export function buildDiagnosticIdentities(
733
1217
  })),
734
1218
  countPopulations: countPopulationIds.map((id) => {
735
1219
  const population = populations.get(id)!;
736
- return { id, subject: population.subject, unit: population.unit, attributes: population.attributes };
1220
+ return {
1221
+ id,
1222
+ subject: population.subject,
1223
+ unit: population.unit,
1224
+ attributes: population.attributes,
1225
+ };
737
1226
  }),
738
1227
  exposureBases: exposureBasisIds.map((id) => {
739
1228
  const basis = exposureBases.get(id)!;
740
- return { id, basis: basis.basis, unit: basis.unit, attributes: basis.attributes };
1229
+ return {
1230
+ id,
1231
+ basis: basis.basis,
1232
+ unit: basis.unit,
1233
+ attributes: basis.attributes,
1234
+ };
741
1235
  }),
742
1236
  amountBases: amountBasisIds.map((id) => {
743
1237
  const basis = amountBases.get(id)!;
@@ -753,12 +1247,18 @@ export function buildDiagnosticIdentities(
753
1247
  };
754
1248
  const frozenScope = deepFreeze(scope);
755
1249
  calculationScopesByInstanceId.set(instance.id, frozenScope);
756
- calculationFingerprints[instance.id] = tag("diagnostic-calculation", "calculation", frozenScope);
1250
+ calculationFingerprints[instance.id] = tag(
1251
+ "diagnostic-calculation",
1252
+ "calculation",
1253
+ frozenScope,
1254
+ );
757
1255
  }
758
1256
 
759
1257
  return {
760
1258
  formulaFingerprints: deepFreeze(formulaFingerprints),
761
- calculationFingerprints: deepFreeze(sortedRecord(Object.entries(calculationFingerprints))),
1259
+ calculationFingerprints: deepFreeze(
1260
+ sortedRecord(Object.entries(calculationFingerprints)),
1261
+ ),
762
1262
  definitionIntegrity: tag("diagnostic-definition", "definition", definition),
763
1263
  calculationScopesByInstanceId,
764
1264
  calculationDependenciesByInstanceId,