@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.
- package/README.md +12 -3
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- 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/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +319 -21
- 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 +321 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.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 +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 +75 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1606 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +40 -15
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +600 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +27 -2
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +764 -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/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- 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/canonical.ts +144 -26
- 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/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +550 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +700 -191
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2558 -132
- package/src/diagnosticReview.ts +1072 -58
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +1273 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +69 -4
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- package/src/version.ts +1 -1
|
@@ -1,15 +1,14 @@
|
|
|
1
|
-
import { MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES, walkDiagnosticExpression, } from "./diagnosticExpressions.js";
|
|
1
|
+
import { MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES, MAX_DIAGNOSTIC_EXPRESSION_NODES, walkDiagnosticExpression, } from "./diagnosticExpressions.js";
|
|
2
2
|
import { buildDiagnosticIdentities, normalizeDiagnosticDefinition, } from "./diagnosticIdentity.js";
|
|
3
3
|
import { DiagnosticValidationError, } from "./types.js";
|
|
4
4
|
import { canonicalJson } from "./canonical.js";
|
|
5
|
+
import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
|
|
6
|
+
import { diagnosticJsonPreflight, hasDiagnosticOwn, isDiagnosticPlainRecord, isDiagnosticToken, isWellFormedDiagnosticString, } from "./diagnosticRuntime.js";
|
|
5
7
|
const compiledDiagnosticDefinitionBrand = Symbol("compiled-diagnostic-definition");
|
|
6
8
|
const authenticCompiledDefinitions = new WeakSet();
|
|
7
9
|
const compiledInternals = new WeakMap();
|
|
8
10
|
function isPlainRecord(value) {
|
|
9
|
-
|
|
10
|
-
return false;
|
|
11
|
-
const prototype = Object.getPrototypeOf(value);
|
|
12
|
-
return prototype === Object.prototype || prototype === null;
|
|
11
|
+
return isDiagnosticPlainRecord(value);
|
|
13
12
|
}
|
|
14
13
|
function propertyPath(parent, key) {
|
|
15
14
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
@@ -19,32 +18,50 @@ function propertyPath(parent, key) {
|
|
|
19
18
|
function pushIssue(issues, code, path, message) {
|
|
20
19
|
issues.push({ domain: "definition", code, path, message });
|
|
21
20
|
}
|
|
22
|
-
function
|
|
23
|
-
for (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
21
|
+
function validateAllowedKeys(value, allowed, path, issues) {
|
|
22
|
+
for (const key of Object.keys(value)) {
|
|
23
|
+
if (!allowed.includes(key)) {
|
|
24
|
+
pushIssue(issues, "unknown-key", propertyPath(path, key), `Unknown key ${key}`);
|
|
25
|
+
}
|
|
26
|
+
else if (value[key] === undefined) {
|
|
27
|
+
pushIssue(issues, "invalid-type", propertyPath(path, key), "Explicit undefined is not allowed");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function validateScalarAttributes(value, path, issues) {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return;
|
|
34
|
+
if (!isPlainRecord(value)) {
|
|
35
|
+
pushIssue(issues, "invalid-type", path, "Attributes must be a plain object");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const [key, scalar] of Object.entries(value)) {
|
|
39
|
+
validateToken(key, propertyPath(path, key), "Attribute key", issues);
|
|
40
|
+
if (scalar !== null &&
|
|
41
|
+
typeof scalar !== "string" &&
|
|
42
|
+
typeof scalar !== "number" &&
|
|
43
|
+
typeof scalar !== "boolean") {
|
|
44
|
+
pushIssue(issues, "invalid-type", propertyPath(path, key), "Attribute value must be a string, finite number, boolean, or null");
|
|
45
|
+
}
|
|
46
|
+
else if (typeof scalar === "number") {
|
|
47
|
+
validateFinite(scalar, propertyPath(path, key), "Attribute number", issues);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof scalar === "string" &&
|
|
50
|
+
!isWellFormedDiagnosticString(scalar)) {
|
|
51
|
+
pushIssue(issues, "invalid-string", propertyPath(path, key), "Attribute string contains invalid Unicode");
|
|
52
|
+
}
|
|
35
53
|
}
|
|
36
|
-
return true;
|
|
37
54
|
}
|
|
38
55
|
function validateToken(value, path, label, issues) {
|
|
39
56
|
if (typeof value !== "string") {
|
|
40
57
|
pushIssue(issues, "invalid-type", path, `${label} must be a string`);
|
|
41
58
|
return false;
|
|
42
59
|
}
|
|
43
|
-
if (!
|
|
60
|
+
if (!isWellFormedDiagnosticString(value)) {
|
|
44
61
|
pushIssue(issues, "invalid-string", path, `${label} contains an invalid Unicode string`);
|
|
45
62
|
return false;
|
|
46
63
|
}
|
|
47
|
-
if (
|
|
64
|
+
if (!isDiagnosticToken(value)) {
|
|
48
65
|
pushIssue(issues, "invalid-string", path, `${label} must be nonempty without surrounding ASCII whitespace`);
|
|
49
66
|
return false;
|
|
50
67
|
}
|
|
@@ -58,62 +75,7 @@ function validateFinite(value, path, label, issues) {
|
|
|
58
75
|
return true;
|
|
59
76
|
}
|
|
60
77
|
function validateJsonValue(value, issues) {
|
|
61
|
-
|
|
62
|
-
const stack = [{ value, path: "$", exiting: false }];
|
|
63
|
-
while (stack.length > 0) {
|
|
64
|
-
const frame = stack.pop();
|
|
65
|
-
if (frame.value === null || typeof frame.value === "boolean")
|
|
66
|
-
continue;
|
|
67
|
-
if (typeof frame.value === "string") {
|
|
68
|
-
if (!hasWellFormedUtf16(frame.value)) {
|
|
69
|
-
pushIssue(issues, "invalid-json-value", frame.path, "String is not well-formed UTF-16 or contains U+0000");
|
|
70
|
-
}
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
if (typeof frame.value === "number") {
|
|
74
|
-
if (!Number.isFinite(frame.value)) {
|
|
75
|
-
pushIssue(issues, "invalid-json-value", frame.path, "JSON numeric value must be finite");
|
|
76
|
-
}
|
|
77
|
-
continue;
|
|
78
|
-
}
|
|
79
|
-
if (typeof frame.value !== "object" || frame.value === undefined) {
|
|
80
|
-
pushIssue(issues, "invalid-json-value", frame.path, "Value is not plain JSON data");
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
const object = frame.value;
|
|
84
|
-
if (frame.exiting) {
|
|
85
|
-
active.delete(object);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (active.has(object)) {
|
|
89
|
-
pushIssue(issues, "cycle", frame.path, "JSON value contains a cycle");
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
if (!Array.isArray(object) && !isPlainRecord(object)) {
|
|
93
|
-
pushIssue(issues, "invalid-json-value", frame.path, "Value must use a plain object or array prototype");
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
active.add(object);
|
|
97
|
-
stack.push({ value: object, path: frame.path, exiting: true });
|
|
98
|
-
if (Array.isArray(object)) {
|
|
99
|
-
for (let index = object.length - 1; index >= 0; index--) {
|
|
100
|
-
stack.push({ value: object[index], path: `${frame.path}[${index}]`, exiting: false });
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
for (const key of Object.keys(object).sort().reverse()) {
|
|
105
|
-
if (!hasWellFormedUtf16(key)) {
|
|
106
|
-
pushIssue(issues, "invalid-json-value", propertyPath(frame.path, key), "Object key is not well-formed UTF-16 or contains U+0000");
|
|
107
|
-
}
|
|
108
|
-
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
109
|
-
if (!descriptor || !("value" in descriptor)) {
|
|
110
|
-
pushIssue(issues, "invalid-json-value", propertyPath(frame.path, key), "JSON objects may contain only data properties");
|
|
111
|
-
continue;
|
|
112
|
-
}
|
|
113
|
-
stack.push({ value: descriptor.value, path: propertyPath(frame.path, key), exiting: false });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
78
|
+
issues.push(...diagnosticJsonPreflight(value, "definition"));
|
|
117
79
|
}
|
|
118
80
|
function requireArray(value, path, issues) {
|
|
119
81
|
if (!Array.isArray(value)) {
|
|
@@ -153,7 +115,8 @@ function transitiveMeasureIds(roots, derivations) {
|
|
|
153
115
|
result.add(id);
|
|
154
116
|
const derivation = derivations.get(id);
|
|
155
117
|
if (derivation) {
|
|
156
|
-
stack.push(...walkDiagnosticExpression(derivation.expression, "claim", "$", [])
|
|
118
|
+
stack.push(...walkDiagnosticExpression(derivation.expression, "claim", "$", [])
|
|
119
|
+
.dependencies);
|
|
157
120
|
}
|
|
158
121
|
}
|
|
159
122
|
return [...result].sort();
|
|
@@ -174,11 +137,22 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
174
137
|
if (!measure)
|
|
175
138
|
return null;
|
|
176
139
|
if (measure.kind !== "amount") {
|
|
177
|
-
return {
|
|
140
|
+
return {
|
|
141
|
+
kind: measure.kind,
|
|
142
|
+
unit: measure.unit,
|
|
143
|
+
reference: semanticReference(measure),
|
|
144
|
+
};
|
|
178
145
|
}
|
|
179
|
-
const basis = measure.basisId
|
|
146
|
+
const basis = measure.basisId
|
|
147
|
+
? amountBases.get(measure.basisId)
|
|
148
|
+
: undefined;
|
|
180
149
|
return basis
|
|
181
|
-
? {
|
|
150
|
+
? {
|
|
151
|
+
kind: "amount",
|
|
152
|
+
unit: measure.unit,
|
|
153
|
+
reference: `amount:${measure.basisId}`,
|
|
154
|
+
amountBasis: projectedAmountBasis(basis),
|
|
155
|
+
}
|
|
182
156
|
: null;
|
|
183
157
|
}
|
|
184
158
|
if (expression.op === "claim-layer") {
|
|
@@ -189,7 +163,10 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
189
163
|
}
|
|
190
164
|
const basis = amountBases.get(measure.basisId);
|
|
191
165
|
const component = basis?.components[0];
|
|
192
|
-
if (!basis ||
|
|
166
|
+
if (!basis ||
|
|
167
|
+
basis.components.length !== 1 ||
|
|
168
|
+
component?.treatment !== "included" ||
|
|
169
|
+
component.limitation.kind !== "unlimited") {
|
|
193
170
|
pushIssue(issues, "incompatible-semantics", path, "Claim layers require one included unlimited source component");
|
|
194
171
|
return null;
|
|
195
172
|
}
|
|
@@ -200,7 +177,8 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
200
177
|
amountBasis: {
|
|
201
178
|
currency: basis.currency,
|
|
202
179
|
perspective: basis.perspective,
|
|
203
|
-
components: [
|
|
180
|
+
components: [
|
|
181
|
+
{
|
|
204
182
|
id: component.id,
|
|
205
183
|
treatment: "included",
|
|
206
184
|
limitation: {
|
|
@@ -210,20 +188,27 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
210
188
|
application: "claim",
|
|
211
189
|
derivation: { kind: "sdk" },
|
|
212
190
|
},
|
|
213
|
-
}
|
|
191
|
+
},
|
|
192
|
+
],
|
|
214
193
|
},
|
|
215
194
|
};
|
|
216
195
|
}
|
|
217
|
-
const children = expression.op === "add"
|
|
218
|
-
|
|
196
|
+
const children = expression.op === "add"
|
|
197
|
+
? expression.terms
|
|
198
|
+
: [expression.left, expression.right];
|
|
199
|
+
const projected = children.map((child, index) => projectClaimExpression(child, expression.op === "add"
|
|
200
|
+
? `${path}.terms[${index}]`
|
|
201
|
+
: `${path}.${index === 0 ? "left" : "right"}`, measures, amountBases, issues));
|
|
219
202
|
if (projected.some((item) => item === null))
|
|
220
203
|
return null;
|
|
221
204
|
const present = projected;
|
|
222
205
|
if (expression.op === "subtract") {
|
|
223
206
|
const [left, right] = present;
|
|
224
|
-
if (left.kind !== right.kind ||
|
|
207
|
+
if (left.kind !== right.kind ||
|
|
208
|
+
left.unit !== right.unit ||
|
|
225
209
|
(left.kind === "amount"
|
|
226
|
-
? projectedBasisKey(left.amountBasis) !==
|
|
210
|
+
? projectedBasisKey(left.amountBasis) !==
|
|
211
|
+
projectedBasisKey(right.amountBasis)
|
|
227
212
|
: left.reference !== right.reference)) {
|
|
228
213
|
pushIssue(issues, "incompatible-semantics", path, "Claim subtraction requires one exact semantic basis");
|
|
229
214
|
return null;
|
|
@@ -243,7 +228,8 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
243
228
|
return first;
|
|
244
229
|
}
|
|
245
230
|
const bases = present.map((item) => item.amountBasis);
|
|
246
|
-
if (bases.some((basis) => basis.currency !== bases[0].currency ||
|
|
231
|
+
if (bases.some((basis) => basis.currency !== bases[0].currency ||
|
|
232
|
+
basis.perspective !== bases[0].perspective)) {
|
|
247
233
|
pushIssue(issues, "incompatible-semantics", path, "Amount addition cannot mix currency or perspective");
|
|
248
234
|
return null;
|
|
249
235
|
}
|
|
@@ -264,10 +250,15 @@ function projectClaimExpression(expression, path, measures, amountBases, issues)
|
|
|
264
250
|
perspective: bases[0].perspective,
|
|
265
251
|
components: components.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
|
|
266
252
|
};
|
|
267
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
kind: "amount",
|
|
255
|
+
unit: first.unit,
|
|
256
|
+
reference: "amount:projected",
|
|
257
|
+
amountBasis,
|
|
258
|
+
};
|
|
268
259
|
}
|
|
269
|
-
function expressionSemantics(expression, path, measures, issues) {
|
|
270
|
-
const walked = walkDiagnosticExpression(expression, "measure", path, issues);
|
|
260
|
+
function expressionSemantics(expression, path, measures, issues, initialDepth = 1) {
|
|
261
|
+
const walked = walkDiagnosticExpression(expression, "measure", path, issues, initialDepth);
|
|
271
262
|
const found = walked.dependencies
|
|
272
263
|
.map((id) => measures.get(id))
|
|
273
264
|
.filter((value) => value !== undefined);
|
|
@@ -276,7 +267,7 @@ function expressionSemantics(expression, path, measures, issues) {
|
|
|
276
267
|
pushIssue(issues, "unknown-reference", path, `Unknown measure ${id}`);
|
|
277
268
|
}
|
|
278
269
|
}
|
|
279
|
-
const signatures = new Set(found.map((measure) =>
|
|
270
|
+
const signatures = new Set(found.map((measure) => canonicalJson([measure.kind, measure.unit, semanticReference(measure)])));
|
|
280
271
|
if (signatures.size > 1) {
|
|
281
272
|
pushIssue(issues, "incompatible-semantics", path, "Expression combines incompatible measure semantics");
|
|
282
273
|
}
|
|
@@ -297,6 +288,224 @@ function validateTolerance(value, path, issues) {
|
|
|
297
288
|
}
|
|
298
289
|
}
|
|
299
290
|
}
|
|
291
|
+
function validateReviewFilter(value, path, axis, issues) {
|
|
292
|
+
if (!isPlainRecord(value)) {
|
|
293
|
+
pushIssue(issues, "invalid-type", path, "Review filter must be an object");
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const periodCoordinates = new Map();
|
|
297
|
+
const validatePeriods = (key, side) => {
|
|
298
|
+
const raw = value[key];
|
|
299
|
+
if (raw === undefined)
|
|
300
|
+
return;
|
|
301
|
+
const entries = requireArray(raw, `${path}.${key}`, issues);
|
|
302
|
+
entries.forEach((entry, index) => {
|
|
303
|
+
const entryPath = `${path}.${key}[${index}]`;
|
|
304
|
+
if (!validateToken(entry, entryPath, `${side} period`, issues))
|
|
305
|
+
return;
|
|
306
|
+
const normalized = normalizeDiagnosticPeriodWithAxis(axis, side, entry);
|
|
307
|
+
if (!normalized)
|
|
308
|
+
pushIssue(issues, "invalid-period", entryPath, `Unknown ${side} period ${JSON.stringify(entry)}`);
|
|
309
|
+
});
|
|
310
|
+
};
|
|
311
|
+
const validateEndpoint = (key, side) => {
|
|
312
|
+
const raw = value[key];
|
|
313
|
+
if (raw === undefined)
|
|
314
|
+
return;
|
|
315
|
+
if (!validateToken(raw, `${path}.${key}`, `${side} period`, issues))
|
|
316
|
+
return;
|
|
317
|
+
const normalized = normalizeDiagnosticPeriodWithAxis(axis, side, raw);
|
|
318
|
+
if (!normalized)
|
|
319
|
+
pushIssue(issues, "invalid-period", `${path}.${key}`, `Unknown ${side} period ${JSON.stringify(raw)}`);
|
|
320
|
+
else
|
|
321
|
+
periodCoordinates.set(key, normalized.coordinate);
|
|
322
|
+
};
|
|
323
|
+
if (value.sourceGroups !== undefined)
|
|
324
|
+
requireArray(value.sourceGroups, `${path}.sourceGroups`, issues).forEach((item, index) => {
|
|
325
|
+
validateToken(item, `${path}.sourceGroups[${index}]`, "Source group", issues);
|
|
326
|
+
});
|
|
327
|
+
validatePeriods("origins", "origin");
|
|
328
|
+
validateEndpoint("originFrom", "origin");
|
|
329
|
+
validateEndpoint("originThrough", "origin");
|
|
330
|
+
validatePeriods("valuations", "valuation");
|
|
331
|
+
validateEndpoint("valuationFrom", "valuation");
|
|
332
|
+
validateEndpoint("valuationThrough", "valuation");
|
|
333
|
+
for (const key of ["minDevelopmentAge", "maxDevelopmentAge"]) {
|
|
334
|
+
const raw = value[key];
|
|
335
|
+
if (raw === undefined)
|
|
336
|
+
continue;
|
|
337
|
+
if (!validateFinite(raw, `${path}.${key}`, key, issues) ||
|
|
338
|
+
!Number.isSafeInteger(raw) ||
|
|
339
|
+
raw < 0) {
|
|
340
|
+
pushIssue(issues, "invalid-number", `${path}.${key}`, `${key} must be a nonnegative safe integer`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if ((periodCoordinates.get("originFrom") ?? -Infinity) >
|
|
344
|
+
(periodCoordinates.get("originThrough") ?? Infinity)) {
|
|
345
|
+
pushIssue(issues, "invalid-input-relationship", path, "originFrom must not follow originThrough");
|
|
346
|
+
}
|
|
347
|
+
if ((periodCoordinates.get("valuationFrom") ?? -Infinity) >
|
|
348
|
+
(periodCoordinates.get("valuationThrough") ?? Infinity)) {
|
|
349
|
+
pushIssue(issues, "invalid-input-relationship", path, "valuationFrom must not follow valuationThrough");
|
|
350
|
+
}
|
|
351
|
+
if (typeof value.minDevelopmentAge === "number" &&
|
|
352
|
+
typeof value.maxDevelopmentAge === "number" &&
|
|
353
|
+
value.minDevelopmentAge > value.maxDevelopmentAge) {
|
|
354
|
+
pushIssue(issues, "invalid-input-relationship", path, "minDevelopmentAge must not exceed maxDevelopmentAge");
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function measureExpressionBasis(expression, measures, amountBases) {
|
|
358
|
+
const ids = walkDiagnosticExpression(expression, "measure", "$", []).dependencies;
|
|
359
|
+
const found = ids.map((id) => measures.get(id));
|
|
360
|
+
if (found.length === 0 ||
|
|
361
|
+
found.some((measure) => measure?.kind !== "amount" || !measure.basisId))
|
|
362
|
+
return null;
|
|
363
|
+
const basisIds = new Set(found.map((measure) => measure.basisId));
|
|
364
|
+
return basisIds.size === 1
|
|
365
|
+
? (amountBases.get([...basisIds][0]) ?? null)
|
|
366
|
+
: null;
|
|
367
|
+
}
|
|
368
|
+
function traceLayerComponents(measureId, measures, amountBases, derivations, seen = new Set()) {
|
|
369
|
+
if (seen.has(measureId))
|
|
370
|
+
return null;
|
|
371
|
+
const derivation = derivations.get(measureId);
|
|
372
|
+
if (!derivation) {
|
|
373
|
+
const measure = measures.get(measureId);
|
|
374
|
+
const basis = measure?.kind === "amount" && measure.basisId
|
|
375
|
+
? amountBases.get(measure.basisId)
|
|
376
|
+
: undefined;
|
|
377
|
+
return basis
|
|
378
|
+
? new Map(basis.components.map((component) => [
|
|
379
|
+
component.id,
|
|
380
|
+
{ rawMeasureId: measureId, hasLayer: false },
|
|
381
|
+
]))
|
|
382
|
+
: null;
|
|
383
|
+
}
|
|
384
|
+
const nextSeen = new Set(seen).add(measureId);
|
|
385
|
+
const visit = (expression) => {
|
|
386
|
+
if (expression.op === "measure")
|
|
387
|
+
return traceLayerComponents(expression.measureId, measures, amountBases, derivations, nextSeen);
|
|
388
|
+
if (expression.op === "claim-layer") {
|
|
389
|
+
const nested = traceLayerComponents(expression.measureId, measures, amountBases, derivations, nextSeen);
|
|
390
|
+
if (!nested || nested.size !== 1)
|
|
391
|
+
return null;
|
|
392
|
+
return new Map([...nested].map(([componentId, trace]) => [
|
|
393
|
+
componentId,
|
|
394
|
+
{ ...trace, hasLayer: true },
|
|
395
|
+
]));
|
|
396
|
+
}
|
|
397
|
+
if (expression.op === "subtract")
|
|
398
|
+
return null;
|
|
399
|
+
const result = new Map();
|
|
400
|
+
for (const term of expression.terms) {
|
|
401
|
+
const traced = visit(term);
|
|
402
|
+
if (!traced)
|
|
403
|
+
return null;
|
|
404
|
+
for (const [componentId, trace] of traced) {
|
|
405
|
+
if (result.has(componentId))
|
|
406
|
+
return null;
|
|
407
|
+
result.set(componentId, trace);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
};
|
|
412
|
+
return visit(derivation.expression);
|
|
413
|
+
}
|
|
414
|
+
function limitationInterval(limitation) {
|
|
415
|
+
if (limitation.kind === "unlimited")
|
|
416
|
+
return { attachment: 0, exhaustion: Infinity, application: null };
|
|
417
|
+
if (limitation.kind !== "layer" ||
|
|
418
|
+
limitation.derivation.kind !== "sdk" ||
|
|
419
|
+
limitation.application === "source-defined")
|
|
420
|
+
return null;
|
|
421
|
+
return {
|
|
422
|
+
attachment: limitation.attachment,
|
|
423
|
+
exhaustion: limitation.limit === null
|
|
424
|
+
? Infinity
|
|
425
|
+
: limitation.attachment + limitation.limit,
|
|
426
|
+
application: limitation.application,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function validateLayerComparability(rule, path, measures, amountBases, derivations, issues) {
|
|
430
|
+
const narrowerBasis = measureExpressionBasis(rule.narrower, measures, amountBases);
|
|
431
|
+
const broaderBasis = measureExpressionBasis(rule.broader, measures, amountBases);
|
|
432
|
+
if (!narrowerBasis ||
|
|
433
|
+
!broaderBasis ||
|
|
434
|
+
narrowerBasis.currency !== broaderBasis.currency) {
|
|
435
|
+
pushIssue(issues, "incompatible-semantics", path, "Layer-order operands must be amounts with the same currency");
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (rule.comparability.kind === "caller-asserted")
|
|
439
|
+
return;
|
|
440
|
+
if (narrowerBasis.perspective !== broaderBasis.perspective) {
|
|
441
|
+
pushIssue(issues, "incompatible-semantics", path, "Compiler-proven layer order requires the same amount perspective");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const proveMeasures = (narrowerMeasureId, broaderMeasureId) => {
|
|
445
|
+
const narrowerMeasure = measures.get(narrowerMeasureId);
|
|
446
|
+
const broaderMeasure = measures.get(broaderMeasureId);
|
|
447
|
+
const narrowBasis = narrowerMeasure?.kind === "amount" && narrowerMeasure.basisId
|
|
448
|
+
? amountBases.get(narrowerMeasure.basisId)
|
|
449
|
+
: undefined;
|
|
450
|
+
const broadBasis = broaderMeasure?.kind === "amount" && broaderMeasure.basisId
|
|
451
|
+
? amountBases.get(broaderMeasure.basisId)
|
|
452
|
+
: undefined;
|
|
453
|
+
if (!narrowBasis ||
|
|
454
|
+
!broadBasis ||
|
|
455
|
+
narrowBasis.currency !== broadBasis.currency ||
|
|
456
|
+
narrowBasis.perspective !== broadBasis.perspective)
|
|
457
|
+
return { valid: false, hasLayer: false };
|
|
458
|
+
const narrowComponents = [...narrowBasis.components].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
459
|
+
const broadComponents = [...broadBasis.components].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
460
|
+
const narrowTraces = traceLayerComponents(narrowerMeasureId, measures, amountBases, derivations);
|
|
461
|
+
const broadTraces = traceLayerComponents(broaderMeasureId, measures, amountBases, derivations);
|
|
462
|
+
if (!narrowTraces ||
|
|
463
|
+
!broadTraces ||
|
|
464
|
+
narrowComponents.length !== broadComponents.length)
|
|
465
|
+
return { valid: false, hasLayer: false };
|
|
466
|
+
let hasLayer = false;
|
|
467
|
+
for (let index = 0; index < narrowComponents.length; index++) {
|
|
468
|
+
const narrower = narrowComponents[index];
|
|
469
|
+
const broader = broadComponents[index];
|
|
470
|
+
const narrowerInterval = limitationInterval(narrower.limitation);
|
|
471
|
+
const broaderInterval = limitationInterval(broader.limitation);
|
|
472
|
+
const narrowerTrace = narrowTraces.get(narrower.id);
|
|
473
|
+
const broaderTrace = broadTraces.get(broader.id);
|
|
474
|
+
if (narrower.id !== broader.id ||
|
|
475
|
+
narrower.treatment !== broader.treatment ||
|
|
476
|
+
narrower.treatment === "unknown" ||
|
|
477
|
+
!narrowerInterval ||
|
|
478
|
+
!broaderInterval ||
|
|
479
|
+
!narrowerTrace ||
|
|
480
|
+
!broaderTrace ||
|
|
481
|
+
narrowerTrace.rawMeasureId !== broaderTrace.rawMeasureId ||
|
|
482
|
+
(broaderInterval.application !== null &&
|
|
483
|
+
narrowerInterval.application !== broaderInterval.application) ||
|
|
484
|
+
narrowerInterval.attachment < broaderInterval.attachment ||
|
|
485
|
+
narrowerInterval.exhaustion > broaderInterval.exhaustion)
|
|
486
|
+
return { valid: false, hasLayer: false };
|
|
487
|
+
hasLayer ||= narrowerTrace.hasLayer || broaderTrace.hasLayer;
|
|
488
|
+
}
|
|
489
|
+
return { valid: true, hasLayer };
|
|
490
|
+
};
|
|
491
|
+
const proveExpressions = (narrower, broader) => {
|
|
492
|
+
if (narrower.op === "measure" && broader.op === "measure")
|
|
493
|
+
return proveMeasures(narrower.measureId, broader.measureId);
|
|
494
|
+
if (narrower.op !== "add" ||
|
|
495
|
+
broader.op !== "add" ||
|
|
496
|
+
narrower.terms.length !== broader.terms.length)
|
|
497
|
+
return { valid: false, hasLayer: false };
|
|
498
|
+
const children = narrower.terms.map((term, index) => proveExpressions(term, broader.terms[index]));
|
|
499
|
+
return {
|
|
500
|
+
valid: children.every((item) => item.valid),
|
|
501
|
+
hasLayer: children.some((item) => item.hasLayer),
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
const proof = proveExpressions(rule.narrower, rule.broader);
|
|
505
|
+
if (!proof.valid || !proof.hasLayer) {
|
|
506
|
+
pushIssue(issues, "incompatible-semantics", path, "Compiler metadata does not prove that the narrower layer is contained by the broader layer");
|
|
507
|
+
}
|
|
508
|
+
}
|
|
300
509
|
function validatePeriodAxis(axis, issues) {
|
|
301
510
|
if (!isPlainRecord(axis)) {
|
|
302
511
|
pushIssue(issues, "invalid-type", "$.periodAxis", "Period axis must be an object");
|
|
@@ -307,17 +516,33 @@ function validatePeriodAxis(axis, issues) {
|
|
|
307
516
|
pushIssue(issues, "invalid-number", "$.periodAxis.ageOffset", "Period age offset must be a safe integer");
|
|
308
517
|
}
|
|
309
518
|
if (axis.kind === "calendar") {
|
|
519
|
+
validateAllowedKeys(axis, [
|
|
520
|
+
"kind",
|
|
521
|
+
"originCadence",
|
|
522
|
+
"valuationCadence",
|
|
523
|
+
"originAnchor",
|
|
524
|
+
"valuationAnchor",
|
|
525
|
+
"ageUnit",
|
|
526
|
+
"ageOffset",
|
|
527
|
+
], "$.periodAxis", issues);
|
|
310
528
|
if (!["month", "quarter", "year"].includes(axis.originCadence)) {
|
|
311
529
|
pushIssue(issues, "invalid-period", "$.periodAxis.originCadence", "Unknown origin cadence");
|
|
312
530
|
}
|
|
313
531
|
if (!["month", "quarter", "year"].includes(axis.valuationCadence)) {
|
|
314
532
|
pushIssue(issues, "invalid-period", "$.periodAxis.valuationCadence", "Unknown valuation cadence");
|
|
315
533
|
}
|
|
534
|
+
if (axis.originAnchor !== "start" && axis.originAnchor !== "end") {
|
|
535
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.originAnchor", "Unknown origin anchor");
|
|
536
|
+
}
|
|
537
|
+
if (axis.valuationAnchor !== "start" && axis.valuationAnchor !== "end") {
|
|
538
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.valuationAnchor", "Unknown valuation anchor");
|
|
539
|
+
}
|
|
316
540
|
if (axis.ageUnit !== "month") {
|
|
317
541
|
pushIssue(issues, "invalid-period", "$.periodAxis.ageUnit", "Calendar axes use month age units");
|
|
318
542
|
}
|
|
319
543
|
return;
|
|
320
544
|
}
|
|
545
|
+
validateAllowedKeys(axis, ["kind", "id", "version", "ageUnit", "ageOffset", "origins", "valuations"], "$.periodAxis", issues);
|
|
321
546
|
if (axis.kind !== "ordered") {
|
|
322
547
|
pushIssue(issues, "invalid-period", "$.periodAxis.kind", "Unknown period-axis kind");
|
|
323
548
|
return;
|
|
@@ -331,6 +556,11 @@ function validatePeriodAxis(axis, issues) {
|
|
|
331
556
|
const coordinateValues = new Set();
|
|
332
557
|
coordinates.forEach((coordinate, index) => {
|
|
333
558
|
const base = `$.periodAxis.${side}[${index}]`;
|
|
559
|
+
if (!isPlainRecord(coordinate)) {
|
|
560
|
+
pushIssue(issues, "invalid-type", base, "Period coordinate must be an object");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
validateAllowedKeys(coordinate, ["label", "aliases", "coordinate"], base, issues);
|
|
334
564
|
if (validateToken(coordinate?.label, `${base}.label`, "Period label", issues)) {
|
|
335
565
|
if (names.has(coordinate.label))
|
|
336
566
|
pushIssue(issues, "duplicate-id", `${base}.label`, `Duplicate period label or alias ${coordinate.label}`);
|
|
@@ -345,7 +575,10 @@ function validatePeriodAxis(axis, issues) {
|
|
|
345
575
|
}
|
|
346
576
|
else
|
|
347
577
|
coordinateValues.add(coordinate.coordinate);
|
|
348
|
-
|
|
578
|
+
const aliases = coordinate.aliases === undefined
|
|
579
|
+
? []
|
|
580
|
+
: requireArray(coordinate.aliases, `${base}.aliases`, issues);
|
|
581
|
+
for (const [aliasIndex, alias] of aliases.entries()) {
|
|
349
582
|
if (!validateToken(alias, `${base}.aliases[${aliasIndex}]`, "Period alias", issues))
|
|
350
583
|
continue;
|
|
351
584
|
if (names.has(alias))
|
|
@@ -363,12 +596,28 @@ function validateDefinition(value) {
|
|
|
363
596
|
throw new DiagnosticValidationError(issues);
|
|
364
597
|
}
|
|
365
598
|
const definition = value;
|
|
599
|
+
validateAllowedKeys(value, [
|
|
600
|
+
"diagnosticDefinitionVersion",
|
|
601
|
+
"id",
|
|
602
|
+
"version",
|
|
603
|
+
"lossRowGrain",
|
|
604
|
+
"measures",
|
|
605
|
+
"countPopulations",
|
|
606
|
+
"exposureBases",
|
|
607
|
+
"amountBases",
|
|
608
|
+
"derivedMeasures",
|
|
609
|
+
"formulas",
|
|
610
|
+
"instances",
|
|
611
|
+
"reviewRules",
|
|
612
|
+
"periodAxis",
|
|
613
|
+
], "$", issues);
|
|
366
614
|
if (definition.diagnosticDefinitionVersion !== "1.0.0") {
|
|
367
615
|
pushIssue(issues, "invalid-type", "$.diagnosticDefinitionVersion", "Diagnostic definition version must be 1.0.0");
|
|
368
616
|
}
|
|
369
617
|
validateToken(definition.id, "$.id", "Definition ID", issues);
|
|
370
618
|
validateToken(definition.version, "$.version", "Definition version", issues);
|
|
371
|
-
if (definition.lossRowGrain !== "claim" &&
|
|
619
|
+
if (definition.lossRowGrain !== "claim" &&
|
|
620
|
+
definition.lossRowGrain !== "aggregate") {
|
|
372
621
|
pushIssue(issues, "invalid-type", "$.lossRowGrain", "Loss row grain must be claim or aggregate");
|
|
373
622
|
}
|
|
374
623
|
const measures = requireArray(definition.measures, "$.measures", issues);
|
|
@@ -386,7 +635,8 @@ function validateDefinition(value) {
|
|
|
386
635
|
collectCatalog(derivations, "$.derivedMeasures", issues);
|
|
387
636
|
const derivationsByOutput = new Map();
|
|
388
637
|
for (const derivation of derivations) {
|
|
389
|
-
if (isPlainRecord(derivation) &&
|
|
638
|
+
if (isPlainRecord(derivation) &&
|
|
639
|
+
typeof derivation.outputMeasureId === "string" &&
|
|
390
640
|
!derivationsByOutput.has(derivation.outputMeasureId)) {
|
|
391
641
|
derivationsByOutput.set(derivation.outputMeasureId, derivation);
|
|
392
642
|
}
|
|
@@ -396,15 +646,41 @@ function validateDefinition(value) {
|
|
|
396
646
|
const reviewRuleMap = collectCatalog(reviewRules, "$.reviewRules", issues);
|
|
397
647
|
populations.forEach((population, index) => {
|
|
398
648
|
const base = `$.countPopulations[${index}]`;
|
|
649
|
+
if (!isPlainRecord(population)) {
|
|
650
|
+
pushIssue(issues, "invalid-type", base, "Count population must be an object");
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
validateAllowedKeys(population, ["id", "displayName", "subject", "unit", "description", "attributes"], base, issues);
|
|
399
654
|
validateToken(population.displayName, `${base}.displayName`, "Population display name", issues);
|
|
400
655
|
validateToken(population.description, `${base}.description`, "Population description", issues);
|
|
401
656
|
validateToken(population.unit, `${base}.unit`, "Population unit", issues);
|
|
402
|
-
if (![
|
|
657
|
+
if (![
|
|
658
|
+
"claim",
|
|
659
|
+
"claimant",
|
|
660
|
+
"policy",
|
|
661
|
+
"occurrence",
|
|
662
|
+
"other",
|
|
663
|
+
"unknown",
|
|
664
|
+
].includes(population.subject)) {
|
|
403
665
|
pushIssue(issues, "invalid-type", `${base}.subject`, "Unknown count-population subject");
|
|
404
666
|
}
|
|
667
|
+
validateScalarAttributes(population.attributes, `${base}.attributes`, issues);
|
|
405
668
|
});
|
|
406
669
|
exposureBases.forEach((basis, index) => {
|
|
407
670
|
const base = `$.exposureBases[${index}]`;
|
|
671
|
+
if (!isPlainRecord(basis)) {
|
|
672
|
+
pushIssue(issues, "invalid-type", base, "Exposure basis must be an object");
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
validateAllowedKeys(basis, [
|
|
676
|
+
"id",
|
|
677
|
+
"displayName",
|
|
678
|
+
"basis",
|
|
679
|
+
"unit",
|
|
680
|
+
"description",
|
|
681
|
+
"sourceDescription",
|
|
682
|
+
"attributes",
|
|
683
|
+
], base, issues);
|
|
408
684
|
validateToken(basis.displayName, `${base}.displayName`, "Exposure-basis display name", issues);
|
|
409
685
|
validateToken(basis.description, `${base}.description`, "Exposure-basis description", issues);
|
|
410
686
|
validateToken(basis.unit, `${base}.unit`, "Exposure-basis unit", issues);
|
|
@@ -413,9 +689,23 @@ function validateDefinition(value) {
|
|
|
413
689
|
if (!["earned", "written", "in-force", "other", "unknown"].includes(basis.basis)) {
|
|
414
690
|
pushIssue(issues, "invalid-type", `${base}.basis`, "Unknown exposure basis");
|
|
415
691
|
}
|
|
692
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
416
693
|
});
|
|
417
694
|
amountBases.forEach((basis, index) => {
|
|
418
695
|
const base = `$.amountBases[${index}]`;
|
|
696
|
+
if (!isPlainRecord(basis)) {
|
|
697
|
+
pushIssue(issues, "invalid-type", base, "Amount basis must be an object");
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
validateAllowedKeys(basis, [
|
|
701
|
+
"id",
|
|
702
|
+
"displayName",
|
|
703
|
+
"currency",
|
|
704
|
+
"perspective",
|
|
705
|
+
"components",
|
|
706
|
+
"sourceDescription",
|
|
707
|
+
"attributes",
|
|
708
|
+
], base, issues);
|
|
419
709
|
validateToken(basis.displayName, `${base}.displayName`, "Amount-basis display name", issues);
|
|
420
710
|
validateToken(basis.currency, `${base}.currency`, "Amount-basis currency", issues);
|
|
421
711
|
if (basis.sourceDescription !== undefined)
|
|
@@ -423,11 +713,13 @@ function validateDefinition(value) {
|
|
|
423
713
|
if (!["gross", "net", "ceded", "other", "unknown"].includes(basis.perspective)) {
|
|
424
714
|
pushIssue(issues, "invalid-type", `${base}.perspective`, "Unknown amount perspective");
|
|
425
715
|
}
|
|
716
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
426
717
|
const components = requireArray(basis.components, `${base}.components`, issues);
|
|
427
718
|
if (components.length === 0)
|
|
428
719
|
pushIssue(issues, "missing-required", `${base}.components`, "Amount basis requires at least one component");
|
|
429
720
|
const componentIds = collectCatalog(components, `${base}.components`, issues);
|
|
430
|
-
if (componentIds.size > 0 &&
|
|
721
|
+
if (componentIds.size > 0 &&
|
|
722
|
+
components.every((component) => component.treatment === "excluded")) {
|
|
431
723
|
pushIssue(issues, "incompatible-semantics", `${base}.components`, "Amount basis cannot exclude every component");
|
|
432
724
|
}
|
|
433
725
|
components.forEach((component, componentIndex) => {
|
|
@@ -436,6 +728,7 @@ function validateDefinition(value) {
|
|
|
436
728
|
pushIssue(issues, "invalid-type", componentBase, "Amount-basis component must be an object");
|
|
437
729
|
return;
|
|
438
730
|
}
|
|
731
|
+
validateAllowedKeys(component, ["id", "treatment", "limitation"], componentBase, issues);
|
|
439
732
|
if (!["included", "excluded", "unknown"].includes(component.treatment)) {
|
|
440
733
|
pushIssue(issues, "invalid-type", `${componentBase}.treatment`, "Unknown component treatment");
|
|
441
734
|
}
|
|
@@ -445,27 +738,46 @@ function validateDefinition(value) {
|
|
|
445
738
|
return;
|
|
446
739
|
}
|
|
447
740
|
if (limitation.kind === "unknown") {
|
|
741
|
+
validateAllowedKeys(limitation, ["kind", "description"], `${componentBase}.limitation`, issues);
|
|
448
742
|
if (limitation.description !== undefined)
|
|
449
743
|
validateToken(limitation.description, `${componentBase}.limitation.description`, "Unknown limitation description", issues);
|
|
450
744
|
}
|
|
451
|
-
else if (limitation.kind === "layer" ||
|
|
452
|
-
|
|
745
|
+
else if (limitation.kind === "layer" ||
|
|
746
|
+
limitation.kind === "pre-limited") {
|
|
747
|
+
validateAllowedKeys(limitation, ["kind", "attachment", "limit", "application", "derivation"], `${componentBase}.limitation`, issues);
|
|
748
|
+
if (!["claim", "occurrence", "policy", "source-defined"].includes(limitation.application)) {
|
|
749
|
+
pushIssue(issues, "invalid-type", `${componentBase}.limitation.application`, "Unknown limitation application");
|
|
750
|
+
}
|
|
751
|
+
if (validateFinite(limitation.attachment, `${componentBase}.limitation.attachment`, "Attachment", issues) &&
|
|
752
|
+
limitation.attachment < 0) {
|
|
453
753
|
pushIssue(issues, "invalid-number", `${componentBase}.limitation.attachment`, "Attachment must be nonnegative");
|
|
454
754
|
}
|
|
455
755
|
if (limitation.limit !== null) {
|
|
456
|
-
if (validateFinite(limitation.limit, `${componentBase}.limitation.limit`, "Layer width", issues) &&
|
|
756
|
+
if (validateFinite(limitation.limit, `${componentBase}.limitation.limit`, "Layer width", issues) &&
|
|
757
|
+
limitation.limit <= 0) {
|
|
457
758
|
pushIssue(issues, "invalid-number", `${componentBase}.limitation.limit`, "Layer width must be positive");
|
|
458
759
|
}
|
|
459
|
-
if (Number.isFinite(limitation.attachment) &&
|
|
760
|
+
if (Number.isFinite(limitation.attachment) &&
|
|
761
|
+
Number.isFinite(limitation.limit) &&
|
|
762
|
+
!Number.isFinite(limitation.attachment + limitation.limit)) {
|
|
460
763
|
pushIssue(issues, "invalid-number", `${componentBase}.limitation.limit`, "Attachment plus layer width must be finite");
|
|
461
764
|
}
|
|
462
765
|
}
|
|
463
766
|
if (limitation.derivation?.kind === "sdk") {
|
|
464
|
-
if (limitation.
|
|
767
|
+
if (isPlainRecord(limitation.derivation))
|
|
768
|
+
validateAllowedKeys(limitation.derivation, ["kind"], `${componentBase}.limitation.derivation`, issues);
|
|
769
|
+
if (limitation.kind !== "layer" ||
|
|
770
|
+
limitation.application !== "claim") {
|
|
465
771
|
pushIssue(issues, "incompatible-semantics", `${componentBase}.limitation.derivation`, "SDK limitations require a claim layer");
|
|
466
772
|
}
|
|
467
773
|
}
|
|
468
774
|
else if (limitation.derivation?.kind === "external") {
|
|
775
|
+
if (isPlainRecord(limitation.derivation))
|
|
776
|
+
validateAllowedKeys(limitation.derivation, ["kind", "actor", "transformationRef"], `${componentBase}.limitation.derivation`, issues);
|
|
777
|
+
if (limitation.derivation.actor !== "caller" &&
|
|
778
|
+
limitation.derivation.actor !== "source") {
|
|
779
|
+
pushIssue(issues, "invalid-type", `${componentBase}.limitation.derivation.actor`, "External derivation actor must be caller or source");
|
|
780
|
+
}
|
|
469
781
|
validateToken(limitation.derivation.transformationRef, `${componentBase}.limitation.derivation.transformationRef`, "Transformation reference", issues);
|
|
470
782
|
}
|
|
471
783
|
else {
|
|
@@ -475,10 +787,31 @@ function validateDefinition(value) {
|
|
|
475
787
|
else if (limitation.kind !== "unlimited") {
|
|
476
788
|
pushIssue(issues, "invalid-type", `${componentBase}.limitation.kind`, "Unknown amount limitation kind");
|
|
477
789
|
}
|
|
790
|
+
else
|
|
791
|
+
validateAllowedKeys(limitation, ["kind"], `${componentBase}.limitation`, issues);
|
|
478
792
|
});
|
|
479
793
|
});
|
|
480
794
|
measures.forEach((measure, index) => {
|
|
481
795
|
const base = `$.measures[${index}]`;
|
|
796
|
+
if (!isPlainRecord(measure)) {
|
|
797
|
+
pushIssue(issues, "invalid-type", base, "Measure must be an object");
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
validateAllowedKeys(measure, [
|
|
801
|
+
"id",
|
|
802
|
+
"displayName",
|
|
803
|
+
"description",
|
|
804
|
+
"source",
|
|
805
|
+
"kind",
|
|
806
|
+
"unit",
|
|
807
|
+
"developmentSemantics",
|
|
808
|
+
"aggregation",
|
|
809
|
+
"missing",
|
|
810
|
+
"basisId",
|
|
811
|
+
"countPopulationId",
|
|
812
|
+
"exposureBasisId",
|
|
813
|
+
"exposureTiming",
|
|
814
|
+
], base, issues);
|
|
482
815
|
validateToken(measure.displayName, `${base}.displayName`, "Measure display name", issues);
|
|
483
816
|
validateToken(measure.description, `${base}.description`, "Measure description", issues);
|
|
484
817
|
validateToken(measure.unit, `${base}.unit`, "Measure unit", issues);
|
|
@@ -490,46 +823,60 @@ function validateDefinition(value) {
|
|
|
490
823
|
pushIssue(issues, "invalid-type", `${base}.developmentSemantics`, "Unknown development semantics");
|
|
491
824
|
}
|
|
492
825
|
if (measure.kind === "amount") {
|
|
493
|
-
const basis = measure.basisId
|
|
826
|
+
const basis = measure.basisId
|
|
827
|
+
? amountBasisMap.get(measure.basisId)
|
|
828
|
+
: undefined;
|
|
494
829
|
if (!basis)
|
|
495
830
|
pushIssue(issues, "unknown-reference", `${base}.basisId`, "Amount measure requires an existing amount basis");
|
|
496
831
|
else if (measure.unit !== basis.currency)
|
|
497
832
|
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Amount measure unit must equal basis currency");
|
|
498
|
-
if (measure.countPopulationId !== undefined ||
|
|
833
|
+
if (measure.countPopulationId !== undefined ||
|
|
834
|
+
measure.exposureBasisId !== undefined ||
|
|
835
|
+
measure.exposureTiming !== undefined) {
|
|
499
836
|
pushIssue(issues, "incompatible-semantics", base, "Amount measure has inapplicable population or exposure semantics");
|
|
500
837
|
}
|
|
501
838
|
}
|
|
502
839
|
else if (measure.kind === "count") {
|
|
503
|
-
const population = measure.countPopulationId
|
|
840
|
+
const population = measure.countPopulationId
|
|
841
|
+
? populationMap.get(measure.countPopulationId)
|
|
842
|
+
: undefined;
|
|
504
843
|
if (!population)
|
|
505
844
|
pushIssue(issues, "unknown-reference", `${base}.countPopulationId`, "Count measure requires an existing count population");
|
|
506
845
|
else if (measure.unit !== population.unit)
|
|
507
846
|
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Count measure unit must equal population unit");
|
|
508
|
-
if (measure.basisId !== undefined ||
|
|
847
|
+
if (measure.basisId !== undefined ||
|
|
848
|
+
measure.exposureBasisId !== undefined ||
|
|
849
|
+
measure.exposureTiming !== undefined) {
|
|
509
850
|
pushIssue(issues, "incompatible-semantics", base, "Count measure has inapplicable amount or exposure semantics");
|
|
510
851
|
}
|
|
511
852
|
}
|
|
512
853
|
else if (measure.kind === "exposure") {
|
|
513
|
-
const basis = measure.exposureBasisId
|
|
854
|
+
const basis = measure.exposureBasisId
|
|
855
|
+
? exposureBasisMap.get(measure.exposureBasisId)
|
|
856
|
+
: undefined;
|
|
514
857
|
if (!basis)
|
|
515
858
|
pushIssue(issues, "unknown-reference", `${base}.exposureBasisId`, "Exposure measure requires an existing exposure basis");
|
|
516
859
|
else if (measure.unit !== basis.unit)
|
|
517
860
|
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Exposure measure unit must equal exposure-basis unit");
|
|
518
861
|
if (measure.source !== "exposure")
|
|
519
862
|
pushIssue(issues, "incompatible-semantics", `${base}.source`, "Exposure measure source must be exposure");
|
|
520
|
-
if (measure.exposureTiming !== "origin-static" &&
|
|
863
|
+
if (measure.exposureTiming !== "origin-static" &&
|
|
864
|
+
measure.exposureTiming !== "valuation-specific") {
|
|
521
865
|
pushIssue(issues, "missing-required", `${base}.exposureTiming`, "Exposure measure requires timing");
|
|
522
866
|
}
|
|
523
867
|
if (measure.missing !== "unknown")
|
|
524
868
|
pushIssue(issues, "incompatible-semantics", `${base}.missing`, "Exposure missingness must remain unknown");
|
|
525
|
-
if (measure.basisId !== undefined ||
|
|
869
|
+
if (measure.basisId !== undefined ||
|
|
870
|
+
measure.countPopulationId !== undefined) {
|
|
526
871
|
pushIssue(issues, "incompatible-semantics", base, "Exposure measure has inapplicable amount or population semantics");
|
|
527
872
|
}
|
|
528
873
|
}
|
|
529
874
|
else {
|
|
530
875
|
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown measure kind");
|
|
531
876
|
}
|
|
532
|
-
if (measure.kind !== "exposure" &&
|
|
877
|
+
if (measure.kind !== "exposure" &&
|
|
878
|
+
measure.source !== "loss" &&
|
|
879
|
+
measure.source !== "derived") {
|
|
533
880
|
pushIssue(issues, "invalid-type", `${base}.source`, "Non-exposure measure source must be loss or derived");
|
|
534
881
|
}
|
|
535
882
|
if (measure.kind !== "exposure" && measure.source === "exposure") {
|
|
@@ -537,7 +884,8 @@ function validateDefinition(value) {
|
|
|
537
884
|
}
|
|
538
885
|
if (measure.kind === "amount" && measure.basisId) {
|
|
539
886
|
const basis = amountBasisMap.get(measure.basisId);
|
|
540
|
-
const hasSdkLimitation = basis?.components.some((component) => (component.limitation.kind === "layer" ||
|
|
887
|
+
const hasSdkLimitation = basis?.components.some((component) => (component.limitation.kind === "layer" ||
|
|
888
|
+
component.limitation.kind === "pre-limited") &&
|
|
541
889
|
component.limitation.derivation.kind === "sdk") ?? false;
|
|
542
890
|
if (hasSdkLimitation && measure.source !== "derived") {
|
|
543
891
|
pushIssue(issues, "incompatible-semantics", `${base}.source`, "SDK-limited amount measures must be derived");
|
|
@@ -548,6 +896,18 @@ function validateDefinition(value) {
|
|
|
548
896
|
const formulaRoleUsage = new Map();
|
|
549
897
|
formulas.forEach((formula, index) => {
|
|
550
898
|
const base = `$.formulas[${index}]`;
|
|
899
|
+
if (!isPlainRecord(formula)) {
|
|
900
|
+
pushIssue(issues, "invalid-type", base, "Formula must be an object");
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
validateAllowedKeys(formula, [
|
|
904
|
+
"id",
|
|
905
|
+
"version",
|
|
906
|
+
"roles",
|
|
907
|
+
"numerator",
|
|
908
|
+
"denominator",
|
|
909
|
+
"denominatorPolicy",
|
|
910
|
+
], base, issues);
|
|
551
911
|
validateToken(formula.version, `${base}.version`, "Formula version", issues);
|
|
552
912
|
if (formula.denominatorPolicy !== "positive-or-null") {
|
|
553
913
|
pushIssue(issues, "incompatible-semantics", `${base}.denominatorPolicy`, "Unknown denominator policy");
|
|
@@ -558,19 +918,30 @@ function validateDefinition(value) {
|
|
|
558
918
|
}
|
|
559
919
|
for (const [roleName, role] of Object.entries(formula.roles)) {
|
|
560
920
|
validateToken(roleName, propertyPath(`${base}.roles`, roleName), "Formula role", issues);
|
|
561
|
-
if (!isPlainRecord(role) ||
|
|
921
|
+
if (!isPlainRecord(role) ||
|
|
922
|
+
!["count", "amount", "exposure"].includes(role.kind)) {
|
|
562
923
|
pushIssue(issues, "invalid-type", propertyPath(`${base}.roles`, roleName), "Formula role has an invalid kind");
|
|
563
924
|
}
|
|
925
|
+
else {
|
|
926
|
+
const rolePath = propertyPath(`${base}.roles`, roleName);
|
|
927
|
+
validateAllowedKeys(role, ["kind", "compatibilityGroup", "developmentSemantics"], rolePath, issues);
|
|
928
|
+
if (role.developmentSemantics !== undefined &&
|
|
929
|
+
!["cumulative", "incremental", "point-in-time", "unknown"].includes(role.developmentSemantics)) {
|
|
930
|
+
pushIssue(issues, "invalid-type", `${rolePath}.developmentSemantics`, "Unknown formula-role development semantics");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
564
933
|
if (role.compatibilityGroup !== undefined)
|
|
565
934
|
validateToken(role.compatibilityGroup, `${propertyPath(`${base}.roles`, roleName)}.compatibilityGroup`, "Compatibility group", issues);
|
|
566
935
|
}
|
|
567
936
|
const numerator = walkDiagnosticExpression(formula.numerator, "role", `${base}.numerator`, issues);
|
|
568
937
|
const denominator = walkDiagnosticExpression(formula.denominator, "role", `${base}.denominator`, issues);
|
|
569
938
|
definitionExpressionNodes += numerator.nodeCount + denominator.nodeCount;
|
|
570
|
-
const used = [
|
|
939
|
+
const used = [
|
|
940
|
+
...new Set([...numerator.dependencies, ...denominator.dependencies]),
|
|
941
|
+
].sort();
|
|
571
942
|
formulaRoleUsage.set(formula.id, used);
|
|
572
943
|
for (const role of used) {
|
|
573
|
-
if (!
|
|
944
|
+
if (!hasDiagnosticOwn(formula.roles, role)) {
|
|
574
945
|
pushIssue(issues, "unknown-reference", base, `Formula ${formula.id} references unknown role ${role}`);
|
|
575
946
|
}
|
|
576
947
|
}
|
|
@@ -578,8 +949,13 @@ function validateDefinition(value) {
|
|
|
578
949
|
if (!used.includes(role))
|
|
579
950
|
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.roles`, role), `Formula role ${role} is unused`);
|
|
580
951
|
}
|
|
581
|
-
for (const [side, walked] of [
|
|
582
|
-
|
|
952
|
+
for (const [side, walked] of [
|
|
953
|
+
["numerator", numerator],
|
|
954
|
+
["denominator", denominator],
|
|
955
|
+
]) {
|
|
956
|
+
const kinds = new Set(walked.dependencies
|
|
957
|
+
.map((role) => formula.roles[role]?.kind)
|
|
958
|
+
.filter(Boolean));
|
|
583
959
|
if (kinds.size > 1)
|
|
584
960
|
pushIssue(issues, "incompatible-semantics", `${base}.${side}`, "Formula arithmetic combines different role kinds");
|
|
585
961
|
}
|
|
@@ -587,6 +963,11 @@ function validateDefinition(value) {
|
|
|
587
963
|
const instanceRuleIds = new Set();
|
|
588
964
|
instances.forEach((instance, index) => {
|
|
589
965
|
const base = `$.instances[${index}]`;
|
|
966
|
+
if (!isPlainRecord(instance)) {
|
|
967
|
+
pushIssue(issues, "invalid-type", base, "Metric instance must be an object");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
validateAllowedKeys(instance, ["id", "version", "formulaId", "bindings", "presentation", "rules"], base, issues);
|
|
590
971
|
validateToken(instance.version, `${base}.version`, "Instance version", issues);
|
|
591
972
|
validateToken(instance.formulaId, `${base}.formulaId`, "Formula reference", issues);
|
|
592
973
|
const formula = formulaMap.get(instance.formulaId);
|
|
@@ -601,7 +982,7 @@ function validateDefinition(value) {
|
|
|
601
982
|
const usedRoles = formulaRoleUsage.get(formula.id) ?? [];
|
|
602
983
|
const bindingSemantics = new Map();
|
|
603
984
|
for (const roleName of usedRoles) {
|
|
604
|
-
if (!
|
|
985
|
+
if (!hasDiagnosticOwn(instance.bindings, roleName)) {
|
|
605
986
|
pushIssue(issues, "missing-required", propertyPath(`${base}.bindings`, roleName), `Missing binding for role ${roleName}`);
|
|
606
987
|
continue;
|
|
607
988
|
}
|
|
@@ -610,7 +991,9 @@ function validateDefinition(value) {
|
|
|
610
991
|
bindingSemantics.set(roleName, semantics);
|
|
611
992
|
definitionExpressionNodes += walkDiagnosticExpression(expression, "measure", propertyPath(`${base}.bindings`, roleName), []).nodeCount;
|
|
612
993
|
const role = formula.roles[roleName];
|
|
613
|
-
const boundMeasures = semantics.measureIds
|
|
994
|
+
const boundMeasures = semantics.measureIds
|
|
995
|
+
.map((id) => measureMap.get(id))
|
|
996
|
+
.filter((item) => item !== undefined);
|
|
614
997
|
if (boundMeasures.some((measure) => measure.kind !== role.kind)) {
|
|
615
998
|
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.bindings`, roleName), `Binding kind does not match role ${roleName}`);
|
|
616
999
|
}
|
|
@@ -623,9 +1006,16 @@ function validateDefinition(value) {
|
|
|
623
1006
|
}
|
|
624
1007
|
}
|
|
625
1008
|
}
|
|
626
|
-
|
|
1009
|
+
const calculationSignatures = new Map();
|
|
1010
|
+
for (const [side, roleExpression] of [
|
|
1011
|
+
["numerator", formula.numerator],
|
|
1012
|
+
["denominator", formula.denominator],
|
|
1013
|
+
]) {
|
|
627
1014
|
const roleIds = walkDiagnosticExpression(roleExpression, "role", `${base}.${side}`, []).dependencies;
|
|
628
|
-
const signatures = new Set(roleIds
|
|
1015
|
+
const signatures = new Set(roleIds
|
|
1016
|
+
.map((roleId) => bindingSemantics.get(roleId)?.signature)
|
|
1017
|
+
.filter((signature) => signature !== null && signature !== undefined));
|
|
1018
|
+
calculationSignatures.set(side, signatures.size === 1 ? [...signatures][0] : null);
|
|
629
1019
|
if (signatures.size > 1) {
|
|
630
1020
|
pushIssue(issues, "incompatible-semantics", `${base}.bindings`, `Bound ${side} arithmetic combines incompatible semantics`);
|
|
631
1021
|
}
|
|
@@ -651,6 +1041,18 @@ function validateDefinition(value) {
|
|
|
651
1041
|
else
|
|
652
1042
|
compatibility.set(role.compatibilityGroup, semantics.signature);
|
|
653
1043
|
}
|
|
1044
|
+
if (!isPlainRecord(instance.presentation)) {
|
|
1045
|
+
pushIssue(issues, "invalid-type", `${base}.presentation`, "Metric presentation must be an object");
|
|
1046
|
+
}
|
|
1047
|
+
else
|
|
1048
|
+
validateAllowedKeys(instance.presentation, [
|
|
1049
|
+
"displayName",
|
|
1050
|
+
"description",
|
|
1051
|
+
"displayUnit",
|
|
1052
|
+
"scale",
|
|
1053
|
+
"numeratorLabel",
|
|
1054
|
+
"denominatorLabel",
|
|
1055
|
+
], `${base}.presentation`, issues);
|
|
654
1056
|
for (const [key, text] of [
|
|
655
1057
|
["displayName", instance.presentation?.displayName],
|
|
656
1058
|
["description", instance.presentation?.description],
|
|
@@ -659,12 +1061,18 @@ function validateDefinition(value) {
|
|
|
659
1061
|
["denominatorLabel", instance.presentation?.denominatorLabel],
|
|
660
1062
|
])
|
|
661
1063
|
validateToken(text, `${base}.presentation.${key}`, `Presentation ${key}`, issues);
|
|
662
|
-
if (!validateFinite(instance.presentation?.scale, `${base}.presentation.scale`, "Presentation scale", issues) ||
|
|
1064
|
+
if (!validateFinite(instance.presentation?.scale, `${base}.presentation.scale`, "Presentation scale", issues) ||
|
|
1065
|
+
instance.presentation.scale <= 0) {
|
|
663
1066
|
pushIssue(issues, "invalid-number", `${base}.presentation.scale`, "Presentation scale must be positive");
|
|
664
1067
|
}
|
|
665
1068
|
requireArray(instance.rules, `${base}.rules`, issues).forEach((rawRule, ruleIndex) => {
|
|
666
|
-
const rule = rawRule;
|
|
667
1069
|
const ruleBase = `${base}.rules[${ruleIndex}]`;
|
|
1070
|
+
if (!isPlainRecord(rawRule)) {
|
|
1071
|
+
pushIssue(issues, "invalid-type", ruleBase, "Metric rule must be an object");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
const rule = rawRule;
|
|
1075
|
+
validateAllowedKeys(rawRule, ["id", "code", "message", "severity", "when"], ruleBase, issues);
|
|
668
1076
|
if (validateToken(rule.id, `${ruleBase}.id`, "Metric rule ID", issues)) {
|
|
669
1077
|
if (rule.id.startsWith("diagnostic/structural/"))
|
|
670
1078
|
pushIssue(issues, "incompatible-semantics", `${ruleBase}.id`, "Metric rule ID uses the reserved structural prefix");
|
|
@@ -674,25 +1082,83 @@ function validateDefinition(value) {
|
|
|
674
1082
|
}
|
|
675
1083
|
validateToken(rule.code, `${ruleBase}.code`, "Metric rule code", issues);
|
|
676
1084
|
validateToken(rule.message, `${ruleBase}.message`, "Metric rule message", issues);
|
|
1085
|
+
if (rule.severity !== "warning" && rule.severity !== "fail")
|
|
1086
|
+
pushIssue(issues, "invalid-type", `${ruleBase}.severity`, "Metric rule severity must be warning or fail");
|
|
1087
|
+
if (!isPlainRecord(rule.when)) {
|
|
1088
|
+
pushIssue(issues, "invalid-type", `${ruleBase}.when`, "Metric rule predicate must be an object");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
validateAllowedKeys(rule.when, ["left", "operator", "right", "tolerance"], `${ruleBase}.when`, issues);
|
|
1092
|
+
if (rule.when.tolerance !== undefined) {
|
|
1093
|
+
if (!isPlainRecord(rule.when.tolerance))
|
|
1094
|
+
pushIssue(issues, "invalid-type", `${ruleBase}.when.tolerance`, "Tolerance must be an object");
|
|
1095
|
+
else
|
|
1096
|
+
validateAllowedKeys(rule.when.tolerance, ["absolute", "relative"], `${ruleBase}.when.tolerance`, issues);
|
|
1097
|
+
}
|
|
1098
|
+
if (!["lt", "lte", "eq", "neq", "gte", "gt"].includes(rule.when.operator))
|
|
1099
|
+
pushIssue(issues, "invalid-type", `${ruleBase}.when.operator`, "Unknown metric comparison operator");
|
|
677
1100
|
validateTolerance(rule.when?.tolerance, `${ruleBase}.when.tolerance`, issues);
|
|
678
|
-
|
|
1101
|
+
const operandSignatures = new Map();
|
|
1102
|
+
for (const [side, operand] of [
|
|
1103
|
+
["left", rule.when?.left],
|
|
1104
|
+
["right", rule.when?.right],
|
|
1105
|
+
]) {
|
|
679
1106
|
const operandPath = `${ruleBase}.when.${side}`;
|
|
1107
|
+
if (isPlainRecord(operand)) {
|
|
1108
|
+
const allowed = operand.source === "measure"
|
|
1109
|
+
? ["source", "expression"]
|
|
1110
|
+
: operand.source === "calculation"
|
|
1111
|
+
? ["source", "field"]
|
|
1112
|
+
: operand.source === "constant"
|
|
1113
|
+
? ["source", "value"]
|
|
1114
|
+
: ["source"];
|
|
1115
|
+
validateAllowedKeys(operand, allowed, operandPath, issues);
|
|
1116
|
+
}
|
|
680
1117
|
if (operand?.source === "measure") {
|
|
681
|
-
const semantic = expressionSemantics(operand.expression, `${operandPath}.expression`, measureMap, issues);
|
|
682
|
-
|
|
683
|
-
|
|
1118
|
+
const semantic = expressionSemantics(operand.expression, `${operandPath}.expression`, measureMap, issues, 2);
|
|
1119
|
+
const operandNodes = 1 +
|
|
1120
|
+
walkDiagnosticExpression(operand.expression, "measure", `${operandPath}.expression`, []).nodeCount;
|
|
1121
|
+
definitionExpressionNodes += operandNodes;
|
|
1122
|
+
if (operandNodes > MAX_DIAGNOSTIC_EXPRESSION_NODES) {
|
|
1123
|
+
pushIssue(issues, "expression-limit", operandPath, `Metric rule operand node count exceeds ${MAX_DIAGNOSTIC_EXPRESSION_NODES}`);
|
|
1124
|
+
}
|
|
1125
|
+
operandSignatures.set(side, semantic.signature);
|
|
684
1126
|
}
|
|
685
|
-
else {
|
|
1127
|
+
else if (operand?.source === "calculation") {
|
|
686
1128
|
definitionExpressionNodes += 1;
|
|
687
|
-
if (operand
|
|
688
|
-
|
|
1129
|
+
if (operand.field !== "numerator" &&
|
|
1130
|
+
operand.field !== "denominator")
|
|
1131
|
+
pushIssue(issues, "invalid-type", `${operandPath}.field`, "Unknown calculation field");
|
|
1132
|
+
else
|
|
1133
|
+
operandSignatures.set(side, calculationSignatures.get(operand.field));
|
|
689
1134
|
}
|
|
1135
|
+
else if (operand?.source === "constant") {
|
|
1136
|
+
definitionExpressionNodes += 1;
|
|
1137
|
+
operandSignatures.set(side, undefined);
|
|
1138
|
+
validateFinite(operand.value, `${operandPath}.value`, "Rule constant", issues);
|
|
1139
|
+
}
|
|
1140
|
+
else
|
|
1141
|
+
pushIssue(issues, "invalid-type", operandPath, "Unknown metric rule operand");
|
|
1142
|
+
}
|
|
1143
|
+
const leftSignature = operandSignatures.get("left");
|
|
1144
|
+
const rightSignature = operandSignatures.get("right");
|
|
1145
|
+
if (leftSignature !== undefined &&
|
|
1146
|
+
rightSignature !== undefined &&
|
|
1147
|
+
leftSignature !== null &&
|
|
1148
|
+
rightSignature !== null &&
|
|
1149
|
+
leftSignature !== rightSignature) {
|
|
1150
|
+
pushIssue(issues, "incompatible-semantics", `${ruleBase}.when`, "Metric comparison operands must have identical quantity semantics");
|
|
690
1151
|
}
|
|
691
1152
|
});
|
|
692
1153
|
});
|
|
693
1154
|
const derivedOutputs = new Map();
|
|
694
1155
|
derivations.forEach((derivation, index) => {
|
|
695
1156
|
const base = `$.derivedMeasures[${index}]`;
|
|
1157
|
+
if (!isPlainRecord(derivation)) {
|
|
1158
|
+
pushIssue(issues, "invalid-type", base, "Derived measure must be an object");
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
validateAllowedKeys(derivation, ["id", "outputMeasureId", "expression"], base, issues);
|
|
696
1162
|
validateToken(derivation.outputMeasureId, `${base}.outputMeasureId`, "Derived output measure", issues);
|
|
697
1163
|
const output = measureMap.get(derivation.outputMeasureId);
|
|
698
1164
|
if (!output)
|
|
@@ -708,8 +1174,11 @@ function validateDefinition(value) {
|
|
|
708
1174
|
if (!measureMap.has(id))
|
|
709
1175
|
pushIssue(issues, "unknown-reference", `${base}.expression`, `Unknown derivation measure ${id}`);
|
|
710
1176
|
const transitiveIds = transitiveMeasureIds(walked.dependencies, derivationsByOutput);
|
|
711
|
-
const transitiveInputs = transitiveIds
|
|
712
|
-
|
|
1177
|
+
const transitiveInputs = transitiveIds
|
|
1178
|
+
.map((id) => measureMap.get(id))
|
|
1179
|
+
.filter((item) => item !== undefined);
|
|
1180
|
+
if (output &&
|
|
1181
|
+
transitiveInputs.some((input) => input.developmentSemantics !== output.developmentSemantics)) {
|
|
713
1182
|
pushIssue(issues, "incompatible-semantics", `${base}.expression`, "Derived output and inputs must share development semantics");
|
|
714
1183
|
}
|
|
715
1184
|
if (definition.lossRowGrain !== "claim") {
|
|
@@ -745,9 +1214,12 @@ function validateDefinition(value) {
|
|
|
745
1214
|
const expected = output.kind === "amount" && output.basisId
|
|
746
1215
|
? amountBasisMap.get(output.basisId)
|
|
747
1216
|
: undefined;
|
|
748
|
-
const matches = output.kind === projection.kind &&
|
|
1217
|
+
const matches = output.kind === projection.kind &&
|
|
1218
|
+
output.unit === projection.unit &&
|
|
749
1219
|
(output.kind === "amount"
|
|
750
|
-
? expected !== undefined &&
|
|
1220
|
+
? expected !== undefined &&
|
|
1221
|
+
projectedBasisKey(projectedAmountBasis(expected)) ===
|
|
1222
|
+
projectedBasisKey(projection.amountBasis)
|
|
751
1223
|
: semanticReference(output) === projection.reference);
|
|
752
1224
|
if (!matches)
|
|
753
1225
|
pushIssue(issues, "incompatible-semantics", `${base}.expression`, "Derived expression does not project the output measure semantics");
|
|
@@ -782,42 +1254,161 @@ function validateDefinition(value) {
|
|
|
782
1254
|
visit(id);
|
|
783
1255
|
reviewRules.forEach((rule, index) => {
|
|
784
1256
|
const base = `$.reviewRules[${index}]`;
|
|
1257
|
+
if (!isPlainRecord(rule)) {
|
|
1258
|
+
pushIssue(issues, "invalid-type", base, "Review rule must be an object");
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
const commonKeys = [
|
|
1262
|
+
"kind",
|
|
1263
|
+
"id",
|
|
1264
|
+
"code",
|
|
1265
|
+
"description",
|
|
1266
|
+
"severity",
|
|
1267
|
+
"tolerance",
|
|
1268
|
+
"missingInput",
|
|
1269
|
+
];
|
|
1270
|
+
const branchKeys = rule.kind === "compare"
|
|
1271
|
+
? ["when"]
|
|
1272
|
+
: rule.kind === "reconcile"
|
|
1273
|
+
? ["actual", "expected"]
|
|
1274
|
+
: rule.kind === "monotonic"
|
|
1275
|
+
? ["expression", "direction"]
|
|
1276
|
+
: rule.kind === "layer-order"
|
|
1277
|
+
? ["narrower", "broader", "comparability"]
|
|
1278
|
+
: rule.kind === "control-total"
|
|
1279
|
+
? ["expression", "expected", "filter", "projection"]
|
|
1280
|
+
: [];
|
|
1281
|
+
validateAllowedKeys(rule, [...commonKeys, ...branchKeys], base, issues);
|
|
1282
|
+
if (rule.tolerance !== undefined) {
|
|
1283
|
+
if (!isPlainRecord(rule.tolerance))
|
|
1284
|
+
pushIssue(issues, "invalid-type", `${base}.tolerance`, "Tolerance must be an object");
|
|
1285
|
+
else
|
|
1286
|
+
validateAllowedKeys(rule.tolerance, ["absolute", "relative"], `${base}.tolerance`, issues);
|
|
1287
|
+
}
|
|
785
1288
|
validateToken(rule.code, `${base}.code`, "Review rule code", issues);
|
|
786
1289
|
validateToken(rule.description, `${base}.description`, "Review rule description", issues);
|
|
787
|
-
if (rule.id
|
|
1290
|
+
if (typeof rule.id === "string" &&
|
|
1291
|
+
rule.id.startsWith("diagnostic/structural/"))
|
|
788
1292
|
pushIssue(issues, "incompatible-semantics", `${base}.id`, "Review rule ID uses the reserved structural prefix");
|
|
1293
|
+
if (rule.severity !== "warning" && rule.severity !== "fail")
|
|
1294
|
+
pushIssue(issues, "invalid-type", `${base}.severity`, "Review severity must be warning or fail");
|
|
1295
|
+
if (rule.missingInput !== "not-evaluated" &&
|
|
1296
|
+
rule.missingInput !== "finding")
|
|
1297
|
+
pushIssue(issues, "invalid-type", `${base}.missingInput`, "Review missing-input policy must be not-evaluated or finding");
|
|
789
1298
|
validateTolerance(rule.tolerance, `${base}.tolerance`, issues);
|
|
790
1299
|
const roots = [];
|
|
791
|
-
|
|
792
|
-
if (
|
|
793
|
-
|
|
794
|
-
|
|
1300
|
+
const operand = (value, path) => {
|
|
1301
|
+
if (isPlainRecord(value) && value.op === "constant") {
|
|
1302
|
+
validateAllowedKeys(value, ["op", "value"], path, issues);
|
|
1303
|
+
validateFinite(value.value, `${path}.value`, "Review constant", issues);
|
|
795
1304
|
definitionExpressionNodes++;
|
|
796
|
-
|
|
797
|
-
roots.push(rule.when.right);
|
|
1305
|
+
}
|
|
798
1306
|
else
|
|
799
|
-
|
|
1307
|
+
roots.push({ expression: value, path });
|
|
1308
|
+
};
|
|
1309
|
+
if (rule.kind === "compare") {
|
|
1310
|
+
if (!isPlainRecord(rule.when))
|
|
1311
|
+
pushIssue(issues, "invalid-type", `${base}.when`, "Compare rule predicate must be an object");
|
|
1312
|
+
else {
|
|
1313
|
+
validateAllowedKeys(rule.when, ["left", "operator", "right"], `${base}.when`, issues);
|
|
1314
|
+
operand(rule.when.left, `${base}.when.left`);
|
|
1315
|
+
operand(rule.when.right, `${base}.when.right`);
|
|
1316
|
+
if (!["lt", "lte", "eq", "neq", "gte", "gt"].includes(rule.when.operator))
|
|
1317
|
+
pushIssue(issues, "invalid-type", `${base}.when.operator`, "Unknown review comparison operator");
|
|
1318
|
+
}
|
|
800
1319
|
}
|
|
801
1320
|
else if (rule.kind === "reconcile") {
|
|
802
|
-
roots.push(rule.actual);
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1321
|
+
roots.push({ expression: rule.actual, path: `${base}.actual` });
|
|
1322
|
+
operand(rule.expected, `${base}.expected`);
|
|
1323
|
+
}
|
|
1324
|
+
else if (rule.kind === "monotonic") {
|
|
1325
|
+
roots.push({ expression: rule.expression, path: `${base}.expression` });
|
|
1326
|
+
if (rule.direction !== "nondecreasing" &&
|
|
1327
|
+
rule.direction !== "nonincreasing")
|
|
1328
|
+
pushIssue(issues, "invalid-type", `${base}.direction`, "Unknown monotonic direction");
|
|
1329
|
+
}
|
|
1330
|
+
else if (rule.kind === "layer-order") {
|
|
1331
|
+
roots.push({ expression: rule.narrower, path: `${base}.narrower` }, { expression: rule.broader, path: `${base}.broader` });
|
|
1332
|
+
if (!isPlainRecord(rule.comparability) ||
|
|
1333
|
+
(rule.comparability.kind !== "compiler-proven" &&
|
|
1334
|
+
rule.comparability.kind !== "caller-asserted")) {
|
|
1335
|
+
pushIssue(issues, "invalid-type", `${base}.comparability`, "Unknown layer comparability evidence");
|
|
1336
|
+
}
|
|
1337
|
+
else if (rule.comparability.kind === "caller-asserted") {
|
|
1338
|
+
validateAllowedKeys(rule.comparability, ["kind", "rationaleArtifactId"], `${base}.comparability`, issues);
|
|
1339
|
+
validateToken(rule.comparability.rationaleArtifactId, `${base}.comparability.rationaleArtifactId`, "Rationale artifact ID", issues);
|
|
1340
|
+
}
|
|
1341
|
+
if (isPlainRecord(rule.comparability) &&
|
|
1342
|
+
rule.comparability.kind === "compiler-proven")
|
|
1343
|
+
validateAllowedKeys(rule.comparability, ["kind"], `${base}.comparability`, issues);
|
|
1344
|
+
if (isPlainRecord(rule.comparability) &&
|
|
1345
|
+
(rule.comparability.kind === "compiler-proven" ||
|
|
1346
|
+
rule.comparability.kind === "caller-asserted")) {
|
|
1347
|
+
validateLayerComparability(rule, `${base}.comparability`, measureMap, amountBasisMap, derivationsByOutput, issues);
|
|
1348
|
+
}
|
|
807
1349
|
}
|
|
808
|
-
else if (rule.kind === "monotonic")
|
|
809
|
-
roots.push(rule.expression);
|
|
810
|
-
else if (rule.kind === "layer-order")
|
|
811
|
-
roots.push(rule.narrower, rule.broader);
|
|
812
1350
|
else if (rule.kind === "control-total") {
|
|
813
|
-
roots.push(rule.expression);
|
|
1351
|
+
roots.push({ expression: rule.expression, path: `${base}.expression` });
|
|
814
1352
|
validateFinite(rule.expected, `${base}.expected`, "Control total", issues);
|
|
1353
|
+
if (rule.filter !== undefined)
|
|
1354
|
+
validateReviewFilter(rule.filter, `${base}.filter`, definition.periodAxis, issues);
|
|
1355
|
+
if (!isPlainRecord(rule.projection) ||
|
|
1356
|
+
!["valuation", "latest-valuation-per-origin", "all-cells"].includes(rule.projection.kind)) {
|
|
1357
|
+
pushIssue(issues, "invalid-type", `${base}.projection`, "Unknown control-total projection");
|
|
1358
|
+
}
|
|
1359
|
+
else if (rule.projection.kind === "valuation") {
|
|
1360
|
+
validateAllowedKeys(rule.projection, ["kind", "valuation"], `${base}.projection`, issues);
|
|
1361
|
+
if (validateToken(rule.projection.valuation, `${base}.projection.valuation`, "Control-total valuation", issues) &&
|
|
1362
|
+
!normalizeDiagnosticPeriodWithAxis(definition.periodAxis, "valuation", rule.projection.valuation)) {
|
|
1363
|
+
pushIssue(issues, "invalid-period", `${base}.projection.valuation`, `Unknown valuation period ${JSON.stringify(rule.projection.valuation)}`);
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
else
|
|
1367
|
+
validateAllowedKeys(rule.projection, ["kind"], `${base}.projection`, issues);
|
|
815
1368
|
}
|
|
816
1369
|
else
|
|
817
1370
|
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown review-rule kind");
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1371
|
+
const compatibleOperands = (left, right, path) => {
|
|
1372
|
+
if (!isPlainRecord(left) || !isPlainRecord(right))
|
|
1373
|
+
return;
|
|
1374
|
+
if (left.op === "constant" || right.op === "constant")
|
|
1375
|
+
return;
|
|
1376
|
+
const leftSignature = expressionSemantics(left, `${path}.left`, measureMap, []).signature;
|
|
1377
|
+
const rightSignature = expressionSemantics(right, `${path}.right`, measureMap, []).signature;
|
|
1378
|
+
if (leftSignature !== null &&
|
|
1379
|
+
rightSignature !== null &&
|
|
1380
|
+
leftSignature !== rightSignature) {
|
|
1381
|
+
pushIssue(issues, "incompatible-semantics", path, "Review comparison operands must have identical quantity semantics");
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
if (rule.kind === "compare" && isPlainRecord(rule.when)) {
|
|
1385
|
+
compatibleOperands(rule.when.left, rule.when.right, `${base}.when`);
|
|
1386
|
+
}
|
|
1387
|
+
else if (rule.kind === "reconcile") {
|
|
1388
|
+
compatibleOperands(rule.actual, rule.expected, base);
|
|
1389
|
+
}
|
|
1390
|
+
roots.forEach(({ expression, path }) => {
|
|
1391
|
+
const semantics = expressionSemantics(expression, path, measureMap, issues);
|
|
1392
|
+
definitionExpressionNodes += walkDiagnosticExpression(expression, "measure", path, []).nodeCount;
|
|
1393
|
+
if (rule.kind === "monotonic") {
|
|
1394
|
+
const leaves = transitiveMeasureIds(semantics.measureIds, derivationsByOutput)
|
|
1395
|
+
.map((id) => measureMap.get(id))
|
|
1396
|
+
.filter((item) => item !== undefined);
|
|
1397
|
+
if (leaves.some((measure) => measure.developmentSemantics !== "cumulative" &&
|
|
1398
|
+
measure.developmentSemantics !== "point-in-time")) {
|
|
1399
|
+
pushIssue(issues, "incompatible-semantics", path, "Monotonic review requires cumulative or point-in-time leaves");
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
if (rule.kind === "control-total" &&
|
|
1403
|
+
rule.projection?.kind === "all-cells") {
|
|
1404
|
+
const leaves = transitiveMeasureIds(semantics.measureIds, derivationsByOutput)
|
|
1405
|
+
.map((id) => measureMap.get(id))
|
|
1406
|
+
.filter((item) => item !== undefined);
|
|
1407
|
+
if (leaves.some((measure) => measure.developmentSemantics === "cumulative" ||
|
|
1408
|
+
measure.exposureTiming === "origin-static")) {
|
|
1409
|
+
pushIssue(issues, "incompatible-semantics", `${base}.projection`, "All-cells control totals cannot repeat cumulative or origin-static measures");
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
821
1412
|
});
|
|
822
1413
|
});
|
|
823
1414
|
if (definitionExpressionNodes > MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES) {
|
|
@@ -828,9 +1419,27 @@ function validateDefinition(value) {
|
|
|
828
1419
|
throw new DiagnosticValidationError(issues);
|
|
829
1420
|
return definition;
|
|
830
1421
|
}
|
|
1422
|
+
/** Internal boundary reuse: direct helpers use the compiler's exact axis contract. */
|
|
1423
|
+
export function validateDiagnosticPeriodAxisInput(value) {
|
|
1424
|
+
const preflight = diagnosticJsonPreflight(value, "configuration").map((issue) => ({
|
|
1425
|
+
...issue,
|
|
1426
|
+
path: `$.periodAxis${issue.path.slice(1)}`,
|
|
1427
|
+
}));
|
|
1428
|
+
if (preflight.length > 0)
|
|
1429
|
+
return preflight;
|
|
1430
|
+
const issues = [];
|
|
1431
|
+
validatePeriodAxis(value, issues);
|
|
1432
|
+
return issues.map((issue) => ({ ...issue, domain: "configuration" }));
|
|
1433
|
+
}
|
|
831
1434
|
function optionalFromNullable(value) {
|
|
832
1435
|
return value === null || value === undefined ? undefined : value;
|
|
833
1436
|
}
|
|
1437
|
+
function omitProperties(value, keys) {
|
|
1438
|
+
const result = { ...value };
|
|
1439
|
+
for (const key of keys)
|
|
1440
|
+
delete result[key];
|
|
1441
|
+
return result;
|
|
1442
|
+
}
|
|
834
1443
|
/**
|
|
835
1444
|
* The normalized wire projection materializes optional semantic fields as
|
|
836
1445
|
* null. Convert only those documented defaults back to the authored view so
|
|
@@ -842,9 +1451,9 @@ function authoredView(value) {
|
|
|
842
1451
|
const tolerance = { ...rule.tolerance };
|
|
843
1452
|
if (rule.kind !== "control-total")
|
|
844
1453
|
return { ...rule, tolerance };
|
|
845
|
-
const filter = rule.filter === null
|
|
1454
|
+
const filter = rule.filter === null || rule.filter === undefined
|
|
846
1455
|
? undefined
|
|
847
|
-
: {
|
|
1456
|
+
: Object.fromEntries(Object.entries({
|
|
848
1457
|
sourceGroups: optionalFromNullable(rule.filter.sourceGroups),
|
|
849
1458
|
origins: optionalFromNullable(rule.filter.origins),
|
|
850
1459
|
originFrom: optionalFromNullable(rule.filter.originFrom),
|
|
@@ -854,15 +1463,26 @@ function authoredView(value) {
|
|
|
854
1463
|
valuationThrough: optionalFromNullable(rule.filter.valuationThrough),
|
|
855
1464
|
minDevelopmentAge: optionalFromNullable(rule.filter.minDevelopmentAge),
|
|
856
1465
|
maxDevelopmentAge: optionalFromNullable(rule.filter.maxDevelopmentAge),
|
|
857
|
-
};
|
|
858
|
-
return {
|
|
1466
|
+
}).filter(([, item]) => item !== undefined));
|
|
1467
|
+
return {
|
|
1468
|
+
...omitProperties(rule, ["filter"]),
|
|
1469
|
+
tolerance,
|
|
1470
|
+
...(filter === undefined ? {} : { filter }),
|
|
1471
|
+
};
|
|
859
1472
|
});
|
|
860
1473
|
return {
|
|
1474
|
+
...definition,
|
|
861
1475
|
diagnosticDefinitionVersion: definition.diagnosticDefinitionVersion,
|
|
862
1476
|
id: definition.id,
|
|
863
1477
|
version: definition.version,
|
|
864
1478
|
lossRowGrain: definition.lossRowGrain,
|
|
865
1479
|
measures: definition.measures.map((measure) => ({
|
|
1480
|
+
...omitProperties(measure, [
|
|
1481
|
+
"basisId",
|
|
1482
|
+
"countPopulationId",
|
|
1483
|
+
"exposureBasisId",
|
|
1484
|
+
"exposureTiming",
|
|
1485
|
+
]),
|
|
866
1486
|
id: measure.id,
|
|
867
1487
|
displayName: measure.displayName,
|
|
868
1488
|
description: measure.description,
|
|
@@ -872,69 +1492,100 @@ function authoredView(value) {
|
|
|
872
1492
|
developmentSemantics: measure.developmentSemantics,
|
|
873
1493
|
aggregation: measure.aggregation,
|
|
874
1494
|
missing: measure.missing,
|
|
875
|
-
...(measure.basisId === null || measure.basisId === undefined
|
|
876
|
-
|
|
1495
|
+
...(measure.basisId === null || measure.basisId === undefined
|
|
1496
|
+
? {}
|
|
1497
|
+
: { basisId: measure.basisId }),
|
|
1498
|
+
...(measure.countPopulationId === null ||
|
|
1499
|
+
measure.countPopulationId === undefined
|
|
877
1500
|
? {}
|
|
878
1501
|
: { countPopulationId: measure.countPopulationId }),
|
|
879
|
-
...(measure.exposureBasisId === null ||
|
|
1502
|
+
...(measure.exposureBasisId === null ||
|
|
1503
|
+
measure.exposureBasisId === undefined
|
|
880
1504
|
? {}
|
|
881
1505
|
: { exposureBasisId: measure.exposureBasisId }),
|
|
882
|
-
...(measure.exposureTiming === null ||
|
|
1506
|
+
...(measure.exposureTiming === null ||
|
|
1507
|
+
measure.exposureTiming === undefined
|
|
883
1508
|
? {}
|
|
884
1509
|
: { exposureTiming: measure.exposureTiming }),
|
|
885
1510
|
})),
|
|
886
|
-
countPopulations: definition.countPopulations.map((population) => ({
|
|
1511
|
+
countPopulations: definition.countPopulations.map((population) => ({
|
|
1512
|
+
...population,
|
|
1513
|
+
})),
|
|
887
1514
|
exposureBases: definition.exposureBases.map((basis) => ({
|
|
1515
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
888
1516
|
id: basis.id,
|
|
889
1517
|
displayName: basis.displayName,
|
|
890
1518
|
basis: basis.basis,
|
|
891
1519
|
unit: basis.unit,
|
|
892
1520
|
description: basis.description,
|
|
893
|
-
...(basis.sourceDescription === null ||
|
|
1521
|
+
...(basis.sourceDescription === null ||
|
|
1522
|
+
basis.sourceDescription === undefined
|
|
894
1523
|
? {}
|
|
895
1524
|
: { sourceDescription: basis.sourceDescription }),
|
|
896
|
-
...(basis.attributes === undefined
|
|
1525
|
+
...(basis.attributes === undefined
|
|
1526
|
+
? {}
|
|
1527
|
+
: { attributes: basis.attributes }),
|
|
897
1528
|
})),
|
|
898
1529
|
amountBases: definition.amountBases.map((basis) => ({
|
|
1530
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
899
1531
|
id: basis.id,
|
|
900
1532
|
displayName: basis.displayName,
|
|
901
1533
|
currency: basis.currency,
|
|
902
1534
|
perspective: basis.perspective,
|
|
903
1535
|
components: basis.components.map((component) => ({
|
|
1536
|
+
...component,
|
|
904
1537
|
id: component.id,
|
|
905
1538
|
treatment: component.treatment,
|
|
906
1539
|
limitation: component.limitation.kind === "unknown"
|
|
907
1540
|
? {
|
|
1541
|
+
...omitProperties(component.limitation, ["description"]),
|
|
908
1542
|
kind: "unknown",
|
|
909
|
-
...(component.limitation.description === null ||
|
|
1543
|
+
...(component.limitation.description === null ||
|
|
1544
|
+
component.limitation.description === undefined
|
|
910
1545
|
? {}
|
|
911
1546
|
: { description: component.limitation.description }),
|
|
912
1547
|
}
|
|
913
1548
|
: component.limitation,
|
|
914
1549
|
})),
|
|
915
|
-
...(basis.sourceDescription === null ||
|
|
1550
|
+
...(basis.sourceDescription === null ||
|
|
1551
|
+
basis.sourceDescription === undefined
|
|
916
1552
|
? {}
|
|
917
1553
|
: { sourceDescription: basis.sourceDescription }),
|
|
918
|
-
...(basis.attributes === undefined
|
|
1554
|
+
...(basis.attributes === undefined
|
|
1555
|
+
? {}
|
|
1556
|
+
: { attributes: basis.attributes }),
|
|
1557
|
+
})),
|
|
1558
|
+
derivedMeasures: definition.derivedMeasures.map((derivation) => ({
|
|
1559
|
+
...derivation,
|
|
919
1560
|
})),
|
|
920
|
-
derivedMeasures: definition.derivedMeasures.map((derivation) => ({ ...derivation })),
|
|
921
1561
|
formulas: definition.formulas.map((formula) => ({
|
|
1562
|
+
...formula,
|
|
922
1563
|
id: formula.id,
|
|
923
1564
|
version: formula.version,
|
|
924
|
-
roles: Object.fromEntries(Object.entries(formula.roles).map(([name, role]) => [
|
|
1565
|
+
roles: Object.fromEntries(Object.entries(formula.roles).map(([name, role]) => [
|
|
1566
|
+
name,
|
|
1567
|
+
{
|
|
1568
|
+
...omitProperties(role, [
|
|
1569
|
+
"compatibilityGroup",
|
|
1570
|
+
"developmentSemantics",
|
|
1571
|
+
]),
|
|
925
1572
|
kind: role.kind,
|
|
926
|
-
...(role.compatibilityGroup === null ||
|
|
1573
|
+
...(role.compatibilityGroup === null ||
|
|
1574
|
+
role.compatibilityGroup === undefined
|
|
927
1575
|
? {}
|
|
928
1576
|
: { compatibilityGroup: role.compatibilityGroup }),
|
|
929
|
-
...(role.developmentSemantics === null ||
|
|
1577
|
+
...(role.developmentSemantics === null ||
|
|
1578
|
+
role.developmentSemantics === undefined
|
|
930
1579
|
? {}
|
|
931
1580
|
: { developmentSemantics: role.developmentSemantics }),
|
|
932
|
-
}
|
|
1581
|
+
},
|
|
1582
|
+
])),
|
|
933
1583
|
numerator: formula.numerator,
|
|
934
1584
|
denominator: formula.denominator,
|
|
935
1585
|
denominatorPolicy: formula.denominatorPolicy,
|
|
936
1586
|
})),
|
|
937
1587
|
instances: definition.instances.map((instance) => ({
|
|
1588
|
+
...instance,
|
|
938
1589
|
id: instance.id,
|
|
939
1590
|
version: instance.version,
|
|
940
1591
|
formulaId: instance.formulaId,
|
|
@@ -960,12 +1611,14 @@ export function compileDiagnosticDefinition(value) {
|
|
|
960
1611
|
authored = authoredView(value);
|
|
961
1612
|
}
|
|
962
1613
|
catch {
|
|
963
|
-
throw new DiagnosticValidationError([
|
|
1614
|
+
throw new DiagnosticValidationError([
|
|
1615
|
+
{
|
|
964
1616
|
domain: "definition",
|
|
965
1617
|
code: "invalid-type",
|
|
966
1618
|
path: "$",
|
|
967
1619
|
message: "Diagnostic definition does not have the required object and array structure",
|
|
968
|
-
}
|
|
1620
|
+
},
|
|
1621
|
+
]);
|
|
969
1622
|
}
|
|
970
1623
|
let definition;
|
|
971
1624
|
try {
|
|
@@ -974,12 +1627,14 @@ export function compileDiagnosticDefinition(value) {
|
|
|
974
1627
|
catch (error) {
|
|
975
1628
|
if (error instanceof DiagnosticValidationError)
|
|
976
1629
|
throw error;
|
|
977
|
-
throw new DiagnosticValidationError([
|
|
1630
|
+
throw new DiagnosticValidationError([
|
|
1631
|
+
{
|
|
978
1632
|
domain: "definition",
|
|
979
1633
|
code: "invalid-type",
|
|
980
1634
|
path: "$",
|
|
981
1635
|
message: "Diagnostic definition contains a malformed nested value",
|
|
982
|
-
}
|
|
1636
|
+
},
|
|
1637
|
+
]);
|
|
983
1638
|
}
|
|
984
1639
|
const normalized = normalizeDiagnosticDefinition(definition);
|
|
985
1640
|
const identities = buildDiagnosticIdentities(normalized);
|
|
@@ -1004,18 +1659,25 @@ export function compileDiagnosticDefinition(value) {
|
|
|
1004
1659
|
calculationScopesByInstanceId: identities.calculationScopesByInstanceId,
|
|
1005
1660
|
calculationDependenciesByInstanceId: identities.calculationDependenciesByInstanceId,
|
|
1006
1661
|
evaluationDependenciesByInstanceId: identities.evaluationDependenciesByInstanceId,
|
|
1007
|
-
derivationsByOutputMeasureId: new Map(normalized.derivedMeasures.map((derivation) => [
|
|
1662
|
+
derivationsByOutputMeasureId: new Map(normalized.derivedMeasures.map((derivation) => [
|
|
1663
|
+
derivation.outputMeasureId,
|
|
1664
|
+
derivation,
|
|
1665
|
+
])),
|
|
1008
1666
|
});
|
|
1009
1667
|
return compiled;
|
|
1010
1668
|
}
|
|
1011
1669
|
export function assertCompiledDiagnosticDefinition(value) {
|
|
1012
|
-
if ((typeof value !== "object" && typeof value !== "function") ||
|
|
1013
|
-
|
|
1670
|
+
if ((typeof value !== "object" && typeof value !== "function") ||
|
|
1671
|
+
value === null ||
|
|
1672
|
+
!authenticCompiledDefinitions.has(value)) {
|
|
1673
|
+
throw new DiagnosticValidationError([
|
|
1674
|
+
{
|
|
1014
1675
|
domain: "definition",
|
|
1015
1676
|
code: "invalid-input-relationship",
|
|
1016
1677
|
path: "$",
|
|
1017
1678
|
message: "Value is not an authentic compiled diagnostic definition",
|
|
1018
|
-
}
|
|
1679
|
+
},
|
|
1680
|
+
]);
|
|
1019
1681
|
}
|
|
1020
1682
|
}
|
|
1021
1683
|
/** @internal Shared only by core diagnostic runtime modules; not re-exported. */
|