@actuarial-ts/core 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +3 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +30 -7
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticDefinitions.d.ts +3 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -1
- package/dist/diagnosticDefinitions.js +844 -182
- package/dist/diagnosticDefinitions.js.map +1 -1
- package/dist/diagnosticDerivations.d.ts +23 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -1
- package/dist/diagnosticDerivations.js +187 -43
- package/dist/diagnosticDerivations.js.map +1 -1
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +228 -20
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticExpressions.d.ts +1 -1
- package/dist/diagnosticExpressions.d.ts.map +1 -1
- package/dist/diagnosticExpressions.js +35 -15
- package/dist/diagnosticExpressions.js.map +1 -1
- package/dist/diagnosticFormulas.d.ts +3 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +160 -26
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts +15 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +318 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticOrdering.d.ts +8 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -0
- package/dist/diagnosticOrdering.js +73 -0
- package/dist/diagnosticOrdering.js.map +1 -0
- package/dist/diagnosticPeriodAxis.d.ts +9 -0
- package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
- package/dist/diagnosticPeriodAxis.js +69 -0
- package/dist/diagnosticPeriodAxis.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +2 -3
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +20 -68
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +47 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1482 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +36 -14
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +448 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticRunner.d.ts +2 -1
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +656 -70
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts +26 -0
- package/dist/diagnosticRuntime.d.ts.map +1 -0
- package/dist/diagnosticRuntime.js +243 -0
- package/dist/diagnosticRuntime.js.map +1 -0
- package/dist/diagnosticSourceOrdering.d.ts +6 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
- package/dist/diagnosticSourceOrdering.js +38 -0
- package/dist/diagnosticSourceOrdering.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts +4 -0
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +32 -4
- package/dist/mack.js.map +1 -1
- package/dist/triangle.d.ts.map +1 -1
- package/dist/triangle.js +59 -9
- package/dist/triangle.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/capping.ts +109 -18
- package/src/diagnosticAggregation.ts +69 -20
- package/src/diagnosticDefinitions.ts +2675 -426
- package/src/diagnosticDerivations.ts +281 -49
- package/src/diagnosticExposure.ts +441 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +691 -191
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2369 -127
- package/src/diagnosticReview.ts +864 -58
- package/src/diagnosticRunner.ts +1076 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/index.ts +31 -3
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- 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[] = [
|
|
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 (!
|
|
112
|
-
issue(
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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(
|
|
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(
|
|
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 {
|
|
10
|
-
|
|
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
|
-
{
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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:
|
|
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 +=
|
|
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(
|
|
81
|
-
|
|
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
|
|
197
|
+
const value = hasDiagnosticOwn(measures, expression.measureId)
|
|
198
|
+
? measures[expression.measureId]
|
|
199
|
+
: undefined;
|
|
92
200
|
return value
|
|
93
|
-
? {
|
|
94
|
-
|
|
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 =
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 {
|
|
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")
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
136
|
-
|
|
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
|
-
): {
|
|
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
|
-
: {
|
|
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
|
}
|