@actuarial-ts/core 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +65 -226
  2. package/dist/capping.d.ts.map +1 -1
  3. package/dist/capping.js +39 -6
  4. package/dist/capping.js.map +1 -1
  5. package/dist/casualtyDiagnostics.d.ts +19 -49
  6. package/dist/casualtyDiagnostics.d.ts.map +1 -1
  7. package/dist/casualtyDiagnostics.js +84 -116
  8. package/dist/casualtyDiagnostics.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +32 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -0
  11. package/dist/diagnosticAggregation.js +73 -0
  12. package/dist/diagnosticAggregation.js.map +1 -0
  13. package/dist/diagnosticDefinitions.d.ts +289 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -0
  15. package/dist/diagnosticDefinitions.js +1688 -0
  16. package/dist/diagnosticDefinitions.js.map +1 -0
  17. package/dist/diagnosticDerivations.d.ts +35 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -0
  19. package/dist/diagnosticDerivations.js +224 -0
  20. package/dist/diagnosticDerivations.js.map +1 -0
  21. package/dist/diagnosticExposure.d.ts +51 -0
  22. package/dist/diagnosticExposure.d.ts.map +1 -0
  23. package/dist/diagnosticExposure.js +273 -0
  24. package/dist/diagnosticExposure.js.map +1 -0
  25. package/dist/diagnosticExpressions.d.ts +56 -0
  26. package/dist/diagnosticExpressions.d.ts.map +1 -0
  27. package/dist/diagnosticExpressions.js +124 -0
  28. package/dist/diagnosticExpressions.js.map +1 -0
  29. package/dist/diagnosticFormulas.d.ts +198 -0
  30. package/dist/diagnosticFormulas.d.ts.map +1 -0
  31. package/dist/diagnosticFormulas.js +219 -0
  32. package/dist/diagnosticFormulas.js.map +1 -0
  33. package/dist/diagnosticIdentity.d.ts +251 -0
  34. package/dist/diagnosticIdentity.d.ts.map +1 -0
  35. package/dist/diagnosticIdentity.js +721 -0
  36. package/dist/diagnosticIdentity.js.map +1 -0
  37. package/dist/diagnosticOrdering.d.ts +8 -0
  38. package/dist/diagnosticOrdering.d.ts.map +1 -0
  39. package/dist/diagnosticOrdering.js +73 -0
  40. package/dist/diagnosticOrdering.js.map +1 -0
  41. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  42. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  43. package/dist/diagnosticPeriodAxis.js +69 -0
  44. package/dist/diagnosticPeriodAxis.js.map +1 -0
  45. package/dist/diagnosticPeriods.d.ts +15 -0
  46. package/dist/diagnosticPeriods.d.ts.map +1 -0
  47. package/dist/diagnosticPeriods.js +50 -0
  48. package/dist/diagnosticPeriods.js.map +1 -0
  49. package/dist/diagnosticPreparation.d.ts +131 -0
  50. package/dist/diagnosticPreparation.d.ts.map +1 -0
  51. package/dist/diagnosticPreparation.js +1539 -0
  52. package/dist/diagnosticPreparation.js.map +1 -0
  53. package/dist/diagnosticReview.d.ts +66 -0
  54. package/dist/diagnosticReview.d.ts.map +1 -0
  55. package/dist/diagnosticReview.js +490 -0
  56. package/dist/diagnosticReview.js.map +1 -0
  57. package/dist/diagnosticRules.d.ts +32 -0
  58. package/dist/diagnosticRules.d.ts.map +1 -0
  59. package/dist/diagnosticRules.js +32 -0
  60. package/dist/diagnosticRules.js.map +1 -0
  61. package/dist/diagnosticRunner.d.ts +86 -0
  62. package/dist/diagnosticRunner.d.ts.map +1 -0
  63. package/dist/diagnosticRunner.js +703 -0
  64. package/dist/diagnosticRunner.js.map +1 -0
  65. package/dist/diagnosticRuntime.d.ts +26 -0
  66. package/dist/diagnosticRuntime.d.ts.map +1 -0
  67. package/dist/diagnosticRuntime.js +243 -0
  68. package/dist/diagnosticRuntime.js.map +1 -0
  69. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  70. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  71. package/dist/diagnosticSourceOrdering.js +38 -0
  72. package/dist/diagnosticSourceOrdering.js.map +1 -0
  73. package/dist/index.d.ts +28 -1
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +16 -1
  76. package/dist/index.js.map +1 -1
  77. package/dist/mack.d.ts +4 -0
  78. package/dist/mack.d.ts.map +1 -1
  79. package/dist/mack.js +32 -4
  80. package/dist/mack.js.map +1 -1
  81. package/dist/triangle.d.ts.map +1 -1
  82. package/dist/triangle.js +59 -9
  83. package/dist/triangle.js.map +1 -1
  84. package/dist/types.d.ts +21 -0
  85. package/dist/types.d.ts.map +1 -1
  86. package/dist/types.js +128 -0
  87. package/dist/types.js.map +1 -1
  88. package/dist/version.d.ts +3 -0
  89. package/dist/version.d.ts.map +1 -0
  90. package/dist/version.js +3 -0
  91. package/dist/version.js.map +1 -0
  92. package/package.json +4 -3
  93. package/src/capping.ts +109 -18
  94. package/src/casualtyDiagnostics.ts +79 -189
  95. package/src/diagnosticAggregation.ts +128 -0
  96. package/src/diagnosticDefinitions.ts +3624 -0
  97. package/src/diagnosticDerivations.ts +321 -0
  98. package/src/diagnosticExposure.ts +482 -0
  99. package/src/diagnosticExpressions.ts +255 -0
  100. package/src/diagnosticFormulas.ts +350 -0
  101. package/src/diagnosticIdentity.ts +1267 -0
  102. package/src/diagnosticOrdering.ts +106 -0
  103. package/src/diagnosticPeriodAxis.ts +92 -0
  104. package/src/diagnosticPeriods.ts +102 -0
  105. package/src/diagnosticPreparation.ts +2475 -0
  106. package/src/diagnosticReview.ts +897 -0
  107. package/src/diagnosticRules.ts +69 -0
  108. package/src/diagnosticRunner.ts +1114 -0
  109. package/src/diagnosticRuntime.ts +264 -0
  110. package/src/diagnosticSourceOrdering.ts +50 -0
  111. package/src/index.ts +184 -1
  112. package/src/mack.ts +72 -9
  113. package/src/triangle.ts +103 -13
  114. package/src/types.ts +196 -0
  115. package/src/version.ts +2 -0
  116. package/dist/metricDiagnostics.d.ts +0 -212
  117. package/dist/metricDiagnostics.d.ts.map +0 -1
  118. package/dist/metricDiagnostics.js +0 -627
  119. package/dist/metricDiagnostics.js.map +0 -1
  120. package/src/metricDiagnostics.ts +0 -876
@@ -0,0 +1,721 @@
1
+ import { canonicalJson, fnv1a64 } from "./canonical.js";
2
+ import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
3
+ import { diagnosticJsonPreflight, isDiagnosticPlainRecord, isDiagnosticToken, normalizeDiagnosticNumber, } from "./diagnosticRuntime.js";
4
+ import { DiagnosticValidationError, } from "./types.js";
5
+ /** Validates selector shapes without interpreting periods against a definition. */
6
+ function validateFilterIdentityInput(filter) {
7
+ const issues = [
8
+ ...diagnosticJsonPreflight(filter, "configuration"),
9
+ ];
10
+ if (issues.length)
11
+ throw new DiagnosticValidationError(issues);
12
+ if (!isDiagnosticPlainRecord(filter))
13
+ throw new DiagnosticValidationError([
14
+ {
15
+ domain: "configuration",
16
+ code: "invalid-type",
17
+ path: "$",
18
+ message: "Filter must be a plain object or null",
19
+ },
20
+ ]);
21
+ const lists = [
22
+ "sourceGroups", "outputGroups", "origins", "valuations", "instanceIds",
23
+ ];
24
+ const labels = [
25
+ "originFrom", "originThrough", "valuationFrom", "valuationThrough",
26
+ ];
27
+ const bounds = ["minDevelopmentAge", "maxDevelopmentAge"];
28
+ const checkToken = (value, path) => {
29
+ if (!isDiagnosticToken(value))
30
+ issues.push({
31
+ domain: "configuration",
32
+ code: "invalid-string",
33
+ path,
34
+ message: "Filter selectors must use nonempty token strings",
35
+ });
36
+ };
37
+ for (const key of Object.keys(filter).sort()) {
38
+ const value = filter[key];
39
+ const path = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
40
+ ? `$.${key}`
41
+ : `$[${JSON.stringify(key)}]`;
42
+ if (lists.includes(key)) {
43
+ if (!Array.isArray(value))
44
+ issues.push({
45
+ domain: "configuration",
46
+ code: "invalid-type",
47
+ path,
48
+ message: "Filter selector must be an array",
49
+ });
50
+ else
51
+ value.forEach((item, index) => checkToken(item, `${path}[${index}]`));
52
+ }
53
+ else if (labels.includes(key))
54
+ checkToken(value, path);
55
+ else if (bounds.includes(key)) {
56
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
57
+ issues.push({
58
+ domain: "configuration",
59
+ code: "invalid-number",
60
+ path,
61
+ message: "Development-age bounds must be nonnegative safe integers",
62
+ });
63
+ }
64
+ else
65
+ issues.push({
66
+ domain: "configuration",
67
+ code: "unknown-key",
68
+ path,
69
+ message: `Unknown filter key ${key}`,
70
+ });
71
+ }
72
+ if (typeof filter.minDevelopmentAge === "number" &&
73
+ typeof filter.maxDevelopmentAge === "number" &&
74
+ filter.minDevelopmentAge > filter.maxDevelopmentAge)
75
+ issues.push({
76
+ domain: "configuration",
77
+ code: "invalid-configuration",
78
+ path: "$",
79
+ message: "Minimum development age exceeds maximum",
80
+ });
81
+ if (issues.length)
82
+ throw new DiagnosticValidationError(issues);
83
+ }
84
+ /** Materializes optional selectors; period aliases must already be canonical. */
85
+ export function normalizeDiagnosticsFilterIdentity(filter) {
86
+ if (filter === null)
87
+ return null;
88
+ validateFilterIdentityInput(filter);
89
+ return deepFreeze({
90
+ sourceGroups: filter.sourceGroups === undefined
91
+ ? null
92
+ : sortedUnique(filter.sourceGroups),
93
+ outputGroups: filter.outputGroups === undefined
94
+ ? null
95
+ : sortedUnique(filter.outputGroups),
96
+ origins: filter.origins === undefined ? null : sortedUnique(filter.origins),
97
+ originFrom: filter.originFrom ?? null,
98
+ originThrough: filter.originThrough ?? null,
99
+ valuations: filter.valuations === undefined ? null : sortedUnique(filter.valuations),
100
+ valuationFrom: filter.valuationFrom ?? null,
101
+ valuationThrough: filter.valuationThrough ?? null,
102
+ minDevelopmentAge: filter.minDevelopmentAge === undefined
103
+ ? null
104
+ : normalizeDiagnosticNumber(filter.minDevelopmentAge),
105
+ maxDevelopmentAge: filter.maxDevelopmentAge === undefined
106
+ ? null
107
+ : normalizeDiagnosticNumber(filter.maxDevelopmentAge),
108
+ instanceIds: filter.instanceIds === undefined ? null : sortedUnique(filter.instanceIds),
109
+ });
110
+ }
111
+ /**
112
+ * Owned identity copy. Source slots are schema-defined; free JSON dimensions
113
+ * must not be interpreted as source evidence just because a key looks similar.
114
+ */
115
+ export function projectDiagnosticIdentity(value) {
116
+ const issues = diagnosticJsonPreflight(value, "input", {
117
+ maxNodes: Number.MAX_SAFE_INTEGER,
118
+ });
119
+ if (issues.length)
120
+ throw new DiagnosticValidationError(issues);
121
+ const clone = (item, sourceSlot = false, freeJson = false, path = "$") => {
122
+ if (item === null || typeof item !== "object")
123
+ return typeof item === "number" && Object.is(item, -0) ? 0 : item;
124
+ if (Array.isArray(item))
125
+ return Object.freeze(item.map((child, index) => clone(child, sourceSlot, freeJson, `${path}[${index}]`)));
126
+ const record = item;
127
+ if (sourceSlot && typeof record.artifactId === "string") {
128
+ const sourceIssues = [];
129
+ for (const key of Object.keys(record)) {
130
+ const fieldPath = `${path}.${key}`;
131
+ if (![
132
+ "artifactId",
133
+ "sourceFile",
134
+ "sourceSheet",
135
+ "sourceRow",
136
+ "sourceCell",
137
+ ].includes(key))
138
+ sourceIssues.push({
139
+ domain: "input",
140
+ code: "unknown-key",
141
+ path: fieldPath,
142
+ message: `Unknown source-location key ${key}`,
143
+ });
144
+ else if (key === "sourceRow") {
145
+ if (record[key] !== null &&
146
+ !(typeof record[key] === "number" &&
147
+ Number.isSafeInteger(record[key]) &&
148
+ record[key] >= 0))
149
+ sourceIssues.push({
150
+ domain: "input",
151
+ code: "invalid-number",
152
+ path: fieldPath,
153
+ message: "Source row must be a nonnegative safe integer or null",
154
+ });
155
+ }
156
+ else if (!(key !== "artifactId" && record[key] === null) &&
157
+ !isDiagnosticToken(record[key]))
158
+ sourceIssues.push({
159
+ domain: "input",
160
+ code: "invalid-string",
161
+ path: fieldPath,
162
+ message: "Source location must use nonempty token strings",
163
+ });
164
+ }
165
+ if (sourceIssues.length)
166
+ throw new DiagnosticValidationError(sourceIssues);
167
+ return normalizeDiagnosticSourceLocation(record);
168
+ }
169
+ const result = Object.create(null);
170
+ for (const key of Object.keys(record).sort()) {
171
+ const opaque = freeJson || key === "groupDimensions" || key === "dimensions";
172
+ result[key] = clone(record[key], !opaque && (key === "source" || key === "sources"), opaque, `${path}.${key}`);
173
+ }
174
+ return Object.freeze(result);
175
+ };
176
+ return clone(value);
177
+ }
178
+ function compareCodeUnits(left, right) {
179
+ return left === right ? 0 : left < right ? -1 : 1;
180
+ }
181
+ function sortedUnique(values) {
182
+ return [...new Set(values)].sort(compareCodeUnits);
183
+ }
184
+ function sortedRecord(entries) {
185
+ const result = Object.create(null);
186
+ for (const [key, value] of [...entries].sort(([left], [right]) => compareCodeUnits(left, right))) {
187
+ result[key] = value;
188
+ }
189
+ return result;
190
+ }
191
+ function normalizeJsonValue(value) {
192
+ if (typeof value === "number" && Object.is(value, -0))
193
+ return 0;
194
+ if (Array.isArray(value))
195
+ return value.map((item) => normalizeJsonValue(item));
196
+ if (value !== null && typeof value === "object") {
197
+ return sortedRecord(Object.entries(value).map(([key, item]) => [key, normalizeJsonValue(item)]));
198
+ }
199
+ return value;
200
+ }
201
+ function deepFreeze(value, seen = new WeakSet()) {
202
+ if (value === null || typeof value !== "object")
203
+ return value;
204
+ if (seen.has(value))
205
+ return value;
206
+ seen.add(value);
207
+ for (const child of Object.values(value))
208
+ deepFreeze(child, seen);
209
+ return Object.freeze(value);
210
+ }
211
+ function attributes(value) {
212
+ return sortedRecord(Object.entries(value ?? {}).map(([key, item]) => [key, normalizeJsonValue(item)]));
213
+ }
214
+ function normalizeLimitation(limitation) {
215
+ if (limitation.kind === "unlimited")
216
+ return { kind: "unlimited" };
217
+ if (limitation.kind === "unknown") {
218
+ return { kind: "unknown", description: limitation.description ?? null };
219
+ }
220
+ return {
221
+ kind: limitation.kind,
222
+ attachment: Object.is(limitation.attachment, -0)
223
+ ? 0
224
+ : limitation.attachment,
225
+ limit: limitation.limit !== null && Object.is(limitation.limit, -0)
226
+ ? 0
227
+ : limitation.limit,
228
+ application: limitation.application,
229
+ derivation: limitation.derivation.kind === "sdk"
230
+ ? { kind: "sdk" }
231
+ : {
232
+ kind: "external",
233
+ actor: limitation.derivation.actor,
234
+ transformationRef: limitation.derivation.transformationRef,
235
+ },
236
+ };
237
+ }
238
+ function normalizeMeasureExpression(expression) {
239
+ if (expression.op === "measure")
240
+ return {
241
+ op: "measure",
242
+ measureId: expression.measureId,
243
+ };
244
+ if (expression.op === "claim-layer") {
245
+ return {
246
+ op: "claim-layer",
247
+ measureId: expression.measureId,
248
+ attachment: Object.is(expression.attachment, -0)
249
+ ? 0
250
+ : expression.attachment,
251
+ limit: expression.limit !== null && Object.is(expression.limit, -0)
252
+ ? 0
253
+ : expression.limit,
254
+ };
255
+ }
256
+ if (expression.op === "subtract") {
257
+ return {
258
+ op: "subtract",
259
+ left: normalizeMeasureExpression(expression.left),
260
+ right: normalizeMeasureExpression(expression.right),
261
+ };
262
+ }
263
+ return {
264
+ op: "add",
265
+ terms: expression.terms.map((term) => normalizeMeasureExpression(term)),
266
+ };
267
+ }
268
+ function normalizeRoleExpression(expression) {
269
+ if (expression.op === "role")
270
+ return { op: "role", role: expression.role };
271
+ if (expression.op === "subtract") {
272
+ return {
273
+ op: "subtract",
274
+ left: normalizeRoleExpression(expression.left),
275
+ right: normalizeRoleExpression(expression.right),
276
+ };
277
+ }
278
+ return { op: "add", terms: expression.terms.map(normalizeRoleExpression) };
279
+ }
280
+ function normalizeTolerance(tolerance) {
281
+ return {
282
+ absolute: Object.is(tolerance?.absolute ?? 0, -0)
283
+ ? 0
284
+ : (tolerance?.absolute ?? 0),
285
+ relative: Object.is(tolerance?.relative ?? 0, -0)
286
+ ? 0
287
+ : (tolerance?.relative ?? 0),
288
+ };
289
+ }
290
+ function normalizeRuleOperand(operand) {
291
+ if (operand.source === "measure") {
292
+ return {
293
+ source: "measure",
294
+ expression: normalizeMeasureExpression(operand.expression),
295
+ };
296
+ }
297
+ if (operand.source === "calculation")
298
+ return { source: "calculation", field: operand.field };
299
+ return {
300
+ source: "constant",
301
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
302
+ };
303
+ }
304
+ function normalizeReviewOperand(operand) {
305
+ if (operand.op === "constant")
306
+ return {
307
+ op: "constant",
308
+ value: Object.is(operand.value, -0) ? 0 : operand.value,
309
+ };
310
+ return normalizeMeasureExpression(operand);
311
+ }
312
+ function normalizeReviewFilter(filter, axis) {
313
+ const normalizeMany = (side, values) => values === undefined
314
+ ? null
315
+ : sortedUnique(values.map((value) => normalizeDiagnosticPeriodWithAxis(axis, side, value).label));
316
+ const normalizeOne = (side, value) => value === undefined
317
+ ? null
318
+ : normalizeDiagnosticPeriodWithAxis(axis, side, value).label;
319
+ return {
320
+ sourceGroups: filter.sourceGroups === undefined
321
+ ? null
322
+ : sortedUnique(filter.sourceGroups),
323
+ origins: normalizeMany("origin", filter.origins),
324
+ originFrom: normalizeOne("origin", filter.originFrom),
325
+ originThrough: normalizeOne("origin", filter.originThrough),
326
+ valuations: normalizeMany("valuation", filter.valuations),
327
+ valuationFrom: normalizeOne("valuation", filter.valuationFrom),
328
+ valuationThrough: normalizeOne("valuation", filter.valuationThrough),
329
+ minDevelopmentAge: filter.minDevelopmentAge === undefined
330
+ ? null
331
+ : Object.is(filter.minDevelopmentAge, -0)
332
+ ? 0
333
+ : filter.minDevelopmentAge,
334
+ maxDevelopmentAge: filter.maxDevelopmentAge === undefined
335
+ ? null
336
+ : Object.is(filter.maxDevelopmentAge, -0)
337
+ ? 0
338
+ : filter.maxDevelopmentAge,
339
+ };
340
+ }
341
+ function normalizePeriodAxis(axis) {
342
+ if (axis.kind === "calendar") {
343
+ return {
344
+ kind: "calendar",
345
+ originCadence: axis.originCadence,
346
+ valuationCadence: axis.valuationCadence,
347
+ originAnchor: axis.originAnchor,
348
+ valuationAnchor: axis.valuationAnchor,
349
+ ageUnit: "month",
350
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
351
+ };
352
+ }
353
+ const coordinates = (values) => values
354
+ .map((coordinate) => ({
355
+ label: coordinate.label,
356
+ aliases: sortedUnique(coordinate.aliases ?? []),
357
+ coordinate: Object.is(coordinate.coordinate, -0)
358
+ ? 0
359
+ : coordinate.coordinate,
360
+ }))
361
+ .sort((left, right) => left.coordinate - right.coordinate ||
362
+ compareCodeUnits(left.label, right.label));
363
+ return {
364
+ kind: "ordered",
365
+ id: axis.id,
366
+ version: axis.version,
367
+ ageUnit: axis.ageUnit,
368
+ ageOffset: Object.is(axis.ageOffset, -0) ? 0 : axis.ageOffset,
369
+ origins: coordinates(axis.origins),
370
+ valuations: coordinates(axis.valuations),
371
+ };
372
+ }
373
+ function topologicalDerivations(derivations) {
374
+ const byOutput = new Map(derivations.map((derivation) => [derivation.outputMeasureId, derivation]));
375
+ const remaining = new Map(derivations.map((derivation) => [derivation.id, derivation]));
376
+ const emittedOutputs = new Set();
377
+ const ordered = [];
378
+ while (remaining.size > 0) {
379
+ const ready = [...remaining.values()]
380
+ .filter((derivation) => expressionDependencies(derivation.expression)
381
+ .filter((id) => byOutput.has(id))
382
+ .every((id) => emittedOutputs.has(id)))
383
+ .sort((left, right) => compareCodeUnits(left.id, right.id));
384
+ if (ready.length === 0)
385
+ return [...derivations].sort((left, right) => compareCodeUnits(left.id, right.id));
386
+ for (const derivation of ready) {
387
+ ordered.push(derivation);
388
+ emittedOutputs.add(derivation.outputMeasureId);
389
+ remaining.delete(derivation.id);
390
+ }
391
+ }
392
+ return ordered;
393
+ }
394
+ function normalizeReviewRule(rule, axis) {
395
+ const base = {
396
+ id: rule.id,
397
+ code: rule.code,
398
+ description: rule.description,
399
+ severity: rule.severity,
400
+ missingInput: rule.missingInput,
401
+ tolerance: normalizeTolerance(rule.tolerance),
402
+ };
403
+ if (rule.kind === "compare") {
404
+ return {
405
+ ...base,
406
+ kind: "compare",
407
+ when: {
408
+ left: normalizeReviewOperand(rule.when.left),
409
+ operator: rule.when.operator,
410
+ right: normalizeReviewOperand(rule.when.right),
411
+ },
412
+ };
413
+ }
414
+ if (rule.kind === "reconcile") {
415
+ return {
416
+ ...base,
417
+ kind: "reconcile",
418
+ actual: normalizeMeasureExpression(rule.actual),
419
+ expected: normalizeReviewOperand(rule.expected),
420
+ };
421
+ }
422
+ if (rule.kind === "monotonic") {
423
+ return {
424
+ ...base,
425
+ kind: "monotonic",
426
+ expression: normalizeMeasureExpression(rule.expression),
427
+ direction: rule.direction,
428
+ };
429
+ }
430
+ if (rule.kind === "layer-order") {
431
+ return {
432
+ ...base,
433
+ kind: "layer-order",
434
+ narrower: normalizeMeasureExpression(rule.narrower),
435
+ broader: normalizeMeasureExpression(rule.broader),
436
+ comparability: normalizeJsonValue(rule.comparability),
437
+ };
438
+ }
439
+ return {
440
+ ...base,
441
+ kind: "control-total",
442
+ expression: normalizeMeasureExpression(rule.expression),
443
+ expected: Object.is(rule.expected, -0) ? 0 : rule.expected,
444
+ filter: rule.filter === undefined
445
+ ? null
446
+ : normalizeReviewFilter(rule.filter, axis),
447
+ projection: rule.projection.kind === "valuation"
448
+ ? {
449
+ kind: "valuation",
450
+ valuation: normalizeDiagnosticPeriodWithAxis(axis, "valuation", rule.projection.valuation).label,
451
+ }
452
+ : normalizeJsonValue(rule.projection),
453
+ };
454
+ }
455
+ export function normalizeDiagnosticDefinition(definition) {
456
+ const normalized = {
457
+ diagnosticDefinitionVersion: "1.0.0",
458
+ id: definition.id,
459
+ version: definition.version,
460
+ lossRowGrain: definition.lossRowGrain,
461
+ measures: definition.measures
462
+ .map((measure) => ({
463
+ id: measure.id,
464
+ displayName: measure.displayName,
465
+ description: measure.description,
466
+ source: measure.source,
467
+ kind: measure.kind,
468
+ unit: measure.unit,
469
+ developmentSemantics: measure.developmentSemantics,
470
+ aggregation: "sum",
471
+ missing: measure.missing,
472
+ basisId: measure.basisId ?? null,
473
+ countPopulationId: measure.countPopulationId ?? null,
474
+ exposureBasisId: measure.exposureBasisId ?? null,
475
+ exposureTiming: measure.exposureTiming ?? null,
476
+ }))
477
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
478
+ countPopulations: definition.countPopulations
479
+ .map((population) => ({
480
+ id: population.id,
481
+ displayName: population.displayName,
482
+ subject: population.subject,
483
+ unit: population.unit,
484
+ description: population.description,
485
+ attributes: attributes(population.attributes),
486
+ }))
487
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
488
+ exposureBases: definition.exposureBases
489
+ .map((basis) => ({
490
+ id: basis.id,
491
+ displayName: basis.displayName,
492
+ basis: basis.basis,
493
+ unit: basis.unit,
494
+ description: basis.description,
495
+ sourceDescription: basis.sourceDescription ?? null,
496
+ attributes: attributes(basis.attributes),
497
+ }))
498
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
499
+ amountBases: definition.amountBases
500
+ .map((basis) => ({
501
+ id: basis.id,
502
+ displayName: basis.displayName,
503
+ currency: basis.currency,
504
+ perspective: basis.perspective,
505
+ components: basis.components
506
+ .map((component) => ({
507
+ id: component.id,
508
+ treatment: component.treatment,
509
+ limitation: normalizeLimitation(component.limitation),
510
+ }))
511
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
512
+ sourceDescription: basis.sourceDescription ?? null,
513
+ attributes: attributes(basis.attributes),
514
+ }))
515
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
516
+ derivedMeasures: topologicalDerivations(definition.derivedMeasures).map((derivation) => ({
517
+ id: derivation.id,
518
+ outputMeasureId: derivation.outputMeasureId,
519
+ expression: normalizeMeasureExpression(derivation.expression),
520
+ })),
521
+ formulas: definition.formulas
522
+ .map((formula) => ({
523
+ id: formula.id,
524
+ version: formula.version,
525
+ roles: sortedRecord(Object.entries(formula.roles).map(([name, role]) => [
526
+ name,
527
+ {
528
+ kind: role.kind,
529
+ compatibilityGroup: role.compatibilityGroup ?? null,
530
+ developmentSemantics: role.developmentSemantics ?? null,
531
+ },
532
+ ])),
533
+ numerator: normalizeRoleExpression(formula.numerator),
534
+ denominator: normalizeRoleExpression(formula.denominator),
535
+ denominatorPolicy: "positive-or-null",
536
+ }))
537
+ .sort((left, right) => compareCodeUnits(left.id, right.id)),
538
+ instances: definition.instances.map((instance) => ({
539
+ id: instance.id,
540
+ version: instance.version,
541
+ formulaId: instance.formulaId,
542
+ bindings: sortedRecord(Object.entries(instance.bindings).map(([role, expression]) => [role, normalizeMeasureExpression(expression)])),
543
+ presentation: {
544
+ displayName: instance.presentation.displayName,
545
+ description: instance.presentation.description,
546
+ displayUnit: instance.presentation.displayUnit,
547
+ scale: Object.is(instance.presentation.scale, -0)
548
+ ? 0
549
+ : instance.presentation.scale,
550
+ numeratorLabel: instance.presentation.numeratorLabel,
551
+ denominatorLabel: instance.presentation.denominatorLabel,
552
+ },
553
+ rules: instance.rules.map((rule) => ({
554
+ id: rule.id,
555
+ code: rule.code,
556
+ message: rule.message,
557
+ severity: rule.severity,
558
+ when: {
559
+ left: normalizeRuleOperand(rule.when.left),
560
+ operator: rule.when.operator,
561
+ right: normalizeRuleOperand(rule.when.right),
562
+ tolerance: normalizeTolerance(rule.when.tolerance),
563
+ },
564
+ })),
565
+ })),
566
+ reviewRules: definition.reviewRules.map((rule) => normalizeReviewRule(rule, definition.periodAxis)),
567
+ periodAxis: normalizePeriodAxis(definition.periodAxis),
568
+ };
569
+ return deepFreeze(normalized);
570
+ }
571
+ export function normalizeDiagnosticSourceLocation(source) {
572
+ return deepFreeze({
573
+ artifactId: source.artifactId,
574
+ sourceFile: source.sourceFile ?? null,
575
+ sourceSheet: source.sourceSheet ?? null,
576
+ sourceRow: source.sourceRow === undefined
577
+ ? null
578
+ : Object.is(source.sourceRow, -0)
579
+ ? 0
580
+ : source.sourceRow,
581
+ sourceCell: source.sourceCell ?? null,
582
+ });
583
+ }
584
+ function expressionDependencies(expression) {
585
+ if (expression.op === "measure" || expression.op === "claim-layer")
586
+ return [expression.measureId];
587
+ if (expression.op === "subtract")
588
+ return sortedUnique([
589
+ ...expressionDependencies(expression.left),
590
+ ...expressionDependencies(expression.right),
591
+ ]);
592
+ return sortedUnique(expression.terms.flatMap(expressionDependencies));
593
+ }
594
+ function ruleMeasureDependencies(operand) {
595
+ return operand.source === "measure"
596
+ ? expressionDependencies(operand.expression)
597
+ : [];
598
+ }
599
+ function tag(kind, property, value) {
600
+ return `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({
601
+ identityVersion: 1,
602
+ kind,
603
+ [property]: value,
604
+ }))}`;
605
+ }
606
+ export function buildDiagnosticIdentities(definition) {
607
+ const formulas = new Map(definition.formulas.map((formula) => [formula.id, formula]));
608
+ const measures = new Map(definition.measures.map((measure) => [measure.id, measure]));
609
+ const derivations = new Map(definition.derivedMeasures.map((derivation) => [
610
+ derivation.outputMeasureId,
611
+ derivation,
612
+ ]));
613
+ const populations = new Map(definition.countPopulations.map((population) => [
614
+ population.id,
615
+ population,
616
+ ]));
617
+ const exposureBases = new Map(definition.exposureBases.map((basis) => [basis.id, basis]));
618
+ const amountBases = new Map(definition.amountBases.map((basis) => [basis.id, basis]));
619
+ const formulaFingerprints = sortedRecord(definition.formulas.map((formula) => [formula.id, tag("diagnostic-formula", "formula", formula)]));
620
+ const calculationFingerprints = Object.create(null);
621
+ const calculationScopesByInstanceId = new Map();
622
+ const calculationDependenciesByInstanceId = new Map();
623
+ const evaluationDependenciesByInstanceId = new Map();
624
+ const transitiveDependencies = (roots) => {
625
+ const found = new Set();
626
+ const stack = [...roots];
627
+ while (stack.length > 0) {
628
+ const id = stack.pop();
629
+ if (found.has(id))
630
+ continue;
631
+ found.add(id);
632
+ const derivation = derivations.get(id);
633
+ if (derivation)
634
+ stack.push(...expressionDependencies(derivation.expression));
635
+ }
636
+ return [...found].sort(compareCodeUnits);
637
+ };
638
+ for (const instance of definition.instances) {
639
+ const calculationDependencies = transitiveDependencies(Object.values(instance.bindings).flatMap(expressionDependencies));
640
+ const evaluationDependencies = transitiveDependencies([
641
+ ...calculationDependencies,
642
+ ...instance.rules.flatMap((rule) => [
643
+ ...ruleMeasureDependencies(rule.when.left),
644
+ ...ruleMeasureDependencies(rule.when.right),
645
+ ]),
646
+ ]);
647
+ calculationDependenciesByInstanceId.set(instance.id, calculationDependencies);
648
+ evaluationDependenciesByInstanceId.set(instance.id, evaluationDependencies);
649
+ const selectedMeasures = calculationDependencies
650
+ .map((id) => measures.get(id))
651
+ .filter(Boolean);
652
+ const selectedDerivations = definition.derivedMeasures.filter((derivation) => calculationDependencies.includes(derivation.outputMeasureId));
653
+ const countPopulationIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.countPopulationId === null ? [] : [measure.countPopulationId]));
654
+ const exposureBasisIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.exposureBasisId === null ? [] : [measure.exposureBasisId]));
655
+ const amountBasisIds = sortedUnique(selectedMeasures.flatMap((measure) => measure.basisId === null ? [] : [measure.basisId]));
656
+ const scope = {
657
+ formulaFingerprint: formulaFingerprints[instance.formulaId],
658
+ instance: {
659
+ id: instance.id,
660
+ version: instance.version,
661
+ formulaId: instance.formulaId,
662
+ bindings: instance.bindings,
663
+ },
664
+ lossRowGrain: definition.lossRowGrain,
665
+ measures: selectedMeasures.map((measure) => ({
666
+ id: measure.id,
667
+ source: measure.source,
668
+ kind: measure.kind,
669
+ unit: measure.unit,
670
+ developmentSemantics: measure.developmentSemantics,
671
+ aggregation: "sum",
672
+ missing: measure.missing,
673
+ basisId: measure.basisId,
674
+ countPopulationId: measure.countPopulationId,
675
+ exposureBasisId: measure.exposureBasisId,
676
+ exposureTiming: measure.exposureTiming,
677
+ })),
678
+ countPopulations: countPopulationIds.map((id) => {
679
+ const population = populations.get(id);
680
+ return {
681
+ id,
682
+ subject: population.subject,
683
+ unit: population.unit,
684
+ attributes: population.attributes,
685
+ };
686
+ }),
687
+ exposureBases: exposureBasisIds.map((id) => {
688
+ const basis = exposureBases.get(id);
689
+ return {
690
+ id,
691
+ basis: basis.basis,
692
+ unit: basis.unit,
693
+ attributes: basis.attributes,
694
+ };
695
+ }),
696
+ amountBases: amountBasisIds.map((id) => {
697
+ const basis = amountBases.get(id);
698
+ return {
699
+ id,
700
+ currency: basis.currency,
701
+ perspective: basis.perspective,
702
+ components: basis.components,
703
+ attributes: basis.attributes,
704
+ };
705
+ }),
706
+ derivedMeasures: selectedDerivations,
707
+ };
708
+ const frozenScope = deepFreeze(scope);
709
+ calculationScopesByInstanceId.set(instance.id, frozenScope);
710
+ calculationFingerprints[instance.id] = tag("diagnostic-calculation", "calculation", frozenScope);
711
+ }
712
+ return {
713
+ formulaFingerprints: deepFreeze(formulaFingerprints),
714
+ calculationFingerprints: deepFreeze(sortedRecord(Object.entries(calculationFingerprints))),
715
+ definitionIntegrity: tag("diagnostic-definition", "definition", definition),
716
+ calculationScopesByInstanceId,
717
+ calculationDependenciesByInstanceId,
718
+ evaluationDependenciesByInstanceId,
719
+ };
720
+ }
721
+ //# sourceMappingURL=diagnosticIdentity.js.map