@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
@@ -2,6 +2,10 @@ import type {
2
2
  DiagnosticValidationIssue,
3
3
  DiagnosticValidationIssueCode,
4
4
  } from "./types.js";
5
+ import {
6
+ isDiagnosticPlainRecord,
7
+ isDiagnosticToken,
8
+ } from "./diagnosticRuntime.js";
5
9
 
6
10
  export const MAX_DIAGNOSTIC_EXPRESSION_DEPTH = 64;
7
11
  export const MAX_DIAGNOSTIC_EXPRESSION_NODES = 10_000;
@@ -56,12 +60,6 @@ interface WalkFrame {
56
60
  readonly ancestors: readonly object[];
57
61
  }
58
62
 
59
- function isRecord(value: unknown): value is Record<string, unknown> {
60
- if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
61
- const prototype: unknown = Object.getPrototypeOf(value);
62
- return prototype === Object.prototype || prototype === null;
63
- }
64
-
65
63
  function issue(
66
64
  issues: DiagnosticValidationIssue[],
67
65
  code: DiagnosticValidationIssueCode,
@@ -71,6 +69,30 @@ function issue(
71
69
  issues.push({ domain: "definition", code, path, message });
72
70
  }
73
71
 
72
+ function propertyPath(parent: string, key: string): string {
73
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
74
+ ? `${parent}.${key}`
75
+ : `${parent}[${JSON.stringify(key)}]`;
76
+ }
77
+
78
+ function rejectUnknownKeys(
79
+ value: Record<string, unknown>,
80
+ allowed: readonly string[],
81
+ path: string,
82
+ issues: DiagnosticValidationIssue[],
83
+ ): void {
84
+ for (const key of Object.keys(value)) {
85
+ if (!allowed.includes(key)) {
86
+ issue(
87
+ issues,
88
+ "unknown-key",
89
+ propertyPath(path, key),
90
+ `Unknown expression key ${key}`,
91
+ );
92
+ }
93
+ }
94
+ }
95
+
74
96
  /**
75
97
  * Iterative structural/resource walk used before recursive normalization. It
76
98
  * deliberately accepts unknown so hostile inputs cannot overflow the host
@@ -81,9 +103,12 @@ export function walkDiagnosticExpression(
81
103
  kind: DiagnosticExpressionKind,
82
104
  path: string,
83
105
  issues: DiagnosticValidationIssue[],
106
+ initialDepth = 1,
84
107
  ): DiagnosticExpressionWalk {
85
108
  const dependencies = new Set<string>();
86
- const stack: WalkFrame[] = [{ value, path, depth: 1, ancestors: [] }];
109
+ const stack: WalkFrame[] = [
110
+ { value, path, depth: initialDepth, ancestors: [] },
111
+ ];
87
112
  let nodeCount = 0;
88
113
  let maxDepth = 0;
89
114
  while (stack.length > 0) {
@@ -108,8 +133,13 @@ export function walkDiagnosticExpression(
108
133
  );
109
134
  break;
110
135
  }
111
- if (!isRecord(frame.value)) {
112
- issue(issues, "invalid-type", frame.path, "Expression node must be a plain object");
136
+ if (!isDiagnosticPlainRecord(frame.value)) {
137
+ issue(
138
+ issues,
139
+ "invalid-type",
140
+ frame.path,
141
+ "Expression node must be a plain object",
142
+ );
113
143
  continue;
114
144
  }
115
145
  if (frame.ancestors.includes(frame.value)) {
@@ -119,23 +149,64 @@ export function walkDiagnosticExpression(
119
149
  const op = frame.value.op;
120
150
  const nextAncestors = [...frame.ancestors, frame.value];
121
151
  if (kind === "role" && op === "role") {
122
- if (typeof frame.value.role === "string") dependencies.add(frame.value.role);
123
- else issue(issues, "invalid-type", `${frame.path}.role`, "Role reference must be a string");
152
+ rejectUnknownKeys(frame.value, ["op", "role"], frame.path, issues);
153
+ if (isDiagnosticToken(frame.value.role))
154
+ dependencies.add(frame.value.role);
155
+ else
156
+ issue(
157
+ issues,
158
+ typeof frame.value.role === "string"
159
+ ? "invalid-string"
160
+ : "invalid-type",
161
+ `${frame.path}.role`,
162
+ "Role reference must be a nonempty token",
163
+ );
124
164
  continue;
125
165
  }
126
166
  if ((kind === "measure" || kind === "claim") && op === "measure") {
127
- if (typeof frame.value.measureId === "string") dependencies.add(frame.value.measureId);
128
- else issue(issues, "invalid-type", `${frame.path}.measureId`, "Measure reference must be a string");
167
+ rejectUnknownKeys(frame.value, ["op", "measureId"], frame.path, issues);
168
+ if (isDiagnosticToken(frame.value.measureId))
169
+ dependencies.add(frame.value.measureId);
170
+ else
171
+ issue(
172
+ issues,
173
+ typeof frame.value.measureId === "string"
174
+ ? "invalid-string"
175
+ : "invalid-type",
176
+ `${frame.path}.measureId`,
177
+ "Measure reference must be a nonempty token",
178
+ );
129
179
  continue;
130
180
  }
131
181
  if (kind === "claim" && op === "claim-layer") {
132
- if (typeof frame.value.measureId === "string") dependencies.add(frame.value.measureId);
133
- else issue(issues, "invalid-type", `${frame.path}.measureId`, "Measure reference must be a string");
182
+ rejectUnknownKeys(
183
+ frame.value,
184
+ ["op", "measureId", "attachment", "limit"],
185
+ frame.path,
186
+ issues,
187
+ );
188
+ if (isDiagnosticToken(frame.value.measureId))
189
+ dependencies.add(frame.value.measureId);
190
+ else
191
+ issue(
192
+ issues,
193
+ typeof frame.value.measureId === "string"
194
+ ? "invalid-string"
195
+ : "invalid-type",
196
+ `${frame.path}.measureId`,
197
+ "Measure reference must be a nonempty token",
198
+ );
134
199
  continue;
135
200
  }
136
201
  if (op === "add") {
202
+ rejectUnknownKeys(frame.value, ["op", "terms"], frame.path, issues);
137
203
  if (!Array.isArray(frame.value.terms) || frame.value.terms.length === 0) {
138
- issue(issues, "invalid-type", `${frame.path}.terms`, "Add expression requires at least one term");
204
+ issue(
205
+ issues,
206
+ "invalid-type",
207
+ `${frame.path}.terms`,
208
+ "Add expression requires at least one term",
209
+ );
139
210
  continue;
140
211
  }
141
212
  for (let index = frame.value.terms.length - 1; index >= 0; index--) {
@@ -149,6 +220,12 @@ export function walkDiagnosticExpression(
149
220
  continue;
150
221
  }
151
222
  if (op === "subtract") {
223
+ rejectUnknownKeys(
224
+ frame.value,
225
+ ["op", "left", "right"],
226
+ frame.path,
227
+ issues,
228
+ );
152
229
  stack.push({
153
230
  value: frame.value.right,
154
231
  path: `${frame.path}.right`,
@@ -163,7 +240,12 @@ export function walkDiagnosticExpression(
163
240
  });
164
241
  continue;
165
242
  }
166
- issue(issues, "invalid-type", `${frame.path}.op`, `Unknown ${kind} expression operator`);
243
+ issue(
244
+ issues,
245
+ "invalid-type",
246
+ `${frame.path}.op`,
247
+ `Unknown ${kind} expression operator`,
248
+ );
167
249
  }
168
250
  return {
169
251
  dependencies: [...dependencies].sort(),
@@ -6,16 +6,96 @@ import type {
6
6
  DiagnosticMeasureStats,
7
7
  DiagnosticMetricPresentation,
8
8
  } from "./diagnosticDefinitions.js";
9
- import type { DiagnosticMeasureExpression, DiagnosticRoleExpression } from "./diagnosticExpressions.js";
10
- import type { DiagnosticExpressionOverflow, DiagnosticRuleNotEvaluatedReason } from "./diagnosticRules.js";
9
+ import type {
10
+ DiagnosticMeasureExpression,
11
+ DiagnosticRoleExpression,
12
+ } from "./diagnosticExpressions.js";
13
+ import type {
14
+ DiagnosticExpressionOverflow,
15
+ DiagnosticRuleNotEvaluatedReason,
16
+ } from "./diagnosticRules.js";
17
+ import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
18
+ import { hasDiagnosticOwn } from "./diagnosticRuntime.js";
11
19
 
12
20
  export const CASUALTY_FORMULA_TEMPLATES = Object.freeze([
13
- { id: "frequency", version: "1.0.0", roles: { claims: { kind: "count" }, exposure: { kind: "exposure" } }, numerator: { op: "role", role: "claims" }, denominator: { op: "role", role: "exposure" }, denominatorPolicy: "positive-or-null" },
14
- { id: "share", version: "1.0.0", roles: { part: { kind: "count", compatibilityGroup: "count-population" }, whole: { kind: "count", compatibilityGroup: "count-population" } }, numerator: { op: "role", role: "part" }, denominator: { op: "role", role: "whole" }, denominatorPolicy: "positive-or-null" },
15
- { id: "paid-to-incurred", version: "1.0.0", roles: { paid: { kind: "amount", compatibilityGroup: "amount-basis", developmentSemantics: "cumulative" }, incurred: { kind: "amount", compatibilityGroup: "amount-basis", developmentSemantics: "cumulative" } }, numerator: { op: "role", role: "paid" }, denominator: { op: "role", role: "incurred" }, denominatorPolicy: "positive-or-null" },
16
- { id: "amount-per-exposure", version: "1.0.0", roles: { amount: { kind: "amount" }, exposure: { kind: "exposure" } }, numerator: { op: "role", role: "amount" }, denominator: { op: "role", role: "exposure" }, denominatorPolicy: "positive-or-null" },
17
- { id: "amount-per-claim", version: "1.0.0", roles: { amount: { kind: "amount" }, claims: { kind: "count" } }, numerator: { op: "role", role: "amount" }, denominator: { op: "role", role: "claims" }, denominatorPolicy: "positive-or-null" },
18
- { id: "case-per-open", version: "1.0.0", roles: { incurred: { kind: "amount", compatibilityGroup: "amount-basis", developmentSemantics: "cumulative" }, paid: { kind: "amount", compatibilityGroup: "amount-basis", developmentSemantics: "cumulative" }, open: { kind: "count", developmentSemantics: "point-in-time" } }, numerator: { op: "subtract", left: { op: "role", role: "incurred" }, right: { op: "role", role: "paid" } }, denominator: { op: "role", role: "open" }, denominatorPolicy: "positive-or-null" },
21
+ {
22
+ id: "frequency",
23
+ version: "1.0.0",
24
+ roles: { claims: { kind: "count" }, exposure: { kind: "exposure" } },
25
+ numerator: { op: "role", role: "claims" },
26
+ denominator: { op: "role", role: "exposure" },
27
+ denominatorPolicy: "positive-or-null",
28
+ },
29
+ {
30
+ id: "share",
31
+ version: "1.0.0",
32
+ roles: {
33
+ part: { kind: "count", compatibilityGroup: "count-population" },
34
+ whole: { kind: "count", compatibilityGroup: "count-population" },
35
+ },
36
+ numerator: { op: "role", role: "part" },
37
+ denominator: { op: "role", role: "whole" },
38
+ denominatorPolicy: "positive-or-null",
39
+ },
40
+ {
41
+ id: "paid-to-incurred",
42
+ version: "1.0.0",
43
+ roles: {
44
+ paid: {
45
+ kind: "amount",
46
+ compatibilityGroup: "amount-basis",
47
+ developmentSemantics: "cumulative",
48
+ },
49
+ incurred: {
50
+ kind: "amount",
51
+ compatibilityGroup: "amount-basis",
52
+ developmentSemantics: "cumulative",
53
+ },
54
+ },
55
+ numerator: { op: "role", role: "paid" },
56
+ denominator: { op: "role", role: "incurred" },
57
+ denominatorPolicy: "positive-or-null",
58
+ },
59
+ {
60
+ id: "amount-per-exposure",
61
+ version: "1.0.0",
62
+ roles: { amount: { kind: "amount" }, exposure: { kind: "exposure" } },
63
+ numerator: { op: "role", role: "amount" },
64
+ denominator: { op: "role", role: "exposure" },
65
+ denominatorPolicy: "positive-or-null",
66
+ },
67
+ {
68
+ id: "amount-per-claim",
69
+ version: "1.0.0",
70
+ roles: { amount: { kind: "amount" }, claims: { kind: "count" } },
71
+ numerator: { op: "role", role: "amount" },
72
+ denominator: { op: "role", role: "claims" },
73
+ denominatorPolicy: "positive-or-null",
74
+ },
75
+ {
76
+ id: "case-per-open",
77
+ version: "1.0.0",
78
+ roles: {
79
+ incurred: {
80
+ kind: "amount",
81
+ compatibilityGroup: "amount-basis",
82
+ developmentSemantics: "cumulative",
83
+ },
84
+ paid: {
85
+ kind: "amount",
86
+ compatibilityGroup: "amount-basis",
87
+ developmentSemantics: "cumulative",
88
+ },
89
+ open: { kind: "count", developmentSemantics: "point-in-time" },
90
+ },
91
+ numerator: {
92
+ op: "subtract",
93
+ left: { op: "role", role: "incurred" },
94
+ right: { op: "role", role: "paid" },
95
+ },
96
+ denominator: { op: "role", role: "open" },
97
+ denominatorPolicy: "positive-or-null",
98
+ },
19
99
  ] as const satisfies readonly DiagnosticFormulaTemplate[]);
20
100
 
21
101
  export interface DiagnosticQuantitySemantics {
@@ -34,11 +114,17 @@ export interface DiagnosticMetricFinding {
34
114
  readonly code: string;
35
115
  readonly message: string;
36
116
  readonly severity: "info" | "warning" | "fail";
37
- readonly category: "structural" | "aggregation" | "calculation" | "rule" | "presentation";
117
+ readonly category:
118
+ | "structural"
119
+ | "aggregation"
120
+ | "calculation"
121
+ | "rule"
122
+ | "presentation";
38
123
  readonly ruleId?: string;
39
124
  readonly measureId?: string;
40
125
  readonly instanceId?: string;
41
126
  readonly expressionPath?: string;
127
+ readonly offendingKey?: string;
42
128
  readonly sourceGroup?: string;
43
129
  readonly group?: string;
44
130
  readonly origin?: string;
@@ -56,12 +142,14 @@ export interface FinalizedDiagnosticMeasure {
56
142
  readonly stats: DiagnosticMeasureStats;
57
143
  readonly readiness: readonly DiagnosticRuleNotEvaluatedReason[];
58
144
  readonly expressionOverflows?: readonly DiagnosticExpressionOverflow[];
145
+ readonly sources?: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
59
146
  }
60
147
 
61
148
  interface ExpressionResult {
62
149
  readonly value: number | null;
63
150
  readonly reasons: readonly DiagnosticRuleNotEvaluatedReason[];
64
151
  readonly overflows: readonly DiagnosticExpressionOverflow[];
152
+ readonly sources: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
65
153
  }
66
154
 
67
155
  function neumaier(values: readonly number[]): number | null {
@@ -69,7 +157,10 @@ function neumaier(values: readonly number[]): number | null {
69
157
  let correction = 0;
70
158
  for (const value of values) {
71
159
  const next = sum + value;
72
- correction += Math.abs(sum) >= Math.abs(value) ? (sum - next) + value : (value - next) + sum;
160
+ correction +=
161
+ Math.abs(sum) >= Math.abs(value)
162
+ ? sum - next + value
163
+ : value - next + sum;
73
164
  sum = next;
74
165
  if (!Number.isFinite(sum) || !Number.isFinite(correction)) return null;
75
166
  }
@@ -77,42 +168,90 @@ function neumaier(values: readonly number[]): number | null {
77
168
  return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
78
169
  }
79
170
 
80
- function uniqueReasons(values: readonly DiagnosticRuleNotEvaluatedReason[]): readonly DiagnosticRuleNotEvaluatedReason[] {
81
- const order: readonly DiagnosticRuleNotEvaluatedReason[] = ["missing", "imputed", "non-finite", "structural-ambiguity", "aggregation-overflow", "expression-overflow", "tolerance-overflow"];
171
+ function uniqueReasons(
172
+ values: readonly DiagnosticRuleNotEvaluatedReason[],
173
+ ): readonly DiagnosticRuleNotEvaluatedReason[] {
174
+ const order: readonly DiagnosticRuleNotEvaluatedReason[] = [
175
+ "missing",
176
+ "imputed",
177
+ "non-finite",
178
+ "structural-ambiguity",
179
+ "aggregation-overflow",
180
+ "expression-overflow",
181
+ "tolerance-overflow",
182
+ ];
82
183
  return order.filter((reason) => values.includes(reason));
83
184
  }
84
185
 
186
+ function pointer(path: string, segment: string | number): string {
187
+ const escaped = String(segment).replaceAll("~", "~0").replaceAll("/", "~1");
188
+ return `${path}/${escaped}`;
189
+ }
190
+
85
191
  export function evaluateDiagnosticMeasureExpression(
86
192
  expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>,
87
193
  measures: Readonly<Record<string, FinalizedDiagnosticMeasure>>,
88
194
  path: string,
89
195
  ): ExpressionResult {
90
196
  if (expression.op === "measure") {
91
- const value = measures[expression.measureId];
197
+ const value = hasDiagnosticOwn(measures, expression.measureId)
198
+ ? measures[expression.measureId]
199
+ : undefined;
92
200
  return value
93
- ? { value: value.quantity.value, reasons: value.readiness, overflows: value.expressionOverflows ?? [] }
94
- : { value: null, reasons: ["missing"], overflows: [] };
201
+ ? {
202
+ value: value.quantity.value,
203
+ reasons: value.readiness,
204
+ overflows: value.expressionOverflows ?? [],
205
+ sources: value.sources ?? [],
206
+ }
207
+ : { value: null, reasons: ["missing"], overflows: [], sources: [] };
95
208
  }
96
- const children = expression.op === "add"
97
- ? expression.terms.map((term, index) => evaluateDiagnosticMeasureExpression(term, measures, `${path}.terms[${index}]`))
98
- : [
99
- evaluateDiagnosticMeasureExpression(expression.left, measures, `${path}.left`),
100
- evaluateDiagnosticMeasureExpression(expression.right, measures, `${path}.right`),
101
- ];
209
+ const children =
210
+ expression.op === "add"
211
+ ? expression.terms.map((term, index) =>
212
+ evaluateDiagnosticMeasureExpression(
213
+ term,
214
+ measures,
215
+ pointer(pointer(path, "terms"), index),
216
+ ),
217
+ )
218
+ : [
219
+ evaluateDiagnosticMeasureExpression(
220
+ expression.left,
221
+ measures,
222
+ pointer(path, "left"),
223
+ ),
224
+ evaluateDiagnosticMeasureExpression(
225
+ expression.right,
226
+ measures,
227
+ pointer(path, "right"),
228
+ ),
229
+ ];
102
230
  const reasons = uniqueReasons(children.flatMap((child) => child.reasons));
103
231
  const overflows = children.flatMap((child) => child.overflows);
104
- if (children.some((child) => child.value === null)) return { value: null, reasons, overflows };
105
- const value = expression.op === "add"
106
- ? neumaier(children.map((child) => child.value!))
107
- : children[0]!.value! - children[1]!.value!;
232
+ const sources = normalizeDiagnosticSourceLocations(
233
+ children.flatMap((child) => child.sources),
234
+ );
235
+ if (children.some((child) => child.value === null))
236
+ return { value: null, reasons, overflows, sources };
237
+ const value =
238
+ expression.op === "add"
239
+ ? neumaier(children.map((child) => child.value!))
240
+ : children[0]!.value! - children[1]!.value!;
108
241
  if (value === null || !Number.isFinite(value)) {
109
242
  return {
110
243
  value: null,
111
244
  reasons: uniqueReasons([...reasons, "expression-overflow"]),
112
- overflows: [...overflows, { expressionPath: path, sources: [] }],
245
+ overflows: [...overflows, { expressionPath: path, sources }],
246
+ sources,
113
247
  };
114
248
  }
115
- return { value: Object.is(value, -0) ? 0 : value, reasons, overflows };
249
+ return {
250
+ value: Object.is(value, -0) ? 0 : value,
251
+ reasons,
252
+ overflows,
253
+ sources,
254
+ };
116
255
  }
117
256
 
118
257
  export function evaluateDiagnosticRoleExpression(
@@ -120,20 +259,69 @@ export function evaluateDiagnosticRoleExpression(
120
259
  bindings: Readonly<Record<string, ExpressionResult>>,
121
260
  path: string,
122
261
  ): ExpressionResult {
123
- if (expression.op === "role") return bindings[expression.role] ?? { value: null, reasons: ["missing"], overflows: [] };
124
- const children = expression.op === "add"
125
- ? expression.terms.map((term, index) => evaluateDiagnosticRoleExpression(term, bindings, `${path}.terms[${index}]`))
126
- : [evaluateDiagnosticRoleExpression(expression.left, bindings, `${path}.left`), evaluateDiagnosticRoleExpression(expression.right, bindings, `${path}.right`)];
262
+ if (expression.op === "role")
263
+ return hasDiagnosticOwn(bindings, expression.role)
264
+ ? bindings[expression.role]!
265
+ : { value: null, reasons: ["missing"], overflows: [], sources: [] };
266
+ const children =
267
+ expression.op === "add"
268
+ ? expression.terms.map((term, index) =>
269
+ evaluateDiagnosticRoleExpression(
270
+ term,
271
+ bindings,
272
+ pointer(pointer(path, "terms"), index),
273
+ ),
274
+ )
275
+ : [
276
+ evaluateDiagnosticRoleExpression(
277
+ expression.left,
278
+ bindings,
279
+ pointer(path, "left"),
280
+ ),
281
+ evaluateDiagnosticRoleExpression(
282
+ expression.right,
283
+ bindings,
284
+ pointer(path, "right"),
285
+ ),
286
+ ];
127
287
  const reasons = uniqueReasons(children.flatMap((child) => child.reasons));
128
288
  const overflows = children.flatMap((child) => child.overflows);
129
- if (children.some((child) => child.value === null)) return { value: null, reasons, overflows };
130
- const value = expression.op === "add" ? neumaier(children.map((child) => child.value!)) : children[0]!.value! - children[1]!.value!;
131
- if (value === null || !Number.isFinite(value)) return { value: null, reasons: uniqueReasons([...reasons, "expression-overflow"]), overflows: [...overflows, { expressionPath: path, sources: [] }] };
132
- return { value: Object.is(value, -0) ? 0 : value, reasons, overflows };
289
+ const sources = normalizeDiagnosticSourceLocations(
290
+ children.flatMap((child) => child.sources),
291
+ );
292
+ if (children.some((child) => child.value === null))
293
+ return { value: null, reasons, overflows, sources };
294
+ const value =
295
+ expression.op === "add"
296
+ ? neumaier(children.map((child) => child.value!))
297
+ : children[0]!.value! - children[1]!.value!;
298
+ if (value === null || !Number.isFinite(value))
299
+ return {
300
+ value: null,
301
+ reasons: uniqueReasons([...reasons, "expression-overflow"]),
302
+ overflows: [...overflows, { expressionPath: path, sources }],
303
+ sources,
304
+ };
305
+ return {
306
+ value: Object.is(value, -0) ? 0 : value,
307
+ reasons,
308
+ overflows,
309
+ sources,
310
+ };
133
311
  }
134
312
 
135
- export function diagnosticRawRatio(numerator: number | null, denominator: number | null): number | null {
136
- if (numerator === null || denominator === null || !Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator <= 0) return null;
313
+ export function diagnosticRawRatio(
314
+ numerator: number | null,
315
+ denominator: number | null,
316
+ ): number | null {
317
+ if (
318
+ numerator === null ||
319
+ denominator === null ||
320
+ !Number.isFinite(numerator) ||
321
+ !Number.isFinite(denominator) ||
322
+ denominator <= 0
323
+ )
324
+ return null;
137
325
  const ratio = safeRatio(numerator, denominator);
138
326
  return ratio !== null && Number.isFinite(ratio) ? ratio : null;
139
327
  }
@@ -141,10 +329,22 @@ export function diagnosticRawRatio(numerator: number | null, denominator: number
141
329
  export function applyDiagnosticPresentation(
142
330
  rawValue: number | null,
143
331
  presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>,
144
- ): { readonly value: number | null; readonly finding: DiagnosticMetricFinding | null } {
332
+ ): {
333
+ readonly value: number | null;
334
+ readonly finding: DiagnosticMetricFinding | null;
335
+ } {
145
336
  if (rawValue === null) return { value: null, finding: null };
146
337
  const value = rawValue * presentation.scale;
147
338
  return Number.isFinite(value)
148
339
  ? { value: Object.is(value, -0) ? 0 : value, finding: null }
149
- : { value: null, finding: { code: "diagnostic-presentation-overflow", message: "Diagnostic presentation scaling overflowed", severity: "fail", category: "presentation", sources: [] } };
340
+ : {
341
+ value: null,
342
+ finding: {
343
+ code: "diagnostic-presentation-overflow",
344
+ message: "Diagnostic presentation scaling overflowed",
345
+ severity: "fail",
346
+ category: "presentation",
347
+ sources: [],
348
+ },
349
+ };
150
350
  }