@actuarial-ts/core 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/README.md +12 -3
  2. package/dist/canonical.d.ts +11 -0
  3. package/dist/canonical.d.ts.map +1 -1
  4. package/dist/canonical.js +136 -26
  5. package/dist/canonical.js.map +1 -1
  6. package/dist/capping.d.ts.map +1 -1
  7. package/dist/capping.js +39 -6
  8. package/dist/capping.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +3 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -1
  11. package/dist/diagnosticAggregation.js +30 -7
  12. package/dist/diagnosticAggregation.js.map +1 -1
  13. package/dist/diagnosticDefinitions.d.ts +3 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  15. package/dist/diagnosticDefinitions.js +844 -182
  16. package/dist/diagnosticDefinitions.js.map +1 -1
  17. package/dist/diagnosticDerivations.d.ts +23 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -1
  19. package/dist/diagnosticDerivations.js +187 -43
  20. package/dist/diagnosticDerivations.js.map +1 -1
  21. package/dist/diagnosticEvidenceIntern.d.ts +16 -0
  22. package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
  23. package/dist/diagnosticEvidenceIntern.js +139 -0
  24. package/dist/diagnosticEvidenceIntern.js.map +1 -0
  25. package/dist/diagnosticExposure.d.ts +2 -2
  26. package/dist/diagnosticExposure.d.ts.map +1 -1
  27. package/dist/diagnosticExposure.js +319 -21
  28. package/dist/diagnosticExposure.js.map +1 -1
  29. package/dist/diagnosticExpressions.d.ts +1 -1
  30. package/dist/diagnosticExpressions.d.ts.map +1 -1
  31. package/dist/diagnosticExpressions.js +35 -15
  32. package/dist/diagnosticExpressions.js.map +1 -1
  33. package/dist/diagnosticFormulas.d.ts +3 -0
  34. package/dist/diagnosticFormulas.d.ts.map +1 -1
  35. package/dist/diagnosticFormulas.js +160 -26
  36. package/dist/diagnosticFormulas.js.map +1 -1
  37. package/dist/diagnosticIdentity.d.ts +15 -1
  38. package/dist/diagnosticIdentity.d.ts.map +1 -1
  39. package/dist/diagnosticIdentity.js +321 -51
  40. package/dist/diagnosticIdentity.js.map +1 -1
  41. package/dist/diagnosticIdentityStream.d.ts +56 -0
  42. package/dist/diagnosticIdentityStream.d.ts.map +1 -0
  43. package/dist/diagnosticIdentityStream.js +472 -0
  44. package/dist/diagnosticIdentityStream.js.map +1 -0
  45. package/dist/diagnosticOrdering.d.ts +8 -0
  46. package/dist/diagnosticOrdering.d.ts.map +1 -0
  47. package/dist/diagnosticOrdering.js +73 -0
  48. package/dist/diagnosticOrdering.js.map +1 -0
  49. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  50. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  51. package/dist/diagnosticPeriodAxis.js +69 -0
  52. package/dist/diagnosticPeriodAxis.js.map +1 -0
  53. package/dist/diagnosticPeriods.d.ts +2 -3
  54. package/dist/diagnosticPeriods.d.ts.map +1 -1
  55. package/dist/diagnosticPeriods.js +20 -68
  56. package/dist/diagnosticPeriods.js.map +1 -1
  57. package/dist/diagnosticPreparation.d.ts +75 -14
  58. package/dist/diagnosticPreparation.d.ts.map +1 -1
  59. package/dist/diagnosticPreparation.js +1606 -112
  60. package/dist/diagnosticPreparation.js.map +1 -1
  61. package/dist/diagnosticReview.d.ts +40 -15
  62. package/dist/diagnosticReview.d.ts.map +1 -1
  63. package/dist/diagnosticReview.js +600 -42
  64. package/dist/diagnosticReview.js.map +1 -1
  65. package/dist/diagnosticReviewSources.d.ts +18 -0
  66. package/dist/diagnosticReviewSources.d.ts.map +1 -0
  67. package/dist/diagnosticReviewSources.js +76 -0
  68. package/dist/diagnosticReviewSources.js.map +1 -0
  69. package/dist/diagnosticReviewStore.d.ts +88 -0
  70. package/dist/diagnosticReviewStore.d.ts.map +1 -0
  71. package/dist/diagnosticReviewStore.js +455 -0
  72. package/dist/diagnosticReviewStore.js.map +1 -0
  73. package/dist/diagnosticRunner.d.ts +27 -2
  74. package/dist/diagnosticRunner.d.ts.map +1 -1
  75. package/dist/diagnosticRunner.js +764 -70
  76. package/dist/diagnosticRunner.js.map +1 -1
  77. package/dist/diagnosticRuntime.d.ts +26 -0
  78. package/dist/diagnosticRuntime.d.ts.map +1 -0
  79. package/dist/diagnosticRuntime.js +243 -0
  80. package/dist/diagnosticRuntime.js.map +1 -0
  81. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  82. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  83. package/dist/diagnosticSourceOrdering.js +38 -0
  84. package/dist/diagnosticSourceOrdering.js.map +1 -0
  85. package/dist/fnvAccumulator.d.ts +6 -0
  86. package/dist/fnvAccumulator.d.ts.map +1 -0
  87. package/dist/fnvAccumulator.js +21 -0
  88. package/dist/fnvAccumulator.js.map +1 -0
  89. package/dist/index.d.ts +15 -8
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +10 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/mack.d.ts +4 -0
  94. package/dist/mack.d.ts.map +1 -1
  95. package/dist/mack.js +32 -4
  96. package/dist/mack.js.map +1 -1
  97. package/dist/triangle.d.ts.map +1 -1
  98. package/dist/triangle.js +59 -9
  99. package/dist/triangle.js.map +1 -1
  100. package/dist/types.d.ts +4 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js +22 -8
  103. package/dist/types.js.map +1 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +3 -2
  107. package/src/canonical.ts +144 -26
  108. package/src/capping.ts +109 -18
  109. package/src/diagnosticAggregation.ts +69 -20
  110. package/src/diagnosticDefinitions.ts +2675 -426
  111. package/src/diagnosticDerivations.ts +281 -49
  112. package/src/diagnosticEvidenceIntern.ts +151 -0
  113. package/src/diagnosticExposure.ts +550 -50
  114. package/src/diagnosticExpressions.ts +99 -17
  115. package/src/diagnosticFormulas.ts +239 -39
  116. package/src/diagnosticIdentity.ts +700 -191
  117. package/src/diagnosticIdentityStream.ts +553 -0
  118. package/src/diagnosticOrdering.ts +106 -0
  119. package/src/diagnosticPeriodAxis.ts +92 -0
  120. package/src/diagnosticPeriods.ts +67 -82
  121. package/src/diagnosticPreparation.ts +2558 -132
  122. package/src/diagnosticReview.ts +1072 -58
  123. package/src/diagnosticReviewSources.ts +85 -0
  124. package/src/diagnosticReviewStore.ts +692 -0
  125. package/src/diagnosticRunner.ts +1273 -72
  126. package/src/diagnosticRuntime.ts +264 -0
  127. package/src/diagnosticSourceOrdering.ts +50 -0
  128. package/src/fnvAccumulator.ts +20 -0
  129. package/src/index.ts +69 -4
  130. package/src/mack.ts +72 -9
  131. package/src/triangle.ts +103 -13
  132. package/src/types.ts +55 -20
  133. package/src/version.ts +1 -1
@@ -1,4 +1,5 @@
1
1
  import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import { createDiagnosticEvidenceInterner } from "./diagnosticEvidenceIntern.js";
2
3
  import type {
3
4
  AmountBasisDefinition,
4
5
  AmountLimitation,
@@ -24,12 +25,252 @@ import type {
24
25
  DiagnosticReviewRule,
25
26
  DiagnosticRuleOperand,
26
27
  DiagnosticSourceLocation,
28
+ DiagnosticsFilter,
27
29
  } from "./diagnosticDefinitions.js";
28
30
  import type {
29
31
  DiagnosticClaimExpression,
30
32
  DiagnosticMeasureExpression,
31
33
  DiagnosticRoleExpression,
32
34
  } from "./diagnosticExpressions.js";
35
+ import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
36
+ import {
37
+ diagnosticJsonPreflight,
38
+ isDiagnosticPlainRecord,
39
+ isDiagnosticToken,
40
+ normalizeDiagnosticNumber,
41
+ } from "./diagnosticRuntime.js";
42
+ import {
43
+ DiagnosticValidationError,
44
+ type DiagnosticValidationIssue,
45
+ } from "./types.js";
46
+
47
+ export type DiagnosticIdentityProjection<T> = T extends DiagnosticSourceLocation
48
+ ? NormalizedDiagnosticSourceLocationIdentity
49
+ : T extends readonly (infer U)[]
50
+ ? readonly DiagnosticIdentityProjection<U>[]
51
+ : T extends object
52
+ ? { readonly [K in keyof T]: DiagnosticIdentityProjection<T[K]> }
53
+ : T;
54
+
55
+ export interface NormalizedDiagnosticsFilterIdentity
56
+ extends NormalizedDiagnosticReviewFilterIdentity {
57
+ readonly outputGroups: readonly string[] | null;
58
+ readonly instanceIds: readonly string[] | null;
59
+ }
60
+
61
+ /** Validates selector shapes without interpreting periods against a definition. */
62
+ function validateFilterIdentityInput(filter: DiagnosticsFilter): void {
63
+ const issues: DiagnosticValidationIssue[] = [
64
+ ...diagnosticJsonPreflight(filter, "configuration"),
65
+ ];
66
+ if (issues.length) throw new DiagnosticValidationError(issues);
67
+ if (!isDiagnosticPlainRecord(filter))
68
+ throw new DiagnosticValidationError([
69
+ {
70
+ domain: "configuration",
71
+ code: "invalid-type",
72
+ path: "$",
73
+ message: "Filter must be a plain object or null",
74
+ },
75
+ ]);
76
+
77
+ const lists = [
78
+ "sourceGroups", "outputGroups", "origins", "valuations", "instanceIds",
79
+ ];
80
+ const labels = [
81
+ "originFrom", "originThrough", "valuationFrom", "valuationThrough",
82
+ ];
83
+ const bounds = ["minDevelopmentAge", "maxDevelopmentAge"];
84
+ const checkToken = (value: unknown, path: string) => {
85
+ if (!isDiagnosticToken(value))
86
+ issues.push({
87
+ domain: "configuration",
88
+ code: "invalid-string",
89
+ path,
90
+ message: "Filter selectors must use nonempty token strings",
91
+ });
92
+ };
93
+ for (const key of Object.keys(filter).sort()) {
94
+ const value: unknown = filter[key];
95
+ const path = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
96
+ ? `$.${key}`
97
+ : `$[${JSON.stringify(key)}]`;
98
+ if (lists.includes(key)) {
99
+ if (!Array.isArray(value))
100
+ issues.push({
101
+ domain: "configuration",
102
+ code: "invalid-type",
103
+ path,
104
+ message: "Filter selector must be an array",
105
+ });
106
+ else
107
+ value.forEach((item, index) => checkToken(item, `${path}[${index}]`));
108
+ } else if (labels.includes(key)) checkToken(value, path);
109
+ else if (bounds.includes(key)) {
110
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
111
+ issues.push({
112
+ domain: "configuration",
113
+ code: "invalid-number",
114
+ path,
115
+ message: "Development-age bounds must be nonnegative safe integers",
116
+ });
117
+ } else
118
+ issues.push({
119
+ domain: "configuration",
120
+ code: "unknown-key",
121
+ path,
122
+ message: `Unknown filter key ${key}`,
123
+ });
124
+ }
125
+ if (
126
+ typeof filter.minDevelopmentAge === "number" &&
127
+ typeof filter.maxDevelopmentAge === "number" &&
128
+ filter.minDevelopmentAge > filter.maxDevelopmentAge
129
+ )
130
+ issues.push({
131
+ domain: "configuration",
132
+ code: "invalid-configuration",
133
+ path: "$",
134
+ message: "Minimum development age exceeds maximum",
135
+ });
136
+ if (issues.length) throw new DiagnosticValidationError(issues);
137
+ }
138
+
139
+ /** Materializes optional selectors; period aliases must already be canonical. */
140
+ export function normalizeDiagnosticsFilterIdentity(
141
+ filter: DiagnosticsFilter | null,
142
+ ): NormalizedDiagnosticsFilterIdentity | null {
143
+ if (filter === null) return null;
144
+ validateFilterIdentityInput(filter);
145
+ return deepFreeze({
146
+ sourceGroups:
147
+ filter.sourceGroups === undefined
148
+ ? null
149
+ : sortedUnique(filter.sourceGroups),
150
+ outputGroups:
151
+ filter.outputGroups === undefined
152
+ ? null
153
+ : sortedUnique(filter.outputGroups),
154
+ origins: filter.origins === undefined ? null : sortedUnique(filter.origins),
155
+ originFrom: filter.originFrom ?? null,
156
+ originThrough: filter.originThrough ?? null,
157
+ valuations:
158
+ filter.valuations === undefined ? null : sortedUnique(filter.valuations),
159
+ valuationFrom: filter.valuationFrom ?? null,
160
+ valuationThrough: filter.valuationThrough ?? null,
161
+ minDevelopmentAge:
162
+ filter.minDevelopmentAge === undefined
163
+ ? null
164
+ : normalizeDiagnosticNumber(filter.minDevelopmentAge),
165
+ maxDevelopmentAge:
166
+ filter.maxDevelopmentAge === undefined
167
+ ? null
168
+ : normalizeDiagnosticNumber(filter.maxDevelopmentAge),
169
+ instanceIds:
170
+ filter.instanceIds === undefined ? null : sortedUnique(filter.instanceIds),
171
+ });
172
+ }
173
+
174
+ /**
175
+ * Owned identity copy. Source slots are schema-defined; free JSON dimensions
176
+ * must not be interpreted as source evidence just because a key looks similar.
177
+ */
178
+ export function projectDiagnosticIdentity<T>(
179
+ value: T,
180
+ ): DiagnosticIdentityProjection<T> {
181
+ const issues = diagnosticJsonPreflight(value, "input", {
182
+ maxNodes: Number.MAX_SAFE_INTEGER,
183
+ });
184
+ if (issues.length) throw new DiagnosticValidationError(issues);
185
+ const sharing = createDiagnosticEvidenceInterner();
186
+ const clone = (
187
+ item: unknown,
188
+ sourceSlot = false,
189
+ freeJson = false,
190
+ path = "$",
191
+ ): unknown => {
192
+ if (item === null || typeof item !== "object")
193
+ return typeof item === "number" && Object.is(item, -0) ? 0 : item;
194
+ const mode = freeJson ? "free" : sourceSlot ? "source" : "plain";
195
+ if (Array.isArray(item))
196
+ return sharing.internOwned(
197
+ Object.freeze(
198
+ item.map((child, index) =>
199
+ clone(child, sourceSlot, freeJson, `${path}[${index}]`),
200
+ ),
201
+ ),
202
+ mode,
203
+ );
204
+ const record = item as Record<string, unknown>;
205
+ if (sourceSlot && typeof record.artifactId === "string") {
206
+ const sourceIssues = [];
207
+ for (const key of Object.keys(record)) {
208
+ const fieldPath = `${path}.${key}`;
209
+ if (
210
+ ![
211
+ "artifactId",
212
+ "sourceFile",
213
+ "sourceSheet",
214
+ "sourceRow",
215
+ "sourceCell",
216
+ ].includes(key)
217
+ )
218
+ sourceIssues.push({
219
+ domain: "input" as const,
220
+ code: "unknown-key" as const,
221
+ path: fieldPath,
222
+ message: `Unknown source-location key ${key}`,
223
+ });
224
+ else if (key === "sourceRow") {
225
+ if (
226
+ record[key] !== null &&
227
+ !(
228
+ typeof record[key] === "number" &&
229
+ Number.isSafeInteger(record[key]) &&
230
+ record[key] >= 0
231
+ )
232
+ )
233
+ sourceIssues.push({
234
+ domain: "input" as const,
235
+ code: "invalid-number" as const,
236
+ path: fieldPath,
237
+ message: "Source row must be a nonnegative safe integer or null",
238
+ });
239
+ } else if (
240
+ !(key !== "artifactId" && record[key] === null) &&
241
+ !isDiagnosticToken(record[key])
242
+ )
243
+ sourceIssues.push({
244
+ domain: "input" as const,
245
+ code: "invalid-string" as const,
246
+ path: fieldPath,
247
+ message: "Source location must use nonempty token strings",
248
+ });
249
+ }
250
+ if (sourceIssues.length)
251
+ throw new DiagnosticValidationError(sourceIssues);
252
+ return sharing.internOwned(
253
+ normalizeDiagnosticSourceLocation(
254
+ record as unknown as DiagnosticSourceLocation,
255
+ ),
256
+ mode,
257
+ );
258
+ }
259
+ const result = Object.create(null) as Record<string, unknown>;
260
+ for (const key of Object.keys(record).sort()) {
261
+ const opaque =
262
+ freeJson || key === "groupDimensions" || key === "dimensions";
263
+ result[key] = clone(
264
+ record[key],
265
+ !opaque && (key === "source" || key === "sources"),
266
+ opaque,
267
+ `${path}.${key}`,
268
+ );
269
+ }
270
+ return sharing.internOwned(Object.freeze(result), mode);
271
+ };
272
+ return clone(value) as DiagnosticIdentityProjection<T>;
273
+ }
33
274
 
34
275
  function compareCodeUnits(left: string, right: string): number {
35
276
  return left === right ? 0 : left < right ? -1 : 1;
@@ -39,9 +280,13 @@ function sortedUnique(values: readonly string[]): readonly string[] {
39
280
  return [...new Set(values)].sort(compareCodeUnits);
40
281
  }
41
282
 
42
- function sortedRecord<T>(entries: readonly (readonly [string, T])[]): Readonly<Record<string, T>> {
283
+ function sortedRecord<T>(
284
+ entries: readonly (readonly [string, T])[],
285
+ ): Readonly<Record<string, T>> {
43
286
  const result = Object.create(null) as Record<string, T>;
44
- for (const [key, value] of [...entries].sort(([left], [right]) => compareCodeUnits(left, right))) {
287
+ for (const [key, value] of [...entries].sort(([left], [right]) =>
288
+ compareCodeUnits(left, right),
289
+ )) {
45
290
  result[key] = value;
46
291
  }
47
292
  return result;
@@ -49,29 +294,39 @@ function sortedRecord<T>(entries: readonly (readonly [string, T])[]): Readonly<R
49
294
 
50
295
  function normalizeJsonValue<T>(value: T): T {
51
296
  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;
297
+ if (Array.isArray(value))
298
+ return value.map((item) => normalizeJsonValue(item)) as T;
53
299
  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;
300
+ return sortedRecord(
301
+ Object.entries(value as Record<string, unknown>).map(
302
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
303
+ ),
304
+ ) as T;
57
305
  }
58
306
  return value;
59
307
  }
60
308
 
61
- function deepFreeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
62
- if (value === null || typeof value !== "object") return value as DiagnosticDeepReadonly<T>;
309
+ function deepFreeze<T>(
310
+ value: T,
311
+ seen = new WeakSet<object>(),
312
+ ): DiagnosticDeepReadonly<T> {
313
+ if (value === null || typeof value !== "object")
314
+ return value as DiagnosticDeepReadonly<T>;
63
315
  if (seen.has(value)) return value as DiagnosticDeepReadonly<T>;
64
316
  seen.add(value);
65
- for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child, seen);
317
+ for (const child of Object.values(value as Record<string, unknown>))
318
+ deepFreeze(child, seen);
66
319
  return Object.freeze(value) as DiagnosticDeepReadonly<T>;
67
320
  }
68
321
 
69
322
  function attributes(
70
323
  value: Readonly<Record<string, string | number | boolean | null>> | undefined,
71
324
  ): Readonly<Record<string, string | number | boolean | null>> {
72
- return sortedRecord(Object.entries(value ?? {}).map(([key, item]) =>
73
- [key, normalizeJsonValue(item)] as const,
74
- ));
325
+ return sortedRecord(
326
+ Object.entries(value ?? {}).map(
327
+ ([key, item]) => [key, normalizeJsonValue(item)] as const,
328
+ ),
329
+ );
75
330
  }
76
331
 
77
332
  export type NormalizedAmountLimitationIdentity =
@@ -80,7 +335,11 @@ export type NormalizedAmountLimitationIdentity =
80
335
  readonly kind: "layer" | "pre-limited";
81
336
  readonly attachment: number;
82
337
  readonly limit: number | null;
83
- readonly application: "claim" | "occurrence" | "policy" | "source-defined";
338
+ readonly application:
339
+ | "claim"
340
+ | "occurrence"
341
+ | "policy"
342
+ | "source-defined";
84
343
  readonly derivation:
85
344
  | { readonly kind: "sdk" }
86
345
  | {
@@ -111,7 +370,9 @@ export interface NormalizedDiagnosticReviewFilterIdentity {
111
370
  export type NormalizedDiagnosticPeriodAxisIdentity =
112
371
  | DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "calendar" }>>
113
372
  | (Omit<
114
- DiagnosticDeepReadonly<Extract<DiagnosticPeriodAxis, { kind: "ordered" }>>,
373
+ DiagnosticDeepReadonly<
374
+ Extract<DiagnosticPeriodAxis, { kind: "ordered" }>
375
+ >,
115
376
  "origins" | "valuations"
116
377
  > & {
117
378
  readonly origins: readonly {
@@ -129,11 +390,16 @@ export type NormalizedDiagnosticPeriodAxisIdentity =
129
390
  export interface NormalizedDiagnosticFormulaIdentity {
130
391
  readonly id: string;
131
392
  readonly version: string;
132
- readonly roles: Readonly<Record<string, {
133
- readonly kind: DiagnosticMeasureKind;
134
- readonly compatibilityGroup: string | null;
135
- readonly developmentSemantics: DiagnosticDevelopmentSemantics | null;
136
- }>>;
393
+ readonly roles: Readonly<
394
+ Record<
395
+ string,
396
+ {
397
+ readonly kind: DiagnosticMeasureKind;
398
+ readonly compatibilityGroup: string | null;
399
+ readonly developmentSemantics: DiagnosticDevelopmentSemantics | null;
400
+ }
401
+ >
402
+ >;
137
403
  readonly numerator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
138
404
  readonly denominator: DiagnosticDeepReadonly<DiagnosticRoleExpression>;
139
405
  readonly denominatorPolicy: "positive-or-null";
@@ -205,7 +471,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
205
471
  readonly subject: DiagnosticCountPopulationDefinition["subject"];
206
472
  readonly unit: string;
207
473
  readonly description: string;
208
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
474
+ readonly attributes: Readonly<
475
+ Record<string, string | number | boolean | null>
476
+ >;
209
477
  }[];
210
478
  readonly exposureBases: readonly {
211
479
  readonly id: string;
@@ -214,7 +482,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
214
482
  readonly unit: string;
215
483
  readonly description: string;
216
484
  readonly sourceDescription: string | null;
217
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
485
+ readonly attributes: Readonly<
486
+ Record<string, string | number | boolean | null>
487
+ >;
218
488
  }[];
219
489
  readonly amountBases: readonly {
220
490
  readonly id: string;
@@ -227,7 +497,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
227
497
  readonly limitation: NormalizedAmountLimitationIdentity;
228
498
  }[];
229
499
  readonly sourceDescription: string | null;
230
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
500
+ readonly attributes: Readonly<
501
+ Record<string, string | number | boolean | null>
502
+ >;
231
503
  }[];
232
504
  readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
233
505
  readonly formulas: readonly NormalizedDiagnosticFormulaIdentity[];
@@ -235,7 +507,9 @@ export interface NormalizedDiagnosticDefinitionIdentity {
235
507
  readonly id: string;
236
508
  readonly version: string;
237
509
  readonly formulaId: string;
238
- readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
510
+ readonly bindings: Readonly<
511
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
512
+ >;
239
513
  readonly presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>;
240
514
  readonly rules: readonly {
241
515
  readonly id: string;
@@ -260,7 +534,9 @@ export interface NormalizedDiagnosticCalculationScope {
260
534
  readonly id: string;
261
535
  readonly version: string;
262
536
  readonly formulaId: string;
263
- readonly bindings: Readonly<Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>>;
537
+ readonly bindings: Readonly<
538
+ Record<string, DiagnosticDeepReadonly<DiagnosticMeasureExpression>>
539
+ >;
264
540
  };
265
541
  readonly lossRowGrain: "claim" | "aggregate";
266
542
  readonly measures: readonly {
@@ -280,13 +556,17 @@ export interface NormalizedDiagnosticCalculationScope {
280
556
  readonly id: string;
281
557
  readonly subject: DiagnosticCountPopulationDefinition["subject"];
282
558
  readonly unit: string;
283
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
559
+ readonly attributes: Readonly<
560
+ Record<string, string | number | boolean | null>
561
+ >;
284
562
  }[];
285
563
  readonly exposureBases: readonly {
286
564
  readonly id: string;
287
565
  readonly basis: DiagnosticExposureBasisDefinition["basis"];
288
566
  readonly unit: string;
289
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
567
+ readonly attributes: Readonly<
568
+ Record<string, string | number | boolean | null>
569
+ >;
290
570
  }[];
291
571
  readonly amountBases: readonly {
292
572
  readonly id: string;
@@ -297,7 +577,9 @@ export interface NormalizedDiagnosticCalculationScope {
297
577
  readonly treatment: "included" | "excluded" | "unknown";
298
578
  readonly limitation: NormalizedAmountLimitationIdentity;
299
579
  }[];
300
- readonly attributes: Readonly<Record<string, string | number | boolean | null>>;
580
+ readonly attributes: Readonly<
581
+ Record<string, string | number | boolean | null>
582
+ >;
301
583
  }[];
302
584
  readonly derivedMeasures: readonly DiagnosticDeepReadonly<DiagnosticDerivedMeasureDefinition>[];
303
585
  }
@@ -310,36 +592,53 @@ export interface NormalizedDiagnosticSourceLocationIdentity {
310
592
  readonly sourceCell: string | null;
311
593
  }
312
594
 
313
- function normalizeLimitation(limitation: AmountLimitation): NormalizedAmountLimitationIdentity {
595
+ function normalizeLimitation(
596
+ limitation: AmountLimitation,
597
+ ): NormalizedAmountLimitationIdentity {
314
598
  if (limitation.kind === "unlimited") return { kind: "unlimited" };
315
599
  if (limitation.kind === "unknown") {
316
600
  return { kind: "unknown", description: limitation.description ?? null };
317
601
  }
318
602
  return {
319
603
  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,
604
+ attachment: Object.is(limitation.attachment, -0)
605
+ ? 0
606
+ : limitation.attachment,
607
+ limit:
608
+ limitation.limit !== null && Object.is(limitation.limit, -0)
609
+ ? 0
610
+ : limitation.limit,
322
611
  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
- },
612
+ derivation:
613
+ limitation.derivation.kind === "sdk"
614
+ ? { kind: "sdk" }
615
+ : {
616
+ kind: "external",
617
+ actor: limitation.derivation.actor,
618
+ transformationRef: limitation.derivation.transformationRef,
619
+ },
330
620
  };
331
621
  }
332
622
 
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>;
623
+ function normalizeMeasureExpression<
624
+ T extends DiagnosticMeasureExpression | DiagnosticClaimExpression,
625
+ >(expression: T): DiagnosticDeepReadonly<T> {
626
+ if (expression.op === "measure")
627
+ return {
628
+ op: "measure",
629
+ measureId: expression.measureId,
630
+ } as DiagnosticDeepReadonly<T>;
337
631
  if (expression.op === "claim-layer") {
338
632
  return {
339
633
  op: "claim-layer",
340
634
  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,
635
+ attachment: Object.is(expression.attachment, -0)
636
+ ? 0
637
+ : expression.attachment,
638
+ limit:
639
+ expression.limit !== null && Object.is(expression.limit, -0)
640
+ ? 0
641
+ : expression.limit,
343
642
  } as DiagnosticDeepReadonly<T>;
344
643
  }
345
644
  if (expression.op === "subtract") {
@@ -355,7 +654,9 @@ function normalizeMeasureExpression<T extends DiagnosticMeasureExpression | Diag
355
654
  } as unknown as DiagnosticDeepReadonly<T>;
356
655
  }
357
656
 
358
- function normalizeRoleExpression(expression: DiagnosticRoleExpression): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
657
+ function normalizeRoleExpression(
658
+ expression: DiagnosticRoleExpression,
659
+ ): DiagnosticDeepReadonly<DiagnosticRoleExpression> {
359
660
  if (expression.op === "role") return { op: "role", role: expression.role };
360
661
  if (expression.op === "subtract") {
361
662
  return {
@@ -371,43 +672,95 @@ function normalizeTolerance(
371
672
  tolerance: { absolute?: number; relative?: number } | undefined,
372
673
  ): NormalizedDiagnosticToleranceIdentity {
373
674
  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),
675
+ absolute: Object.is(tolerance?.absolute ?? 0, -0)
676
+ ? 0
677
+ : (tolerance?.absolute ?? 0),
678
+ relative: Object.is(tolerance?.relative ?? 0, -0)
679
+ ? 0
680
+ : (tolerance?.relative ?? 0),
376
681
  };
377
682
  }
378
683
 
379
- function normalizeRuleOperand(operand: DiagnosticRuleOperand): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
684
+ function normalizeRuleOperand(
685
+ operand: DiagnosticRuleOperand,
686
+ ): DiagnosticDeepReadonly<DiagnosticRuleOperand> {
380
687
  if (operand.source === "measure") {
381
- return { source: "measure", expression: normalizeMeasureExpression(operand.expression) };
688
+ return {
689
+ source: "measure",
690
+ expression: normalizeMeasureExpression(operand.expression),
691
+ };
382
692
  }
383
- if (operand.source === "calculation") return { source: "calculation", field: operand.field };
384
- return { source: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
693
+ if (operand.source === "calculation")
694
+ return { source: "calculation", field: operand.field };
695
+ return {
696
+ source: "constant",
697
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
698
+ };
385
699
  }
386
700
 
387
- function normalizeReviewOperand(operand: DiagnosticReviewOperand): DiagnosticDeepReadonly<DiagnosticReviewOperand> {
388
- if (operand.op === "constant") return { op: "constant", value: Object.is(operand.value, -0) ? 0 : operand.value };
701
+ function normalizeReviewOperand(
702
+ operand: DiagnosticReviewOperand,
703
+ ): DiagnosticDeepReadonly<DiagnosticReviewOperand> {
704
+ if (operand.op === "constant")
705
+ return {
706
+ op: "constant",
707
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
708
+ };
389
709
  return normalizeMeasureExpression(operand);
390
710
  }
391
711
 
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
712
+ function normalizeReviewFilter(
713
+ filter: DiagnosticReviewFilter,
714
+ axis: DiagnosticPeriodAxis,
715
+ ): NormalizedDiagnosticReviewFilterIdentity {
716
+ const normalizeMany = (
717
+ side: "origin" | "valuation",
718
+ values: readonly string[] | undefined,
719
+ ) =>
720
+ values === undefined
402
721
  ? null
403
- : Object.is(filter.minDevelopmentAge, -0) ? 0 : filter.minDevelopmentAge,
404
- maxDevelopmentAge: filter.maxDevelopmentAge === undefined
722
+ : sortedUnique(
723
+ values.map(
724
+ (value) =>
725
+ normalizeDiagnosticPeriodWithAxis(axis, side, value)!.label,
726
+ ),
727
+ );
728
+ const normalizeOne = (
729
+ side: "origin" | "valuation",
730
+ value: string | undefined,
731
+ ) =>
732
+ value === undefined
405
733
  ? null
406
- : Object.is(filter.maxDevelopmentAge, -0) ? 0 : filter.maxDevelopmentAge,
734
+ : normalizeDiagnosticPeriodWithAxis(axis, side, value)!.label;
735
+ return {
736
+ sourceGroups:
737
+ filter.sourceGroups === undefined
738
+ ? null
739
+ : sortedUnique(filter.sourceGroups),
740
+ origins: normalizeMany("origin", filter.origins),
741
+ originFrom: normalizeOne("origin", filter.originFrom),
742
+ originThrough: normalizeOne("origin", filter.originThrough),
743
+ valuations: normalizeMany("valuation", filter.valuations),
744
+ valuationFrom: normalizeOne("valuation", filter.valuationFrom),
745
+ valuationThrough: normalizeOne("valuation", filter.valuationThrough),
746
+ minDevelopmentAge:
747
+ filter.minDevelopmentAge === undefined
748
+ ? null
749
+ : Object.is(filter.minDevelopmentAge, -0)
750
+ ? 0
751
+ : filter.minDevelopmentAge,
752
+ maxDevelopmentAge:
753
+ filter.maxDevelopmentAge === undefined
754
+ ? null
755
+ : Object.is(filter.maxDevelopmentAge, -0)
756
+ ? 0
757
+ : filter.maxDevelopmentAge,
407
758
  };
408
759
  }
409
760
 
410
- function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPeriodAxisIdentity {
761
+ function normalizePeriodAxis(
762
+ axis: DiagnosticPeriodAxis,
763
+ ): NormalizedDiagnosticPeriodAxisIdentity {
411
764
  if (axis.kind === "calendar") {
412
765
  return {
413
766
  kind: "calendar",
@@ -419,11 +772,20 @@ function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPe
419
772
  ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
420
773
  };
421
774
  }
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));
775
+ const coordinates = (values: typeof axis.origins) =>
776
+ values
777
+ .map((coordinate) => ({
778
+ label: coordinate.label,
779
+ aliases: sortedUnique(coordinate.aliases ?? []),
780
+ coordinate: Object.is(coordinate.coordinate, -0)
781
+ ? 0
782
+ : coordinate.coordinate,
783
+ }))
784
+ .sort(
785
+ (left, right) =>
786
+ left.coordinate - right.coordinate ||
787
+ compareCodeUnits(left.label, right.label),
788
+ );
427
789
  return {
428
790
  kind: "ordered",
429
791
  id: axis.id,
@@ -438,17 +800,26 @@ function normalizePeriodAxis(axis: DiagnosticPeriodAxis): NormalizedDiagnosticPe
438
800
  function topologicalDerivations(
439
801
  derivations: readonly DiagnosticDerivedMeasureDefinition[],
440
802
  ): readonly DiagnosticDerivedMeasureDefinition[] {
441
- const byOutput = new Map(derivations.map((derivation) => [derivation.outputMeasureId, derivation]));
442
- const remaining = new Map(derivations.map((derivation) => [derivation.id, derivation]));
803
+ const byOutput = new Map(
804
+ derivations.map((derivation) => [derivation.outputMeasureId, derivation]),
805
+ );
806
+ const remaining = new Map(
807
+ derivations.map((derivation) => [derivation.id, derivation]),
808
+ );
443
809
  const emittedOutputs = new Set<string>();
444
810
  const ordered: DiagnosticDerivedMeasureDefinition[] = [];
445
811
  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));
812
+ const ready = [...remaining.values()]
813
+ .filter((derivation) =>
814
+ expressionDependencies(derivation.expression)
815
+ .filter((id) => byOutput.has(id))
816
+ .every((id) => emittedOutputs.has(id)),
817
+ )
818
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
819
+ if (ready.length === 0)
820
+ return [...derivations].sort((left, right) =>
821
+ compareCodeUnits(left.id, right.id),
822
+ );
452
823
  for (const derivation of ready) {
453
824
  ordered.push(derivation);
454
825
  emittedOutputs.add(derivation.outputMeasureId);
@@ -458,7 +829,10 @@ function topologicalDerivations(
458
829
  return ordered;
459
830
  }
460
831
 
461
- function normalizeReviewRule(rule: DiagnosticReviewRule): NormalizedDiagnosticReviewRuleIdentity {
832
+ function normalizeReviewRule(
833
+ rule: DiagnosticReviewRule,
834
+ axis: DiagnosticPeriodAxis,
835
+ ): NormalizedDiagnosticReviewRuleIdentity {
462
836
  const base: NormalizedDiagnosticReviewRuleBase = {
463
837
  id: rule.id,
464
838
  code: rule.code,
@@ -508,8 +882,21 @@ function normalizeReviewRule(rule: DiagnosticReviewRule): NormalizedDiagnosticRe
508
882
  kind: "control-total",
509
883
  expression: normalizeMeasureExpression(rule.expression),
510
884
  expected: Object.is(rule.expected, -0) ? 0 : rule.expected,
511
- filter: rule.filter === undefined ? null : normalizeReviewFilter(rule.filter),
512
- projection: normalizeJsonValue(rule.projection),
885
+ filter:
886
+ rule.filter === undefined
887
+ ? null
888
+ : normalizeReviewFilter(rule.filter, axis),
889
+ projection:
890
+ rule.projection.kind === "valuation"
891
+ ? {
892
+ kind: "valuation",
893
+ valuation: normalizeDiagnosticPeriodWithAxis(
894
+ axis,
895
+ "valuation",
896
+ rule.projection.valuation,
897
+ )!.label,
898
+ }
899
+ : normalizeJsonValue(rule.projection),
513
900
  };
514
901
  }
515
902
 
@@ -521,80 +908,107 @@ export function normalizeDiagnosticDefinition(
521
908
  id: definition.id,
522
909
  version: definition.version,
523
910
  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)),
911
+ measures: definition.measures
912
+ .map((measure) => ({
913
+ id: measure.id,
914
+ displayName: measure.displayName,
915
+ description: measure.description,
916
+ source: measure.source,
917
+ kind: measure.kind,
918
+ unit: measure.unit,
919
+ developmentSemantics: measure.developmentSemantics,
920
+ aggregation: "sum" as const,
921
+ missing: measure.missing,
922
+ basisId: measure.basisId ?? null,
923
+ countPopulationId: measure.countPopulationId ?? null,
924
+ exposureBasisId: measure.exposureBasisId ?? null,
925
+ exposureTiming: measure.exposureTiming ?? null,
926
+ }))
927
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
928
+ countPopulations: definition.countPopulations
929
+ .map((population) => ({
930
+ id: population.id,
931
+ displayName: population.displayName,
932
+ subject: population.subject,
933
+ unit: population.unit,
934
+ description: population.description,
935
+ attributes: attributes(population.attributes),
936
+ }))
937
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
938
+ exposureBases: definition.exposureBases
939
+ .map((basis) => ({
940
+ id: basis.id,
941
+ displayName: basis.displayName,
942
+ basis: basis.basis,
943
+ unit: basis.unit,
944
+ description: basis.description,
945
+ sourceDescription: basis.sourceDescription ?? null,
946
+ attributes: attributes(basis.attributes),
947
+ }))
948
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
949
+ amountBases: definition.amountBases
950
+ .map((basis) => ({
951
+ id: basis.id,
952
+ displayName: basis.displayName,
953
+ currency: basis.currency,
954
+ perspective: basis.perspective,
955
+ components: basis.components
956
+ .map((component) => ({
957
+ id: component.id,
958
+ treatment: component.treatment,
959
+ limitation: normalizeLimitation(component.limitation),
960
+ }))
961
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
962
+ sourceDescription: basis.sourceDescription ?? null,
963
+ attributes: attributes(basis.attributes),
964
+ }))
965
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
966
+ derivedMeasures: topologicalDerivations(definition.derivedMeasures).map(
967
+ (derivation) => ({
968
+ id: derivation.id,
969
+ outputMeasureId: derivation.outputMeasureId,
970
+ expression: normalizeMeasureExpression(derivation.expression),
971
+ }),
972
+ ),
973
+ formulas: definition.formulas
974
+ .map((formula) => ({
975
+ id: formula.id,
976
+ version: formula.version,
977
+ roles: sortedRecord(
978
+ Object.entries(formula.roles).map(
979
+ ([name, role]) =>
980
+ [
981
+ name,
982
+ {
983
+ kind: role.kind,
984
+ compatibilityGroup: role.compatibilityGroup ?? null,
985
+ developmentSemantics: role.developmentSemantics ?? null,
986
+ },
987
+ ] as const,
988
+ ),
989
+ ),
990
+ numerator: normalizeRoleExpression(formula.numerator),
991
+ denominator: normalizeRoleExpression(formula.denominator),
992
+ denominatorPolicy: "positive-or-null" as const,
993
+ }))
994
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
586
995
  instances: definition.instances.map((instance) => ({
587
996
  id: instance.id,
588
997
  version: instance.version,
589
998
  formulaId: instance.formulaId,
590
- bindings: sortedRecord(Object.entries(instance.bindings).map(([role, expression]) =>
591
- [role, normalizeMeasureExpression(expression)] as const,
592
- )),
999
+ bindings: sortedRecord(
1000
+ Object.entries(instance.bindings).map(
1001
+ ([role, expression]) =>
1002
+ [role, normalizeMeasureExpression(expression)] as const,
1003
+ ),
1004
+ ),
593
1005
  presentation: {
594
1006
  displayName: instance.presentation.displayName,
595
1007
  description: instance.presentation.description,
596
1008
  displayUnit: instance.presentation.displayUnit,
597
- scale: Object.is(instance.presentation.scale, -0) ? 0 : instance.presentation.scale,
1009
+ scale: Object.is(instance.presentation.scale, -0)
1010
+ ? 0
1011
+ : instance.presentation.scale,
598
1012
  numeratorLabel: instance.presentation.numeratorLabel,
599
1013
  denominatorLabel: instance.presentation.denominatorLabel,
600
1014
  },
@@ -611,7 +1025,9 @@ export function normalizeDiagnosticDefinition(
611
1025
  },
612
1026
  })),
613
1027
  })),
614
- reviewRules: definition.reviewRules.map(normalizeReviewRule),
1028
+ reviewRules: definition.reviewRules.map((rule) =>
1029
+ normalizeReviewRule(rule, definition.periodAxis),
1030
+ ),
615
1031
  periodAxis: normalizePeriodAxis(definition.periodAxis),
616
1032
  };
617
1033
  return deepFreeze(normalized);
@@ -624,60 +1040,116 @@ export function normalizeDiagnosticSourceLocation(
624
1040
  artifactId: source.artifactId,
625
1041
  sourceFile: source.sourceFile ?? null,
626
1042
  sourceSheet: source.sourceSheet ?? null,
627
- sourceRow: source.sourceRow === undefined ? null : Object.is(source.sourceRow, -0) ? 0 : source.sourceRow,
1043
+ sourceRow:
1044
+ source.sourceRow === undefined
1045
+ ? null
1046
+ : Object.is(source.sourceRow, -0)
1047
+ ? 0
1048
+ : source.sourceRow,
628
1049
  sourceCell: source.sourceCell ?? null,
629
1050
  });
630
1051
  }
631
1052
 
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
- ]);
1053
+ function expressionDependencies(
1054
+ expression: DiagnosticMeasureExpression | DiagnosticClaimExpression,
1055
+ ): readonly string[] {
1056
+ if (expression.op === "measure" || expression.op === "claim-layer")
1057
+ return [expression.measureId];
1058
+ if (expression.op === "subtract")
1059
+ return sortedUnique([
1060
+ ...expressionDependencies(expression.left),
1061
+ ...expressionDependencies(expression.right),
1062
+ ]);
638
1063
  return sortedUnique(expression.terms.flatMap(expressionDependencies));
639
1064
  }
640
1065
 
641
- function ruleMeasureDependencies(operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>): readonly string[] {
642
- return operand.source === "measure" ? expressionDependencies(operand.expression) : [];
1066
+ function ruleMeasureDependencies(
1067
+ operand: DiagnosticDeepReadonly<DiagnosticRuleOperand>,
1068
+ ): readonly string[] {
1069
+ return operand.source === "measure"
1070
+ ? expressionDependencies(operand.expression)
1071
+ : [];
643
1072
  }
644
1073
 
645
1074
  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
- }))}`;
1075
+ return `fnv1a64-jcs-v1:${fnv1a64(
1076
+ canonicalJson({
1077
+ identityVersion: 1,
1078
+ kind,
1079
+ [property]: value,
1080
+ }),
1081
+ )}`;
651
1082
  }
652
1083
 
653
1084
  export interface BuiltDiagnosticIdentities {
654
1085
  readonly formulaFingerprints: Readonly<Record<string, string>>;
655
1086
  readonly calculationFingerprints: Readonly<Record<string, string>>;
656
1087
  readonly definitionIntegrity: string;
657
- readonly calculationScopesByInstanceId: ReadonlyMap<string, NormalizedDiagnosticCalculationScope>;
658
- readonly calculationDependenciesByInstanceId: ReadonlyMap<string, readonly string[]>;
659
- readonly evaluationDependenciesByInstanceId: ReadonlyMap<string, readonly string[]>;
1088
+ readonly calculationScopesByInstanceId: ReadonlyMap<
1089
+ string,
1090
+ NormalizedDiagnosticCalculationScope
1091
+ >;
1092
+ readonly calculationDependenciesByInstanceId: ReadonlyMap<
1093
+ string,
1094
+ readonly string[]
1095
+ >;
1096
+ readonly evaluationDependenciesByInstanceId: ReadonlyMap<
1097
+ string,
1098
+ readonly string[]
1099
+ >;
660
1100
  }
661
1101
 
662
1102
  export function buildDiagnosticIdentities(
663
1103
  definition: DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity>,
664
1104
  ): 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
- ));
1105
+ const formulas = new Map(
1106
+ definition.formulas.map((formula) => [formula.id, formula]),
1107
+ );
1108
+ const measures = new Map(
1109
+ definition.measures.map((measure) => [measure.id, measure]),
1110
+ );
1111
+ const derivations = new Map(
1112
+ definition.derivedMeasures.map((derivation) => [
1113
+ derivation.outputMeasureId,
1114
+ derivation,
1115
+ ]),
1116
+ );
1117
+ const populations = new Map(
1118
+ definition.countPopulations.map((population) => [
1119
+ population.id,
1120
+ population,
1121
+ ]),
1122
+ );
1123
+ const exposureBases = new Map(
1124
+ definition.exposureBases.map((basis) => [basis.id, basis]),
1125
+ );
1126
+ const amountBases = new Map(
1127
+ definition.amountBases.map((basis) => [basis.id, basis]),
1128
+ );
1129
+
1130
+ const formulaFingerprints = sortedRecord(
1131
+ definition.formulas.map(
1132
+ (formula) =>
1133
+ [formula.id, tag("diagnostic-formula", "formula", formula)] as const,
1134
+ ),
1135
+ );
675
1136
  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[]>();
1137
+ const calculationScopesByInstanceId = new Map<
1138
+ string,
1139
+ NormalizedDiagnosticCalculationScope
1140
+ >();
1141
+ const calculationDependenciesByInstanceId = new Map<
1142
+ string,
1143
+ readonly string[]
1144
+ >();
1145
+ const evaluationDependenciesByInstanceId = new Map<
1146
+ string,
1147
+ readonly string[]
1148
+ >();
679
1149
 
680
- const transitiveDependencies = (roots: readonly string[]): readonly string[] => {
1150
+ const transitiveDependencies = (
1151
+ roots: readonly string[],
1152
+ ): readonly string[] => {
681
1153
  const found = new Set<string>();
682
1154
  const stack = [...roots];
683
1155
  while (stack.length > 0) {
@@ -685,7 +1157,8 @@ export function buildDiagnosticIdentities(
685
1157
  if (found.has(id)) continue;
686
1158
  found.add(id);
687
1159
  const derivation = derivations.get(id);
688
- if (derivation) stack.push(...expressionDependencies(derivation.expression));
1160
+ if (derivation)
1161
+ stack.push(...expressionDependencies(derivation.expression));
689
1162
  }
690
1163
  return [...found].sort(compareCodeUnits);
691
1164
  };
@@ -701,14 +1174,34 @@ export function buildDiagnosticIdentities(
701
1174
  ...ruleMeasureDependencies(rule.when.right),
702
1175
  ]),
703
1176
  ]);
704
- calculationDependenciesByInstanceId.set(instance.id, calculationDependencies);
1177
+ calculationDependenciesByInstanceId.set(
1178
+ instance.id,
1179
+ calculationDependencies,
1180
+ );
705
1181
  evaluationDependenciesByInstanceId.set(instance.id, evaluationDependencies);
706
1182
 
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]));
1183
+ const selectedMeasures = calculationDependencies
1184
+ .map((id) => measures.get(id)!)
1185
+ .filter(Boolean);
1186
+ const selectedDerivations = definition.derivedMeasures.filter(
1187
+ (derivation) =>
1188
+ calculationDependencies.includes(derivation.outputMeasureId),
1189
+ );
1190
+ const countPopulationIds = sortedUnique(
1191
+ selectedMeasures.flatMap((measure) =>
1192
+ measure.countPopulationId === null ? [] : [measure.countPopulationId],
1193
+ ),
1194
+ );
1195
+ const exposureBasisIds = sortedUnique(
1196
+ selectedMeasures.flatMap((measure) =>
1197
+ measure.exposureBasisId === null ? [] : [measure.exposureBasisId],
1198
+ ),
1199
+ );
1200
+ const amountBasisIds = sortedUnique(
1201
+ selectedMeasures.flatMap((measure) =>
1202
+ measure.basisId === null ? [] : [measure.basisId],
1203
+ ),
1204
+ );
712
1205
  const scope: NormalizedDiagnosticCalculationScope = {
713
1206
  formulaFingerprint: formulaFingerprints[instance.formulaId]!,
714
1207
  instance: {
@@ -733,11 +1226,21 @@ export function buildDiagnosticIdentities(
733
1226
  })),
734
1227
  countPopulations: countPopulationIds.map((id) => {
735
1228
  const population = populations.get(id)!;
736
- return { id, subject: population.subject, unit: population.unit, attributes: population.attributes };
1229
+ return {
1230
+ id,
1231
+ subject: population.subject,
1232
+ unit: population.unit,
1233
+ attributes: population.attributes,
1234
+ };
737
1235
  }),
738
1236
  exposureBases: exposureBasisIds.map((id) => {
739
1237
  const basis = exposureBases.get(id)!;
740
- return { id, basis: basis.basis, unit: basis.unit, attributes: basis.attributes };
1238
+ return {
1239
+ id,
1240
+ basis: basis.basis,
1241
+ unit: basis.unit,
1242
+ attributes: basis.attributes,
1243
+ };
741
1244
  }),
742
1245
  amountBases: amountBasisIds.map((id) => {
743
1246
  const basis = amountBases.get(id)!;
@@ -753,12 +1256,18 @@ export function buildDiagnosticIdentities(
753
1256
  };
754
1257
  const frozenScope = deepFreeze(scope);
755
1258
  calculationScopesByInstanceId.set(instance.id, frozenScope);
756
- calculationFingerprints[instance.id] = tag("diagnostic-calculation", "calculation", frozenScope);
1259
+ calculationFingerprints[instance.id] = tag(
1260
+ "diagnostic-calculation",
1261
+ "calculation",
1262
+ frozenScope,
1263
+ );
757
1264
  }
758
1265
 
759
1266
  return {
760
1267
  formulaFingerprints: deepFreeze(formulaFingerprints),
761
- calculationFingerprints: deepFreeze(sortedRecord(Object.entries(calculationFingerprints))),
1268
+ calculationFingerprints: deepFreeze(
1269
+ sortedRecord(Object.entries(calculationFingerprints)),
1270
+ ),
762
1271
  definitionIntegrity: tag("diagnostic-definition", "definition", definition),
763
1272
  calculationScopesByInstanceId,
764
1273
  calculationDependenciesByInstanceId,