@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.
- package/README.md +65 -226
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/casualtyDiagnostics.d.ts +19 -49
- package/dist/casualtyDiagnostics.d.ts.map +1 -1
- package/dist/casualtyDiagnostics.js +84 -116
- package/dist/casualtyDiagnostics.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +32 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -0
- package/dist/diagnosticAggregation.js +73 -0
- package/dist/diagnosticAggregation.js.map +1 -0
- package/dist/diagnosticDefinitions.d.ts +289 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -0
- package/dist/diagnosticDefinitions.js +1688 -0
- package/dist/diagnosticDefinitions.js.map +1 -0
- package/dist/diagnosticDerivations.d.ts +35 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -0
- package/dist/diagnosticDerivations.js +224 -0
- package/dist/diagnosticDerivations.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +51 -0
- package/dist/diagnosticExposure.d.ts.map +1 -0
- package/dist/diagnosticExposure.js +273 -0
- package/dist/diagnosticExposure.js.map +1 -0
- package/dist/diagnosticExpressions.d.ts +56 -0
- package/dist/diagnosticExpressions.d.ts.map +1 -0
- package/dist/diagnosticExpressions.js +124 -0
- package/dist/diagnosticExpressions.js.map +1 -0
- package/dist/diagnosticFormulas.d.ts +198 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -0
- package/dist/diagnosticFormulas.js +219 -0
- package/dist/diagnosticFormulas.js.map +1 -0
- package/dist/diagnosticIdentity.d.ts +251 -0
- package/dist/diagnosticIdentity.d.ts.map +1 -0
- package/dist/diagnosticIdentity.js +721 -0
- package/dist/diagnosticIdentity.js.map +1 -0
- 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 +15 -0
- package/dist/diagnosticPeriods.d.ts.map +1 -0
- package/dist/diagnosticPeriods.js +50 -0
- package/dist/diagnosticPeriods.js.map +1 -0
- package/dist/diagnosticPreparation.d.ts +131 -0
- package/dist/diagnosticPreparation.d.ts.map +1 -0
- package/dist/diagnosticPreparation.js +1539 -0
- package/dist/diagnosticPreparation.js.map +1 -0
- package/dist/diagnosticReview.d.ts +66 -0
- package/dist/diagnosticReview.d.ts.map +1 -0
- package/dist/diagnosticReview.js +490 -0
- package/dist/diagnosticReview.js.map +1 -0
- package/dist/diagnosticRules.d.ts +32 -0
- package/dist/diagnosticRules.d.ts.map +1 -0
- package/dist/diagnosticRules.js +32 -0
- package/dist/diagnosticRules.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +86 -0
- package/dist/diagnosticRunner.d.ts.map +1 -0
- package/dist/diagnosticRunner.js +703 -0
- package/dist/diagnosticRunner.js.map +1 -0
- 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 +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -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 +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +128 -0
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/package.json +4 -3
- package/src/capping.ts +109 -18
- package/src/casualtyDiagnostics.ts +79 -189
- package/src/diagnosticAggregation.ts +128 -0
- package/src/diagnosticDefinitions.ts +3624 -0
- package/src/diagnosticDerivations.ts +321 -0
- package/src/diagnosticExposure.ts +482 -0
- package/src/diagnosticExpressions.ts +255 -0
- package/src/diagnosticFormulas.ts +350 -0
- package/src/diagnosticIdentity.ts +1267 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +102 -0
- package/src/diagnosticPreparation.ts +2475 -0
- package/src/diagnosticReview.ts +897 -0
- package/src/diagnosticRules.ts +69 -0
- package/src/diagnosticRunner.ts +1114 -0
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/index.ts +184 -1
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +196 -0
- package/src/version.ts +2 -0
- package/dist/metricDiagnostics.d.ts +0 -212
- package/dist/metricDiagnostics.d.ts.map +0 -1
- package/dist/metricDiagnostics.js +0 -627
- package/dist/metricDiagnostics.js.map +0 -1
- package/src/metricDiagnostics.ts +0 -876
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DiagnosticValidationIssue,
|
|
3
|
+
DiagnosticValidationIssueCode,
|
|
4
|
+
} from "./types.js";
|
|
5
|
+
import {
|
|
6
|
+
isDiagnosticPlainRecord,
|
|
7
|
+
isDiagnosticToken,
|
|
8
|
+
} from "./diagnosticRuntime.js";
|
|
9
|
+
|
|
10
|
+
export const MAX_DIAGNOSTIC_EXPRESSION_DEPTH = 64;
|
|
11
|
+
export const MAX_DIAGNOSTIC_EXPRESSION_NODES = 10_000;
|
|
12
|
+
export const MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES = 100_000;
|
|
13
|
+
|
|
14
|
+
export type DiagnosticMeasureExpression =
|
|
15
|
+
| { op: "measure"; measureId: string }
|
|
16
|
+
| { op: "add"; terms: readonly DiagnosticMeasureExpression[] }
|
|
17
|
+
| {
|
|
18
|
+
op: "subtract";
|
|
19
|
+
left: DiagnosticMeasureExpression;
|
|
20
|
+
right: DiagnosticMeasureExpression;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type DiagnosticRoleExpression =
|
|
24
|
+
| { op: "role"; role: string }
|
|
25
|
+
| { op: "add"; terms: readonly DiagnosticRoleExpression[] }
|
|
26
|
+
| {
|
|
27
|
+
op: "subtract";
|
|
28
|
+
left: DiagnosticRoleExpression;
|
|
29
|
+
right: DiagnosticRoleExpression;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type DiagnosticClaimExpression =
|
|
33
|
+
| { op: "measure"; measureId: string }
|
|
34
|
+
| { op: "add"; terms: readonly DiagnosticClaimExpression[] }
|
|
35
|
+
| {
|
|
36
|
+
op: "subtract";
|
|
37
|
+
left: DiagnosticClaimExpression;
|
|
38
|
+
right: DiagnosticClaimExpression;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
op: "claim-layer";
|
|
42
|
+
measureId: string;
|
|
43
|
+
attachment: number;
|
|
44
|
+
/** Layer width; null means unlimited above attachment. */
|
|
45
|
+
limit: number | null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type DiagnosticExpressionKind = "measure" | "role" | "claim";
|
|
49
|
+
|
|
50
|
+
export interface DiagnosticExpressionWalk {
|
|
51
|
+
readonly dependencies: readonly string[];
|
|
52
|
+
readonly nodeCount: number;
|
|
53
|
+
readonly maxDepth: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface WalkFrame {
|
|
57
|
+
readonly value: unknown;
|
|
58
|
+
readonly path: string;
|
|
59
|
+
readonly depth: number;
|
|
60
|
+
readonly ancestors: readonly object[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function issue(
|
|
64
|
+
issues: DiagnosticValidationIssue[],
|
|
65
|
+
code: DiagnosticValidationIssueCode,
|
|
66
|
+
path: string,
|
|
67
|
+
message: string,
|
|
68
|
+
): void {
|
|
69
|
+
issues.push({ domain: "definition", code, path, message });
|
|
70
|
+
}
|
|
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
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Iterative structural/resource walk used before recursive normalization. It
|
|
98
|
+
* deliberately accepts unknown so hostile inputs cannot overflow the host
|
|
99
|
+
* stack before the compiler can return a typed issue.
|
|
100
|
+
*/
|
|
101
|
+
export function walkDiagnosticExpression(
|
|
102
|
+
value: unknown,
|
|
103
|
+
kind: DiagnosticExpressionKind,
|
|
104
|
+
path: string,
|
|
105
|
+
issues: DiagnosticValidationIssue[],
|
|
106
|
+
initialDepth = 1,
|
|
107
|
+
): DiagnosticExpressionWalk {
|
|
108
|
+
const dependencies = new Set<string>();
|
|
109
|
+
const stack: WalkFrame[] = [
|
|
110
|
+
{ value, path, depth: initialDepth, ancestors: [] },
|
|
111
|
+
];
|
|
112
|
+
let nodeCount = 0;
|
|
113
|
+
let maxDepth = 0;
|
|
114
|
+
while (stack.length > 0) {
|
|
115
|
+
const frame = stack.pop()!;
|
|
116
|
+
nodeCount++;
|
|
117
|
+
maxDepth = Math.max(maxDepth, frame.depth);
|
|
118
|
+
if (frame.depth > MAX_DIAGNOSTIC_EXPRESSION_DEPTH) {
|
|
119
|
+
issue(
|
|
120
|
+
issues,
|
|
121
|
+
"expression-limit",
|
|
122
|
+
frame.path,
|
|
123
|
+
`Expression depth exceeds ${MAX_DIAGNOSTIC_EXPRESSION_DEPTH}`,
|
|
124
|
+
);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (nodeCount > MAX_DIAGNOSTIC_EXPRESSION_NODES) {
|
|
128
|
+
issue(
|
|
129
|
+
issues,
|
|
130
|
+
"expression-limit",
|
|
131
|
+
path,
|
|
132
|
+
`Expression node count exceeds ${MAX_DIAGNOSTIC_EXPRESSION_NODES}`,
|
|
133
|
+
);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
if (!isDiagnosticPlainRecord(frame.value)) {
|
|
137
|
+
issue(
|
|
138
|
+
issues,
|
|
139
|
+
"invalid-type",
|
|
140
|
+
frame.path,
|
|
141
|
+
"Expression node must be a plain object",
|
|
142
|
+
);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (frame.ancestors.includes(frame.value)) {
|
|
146
|
+
issue(issues, "cycle", frame.path, "Expression contains a cycle");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const op = frame.value.op;
|
|
150
|
+
const nextAncestors = [...frame.ancestors, frame.value];
|
|
151
|
+
if (kind === "role" && op === "role") {
|
|
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
|
+
);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if ((kind === "measure" || kind === "claim") && op === "measure") {
|
|
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
|
+
);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (kind === "claim" && op === "claim-layer") {
|
|
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
|
+
);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (op === "add") {
|
|
202
|
+
rejectUnknownKeys(frame.value, ["op", "terms"], frame.path, issues);
|
|
203
|
+
if (!Array.isArray(frame.value.terms) || frame.value.terms.length === 0) {
|
|
204
|
+
issue(
|
|
205
|
+
issues,
|
|
206
|
+
"invalid-type",
|
|
207
|
+
`${frame.path}.terms`,
|
|
208
|
+
"Add expression requires at least one term",
|
|
209
|
+
);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
for (let index = frame.value.terms.length - 1; index >= 0; index--) {
|
|
213
|
+
stack.push({
|
|
214
|
+
value: frame.value.terms[index],
|
|
215
|
+
path: `${frame.path}.terms[${index}]`,
|
|
216
|
+
depth: frame.depth + 1,
|
|
217
|
+
ancestors: nextAncestors,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (op === "subtract") {
|
|
223
|
+
rejectUnknownKeys(
|
|
224
|
+
frame.value,
|
|
225
|
+
["op", "left", "right"],
|
|
226
|
+
frame.path,
|
|
227
|
+
issues,
|
|
228
|
+
);
|
|
229
|
+
stack.push({
|
|
230
|
+
value: frame.value.right,
|
|
231
|
+
path: `${frame.path}.right`,
|
|
232
|
+
depth: frame.depth + 1,
|
|
233
|
+
ancestors: nextAncestors,
|
|
234
|
+
});
|
|
235
|
+
stack.push({
|
|
236
|
+
value: frame.value.left,
|
|
237
|
+
path: `${frame.path}.left`,
|
|
238
|
+
depth: frame.depth + 1,
|
|
239
|
+
ancestors: nextAncestors,
|
|
240
|
+
});
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
issue(
|
|
244
|
+
issues,
|
|
245
|
+
"invalid-type",
|
|
246
|
+
`${frame.path}.op`,
|
|
247
|
+
`Unknown ${kind} expression operator`,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
dependencies: [...dependencies].sort(),
|
|
252
|
+
nodeCount,
|
|
253
|
+
maxDepth,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { safeRatio } from "./util.js";
|
|
2
|
+
import type {
|
|
3
|
+
DiagnosticDeepReadonly,
|
|
4
|
+
DiagnosticFormulaTemplate,
|
|
5
|
+
DiagnosticMeasureKind,
|
|
6
|
+
DiagnosticMeasureStats,
|
|
7
|
+
DiagnosticMetricPresentation,
|
|
8
|
+
} from "./diagnosticDefinitions.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";
|
|
19
|
+
|
|
20
|
+
export const CASUALTY_FORMULA_TEMPLATES = Object.freeze([
|
|
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
|
+
},
|
|
99
|
+
] as const satisfies readonly DiagnosticFormulaTemplate[]);
|
|
100
|
+
|
|
101
|
+
export interface DiagnosticQuantitySemantics {
|
|
102
|
+
readonly kind: DiagnosticMeasureKind;
|
|
103
|
+
readonly unit: string;
|
|
104
|
+
readonly basisId?: string;
|
|
105
|
+
readonly countPopulationId?: string;
|
|
106
|
+
readonly exposureBasisId?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DiagnosticQuantity extends DiagnosticQuantitySemantics {
|
|
110
|
+
readonly value: number | null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface DiagnosticMetricFinding {
|
|
114
|
+
readonly code: string;
|
|
115
|
+
readonly message: string;
|
|
116
|
+
readonly severity: "info" | "warning" | "fail";
|
|
117
|
+
readonly category:
|
|
118
|
+
| "structural"
|
|
119
|
+
| "aggregation"
|
|
120
|
+
| "calculation"
|
|
121
|
+
| "rule"
|
|
122
|
+
| "presentation";
|
|
123
|
+
readonly ruleId?: string;
|
|
124
|
+
readonly measureId?: string;
|
|
125
|
+
readonly instanceId?: string;
|
|
126
|
+
readonly expressionPath?: string;
|
|
127
|
+
readonly offendingKey?: string;
|
|
128
|
+
readonly sourceGroup?: string;
|
|
129
|
+
readonly group?: string;
|
|
130
|
+
readonly origin?: string;
|
|
131
|
+
readonly valuation?: string;
|
|
132
|
+
readonly developmentAge?: number;
|
|
133
|
+
readonly ageUnit?: string;
|
|
134
|
+
readonly recordId?: string;
|
|
135
|
+
readonly claimId?: string;
|
|
136
|
+
readonly exposureKey?: string;
|
|
137
|
+
readonly sources: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface FinalizedDiagnosticMeasure {
|
|
141
|
+
readonly quantity: DiagnosticQuantity;
|
|
142
|
+
readonly stats: DiagnosticMeasureStats;
|
|
143
|
+
readonly readiness: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
144
|
+
readonly expressionOverflows?: readonly DiagnosticExpressionOverflow[];
|
|
145
|
+
readonly sources?: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
interface ExpressionResult {
|
|
149
|
+
readonly value: number | null;
|
|
150
|
+
readonly reasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
151
|
+
readonly overflows: readonly DiagnosticExpressionOverflow[];
|
|
152
|
+
readonly sources: readonly import("./diagnosticDefinitions.js").DiagnosticSourceLocation[];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function neumaier(values: readonly number[]): number | null {
|
|
156
|
+
let sum = 0;
|
|
157
|
+
let correction = 0;
|
|
158
|
+
for (const value of values) {
|
|
159
|
+
const next = sum + value;
|
|
160
|
+
correction +=
|
|
161
|
+
Math.abs(sum) >= Math.abs(value)
|
|
162
|
+
? sum - next + value
|
|
163
|
+
: value - next + sum;
|
|
164
|
+
sum = next;
|
|
165
|
+
if (!Number.isFinite(sum) || !Number.isFinite(correction)) return null;
|
|
166
|
+
}
|
|
167
|
+
const result = sum + correction;
|
|
168
|
+
return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
|
|
169
|
+
}
|
|
170
|
+
|
|
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
|
+
];
|
|
183
|
+
return order.filter((reason) => values.includes(reason));
|
|
184
|
+
}
|
|
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
|
+
|
|
191
|
+
export function evaluateDiagnosticMeasureExpression(
|
|
192
|
+
expression: DiagnosticDeepReadonly<DiagnosticMeasureExpression>,
|
|
193
|
+
measures: Readonly<Record<string, FinalizedDiagnosticMeasure>>,
|
|
194
|
+
path: string,
|
|
195
|
+
): ExpressionResult {
|
|
196
|
+
if (expression.op === "measure") {
|
|
197
|
+
const value = hasDiagnosticOwn(measures, expression.measureId)
|
|
198
|
+
? measures[expression.measureId]
|
|
199
|
+
: undefined;
|
|
200
|
+
return value
|
|
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: [] };
|
|
208
|
+
}
|
|
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
|
+
];
|
|
230
|
+
const reasons = uniqueReasons(children.flatMap((child) => child.reasons));
|
|
231
|
+
const overflows = children.flatMap((child) => child.overflows);
|
|
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!;
|
|
241
|
+
if (value === null || !Number.isFinite(value)) {
|
|
242
|
+
return {
|
|
243
|
+
value: null,
|
|
244
|
+
reasons: uniqueReasons([...reasons, "expression-overflow"]),
|
|
245
|
+
overflows: [...overflows, { expressionPath: path, sources }],
|
|
246
|
+
sources,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
value: Object.is(value, -0) ? 0 : value,
|
|
251
|
+
reasons,
|
|
252
|
+
overflows,
|
|
253
|
+
sources,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function evaluateDiagnosticRoleExpression(
|
|
258
|
+
expression: DiagnosticDeepReadonly<DiagnosticRoleExpression>,
|
|
259
|
+
bindings: Readonly<Record<string, ExpressionResult>>,
|
|
260
|
+
path: string,
|
|
261
|
+
): ExpressionResult {
|
|
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
|
+
];
|
|
287
|
+
const reasons = uniqueReasons(children.flatMap((child) => child.reasons));
|
|
288
|
+
const overflows = children.flatMap((child) => child.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
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
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;
|
|
325
|
+
const ratio = safeRatio(numerator, denominator);
|
|
326
|
+
return ratio !== null && Number.isFinite(ratio) ? ratio : null;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function applyDiagnosticPresentation(
|
|
330
|
+
rawValue: number | null,
|
|
331
|
+
presentation: DiagnosticDeepReadonly<DiagnosticMetricPresentation>,
|
|
332
|
+
): {
|
|
333
|
+
readonly value: number | null;
|
|
334
|
+
readonly finding: DiagnosticMetricFinding | null;
|
|
335
|
+
} {
|
|
336
|
+
if (rawValue === null) return { value: null, finding: null };
|
|
337
|
+
const value = rawValue * presentation.scale;
|
|
338
|
+
return Number.isFinite(value)
|
|
339
|
+
? { value: Object.is(value, -0) ? 0 : value, finding: null }
|
|
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
|
+
};
|
|
350
|
+
}
|