@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,1688 @@
|
|
|
1
|
+
import { MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES, MAX_DIAGNOSTIC_EXPRESSION_NODES, walkDiagnosticExpression, } from "./diagnosticExpressions.js";
|
|
2
|
+
import { buildDiagnosticIdentities, normalizeDiagnosticDefinition, } from "./diagnosticIdentity.js";
|
|
3
|
+
import { DiagnosticValidationError, } from "./types.js";
|
|
4
|
+
import { canonicalJson } from "./canonical.js";
|
|
5
|
+
import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
|
|
6
|
+
import { diagnosticJsonPreflight, hasDiagnosticOwn, isDiagnosticPlainRecord, isDiagnosticToken, isWellFormedDiagnosticString, } from "./diagnosticRuntime.js";
|
|
7
|
+
const compiledDiagnosticDefinitionBrand = Symbol("compiled-diagnostic-definition");
|
|
8
|
+
const authenticCompiledDefinitions = new WeakSet();
|
|
9
|
+
const compiledInternals = new WeakMap();
|
|
10
|
+
function isPlainRecord(value) {
|
|
11
|
+
return isDiagnosticPlainRecord(value);
|
|
12
|
+
}
|
|
13
|
+
function propertyPath(parent, key) {
|
|
14
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
15
|
+
? `${parent}.${key}`
|
|
16
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
17
|
+
}
|
|
18
|
+
function pushIssue(issues, code, path, message) {
|
|
19
|
+
issues.push({ domain: "definition", code, path, message });
|
|
20
|
+
}
|
|
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
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function validateToken(value, path, label, issues) {
|
|
56
|
+
if (typeof value !== "string") {
|
|
57
|
+
pushIssue(issues, "invalid-type", path, `${label} must be a string`);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!isWellFormedDiagnosticString(value)) {
|
|
61
|
+
pushIssue(issues, "invalid-string", path, `${label} contains an invalid Unicode string`);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (!isDiagnosticToken(value)) {
|
|
65
|
+
pushIssue(issues, "invalid-string", path, `${label} must be nonempty without surrounding ASCII whitespace`);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
function validateFinite(value, path, label, issues) {
|
|
71
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
72
|
+
pushIssue(issues, "invalid-number", path, `${label} must be finite`);
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
function validateJsonValue(value, issues) {
|
|
78
|
+
issues.push(...diagnosticJsonPreflight(value, "definition"));
|
|
79
|
+
}
|
|
80
|
+
function requireArray(value, path, issues) {
|
|
81
|
+
if (!Array.isArray(value)) {
|
|
82
|
+
pushIssue(issues, "invalid-type", path, "Expected an array");
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
function collectCatalog(values, path, issues) {
|
|
88
|
+
const result = new Map();
|
|
89
|
+
values.forEach((value, index) => {
|
|
90
|
+
const idPath = `${path}[${index}].id`;
|
|
91
|
+
if (!validateToken(value?.id, idPath, "ID", issues))
|
|
92
|
+
return;
|
|
93
|
+
if (result.has(value.id)) {
|
|
94
|
+
pushIssue(issues, "duplicate-id", idPath, `Duplicate ID ${value.id}`);
|
|
95
|
+
}
|
|
96
|
+
else
|
|
97
|
+
result.set(value.id, value);
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function semanticReference(measure) {
|
|
102
|
+
if (measure.kind === "amount")
|
|
103
|
+
return `amount:${measure.basisId ?? ""}`;
|
|
104
|
+
if (measure.kind === "count")
|
|
105
|
+
return `count:${measure.countPopulationId ?? ""}`;
|
|
106
|
+
return `exposure:${measure.exposureBasisId ?? ""}`;
|
|
107
|
+
}
|
|
108
|
+
function transitiveMeasureIds(roots, derivations) {
|
|
109
|
+
const result = new Set();
|
|
110
|
+
const stack = [...roots];
|
|
111
|
+
while (stack.length > 0) {
|
|
112
|
+
const id = stack.pop();
|
|
113
|
+
if (result.has(id))
|
|
114
|
+
continue;
|
|
115
|
+
result.add(id);
|
|
116
|
+
const derivation = derivations.get(id);
|
|
117
|
+
if (derivation) {
|
|
118
|
+
stack.push(...walkDiagnosticExpression(derivation.expression, "claim", "$", [])
|
|
119
|
+
.dependencies);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return [...result].sort();
|
|
123
|
+
}
|
|
124
|
+
function projectedAmountBasis(basis) {
|
|
125
|
+
return {
|
|
126
|
+
currency: basis.currency,
|
|
127
|
+
perspective: basis.perspective,
|
|
128
|
+
components: [...basis.components].sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function projectedBasisKey(basis) {
|
|
132
|
+
return canonicalJson(basis);
|
|
133
|
+
}
|
|
134
|
+
function projectClaimExpression(expression, path, measures, amountBases, issues) {
|
|
135
|
+
if (expression.op === "measure") {
|
|
136
|
+
const measure = measures.get(expression.measureId);
|
|
137
|
+
if (!measure)
|
|
138
|
+
return null;
|
|
139
|
+
if (measure.kind !== "amount") {
|
|
140
|
+
return {
|
|
141
|
+
kind: measure.kind,
|
|
142
|
+
unit: measure.unit,
|
|
143
|
+
reference: semanticReference(measure),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const basis = measure.basisId
|
|
147
|
+
? amountBases.get(measure.basisId)
|
|
148
|
+
: undefined;
|
|
149
|
+
return basis
|
|
150
|
+
? {
|
|
151
|
+
kind: "amount",
|
|
152
|
+
unit: measure.unit,
|
|
153
|
+
reference: `amount:${measure.basisId}`,
|
|
154
|
+
amountBasis: projectedAmountBasis(basis),
|
|
155
|
+
}
|
|
156
|
+
: null;
|
|
157
|
+
}
|
|
158
|
+
if (expression.op === "claim-layer") {
|
|
159
|
+
const measure = measures.get(expression.measureId);
|
|
160
|
+
if (!measure || measure.kind !== "amount" || !measure.basisId) {
|
|
161
|
+
pushIssue(issues, "incompatible-semantics", path, "Claim layers require an amount measure");
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const basis = amountBases.get(measure.basisId);
|
|
165
|
+
const component = basis?.components[0];
|
|
166
|
+
if (!basis ||
|
|
167
|
+
basis.components.length !== 1 ||
|
|
168
|
+
component?.treatment !== "included" ||
|
|
169
|
+
component.limitation.kind !== "unlimited") {
|
|
170
|
+
pushIssue(issues, "incompatible-semantics", path, "Claim layers require one included unlimited source component");
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
return {
|
|
174
|
+
kind: "amount",
|
|
175
|
+
unit: measure.unit,
|
|
176
|
+
reference: "amount:projected",
|
|
177
|
+
amountBasis: {
|
|
178
|
+
currency: basis.currency,
|
|
179
|
+
perspective: basis.perspective,
|
|
180
|
+
components: [
|
|
181
|
+
{
|
|
182
|
+
id: component.id,
|
|
183
|
+
treatment: "included",
|
|
184
|
+
limitation: {
|
|
185
|
+
kind: "layer",
|
|
186
|
+
attachment: expression.attachment,
|
|
187
|
+
limit: expression.limit,
|
|
188
|
+
application: "claim",
|
|
189
|
+
derivation: { kind: "sdk" },
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
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));
|
|
202
|
+
if (projected.some((item) => item === null))
|
|
203
|
+
return null;
|
|
204
|
+
const present = projected;
|
|
205
|
+
if (expression.op === "subtract") {
|
|
206
|
+
const [left, right] = present;
|
|
207
|
+
if (left.kind !== right.kind ||
|
|
208
|
+
left.unit !== right.unit ||
|
|
209
|
+
(left.kind === "amount"
|
|
210
|
+
? projectedBasisKey(left.amountBasis) !==
|
|
211
|
+
projectedBasisKey(right.amountBasis)
|
|
212
|
+
: left.reference !== right.reference)) {
|
|
213
|
+
pushIssue(issues, "incompatible-semantics", path, "Claim subtraction requires one exact semantic basis");
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return left;
|
|
217
|
+
}
|
|
218
|
+
const first = present[0];
|
|
219
|
+
if (present.some((item) => item.kind !== first.kind || item.unit !== first.unit)) {
|
|
220
|
+
pushIssue(issues, "incompatible-semantics", path, "Claim addition requires one kind and unit");
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (first.kind !== "amount") {
|
|
224
|
+
if (present.some((item) => item.reference !== first.reference)) {
|
|
225
|
+
pushIssue(issues, "incompatible-semantics", path, "Claim addition requires one exact semantic basis");
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
return first;
|
|
229
|
+
}
|
|
230
|
+
const bases = present.map((item) => item.amountBasis);
|
|
231
|
+
if (bases.some((basis) => basis.currency !== bases[0].currency ||
|
|
232
|
+
basis.perspective !== bases[0].perspective)) {
|
|
233
|
+
pushIssue(issues, "incompatible-semantics", path, "Amount addition cannot mix currency or perspective");
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const componentIds = new Set();
|
|
237
|
+
const components = [];
|
|
238
|
+
for (const basis of bases) {
|
|
239
|
+
for (const component of basis.components) {
|
|
240
|
+
if (componentIds.has(component.id)) {
|
|
241
|
+
pushIssue(issues, "incompatible-semantics", path, `Amount addition overlaps component ${component.id}`);
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
componentIds.add(component.id);
|
|
245
|
+
components.push(component);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const amountBasis = {
|
|
249
|
+
currency: bases[0].currency,
|
|
250
|
+
perspective: bases[0].perspective,
|
|
251
|
+
components: components.sort((left, right) => left.id < right.id ? -1 : left.id > right.id ? 1 : 0),
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
kind: "amount",
|
|
255
|
+
unit: first.unit,
|
|
256
|
+
reference: "amount:projected",
|
|
257
|
+
amountBasis,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function expressionSemantics(expression, path, measures, issues, initialDepth = 1) {
|
|
261
|
+
const walked = walkDiagnosticExpression(expression, "measure", path, issues, initialDepth);
|
|
262
|
+
const found = walked.dependencies
|
|
263
|
+
.map((id) => measures.get(id))
|
|
264
|
+
.filter((value) => value !== undefined);
|
|
265
|
+
for (const id of walked.dependencies) {
|
|
266
|
+
if (!measures.has(id)) {
|
|
267
|
+
pushIssue(issues, "unknown-reference", path, `Unknown measure ${id}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const signatures = new Set(found.map((measure) => canonicalJson([measure.kind, measure.unit, semanticReference(measure)])));
|
|
271
|
+
if (signatures.size > 1) {
|
|
272
|
+
pushIssue(issues, "incompatible-semantics", path, "Expression combines incompatible measure semantics");
|
|
273
|
+
}
|
|
274
|
+
return {
|
|
275
|
+
measureIds: walked.dependencies,
|
|
276
|
+
signature: signatures.size === 1 ? [...signatures][0] : null,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function validateTolerance(value, path, issues) {
|
|
280
|
+
for (const key of ["absolute", "relative"]) {
|
|
281
|
+
const component = value?.[key];
|
|
282
|
+
if (component === undefined)
|
|
283
|
+
continue;
|
|
284
|
+
if (!validateFinite(component, `${path}.${key}`, `${key} tolerance`, issues))
|
|
285
|
+
continue;
|
|
286
|
+
if (component < 0) {
|
|
287
|
+
pushIssue(issues, "invalid-number", `${path}.${key}`, `${key} tolerance must be nonnegative`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
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
|
+
}
|
|
509
|
+
function validatePeriodAxis(axis, issues) {
|
|
510
|
+
if (!isPlainRecord(axis)) {
|
|
511
|
+
pushIssue(issues, "invalid-type", "$.periodAxis", "Period axis must be an object");
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
if (!validateFinite(axis.ageOffset, "$.periodAxis.ageOffset", "Period age offset", issues) ||
|
|
515
|
+
!Number.isSafeInteger(axis.ageOffset)) {
|
|
516
|
+
pushIssue(issues, "invalid-number", "$.periodAxis.ageOffset", "Period age offset must be a safe integer");
|
|
517
|
+
}
|
|
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);
|
|
528
|
+
if (!["month", "quarter", "year"].includes(axis.originCadence)) {
|
|
529
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.originCadence", "Unknown origin cadence");
|
|
530
|
+
}
|
|
531
|
+
if (!["month", "quarter", "year"].includes(axis.valuationCadence)) {
|
|
532
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.valuationCadence", "Unknown valuation cadence");
|
|
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
|
+
}
|
|
540
|
+
if (axis.ageUnit !== "month") {
|
|
541
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.ageUnit", "Calendar axes use month age units");
|
|
542
|
+
}
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
validateAllowedKeys(axis, ["kind", "id", "version", "ageUnit", "ageOffset", "origins", "valuations"], "$.periodAxis", issues);
|
|
546
|
+
if (axis.kind !== "ordered") {
|
|
547
|
+
pushIssue(issues, "invalid-period", "$.periodAxis.kind", "Unknown period-axis kind");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
validateToken(axis.id, "$.periodAxis.id", "Ordered-axis ID", issues);
|
|
551
|
+
validateToken(axis.version, "$.periodAxis.version", "Ordered-axis version", issues);
|
|
552
|
+
validateToken(axis.ageUnit, "$.periodAxis.ageUnit", "Ordered-axis age unit", issues);
|
|
553
|
+
for (const side of ["origins", "valuations"]) {
|
|
554
|
+
const coordinates = requireArray(axis[side], `$.periodAxis.${side}`, issues);
|
|
555
|
+
const names = new Set();
|
|
556
|
+
const coordinateValues = new Set();
|
|
557
|
+
coordinates.forEach((coordinate, index) => {
|
|
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);
|
|
564
|
+
if (validateToken(coordinate?.label, `${base}.label`, "Period label", issues)) {
|
|
565
|
+
if (names.has(coordinate.label))
|
|
566
|
+
pushIssue(issues, "duplicate-id", `${base}.label`, `Duplicate period label or alias ${coordinate.label}`);
|
|
567
|
+
names.add(coordinate.label);
|
|
568
|
+
}
|
|
569
|
+
if (!validateFinite(coordinate?.coordinate, `${base}.coordinate`, "Period coordinate", issues) ||
|
|
570
|
+
!Number.isSafeInteger(coordinate.coordinate)) {
|
|
571
|
+
pushIssue(issues, "invalid-number", `${base}.coordinate`, "Period coordinate must be a safe integer");
|
|
572
|
+
}
|
|
573
|
+
else if (coordinateValues.has(coordinate.coordinate)) {
|
|
574
|
+
pushIssue(issues, "duplicate-id", `${base}.coordinate`, `Duplicate period coordinate ${coordinate.coordinate}`);
|
|
575
|
+
}
|
|
576
|
+
else
|
|
577
|
+
coordinateValues.add(coordinate.coordinate);
|
|
578
|
+
const aliases = coordinate.aliases === undefined
|
|
579
|
+
? []
|
|
580
|
+
: requireArray(coordinate.aliases, `${base}.aliases`, issues);
|
|
581
|
+
for (const [aliasIndex, alias] of aliases.entries()) {
|
|
582
|
+
if (!validateToken(alias, `${base}.aliases[${aliasIndex}]`, "Period alias", issues))
|
|
583
|
+
continue;
|
|
584
|
+
if (names.has(alias))
|
|
585
|
+
pushIssue(issues, "duplicate-id", `${base}.aliases[${aliasIndex}]`, `Duplicate period label or alias ${alias}`);
|
|
586
|
+
names.add(alias);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function validateDefinition(value) {
|
|
592
|
+
const issues = [];
|
|
593
|
+
validateJsonValue(value, issues);
|
|
594
|
+
if (!isPlainRecord(value)) {
|
|
595
|
+
pushIssue(issues, "invalid-type", "$", "Diagnostic definition must be a plain object");
|
|
596
|
+
throw new DiagnosticValidationError(issues);
|
|
597
|
+
}
|
|
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);
|
|
614
|
+
if (definition.diagnosticDefinitionVersion !== "1.0.0") {
|
|
615
|
+
pushIssue(issues, "invalid-type", "$.diagnosticDefinitionVersion", "Diagnostic definition version must be 1.0.0");
|
|
616
|
+
}
|
|
617
|
+
validateToken(definition.id, "$.id", "Definition ID", issues);
|
|
618
|
+
validateToken(definition.version, "$.version", "Definition version", issues);
|
|
619
|
+
if (definition.lossRowGrain !== "claim" &&
|
|
620
|
+
definition.lossRowGrain !== "aggregate") {
|
|
621
|
+
pushIssue(issues, "invalid-type", "$.lossRowGrain", "Loss row grain must be claim or aggregate");
|
|
622
|
+
}
|
|
623
|
+
const measures = requireArray(definition.measures, "$.measures", issues);
|
|
624
|
+
const populations = requireArray(definition.countPopulations, "$.countPopulations", issues);
|
|
625
|
+
const exposureBases = requireArray(definition.exposureBases, "$.exposureBases", issues);
|
|
626
|
+
const amountBases = requireArray(definition.amountBases, "$.amountBases", issues);
|
|
627
|
+
const derivations = requireArray(definition.derivedMeasures, "$.derivedMeasures", issues);
|
|
628
|
+
const formulas = requireArray(definition.formulas, "$.formulas", issues);
|
|
629
|
+
const instances = requireArray(definition.instances, "$.instances", issues);
|
|
630
|
+
const reviewRules = requireArray(definition.reviewRules, "$.reviewRules", issues);
|
|
631
|
+
const measureMap = collectCatalog(measures, "$.measures", issues);
|
|
632
|
+
const populationMap = collectCatalog(populations, "$.countPopulations", issues);
|
|
633
|
+
const exposureBasisMap = collectCatalog(exposureBases, "$.exposureBases", issues);
|
|
634
|
+
const amountBasisMap = collectCatalog(amountBases, "$.amountBases", issues);
|
|
635
|
+
collectCatalog(derivations, "$.derivedMeasures", issues);
|
|
636
|
+
const derivationsByOutput = new Map();
|
|
637
|
+
for (const derivation of derivations) {
|
|
638
|
+
if (isPlainRecord(derivation) &&
|
|
639
|
+
typeof derivation.outputMeasureId === "string" &&
|
|
640
|
+
!derivationsByOutput.has(derivation.outputMeasureId)) {
|
|
641
|
+
derivationsByOutput.set(derivation.outputMeasureId, derivation);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const formulaMap = collectCatalog(formulas, "$.formulas", issues);
|
|
645
|
+
collectCatalog(instances, "$.instances", issues);
|
|
646
|
+
const reviewRuleMap = collectCatalog(reviewRules, "$.reviewRules", issues);
|
|
647
|
+
populations.forEach((population, index) => {
|
|
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);
|
|
654
|
+
validateToken(population.displayName, `${base}.displayName`, "Population display name", issues);
|
|
655
|
+
validateToken(population.description, `${base}.description`, "Population description", issues);
|
|
656
|
+
validateToken(population.unit, `${base}.unit`, "Population unit", issues);
|
|
657
|
+
if (![
|
|
658
|
+
"claim",
|
|
659
|
+
"claimant",
|
|
660
|
+
"policy",
|
|
661
|
+
"occurrence",
|
|
662
|
+
"other",
|
|
663
|
+
"unknown",
|
|
664
|
+
].includes(population.subject)) {
|
|
665
|
+
pushIssue(issues, "invalid-type", `${base}.subject`, "Unknown count-population subject");
|
|
666
|
+
}
|
|
667
|
+
validateScalarAttributes(population.attributes, `${base}.attributes`, issues);
|
|
668
|
+
});
|
|
669
|
+
exposureBases.forEach((basis, index) => {
|
|
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);
|
|
684
|
+
validateToken(basis.displayName, `${base}.displayName`, "Exposure-basis display name", issues);
|
|
685
|
+
validateToken(basis.description, `${base}.description`, "Exposure-basis description", issues);
|
|
686
|
+
validateToken(basis.unit, `${base}.unit`, "Exposure-basis unit", issues);
|
|
687
|
+
if (basis.sourceDescription !== undefined)
|
|
688
|
+
validateToken(basis.sourceDescription, `${base}.sourceDescription`, "Exposure source description", issues);
|
|
689
|
+
if (!["earned", "written", "in-force", "other", "unknown"].includes(basis.basis)) {
|
|
690
|
+
pushIssue(issues, "invalid-type", `${base}.basis`, "Unknown exposure basis");
|
|
691
|
+
}
|
|
692
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
693
|
+
});
|
|
694
|
+
amountBases.forEach((basis, index) => {
|
|
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);
|
|
709
|
+
validateToken(basis.displayName, `${base}.displayName`, "Amount-basis display name", issues);
|
|
710
|
+
validateToken(basis.currency, `${base}.currency`, "Amount-basis currency", issues);
|
|
711
|
+
if (basis.sourceDescription !== undefined)
|
|
712
|
+
validateToken(basis.sourceDescription, `${base}.sourceDescription`, "Amount source description", issues);
|
|
713
|
+
if (!["gross", "net", "ceded", "other", "unknown"].includes(basis.perspective)) {
|
|
714
|
+
pushIssue(issues, "invalid-type", `${base}.perspective`, "Unknown amount perspective");
|
|
715
|
+
}
|
|
716
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
717
|
+
const components = requireArray(basis.components, `${base}.components`, issues);
|
|
718
|
+
if (components.length === 0)
|
|
719
|
+
pushIssue(issues, "missing-required", `${base}.components`, "Amount basis requires at least one component");
|
|
720
|
+
const componentIds = collectCatalog(components, `${base}.components`, issues);
|
|
721
|
+
if (componentIds.size > 0 &&
|
|
722
|
+
components.every((component) => component.treatment === "excluded")) {
|
|
723
|
+
pushIssue(issues, "incompatible-semantics", `${base}.components`, "Amount basis cannot exclude every component");
|
|
724
|
+
}
|
|
725
|
+
components.forEach((component, componentIndex) => {
|
|
726
|
+
const componentBase = `${base}.components[${componentIndex}]`;
|
|
727
|
+
if (!isPlainRecord(component)) {
|
|
728
|
+
pushIssue(issues, "invalid-type", componentBase, "Amount-basis component must be an object");
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
validateAllowedKeys(component, ["id", "treatment", "limitation"], componentBase, issues);
|
|
732
|
+
if (!["included", "excluded", "unknown"].includes(component.treatment)) {
|
|
733
|
+
pushIssue(issues, "invalid-type", `${componentBase}.treatment`, "Unknown component treatment");
|
|
734
|
+
}
|
|
735
|
+
const limitation = component.limitation;
|
|
736
|
+
if (!isPlainRecord(limitation)) {
|
|
737
|
+
pushIssue(issues, "invalid-type", `${componentBase}.limitation`, "Amount limitation must be an object");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
if (limitation.kind === "unknown") {
|
|
741
|
+
validateAllowedKeys(limitation, ["kind", "description"], `${componentBase}.limitation`, issues);
|
|
742
|
+
if (limitation.description !== undefined)
|
|
743
|
+
validateToken(limitation.description, `${componentBase}.limitation.description`, "Unknown limitation description", issues);
|
|
744
|
+
}
|
|
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) {
|
|
753
|
+
pushIssue(issues, "invalid-number", `${componentBase}.limitation.attachment`, "Attachment must be nonnegative");
|
|
754
|
+
}
|
|
755
|
+
if (limitation.limit !== null) {
|
|
756
|
+
if (validateFinite(limitation.limit, `${componentBase}.limitation.limit`, "Layer width", issues) &&
|
|
757
|
+
limitation.limit <= 0) {
|
|
758
|
+
pushIssue(issues, "invalid-number", `${componentBase}.limitation.limit`, "Layer width must be positive");
|
|
759
|
+
}
|
|
760
|
+
if (Number.isFinite(limitation.attachment) &&
|
|
761
|
+
Number.isFinite(limitation.limit) &&
|
|
762
|
+
!Number.isFinite(limitation.attachment + limitation.limit)) {
|
|
763
|
+
pushIssue(issues, "invalid-number", `${componentBase}.limitation.limit`, "Attachment plus layer width must be finite");
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
if (limitation.derivation?.kind === "sdk") {
|
|
767
|
+
if (isPlainRecord(limitation.derivation))
|
|
768
|
+
validateAllowedKeys(limitation.derivation, ["kind"], `${componentBase}.limitation.derivation`, issues);
|
|
769
|
+
if (limitation.kind !== "layer" ||
|
|
770
|
+
limitation.application !== "claim") {
|
|
771
|
+
pushIssue(issues, "incompatible-semantics", `${componentBase}.limitation.derivation`, "SDK limitations require a claim layer");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
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
|
+
}
|
|
781
|
+
validateToken(limitation.derivation.transformationRef, `${componentBase}.limitation.derivation.transformationRef`, "Transformation reference", issues);
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
pushIssue(issues, "invalid-type", `${componentBase}.limitation.derivation`, "Limited basis requires a derivation record");
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
else if (limitation.kind !== "unlimited") {
|
|
788
|
+
pushIssue(issues, "invalid-type", `${componentBase}.limitation.kind`, "Unknown amount limitation kind");
|
|
789
|
+
}
|
|
790
|
+
else
|
|
791
|
+
validateAllowedKeys(limitation, ["kind"], `${componentBase}.limitation`, issues);
|
|
792
|
+
});
|
|
793
|
+
});
|
|
794
|
+
measures.forEach((measure, index) => {
|
|
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);
|
|
815
|
+
validateToken(measure.displayName, `${base}.displayName`, "Measure display name", issues);
|
|
816
|
+
validateToken(measure.description, `${base}.description`, "Measure description", issues);
|
|
817
|
+
validateToken(measure.unit, `${base}.unit`, "Measure unit", issues);
|
|
818
|
+
if (measure.aggregation !== "sum")
|
|
819
|
+
pushIssue(issues, "incompatible-semantics", `${base}.aggregation`, "Only sum aggregation is supported");
|
|
820
|
+
if (measure.missing !== "unknown" && measure.missing !== "zero")
|
|
821
|
+
pushIssue(issues, "invalid-type", `${base}.missing`, "Unknown missing-value policy");
|
|
822
|
+
if (!["cumulative", "incremental", "point-in-time", "unknown"].includes(measure.developmentSemantics)) {
|
|
823
|
+
pushIssue(issues, "invalid-type", `${base}.developmentSemantics`, "Unknown development semantics");
|
|
824
|
+
}
|
|
825
|
+
if (measure.kind === "amount") {
|
|
826
|
+
const basis = measure.basisId
|
|
827
|
+
? amountBasisMap.get(measure.basisId)
|
|
828
|
+
: undefined;
|
|
829
|
+
if (!basis)
|
|
830
|
+
pushIssue(issues, "unknown-reference", `${base}.basisId`, "Amount measure requires an existing amount basis");
|
|
831
|
+
else if (measure.unit !== basis.currency)
|
|
832
|
+
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Amount measure unit must equal basis currency");
|
|
833
|
+
if (measure.countPopulationId !== undefined ||
|
|
834
|
+
measure.exposureBasisId !== undefined ||
|
|
835
|
+
measure.exposureTiming !== undefined) {
|
|
836
|
+
pushIssue(issues, "incompatible-semantics", base, "Amount measure has inapplicable population or exposure semantics");
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
else if (measure.kind === "count") {
|
|
840
|
+
const population = measure.countPopulationId
|
|
841
|
+
? populationMap.get(measure.countPopulationId)
|
|
842
|
+
: undefined;
|
|
843
|
+
if (!population)
|
|
844
|
+
pushIssue(issues, "unknown-reference", `${base}.countPopulationId`, "Count measure requires an existing count population");
|
|
845
|
+
else if (measure.unit !== population.unit)
|
|
846
|
+
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Count measure unit must equal population unit");
|
|
847
|
+
if (measure.basisId !== undefined ||
|
|
848
|
+
measure.exposureBasisId !== undefined ||
|
|
849
|
+
measure.exposureTiming !== undefined) {
|
|
850
|
+
pushIssue(issues, "incompatible-semantics", base, "Count measure has inapplicable amount or exposure semantics");
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
else if (measure.kind === "exposure") {
|
|
854
|
+
const basis = measure.exposureBasisId
|
|
855
|
+
? exposureBasisMap.get(measure.exposureBasisId)
|
|
856
|
+
: undefined;
|
|
857
|
+
if (!basis)
|
|
858
|
+
pushIssue(issues, "unknown-reference", `${base}.exposureBasisId`, "Exposure measure requires an existing exposure basis");
|
|
859
|
+
else if (measure.unit !== basis.unit)
|
|
860
|
+
pushIssue(issues, "incompatible-semantics", `${base}.unit`, "Exposure measure unit must equal exposure-basis unit");
|
|
861
|
+
if (measure.source !== "exposure")
|
|
862
|
+
pushIssue(issues, "incompatible-semantics", `${base}.source`, "Exposure measure source must be exposure");
|
|
863
|
+
if (measure.exposureTiming !== "origin-static" &&
|
|
864
|
+
measure.exposureTiming !== "valuation-specific") {
|
|
865
|
+
pushIssue(issues, "missing-required", `${base}.exposureTiming`, "Exposure measure requires timing");
|
|
866
|
+
}
|
|
867
|
+
if (measure.missing !== "unknown")
|
|
868
|
+
pushIssue(issues, "incompatible-semantics", `${base}.missing`, "Exposure missingness must remain unknown");
|
|
869
|
+
if (measure.basisId !== undefined ||
|
|
870
|
+
measure.countPopulationId !== undefined) {
|
|
871
|
+
pushIssue(issues, "incompatible-semantics", base, "Exposure measure has inapplicable amount or population semantics");
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown measure kind");
|
|
876
|
+
}
|
|
877
|
+
if (measure.kind !== "exposure" &&
|
|
878
|
+
measure.source !== "loss" &&
|
|
879
|
+
measure.source !== "derived") {
|
|
880
|
+
pushIssue(issues, "invalid-type", `${base}.source`, "Non-exposure measure source must be loss or derived");
|
|
881
|
+
}
|
|
882
|
+
if (measure.kind !== "exposure" && measure.source === "exposure") {
|
|
883
|
+
pushIssue(issues, "incompatible-semantics", `${base}.source`, "Only exposure measures may use exposure source");
|
|
884
|
+
}
|
|
885
|
+
if (measure.kind === "amount" && measure.basisId) {
|
|
886
|
+
const basis = amountBasisMap.get(measure.basisId);
|
|
887
|
+
const hasSdkLimitation = basis?.components.some((component) => (component.limitation.kind === "layer" ||
|
|
888
|
+
component.limitation.kind === "pre-limited") &&
|
|
889
|
+
component.limitation.derivation.kind === "sdk") ?? false;
|
|
890
|
+
if (hasSdkLimitation && measure.source !== "derived") {
|
|
891
|
+
pushIssue(issues, "incompatible-semantics", `${base}.source`, "SDK-limited amount measures must be derived");
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
let definitionExpressionNodes = 0;
|
|
896
|
+
const formulaRoleUsage = new Map();
|
|
897
|
+
formulas.forEach((formula, index) => {
|
|
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);
|
|
911
|
+
validateToken(formula.version, `${base}.version`, "Formula version", issues);
|
|
912
|
+
if (formula.denominatorPolicy !== "positive-or-null") {
|
|
913
|
+
pushIssue(issues, "incompatible-semantics", `${base}.denominatorPolicy`, "Unknown denominator policy");
|
|
914
|
+
}
|
|
915
|
+
if (!isPlainRecord(formula.roles)) {
|
|
916
|
+
pushIssue(issues, "invalid-type", `${base}.roles`, "Formula roles must be an object");
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
for (const [roleName, role] of Object.entries(formula.roles)) {
|
|
920
|
+
validateToken(roleName, propertyPath(`${base}.roles`, roleName), "Formula role", issues);
|
|
921
|
+
if (!isPlainRecord(role) ||
|
|
922
|
+
!["count", "amount", "exposure"].includes(role.kind)) {
|
|
923
|
+
pushIssue(issues, "invalid-type", propertyPath(`${base}.roles`, roleName), "Formula role has an invalid kind");
|
|
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
|
+
}
|
|
933
|
+
if (role.compatibilityGroup !== undefined)
|
|
934
|
+
validateToken(role.compatibilityGroup, `${propertyPath(`${base}.roles`, roleName)}.compatibilityGroup`, "Compatibility group", issues);
|
|
935
|
+
}
|
|
936
|
+
const numerator = walkDiagnosticExpression(formula.numerator, "role", `${base}.numerator`, issues);
|
|
937
|
+
const denominator = walkDiagnosticExpression(formula.denominator, "role", `${base}.denominator`, issues);
|
|
938
|
+
definitionExpressionNodes += numerator.nodeCount + denominator.nodeCount;
|
|
939
|
+
const used = [
|
|
940
|
+
...new Set([...numerator.dependencies, ...denominator.dependencies]),
|
|
941
|
+
].sort();
|
|
942
|
+
formulaRoleUsage.set(formula.id, used);
|
|
943
|
+
for (const role of used) {
|
|
944
|
+
if (!hasDiagnosticOwn(formula.roles, role)) {
|
|
945
|
+
pushIssue(issues, "unknown-reference", base, `Formula ${formula.id} references unknown role ${role}`);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
for (const role of Object.keys(formula.roles)) {
|
|
949
|
+
if (!used.includes(role))
|
|
950
|
+
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.roles`, role), `Formula role ${role} is unused`);
|
|
951
|
+
}
|
|
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));
|
|
959
|
+
if (kinds.size > 1)
|
|
960
|
+
pushIssue(issues, "incompatible-semantics", `${base}.${side}`, "Formula arithmetic combines different role kinds");
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
const instanceRuleIds = new Set();
|
|
964
|
+
instances.forEach((instance, index) => {
|
|
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);
|
|
971
|
+
validateToken(instance.version, `${base}.version`, "Instance version", issues);
|
|
972
|
+
validateToken(instance.formulaId, `${base}.formulaId`, "Formula reference", issues);
|
|
973
|
+
const formula = formulaMap.get(instance.formulaId);
|
|
974
|
+
if (!formula) {
|
|
975
|
+
pushIssue(issues, "unknown-reference", `${base}.formulaId`, `Unknown formula ${instance.formulaId}`);
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (!isPlainRecord(instance.bindings)) {
|
|
979
|
+
pushIssue(issues, "invalid-type", `${base}.bindings`, "Instance bindings must be an object");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
982
|
+
const usedRoles = formulaRoleUsage.get(formula.id) ?? [];
|
|
983
|
+
const bindingSemantics = new Map();
|
|
984
|
+
for (const roleName of usedRoles) {
|
|
985
|
+
if (!hasDiagnosticOwn(instance.bindings, roleName)) {
|
|
986
|
+
pushIssue(issues, "missing-required", propertyPath(`${base}.bindings`, roleName), `Missing binding for role ${roleName}`);
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
const expression = instance.bindings[roleName];
|
|
990
|
+
const semantics = expressionSemantics(expression, propertyPath(`${base}.bindings`, roleName), measureMap, issues);
|
|
991
|
+
bindingSemantics.set(roleName, semantics);
|
|
992
|
+
definitionExpressionNodes += walkDiagnosticExpression(expression, "measure", propertyPath(`${base}.bindings`, roleName), []).nodeCount;
|
|
993
|
+
const role = formula.roles[roleName];
|
|
994
|
+
const boundMeasures = semantics.measureIds
|
|
995
|
+
.map((id) => measureMap.get(id))
|
|
996
|
+
.filter((item) => item !== undefined);
|
|
997
|
+
if (boundMeasures.some((measure) => measure.kind !== role.kind)) {
|
|
998
|
+
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.bindings`, roleName), `Binding kind does not match role ${roleName}`);
|
|
999
|
+
}
|
|
1000
|
+
if (role.developmentSemantics !== undefined) {
|
|
1001
|
+
const leaves = transitiveMeasureIds(semantics.measureIds, derivationsByOutput)
|
|
1002
|
+
.map((id) => measureMap.get(id))
|
|
1003
|
+
.filter((measure) => measure !== undefined);
|
|
1004
|
+
if (leaves.some((measure) => measure.developmentSemantics !== role.developmentSemantics)) {
|
|
1005
|
+
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.bindings`, roleName), `Transitive binding development semantics do not match role ${roleName}`);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const calculationSignatures = new Map();
|
|
1010
|
+
for (const [side, roleExpression] of [
|
|
1011
|
+
["numerator", formula.numerator],
|
|
1012
|
+
["denominator", formula.denominator],
|
|
1013
|
+
]) {
|
|
1014
|
+
const roleIds = walkDiagnosticExpression(roleExpression, "role", `${base}.${side}`, []).dependencies;
|
|
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);
|
|
1019
|
+
if (signatures.size > 1) {
|
|
1020
|
+
pushIssue(issues, "incompatible-semantics", `${base}.bindings`, `Bound ${side} arithmetic combines incompatible semantics`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
for (const binding of Object.keys(instance.bindings)) {
|
|
1024
|
+
if (!usedRoles.includes(binding)) {
|
|
1025
|
+
pushIssue(issues, "unknown-reference", propertyPath(`${base}.bindings`, binding), `Unexpected binding for role ${binding}`);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
const compatibility = new Map();
|
|
1029
|
+
for (const roleName of usedRoles) {
|
|
1030
|
+
const role = formula.roles[roleName];
|
|
1031
|
+
const binding = instance.bindings[roleName];
|
|
1032
|
+
if (!role.compatibilityGroup || !binding)
|
|
1033
|
+
continue;
|
|
1034
|
+
const semantics = expressionSemantics(binding, propertyPath(`${base}.bindings`, roleName), measureMap, []);
|
|
1035
|
+
if (semantics.signature === null)
|
|
1036
|
+
continue;
|
|
1037
|
+
const previous = compatibility.get(role.compatibilityGroup);
|
|
1038
|
+
if (previous !== undefined && previous !== semantics.signature) {
|
|
1039
|
+
pushIssue(issues, "incompatible-semantics", propertyPath(`${base}.bindings`, roleName), `Compatibility group ${role.compatibilityGroup} binds different semantics`);
|
|
1040
|
+
}
|
|
1041
|
+
else
|
|
1042
|
+
compatibility.set(role.compatibilityGroup, semantics.signature);
|
|
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);
|
|
1056
|
+
for (const [key, text] of [
|
|
1057
|
+
["displayName", instance.presentation?.displayName],
|
|
1058
|
+
["description", instance.presentation?.description],
|
|
1059
|
+
["displayUnit", instance.presentation?.displayUnit],
|
|
1060
|
+
["numeratorLabel", instance.presentation?.numeratorLabel],
|
|
1061
|
+
["denominatorLabel", instance.presentation?.denominatorLabel],
|
|
1062
|
+
])
|
|
1063
|
+
validateToken(text, `${base}.presentation.${key}`, `Presentation ${key}`, issues);
|
|
1064
|
+
if (!validateFinite(instance.presentation?.scale, `${base}.presentation.scale`, "Presentation scale", issues) ||
|
|
1065
|
+
instance.presentation.scale <= 0) {
|
|
1066
|
+
pushIssue(issues, "invalid-number", `${base}.presentation.scale`, "Presentation scale must be positive");
|
|
1067
|
+
}
|
|
1068
|
+
requireArray(instance.rules, `${base}.rules`, issues).forEach((rawRule, ruleIndex) => {
|
|
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);
|
|
1076
|
+
if (validateToken(rule.id, `${ruleBase}.id`, "Metric rule ID", issues)) {
|
|
1077
|
+
if (rule.id.startsWith("diagnostic/structural/"))
|
|
1078
|
+
pushIssue(issues, "incompatible-semantics", `${ruleBase}.id`, "Metric rule ID uses the reserved structural prefix");
|
|
1079
|
+
if (instanceRuleIds.has(rule.id) || reviewRuleMap.has(rule.id))
|
|
1080
|
+
pushIssue(issues, "duplicate-id", `${ruleBase}.id`, `Duplicate rule ID ${rule.id}`);
|
|
1081
|
+
instanceRuleIds.add(rule.id);
|
|
1082
|
+
}
|
|
1083
|
+
validateToken(rule.code, `${ruleBase}.code`, "Metric rule code", issues);
|
|
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");
|
|
1100
|
+
validateTolerance(rule.when?.tolerance, `${ruleBase}.when.tolerance`, issues);
|
|
1101
|
+
const operandSignatures = new Map();
|
|
1102
|
+
for (const [side, operand] of [
|
|
1103
|
+
["left", rule.when?.left],
|
|
1104
|
+
["right", rule.when?.right],
|
|
1105
|
+
]) {
|
|
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
|
+
}
|
|
1117
|
+
if (operand?.source === "measure") {
|
|
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);
|
|
1126
|
+
}
|
|
1127
|
+
else if (operand?.source === "calculation") {
|
|
1128
|
+
definitionExpressionNodes += 1;
|
|
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));
|
|
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");
|
|
1151
|
+
}
|
|
1152
|
+
});
|
|
1153
|
+
});
|
|
1154
|
+
const derivedOutputs = new Map();
|
|
1155
|
+
derivations.forEach((derivation, index) => {
|
|
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);
|
|
1162
|
+
validateToken(derivation.outputMeasureId, `${base}.outputMeasureId`, "Derived output measure", issues);
|
|
1163
|
+
const output = measureMap.get(derivation.outputMeasureId);
|
|
1164
|
+
if (!output)
|
|
1165
|
+
pushIssue(issues, "unknown-reference", `${base}.outputMeasureId`, `Unknown derived output ${derivation.outputMeasureId}`);
|
|
1166
|
+
else if (output.source !== "derived")
|
|
1167
|
+
pushIssue(issues, "incompatible-semantics", `${base}.outputMeasureId`, "Derivation output must declare source derived");
|
|
1168
|
+
if (derivedOutputs.has(derivation.outputMeasureId))
|
|
1169
|
+
pushIssue(issues, "duplicate-id", `${base}.outputMeasureId`, `Duplicate derived output ${derivation.outputMeasureId}`);
|
|
1170
|
+
derivedOutputs.set(derivation.outputMeasureId, derivation);
|
|
1171
|
+
const walked = walkDiagnosticExpression(derivation.expression, "claim", `${base}.expression`, issues);
|
|
1172
|
+
definitionExpressionNodes += walked.nodeCount;
|
|
1173
|
+
for (const id of walked.dependencies)
|
|
1174
|
+
if (!measureMap.has(id))
|
|
1175
|
+
pushIssue(issues, "unknown-reference", `${base}.expression`, `Unknown derivation measure ${id}`);
|
|
1176
|
+
const transitiveIds = transitiveMeasureIds(walked.dependencies, derivationsByOutput);
|
|
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)) {
|
|
1182
|
+
pushIssue(issues, "incompatible-semantics", `${base}.expression`, "Derived output and inputs must share development semantics");
|
|
1183
|
+
}
|
|
1184
|
+
if (definition.lossRowGrain !== "claim") {
|
|
1185
|
+
pushIssue(issues, "incompatible-semantics", base, "Derived measures require claim-grain input");
|
|
1186
|
+
}
|
|
1187
|
+
const expressionStack = [derivation.expression];
|
|
1188
|
+
while (expressionStack.length > 0) {
|
|
1189
|
+
const current = expressionStack.pop();
|
|
1190
|
+
if (!isPlainRecord(current))
|
|
1191
|
+
continue;
|
|
1192
|
+
if (current.op === "claim-layer") {
|
|
1193
|
+
const attachment = current.attachment;
|
|
1194
|
+
const limit = current.limit;
|
|
1195
|
+
const attachmentOk = validateFinite(attachment, `${base}.expression.attachment`, "Claim-layer attachment", issues);
|
|
1196
|
+
if (attachmentOk && attachment < 0)
|
|
1197
|
+
pushIssue(issues, "invalid-number", `${base}.expression.attachment`, "Claim-layer attachment must be nonnegative");
|
|
1198
|
+
if (limit !== null) {
|
|
1199
|
+
const limitOk = validateFinite(limit, `${base}.expression.limit`, "Claim-layer width", issues);
|
|
1200
|
+
if (limitOk && limit <= 0)
|
|
1201
|
+
pushIssue(issues, "invalid-number", `${base}.expression.limit`, "Claim-layer width must be positive");
|
|
1202
|
+
if (attachmentOk && limitOk && !Number.isFinite(attachment + limit)) {
|
|
1203
|
+
pushIssue(issues, "invalid-number", `${base}.expression.limit`, "Attachment plus claim-layer width must be finite");
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else if (current.op === "add" && Array.isArray(current.terms))
|
|
1208
|
+
expressionStack.push(...current.terms);
|
|
1209
|
+
else if (current.op === "subtract")
|
|
1210
|
+
expressionStack.push(current.left, current.right);
|
|
1211
|
+
}
|
|
1212
|
+
const projection = projectClaimExpression(derivation.expression, `${base}.expression`, measureMap, amountBasisMap, issues);
|
|
1213
|
+
if (output && projection) {
|
|
1214
|
+
const expected = output.kind === "amount" && output.basisId
|
|
1215
|
+
? amountBasisMap.get(output.basisId)
|
|
1216
|
+
: undefined;
|
|
1217
|
+
const matches = output.kind === projection.kind &&
|
|
1218
|
+
output.unit === projection.unit &&
|
|
1219
|
+
(output.kind === "amount"
|
|
1220
|
+
? expected !== undefined &&
|
|
1221
|
+
projectedBasisKey(projectedAmountBasis(expected)) ===
|
|
1222
|
+
projectedBasisKey(projection.amountBasis)
|
|
1223
|
+
: semanticReference(output) === projection.reference);
|
|
1224
|
+
if (!matches)
|
|
1225
|
+
pushIssue(issues, "incompatible-semantics", `${base}.expression`, "Derived expression does not project the output measure semantics");
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
for (const [measureId, measure] of measureMap) {
|
|
1229
|
+
if (measure.source === "derived" && !derivedOutputs.has(measureId)) {
|
|
1230
|
+
pushIssue(issues, "missing-required", "$.derivedMeasures", `Derived measure ${measureId} requires exactly one derivation`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
const visiting = new Set();
|
|
1234
|
+
const visited = new Set();
|
|
1235
|
+
const visit = (id) => {
|
|
1236
|
+
if (visited.has(id))
|
|
1237
|
+
return;
|
|
1238
|
+
if (visiting.has(id)) {
|
|
1239
|
+
pushIssue(issues, "cycle", "$.derivedMeasures", `Derived measure graph contains a cycle at ${id}`);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
visiting.add(id);
|
|
1243
|
+
const derivation = derivedOutputs.get(id);
|
|
1244
|
+
if (derivation) {
|
|
1245
|
+
const deps = walkDiagnosticExpression(derivation.expression, "claim", "$.derivedMeasures", []).dependencies;
|
|
1246
|
+
for (const dependency of deps)
|
|
1247
|
+
if (derivedOutputs.has(dependency))
|
|
1248
|
+
visit(dependency);
|
|
1249
|
+
}
|
|
1250
|
+
visiting.delete(id);
|
|
1251
|
+
visited.add(id);
|
|
1252
|
+
};
|
|
1253
|
+
for (const id of derivedOutputs.keys())
|
|
1254
|
+
visit(id);
|
|
1255
|
+
reviewRules.forEach((rule, index) => {
|
|
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
|
+
}
|
|
1288
|
+
validateToken(rule.code, `${base}.code`, "Review rule code", issues);
|
|
1289
|
+
validateToken(rule.description, `${base}.description`, "Review rule description", issues);
|
|
1290
|
+
if (typeof rule.id === "string" &&
|
|
1291
|
+
rule.id.startsWith("diagnostic/structural/"))
|
|
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");
|
|
1298
|
+
validateTolerance(rule.tolerance, `${base}.tolerance`, issues);
|
|
1299
|
+
const roots = [];
|
|
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);
|
|
1304
|
+
definitionExpressionNodes++;
|
|
1305
|
+
}
|
|
1306
|
+
else
|
|
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
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
else if (rule.kind === "reconcile") {
|
|
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
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
else if (rule.kind === "control-total") {
|
|
1351
|
+
roots.push({ expression: rule.expression, path: `${base}.expression` });
|
|
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);
|
|
1368
|
+
}
|
|
1369
|
+
else
|
|
1370
|
+
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown review-rule kind");
|
|
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
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
});
|
|
1414
|
+
if (definitionExpressionNodes > MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES) {
|
|
1415
|
+
pushIssue(issues, "expression-limit", "$", `Definition expression node count exceeds ${MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES}`);
|
|
1416
|
+
}
|
|
1417
|
+
validatePeriodAxis(definition.periodAxis, issues);
|
|
1418
|
+
if (issues.length > 0)
|
|
1419
|
+
throw new DiagnosticValidationError(issues);
|
|
1420
|
+
return definition;
|
|
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
|
+
}
|
|
1434
|
+
function optionalFromNullable(value) {
|
|
1435
|
+
return value === null || value === undefined ? undefined : value;
|
|
1436
|
+
}
|
|
1437
|
+
function omitProperties(value, keys) {
|
|
1438
|
+
const result = { ...value };
|
|
1439
|
+
for (const key of keys)
|
|
1440
|
+
delete result[key];
|
|
1441
|
+
return result;
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* The normalized wire projection materializes optional semantic fields as
|
|
1445
|
+
* null. Convert only those documented defaults back to the authored view so
|
|
1446
|
+
* both accepted inputs travel through the exact same semantic compiler.
|
|
1447
|
+
*/
|
|
1448
|
+
function authoredView(value) {
|
|
1449
|
+
const definition = value;
|
|
1450
|
+
const reviewRules = definition.reviewRules.map((rule) => {
|
|
1451
|
+
const tolerance = { ...rule.tolerance };
|
|
1452
|
+
if (rule.kind !== "control-total")
|
|
1453
|
+
return { ...rule, tolerance };
|
|
1454
|
+
const filter = rule.filter === null || rule.filter === undefined
|
|
1455
|
+
? undefined
|
|
1456
|
+
: Object.fromEntries(Object.entries({
|
|
1457
|
+
sourceGroups: optionalFromNullable(rule.filter.sourceGroups),
|
|
1458
|
+
origins: optionalFromNullable(rule.filter.origins),
|
|
1459
|
+
originFrom: optionalFromNullable(rule.filter.originFrom),
|
|
1460
|
+
originThrough: optionalFromNullable(rule.filter.originThrough),
|
|
1461
|
+
valuations: optionalFromNullable(rule.filter.valuations),
|
|
1462
|
+
valuationFrom: optionalFromNullable(rule.filter.valuationFrom),
|
|
1463
|
+
valuationThrough: optionalFromNullable(rule.filter.valuationThrough),
|
|
1464
|
+
minDevelopmentAge: optionalFromNullable(rule.filter.minDevelopmentAge),
|
|
1465
|
+
maxDevelopmentAge: optionalFromNullable(rule.filter.maxDevelopmentAge),
|
|
1466
|
+
}).filter(([, item]) => item !== undefined));
|
|
1467
|
+
return {
|
|
1468
|
+
...omitProperties(rule, ["filter"]),
|
|
1469
|
+
tolerance,
|
|
1470
|
+
...(filter === undefined ? {} : { filter }),
|
|
1471
|
+
};
|
|
1472
|
+
});
|
|
1473
|
+
return {
|
|
1474
|
+
...definition,
|
|
1475
|
+
diagnosticDefinitionVersion: definition.diagnosticDefinitionVersion,
|
|
1476
|
+
id: definition.id,
|
|
1477
|
+
version: definition.version,
|
|
1478
|
+
lossRowGrain: definition.lossRowGrain,
|
|
1479
|
+
measures: definition.measures.map((measure) => ({
|
|
1480
|
+
...omitProperties(measure, [
|
|
1481
|
+
"basisId",
|
|
1482
|
+
"countPopulationId",
|
|
1483
|
+
"exposureBasisId",
|
|
1484
|
+
"exposureTiming",
|
|
1485
|
+
]),
|
|
1486
|
+
id: measure.id,
|
|
1487
|
+
displayName: measure.displayName,
|
|
1488
|
+
description: measure.description,
|
|
1489
|
+
source: measure.source,
|
|
1490
|
+
kind: measure.kind,
|
|
1491
|
+
unit: measure.unit,
|
|
1492
|
+
developmentSemantics: measure.developmentSemantics,
|
|
1493
|
+
aggregation: measure.aggregation,
|
|
1494
|
+
missing: measure.missing,
|
|
1495
|
+
...(measure.basisId === null || measure.basisId === undefined
|
|
1496
|
+
? {}
|
|
1497
|
+
: { basisId: measure.basisId }),
|
|
1498
|
+
...(measure.countPopulationId === null ||
|
|
1499
|
+
measure.countPopulationId === undefined
|
|
1500
|
+
? {}
|
|
1501
|
+
: { countPopulationId: measure.countPopulationId }),
|
|
1502
|
+
...(measure.exposureBasisId === null ||
|
|
1503
|
+
measure.exposureBasisId === undefined
|
|
1504
|
+
? {}
|
|
1505
|
+
: { exposureBasisId: measure.exposureBasisId }),
|
|
1506
|
+
...(measure.exposureTiming === null ||
|
|
1507
|
+
measure.exposureTiming === undefined
|
|
1508
|
+
? {}
|
|
1509
|
+
: { exposureTiming: measure.exposureTiming }),
|
|
1510
|
+
})),
|
|
1511
|
+
countPopulations: definition.countPopulations.map((population) => ({
|
|
1512
|
+
...population,
|
|
1513
|
+
})),
|
|
1514
|
+
exposureBases: definition.exposureBases.map((basis) => ({
|
|
1515
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
1516
|
+
id: basis.id,
|
|
1517
|
+
displayName: basis.displayName,
|
|
1518
|
+
basis: basis.basis,
|
|
1519
|
+
unit: basis.unit,
|
|
1520
|
+
description: basis.description,
|
|
1521
|
+
...(basis.sourceDescription === null ||
|
|
1522
|
+
basis.sourceDescription === undefined
|
|
1523
|
+
? {}
|
|
1524
|
+
: { sourceDescription: basis.sourceDescription }),
|
|
1525
|
+
...(basis.attributes === undefined
|
|
1526
|
+
? {}
|
|
1527
|
+
: { attributes: basis.attributes }),
|
|
1528
|
+
})),
|
|
1529
|
+
amountBases: definition.amountBases.map((basis) => ({
|
|
1530
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
1531
|
+
id: basis.id,
|
|
1532
|
+
displayName: basis.displayName,
|
|
1533
|
+
currency: basis.currency,
|
|
1534
|
+
perspective: basis.perspective,
|
|
1535
|
+
components: basis.components.map((component) => ({
|
|
1536
|
+
...component,
|
|
1537
|
+
id: component.id,
|
|
1538
|
+
treatment: component.treatment,
|
|
1539
|
+
limitation: component.limitation.kind === "unknown"
|
|
1540
|
+
? {
|
|
1541
|
+
...omitProperties(component.limitation, ["description"]),
|
|
1542
|
+
kind: "unknown",
|
|
1543
|
+
...(component.limitation.description === null ||
|
|
1544
|
+
component.limitation.description === undefined
|
|
1545
|
+
? {}
|
|
1546
|
+
: { description: component.limitation.description }),
|
|
1547
|
+
}
|
|
1548
|
+
: component.limitation,
|
|
1549
|
+
})),
|
|
1550
|
+
...(basis.sourceDescription === null ||
|
|
1551
|
+
basis.sourceDescription === undefined
|
|
1552
|
+
? {}
|
|
1553
|
+
: { sourceDescription: basis.sourceDescription }),
|
|
1554
|
+
...(basis.attributes === undefined
|
|
1555
|
+
? {}
|
|
1556
|
+
: { attributes: basis.attributes }),
|
|
1557
|
+
})),
|
|
1558
|
+
derivedMeasures: definition.derivedMeasures.map((derivation) => ({
|
|
1559
|
+
...derivation,
|
|
1560
|
+
})),
|
|
1561
|
+
formulas: definition.formulas.map((formula) => ({
|
|
1562
|
+
...formula,
|
|
1563
|
+
id: formula.id,
|
|
1564
|
+
version: formula.version,
|
|
1565
|
+
roles: Object.fromEntries(Object.entries(formula.roles).map(([name, role]) => [
|
|
1566
|
+
name,
|
|
1567
|
+
{
|
|
1568
|
+
...omitProperties(role, [
|
|
1569
|
+
"compatibilityGroup",
|
|
1570
|
+
"developmentSemantics",
|
|
1571
|
+
]),
|
|
1572
|
+
kind: role.kind,
|
|
1573
|
+
...(role.compatibilityGroup === null ||
|
|
1574
|
+
role.compatibilityGroup === undefined
|
|
1575
|
+
? {}
|
|
1576
|
+
: { compatibilityGroup: role.compatibilityGroup }),
|
|
1577
|
+
...(role.developmentSemantics === null ||
|
|
1578
|
+
role.developmentSemantics === undefined
|
|
1579
|
+
? {}
|
|
1580
|
+
: { developmentSemantics: role.developmentSemantics }),
|
|
1581
|
+
},
|
|
1582
|
+
])),
|
|
1583
|
+
numerator: formula.numerator,
|
|
1584
|
+
denominator: formula.denominator,
|
|
1585
|
+
denominatorPolicy: formula.denominatorPolicy,
|
|
1586
|
+
})),
|
|
1587
|
+
instances: definition.instances.map((instance) => ({
|
|
1588
|
+
...instance,
|
|
1589
|
+
id: instance.id,
|
|
1590
|
+
version: instance.version,
|
|
1591
|
+
formulaId: instance.formulaId,
|
|
1592
|
+
bindings: instance.bindings,
|
|
1593
|
+
presentation: instance.presentation,
|
|
1594
|
+
rules: instance.rules.map((rule) => ({
|
|
1595
|
+
...rule,
|
|
1596
|
+
when: { ...rule.when, tolerance: { ...rule.when.tolerance } },
|
|
1597
|
+
})),
|
|
1598
|
+
})),
|
|
1599
|
+
reviewRules,
|
|
1600
|
+
periodAxis: definition.periodAxis,
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
/** Compiles, validates, normalizes, fingerprints, and freezes a diagnostic definition. */
|
|
1604
|
+
export function compileDiagnosticDefinition(value) {
|
|
1605
|
+
const boundaryIssues = [];
|
|
1606
|
+
validateJsonValue(value, boundaryIssues);
|
|
1607
|
+
if (boundaryIssues.length > 0)
|
|
1608
|
+
throw new DiagnosticValidationError(boundaryIssues);
|
|
1609
|
+
let authored;
|
|
1610
|
+
try {
|
|
1611
|
+
authored = authoredView(value);
|
|
1612
|
+
}
|
|
1613
|
+
catch {
|
|
1614
|
+
throw new DiagnosticValidationError([
|
|
1615
|
+
{
|
|
1616
|
+
domain: "definition",
|
|
1617
|
+
code: "invalid-type",
|
|
1618
|
+
path: "$",
|
|
1619
|
+
message: "Diagnostic definition does not have the required object and array structure",
|
|
1620
|
+
},
|
|
1621
|
+
]);
|
|
1622
|
+
}
|
|
1623
|
+
let definition;
|
|
1624
|
+
try {
|
|
1625
|
+
definition = validateDefinition(authored);
|
|
1626
|
+
}
|
|
1627
|
+
catch (error) {
|
|
1628
|
+
if (error instanceof DiagnosticValidationError)
|
|
1629
|
+
throw error;
|
|
1630
|
+
throw new DiagnosticValidationError([
|
|
1631
|
+
{
|
|
1632
|
+
domain: "definition",
|
|
1633
|
+
code: "invalid-type",
|
|
1634
|
+
path: "$",
|
|
1635
|
+
message: "Diagnostic definition contains a malformed nested value",
|
|
1636
|
+
},
|
|
1637
|
+
]);
|
|
1638
|
+
}
|
|
1639
|
+
const normalized = normalizeDiagnosticDefinition(definition);
|
|
1640
|
+
const identities = buildDiagnosticIdentities(normalized);
|
|
1641
|
+
const compiledBase = {
|
|
1642
|
+
definition: normalized,
|
|
1643
|
+
formulaFingerprints: identities.formulaFingerprints,
|
|
1644
|
+
calculationFingerprints: identities.calculationFingerprints,
|
|
1645
|
+
definitionIntegrity: identities.definitionIntegrity,
|
|
1646
|
+
};
|
|
1647
|
+
Object.defineProperty(compiledBase, compiledDiagnosticDefinitionBrand, {
|
|
1648
|
+
value: true,
|
|
1649
|
+
enumerable: false,
|
|
1650
|
+
configurable: false,
|
|
1651
|
+
writable: false,
|
|
1652
|
+
});
|
|
1653
|
+
const compiled = compiledBase;
|
|
1654
|
+
Object.freeze(compiled);
|
|
1655
|
+
authenticCompiledDefinitions.add(compiled);
|
|
1656
|
+
compiledInternals.set(compiled, {
|
|
1657
|
+
formulasById: new Map(normalized.formulas.map((formula) => [formula.id, formula])),
|
|
1658
|
+
measuresById: new Map(definition.measures.map((measure) => [measure.id, measure])),
|
|
1659
|
+
calculationScopesByInstanceId: identities.calculationScopesByInstanceId,
|
|
1660
|
+
calculationDependenciesByInstanceId: identities.calculationDependenciesByInstanceId,
|
|
1661
|
+
evaluationDependenciesByInstanceId: identities.evaluationDependenciesByInstanceId,
|
|
1662
|
+
derivationsByOutputMeasureId: new Map(normalized.derivedMeasures.map((derivation) => [
|
|
1663
|
+
derivation.outputMeasureId,
|
|
1664
|
+
derivation,
|
|
1665
|
+
])),
|
|
1666
|
+
});
|
|
1667
|
+
return compiled;
|
|
1668
|
+
}
|
|
1669
|
+
export function assertCompiledDiagnosticDefinition(value) {
|
|
1670
|
+
if ((typeof value !== "object" && typeof value !== "function") ||
|
|
1671
|
+
value === null ||
|
|
1672
|
+
!authenticCompiledDefinitions.has(value)) {
|
|
1673
|
+
throw new DiagnosticValidationError([
|
|
1674
|
+
{
|
|
1675
|
+
domain: "definition",
|
|
1676
|
+
code: "invalid-input-relationship",
|
|
1677
|
+
path: "$",
|
|
1678
|
+
message: "Value is not an authentic compiled diagnostic definition",
|
|
1679
|
+
},
|
|
1680
|
+
]);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
/** @internal Shared only by core diagnostic runtime modules; not re-exported. */
|
|
1684
|
+
export function getCompiledDiagnosticDefinitionInternals(value) {
|
|
1685
|
+
assertCompiledDiagnosticDefinition(value);
|
|
1686
|
+
return compiledInternals.get(value);
|
|
1687
|
+
}
|
|
1688
|
+
//# sourceMappingURL=diagnosticDefinitions.js.map
|