@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,3624 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES,
|
|
3
|
+
MAX_DIAGNOSTIC_EXPRESSION_NODES,
|
|
4
|
+
walkDiagnosticExpression,
|
|
5
|
+
type DiagnosticClaimExpression,
|
|
6
|
+
type DiagnosticMeasureExpression,
|
|
7
|
+
type DiagnosticRoleExpression,
|
|
8
|
+
} from "./diagnosticExpressions.js";
|
|
9
|
+
import {
|
|
10
|
+
buildDiagnosticIdentities,
|
|
11
|
+
normalizeDiagnosticDefinition,
|
|
12
|
+
type NormalizedDiagnosticCalculationScope,
|
|
13
|
+
type NormalizedDiagnosticDefinitionIdentity,
|
|
14
|
+
type NormalizedDiagnosticFormulaIdentity,
|
|
15
|
+
} from "./diagnosticIdentity.js";
|
|
16
|
+
import {
|
|
17
|
+
DiagnosticValidationError,
|
|
18
|
+
type DiagnosticValidationIssue,
|
|
19
|
+
type DiagnosticValidationIssueCode,
|
|
20
|
+
} from "./types.js";
|
|
21
|
+
import { canonicalJson } from "./canonical.js";
|
|
22
|
+
import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
|
|
23
|
+
import {
|
|
24
|
+
diagnosticJsonPreflight,
|
|
25
|
+
hasDiagnosticOwn,
|
|
26
|
+
isDiagnosticPlainRecord,
|
|
27
|
+
isDiagnosticToken,
|
|
28
|
+
isWellFormedDiagnosticString,
|
|
29
|
+
} from "./diagnosticRuntime.js";
|
|
30
|
+
|
|
31
|
+
export type DiagnosticMeasureKind = "count" | "amount" | "exposure";
|
|
32
|
+
export type DiagnosticMeasureSource = "loss" | "exposure" | "derived";
|
|
33
|
+
export type DiagnosticMissingPolicy = "unknown" | "zero";
|
|
34
|
+
export type DiagnosticAggregation = "sum";
|
|
35
|
+
export type DiagnosticExposureTiming = "origin-static" | "valuation-specific";
|
|
36
|
+
export type DiagnosticDevelopmentSemantics =
|
|
37
|
+
| "cumulative"
|
|
38
|
+
| "incremental"
|
|
39
|
+
| "point-in-time"
|
|
40
|
+
| "unknown";
|
|
41
|
+
|
|
42
|
+
export type JsonValue =
|
|
43
|
+
| null
|
|
44
|
+
| boolean
|
|
45
|
+
| number
|
|
46
|
+
| string
|
|
47
|
+
| readonly JsonValue[]
|
|
48
|
+
| { readonly [key: string]: JsonValue };
|
|
49
|
+
|
|
50
|
+
export type DiagnosticDeepReadonly<T> = T extends readonly (infer U)[]
|
|
51
|
+
? readonly DiagnosticDeepReadonly<U>[]
|
|
52
|
+
: T extends object
|
|
53
|
+
? { readonly [K in keyof T]: DiagnosticDeepReadonly<T[K]> }
|
|
54
|
+
: T;
|
|
55
|
+
|
|
56
|
+
export interface DiagnosticMeasureDefinition {
|
|
57
|
+
id: string;
|
|
58
|
+
displayName: string;
|
|
59
|
+
description: string;
|
|
60
|
+
source: DiagnosticMeasureSource;
|
|
61
|
+
kind: DiagnosticMeasureKind;
|
|
62
|
+
unit: string;
|
|
63
|
+
developmentSemantics: DiagnosticDevelopmentSemantics;
|
|
64
|
+
aggregation: DiagnosticAggregation;
|
|
65
|
+
missing: DiagnosticMissingPolicy;
|
|
66
|
+
basisId?: string;
|
|
67
|
+
countPopulationId?: string;
|
|
68
|
+
exposureBasisId?: string;
|
|
69
|
+
exposureTiming?: DiagnosticExposureTiming;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DiagnosticCountPopulationDefinition {
|
|
73
|
+
id: string;
|
|
74
|
+
displayName: string;
|
|
75
|
+
subject: "claim" | "claimant" | "policy" | "occurrence" | "other" | "unknown";
|
|
76
|
+
unit: string;
|
|
77
|
+
description: string;
|
|
78
|
+
attributes?: Readonly<Record<string, string | number | boolean | null>>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface DiagnosticExposureBasisDefinition {
|
|
82
|
+
id: string;
|
|
83
|
+
displayName: string;
|
|
84
|
+
basis: "earned" | "written" | "in-force" | "other" | "unknown";
|
|
85
|
+
unit: string;
|
|
86
|
+
description: string;
|
|
87
|
+
sourceDescription?: string;
|
|
88
|
+
attributes?: Readonly<Record<string, string | number | boolean | null>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type AmountPerspective = "gross" | "net" | "ceded" | "other" | "unknown";
|
|
92
|
+
|
|
93
|
+
export type AmountLimitation =
|
|
94
|
+
| { kind: "unlimited" }
|
|
95
|
+
| {
|
|
96
|
+
kind: "layer" | "pre-limited";
|
|
97
|
+
attachment: number;
|
|
98
|
+
limit: number | null;
|
|
99
|
+
application: "claim" | "occurrence" | "policy" | "source-defined";
|
|
100
|
+
derivation:
|
|
101
|
+
| { kind: "sdk" }
|
|
102
|
+
| {
|
|
103
|
+
kind: "external";
|
|
104
|
+
actor: "caller" | "source";
|
|
105
|
+
transformationRef: string;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
| { kind: "unknown"; description?: string };
|
|
109
|
+
|
|
110
|
+
export interface AmountBasisComponent {
|
|
111
|
+
id: string;
|
|
112
|
+
treatment: "included" | "excluded" | "unknown";
|
|
113
|
+
limitation: AmountLimitation;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface AmountBasisDefinition {
|
|
117
|
+
id: string;
|
|
118
|
+
displayName: string;
|
|
119
|
+
currency: string;
|
|
120
|
+
perspective: AmountPerspective;
|
|
121
|
+
components: readonly AmountBasisComponent[];
|
|
122
|
+
sourceDescription?: string;
|
|
123
|
+
attributes?: Readonly<Record<string, string | number | boolean | null>>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DiagnosticFormulaRole {
|
|
127
|
+
kind: DiagnosticMeasureKind;
|
|
128
|
+
compatibilityGroup?: string;
|
|
129
|
+
developmentSemantics?: DiagnosticDevelopmentSemantics;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface DiagnosticFormulaTemplate {
|
|
133
|
+
id: string;
|
|
134
|
+
version: string;
|
|
135
|
+
roles: Readonly<Record<string, DiagnosticFormulaRole>>;
|
|
136
|
+
numerator: DiagnosticRoleExpression;
|
|
137
|
+
denominator: DiagnosticRoleExpression;
|
|
138
|
+
denominatorPolicy: "positive-or-null";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface DiagnosticMetricPresentation {
|
|
142
|
+
displayName: string;
|
|
143
|
+
description: string;
|
|
144
|
+
displayUnit: string;
|
|
145
|
+
scale: number;
|
|
146
|
+
numeratorLabel: string;
|
|
147
|
+
denominatorLabel: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export type DiagnosticRuleOperand =
|
|
151
|
+
| { source: "measure"; expression: DiagnosticMeasureExpression }
|
|
152
|
+
| { source: "calculation"; field: "numerator" | "denominator" }
|
|
153
|
+
| { source: "constant"; value: number };
|
|
154
|
+
|
|
155
|
+
export interface DiagnosticComparisonPredicate {
|
|
156
|
+
left: DiagnosticRuleOperand;
|
|
157
|
+
operator: "lt" | "lte" | "eq" | "neq" | "gte" | "gt";
|
|
158
|
+
right: DiagnosticRuleOperand;
|
|
159
|
+
tolerance?: { absolute?: number; relative?: number };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface DiagnosticComparisonRule {
|
|
163
|
+
id: string;
|
|
164
|
+
code: string;
|
|
165
|
+
message: string;
|
|
166
|
+
severity: "warning" | "fail";
|
|
167
|
+
when: DiagnosticComparisonPredicate;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface DiagnosticMetricInstance {
|
|
171
|
+
id: string;
|
|
172
|
+
version: string;
|
|
173
|
+
formulaId: string;
|
|
174
|
+
bindings: Readonly<Record<string, DiagnosticMeasureExpression>>;
|
|
175
|
+
presentation: DiagnosticMetricPresentation;
|
|
176
|
+
rules: readonly DiagnosticComparisonRule[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface DiagnosticPeriodCoordinate {
|
|
180
|
+
label: string;
|
|
181
|
+
aliases?: readonly string[];
|
|
182
|
+
coordinate: number;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export type DiagnosticPeriodAxis =
|
|
186
|
+
| {
|
|
187
|
+
kind: "calendar";
|
|
188
|
+
originCadence: "month" | "quarter" | "year";
|
|
189
|
+
valuationCadence: "month" | "quarter" | "year";
|
|
190
|
+
originAnchor: "start" | "end";
|
|
191
|
+
valuationAnchor: "start" | "end";
|
|
192
|
+
ageUnit: "month";
|
|
193
|
+
ageOffset: number;
|
|
194
|
+
}
|
|
195
|
+
| {
|
|
196
|
+
kind: "ordered";
|
|
197
|
+
id: string;
|
|
198
|
+
version: string;
|
|
199
|
+
ageUnit: string;
|
|
200
|
+
ageOffset: number;
|
|
201
|
+
origins: readonly DiagnosticPeriodCoordinate[];
|
|
202
|
+
valuations: readonly DiagnosticPeriodCoordinate[];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export interface DiagnosticDerivedMeasureDefinition {
|
|
206
|
+
id: string;
|
|
207
|
+
outputMeasureId: string;
|
|
208
|
+
expression: DiagnosticClaimExpression;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface DiagnosticsFilter {
|
|
212
|
+
sourceGroups?: readonly string[];
|
|
213
|
+
outputGroups?: readonly string[];
|
|
214
|
+
origins?: readonly string[];
|
|
215
|
+
originFrom?: string;
|
|
216
|
+
originThrough?: string;
|
|
217
|
+
valuations?: readonly string[];
|
|
218
|
+
valuationFrom?: string;
|
|
219
|
+
valuationThrough?: string;
|
|
220
|
+
minDevelopmentAge?: number;
|
|
221
|
+
maxDevelopmentAge?: number;
|
|
222
|
+
instanceIds?: readonly string[];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
interface DiagnosticReviewRuleBase {
|
|
226
|
+
id: string;
|
|
227
|
+
code: string;
|
|
228
|
+
description: string;
|
|
229
|
+
severity: "warning" | "fail";
|
|
230
|
+
tolerance?: { absolute?: number; relative?: number };
|
|
231
|
+
missingInput: "not-evaluated" | "finding";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type DiagnosticReviewOperand =
|
|
235
|
+
| DiagnosticMeasureExpression
|
|
236
|
+
| { op: "constant"; value: number };
|
|
237
|
+
|
|
238
|
+
export interface DiagnosticReviewPredicate {
|
|
239
|
+
left: DiagnosticReviewOperand;
|
|
240
|
+
operator: "lt" | "lte" | "eq" | "neq" | "gte" | "gt";
|
|
241
|
+
right: DiagnosticReviewOperand;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export type DiagnosticControlTotalProjection =
|
|
245
|
+
| { kind: "valuation"; valuation: string }
|
|
246
|
+
| { kind: "latest-valuation-per-origin" }
|
|
247
|
+
| { kind: "all-cells" };
|
|
248
|
+
|
|
249
|
+
export type DiagnosticReviewFilter = Omit<
|
|
250
|
+
DiagnosticsFilter,
|
|
251
|
+
"outputGroups" | "instanceIds"
|
|
252
|
+
>;
|
|
253
|
+
|
|
254
|
+
export type DiagnosticReviewRule =
|
|
255
|
+
| (DiagnosticReviewRuleBase & {
|
|
256
|
+
kind: "compare";
|
|
257
|
+
when: DiagnosticReviewPredicate;
|
|
258
|
+
})
|
|
259
|
+
| (DiagnosticReviewRuleBase & {
|
|
260
|
+
kind: "reconcile";
|
|
261
|
+
actual: DiagnosticMeasureExpression;
|
|
262
|
+
expected: DiagnosticReviewOperand;
|
|
263
|
+
})
|
|
264
|
+
| (DiagnosticReviewRuleBase & {
|
|
265
|
+
kind: "monotonic";
|
|
266
|
+
expression: DiagnosticMeasureExpression;
|
|
267
|
+
direction: "nondecreasing" | "nonincreasing";
|
|
268
|
+
})
|
|
269
|
+
| (DiagnosticReviewRuleBase & {
|
|
270
|
+
kind: "layer-order";
|
|
271
|
+
narrower: DiagnosticMeasureExpression;
|
|
272
|
+
broader: DiagnosticMeasureExpression;
|
|
273
|
+
comparability:
|
|
274
|
+
| { kind: "compiler-proven" }
|
|
275
|
+
| { kind: "caller-asserted"; rationaleArtifactId: string };
|
|
276
|
+
})
|
|
277
|
+
| (DiagnosticReviewRuleBase & {
|
|
278
|
+
kind: "control-total";
|
|
279
|
+
expression: DiagnosticMeasureExpression;
|
|
280
|
+
expected: number;
|
|
281
|
+
filter?: DiagnosticReviewFilter;
|
|
282
|
+
projection: DiagnosticControlTotalProjection;
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
export interface DiagnosticDefinition {
|
|
286
|
+
diagnosticDefinitionVersion: "1.0.0";
|
|
287
|
+
id: string;
|
|
288
|
+
version: string;
|
|
289
|
+
lossRowGrain: "claim" | "aggregate";
|
|
290
|
+
measures: readonly DiagnosticMeasureDefinition[];
|
|
291
|
+
countPopulations: readonly DiagnosticCountPopulationDefinition[];
|
|
292
|
+
exposureBases: readonly DiagnosticExposureBasisDefinition[];
|
|
293
|
+
amountBases: readonly AmountBasisDefinition[];
|
|
294
|
+
derivedMeasures: readonly DiagnosticDerivedMeasureDefinition[];
|
|
295
|
+
formulas: readonly DiagnosticFormulaTemplate[];
|
|
296
|
+
instances: readonly DiagnosticMetricInstance[];
|
|
297
|
+
reviewRules: readonly DiagnosticReviewRule[];
|
|
298
|
+
periodAxis: DiagnosticPeriodAxis;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export interface DiagnosticSourceLocation {
|
|
302
|
+
readonly artifactId: string;
|
|
303
|
+
readonly sourceFile?: string;
|
|
304
|
+
readonly sourceSheet?: string;
|
|
305
|
+
readonly sourceRow?: number;
|
|
306
|
+
readonly sourceCell?: string;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export interface DiagnosticMeasureStats {
|
|
310
|
+
readonly value: number | null;
|
|
311
|
+
readonly sum: number | null;
|
|
312
|
+
readonly observed: number;
|
|
313
|
+
readonly missing: number;
|
|
314
|
+
readonly nonFinite: number;
|
|
315
|
+
readonly imputedZero: number;
|
|
316
|
+
readonly deduplicated: number;
|
|
317
|
+
readonly structural: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const compiledDiagnosticDefinitionBrand: unique symbol = Symbol(
|
|
321
|
+
"compiled-diagnostic-definition",
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
export interface CompiledDiagnosticDefinition {
|
|
325
|
+
readonly [compiledDiagnosticDefinitionBrand]: true;
|
|
326
|
+
readonly definition: DiagnosticDeepReadonly<NormalizedDiagnosticDefinitionIdentity>;
|
|
327
|
+
readonly formulaFingerprints: Readonly<Record<string, string>>;
|
|
328
|
+
readonly calculationFingerprints: Readonly<Record<string, string>>;
|
|
329
|
+
readonly definitionIntegrity: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export interface CompiledDiagnosticDefinitionInternals {
|
|
333
|
+
readonly formulasById: ReadonlyMap<
|
|
334
|
+
string,
|
|
335
|
+
NormalizedDiagnosticFormulaIdentity
|
|
336
|
+
>;
|
|
337
|
+
readonly measuresById: ReadonlyMap<string, DiagnosticMeasureDefinition>;
|
|
338
|
+
readonly calculationScopesByInstanceId: ReadonlyMap<
|
|
339
|
+
string,
|
|
340
|
+
NormalizedDiagnosticCalculationScope
|
|
341
|
+
>;
|
|
342
|
+
readonly calculationDependenciesByInstanceId: ReadonlyMap<
|
|
343
|
+
string,
|
|
344
|
+
readonly string[]
|
|
345
|
+
>;
|
|
346
|
+
readonly evaluationDependenciesByInstanceId: ReadonlyMap<
|
|
347
|
+
string,
|
|
348
|
+
readonly string[]
|
|
349
|
+
>;
|
|
350
|
+
readonly derivationsByOutputMeasureId: ReadonlyMap<
|
|
351
|
+
string,
|
|
352
|
+
DiagnosticDerivedMeasureDefinition
|
|
353
|
+
>;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const authenticCompiledDefinitions = new WeakSet<object>();
|
|
357
|
+
const compiledInternals = new WeakMap<
|
|
358
|
+
object,
|
|
359
|
+
CompiledDiagnosticDefinitionInternals
|
|
360
|
+
>();
|
|
361
|
+
|
|
362
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
363
|
+
return isDiagnosticPlainRecord(value);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function propertyPath(parent: string, key: string): string {
|
|
367
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
368
|
+
? `${parent}.${key}`
|
|
369
|
+
: `${parent}[${JSON.stringify(key)}]`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function pushIssue(
|
|
373
|
+
issues: DiagnosticValidationIssue[],
|
|
374
|
+
code: DiagnosticValidationIssueCode,
|
|
375
|
+
path: string,
|
|
376
|
+
message: string,
|
|
377
|
+
): void {
|
|
378
|
+
issues.push({ domain: "definition", code, path, message });
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function validateAllowedKeys(
|
|
382
|
+
value: Record<string, unknown>,
|
|
383
|
+
allowed: readonly string[],
|
|
384
|
+
path: string,
|
|
385
|
+
issues: DiagnosticValidationIssue[],
|
|
386
|
+
): void {
|
|
387
|
+
for (const key of Object.keys(value)) {
|
|
388
|
+
if (!allowed.includes(key)) {
|
|
389
|
+
pushIssue(
|
|
390
|
+
issues,
|
|
391
|
+
"unknown-key",
|
|
392
|
+
propertyPath(path, key),
|
|
393
|
+
`Unknown key ${key}`,
|
|
394
|
+
);
|
|
395
|
+
} else if (value[key] === undefined) {
|
|
396
|
+
pushIssue(
|
|
397
|
+
issues,
|
|
398
|
+
"invalid-type",
|
|
399
|
+
propertyPath(path, key),
|
|
400
|
+
"Explicit undefined is not allowed",
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function validateScalarAttributes(
|
|
407
|
+
value: unknown,
|
|
408
|
+
path: string,
|
|
409
|
+
issues: DiagnosticValidationIssue[],
|
|
410
|
+
): void {
|
|
411
|
+
if (value === undefined) return;
|
|
412
|
+
if (!isPlainRecord(value)) {
|
|
413
|
+
pushIssue(
|
|
414
|
+
issues,
|
|
415
|
+
"invalid-type",
|
|
416
|
+
path,
|
|
417
|
+
"Attributes must be a plain object",
|
|
418
|
+
);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
for (const [key, scalar] of Object.entries(value)) {
|
|
422
|
+
validateToken(key, propertyPath(path, key), "Attribute key", issues);
|
|
423
|
+
if (
|
|
424
|
+
scalar !== null &&
|
|
425
|
+
typeof scalar !== "string" &&
|
|
426
|
+
typeof scalar !== "number" &&
|
|
427
|
+
typeof scalar !== "boolean"
|
|
428
|
+
) {
|
|
429
|
+
pushIssue(
|
|
430
|
+
issues,
|
|
431
|
+
"invalid-type",
|
|
432
|
+
propertyPath(path, key),
|
|
433
|
+
"Attribute value must be a string, finite number, boolean, or null",
|
|
434
|
+
);
|
|
435
|
+
} else if (typeof scalar === "number") {
|
|
436
|
+
validateFinite(
|
|
437
|
+
scalar,
|
|
438
|
+
propertyPath(path, key),
|
|
439
|
+
"Attribute number",
|
|
440
|
+
issues,
|
|
441
|
+
);
|
|
442
|
+
} else if (
|
|
443
|
+
typeof scalar === "string" &&
|
|
444
|
+
!isWellFormedDiagnosticString(scalar)
|
|
445
|
+
) {
|
|
446
|
+
pushIssue(
|
|
447
|
+
issues,
|
|
448
|
+
"invalid-string",
|
|
449
|
+
propertyPath(path, key),
|
|
450
|
+
"Attribute string contains invalid Unicode",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function validateToken(
|
|
457
|
+
value: unknown,
|
|
458
|
+
path: string,
|
|
459
|
+
label: string,
|
|
460
|
+
issues: DiagnosticValidationIssue[],
|
|
461
|
+
): value is string {
|
|
462
|
+
if (typeof value !== "string") {
|
|
463
|
+
pushIssue(issues, "invalid-type", path, `${label} must be a string`);
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
if (!isWellFormedDiagnosticString(value)) {
|
|
467
|
+
pushIssue(
|
|
468
|
+
issues,
|
|
469
|
+
"invalid-string",
|
|
470
|
+
path,
|
|
471
|
+
`${label} contains an invalid Unicode string`,
|
|
472
|
+
);
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
if (!isDiagnosticToken(value)) {
|
|
476
|
+
pushIssue(
|
|
477
|
+
issues,
|
|
478
|
+
"invalid-string",
|
|
479
|
+
path,
|
|
480
|
+
`${label} must be nonempty without surrounding ASCII whitespace`,
|
|
481
|
+
);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function validateFinite(
|
|
488
|
+
value: unknown,
|
|
489
|
+
path: string,
|
|
490
|
+
label: string,
|
|
491
|
+
issues: DiagnosticValidationIssue[],
|
|
492
|
+
): value is number {
|
|
493
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
494
|
+
pushIssue(issues, "invalid-number", path, `${label} must be finite`);
|
|
495
|
+
return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function validateJsonValue(
|
|
501
|
+
value: unknown,
|
|
502
|
+
issues: DiagnosticValidationIssue[],
|
|
503
|
+
): void {
|
|
504
|
+
issues.push(...diagnosticJsonPreflight(value, "definition"));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function requireArray(
|
|
508
|
+
value: unknown,
|
|
509
|
+
path: string,
|
|
510
|
+
issues: DiagnosticValidationIssue[],
|
|
511
|
+
): readonly unknown[] {
|
|
512
|
+
if (!Array.isArray(value)) {
|
|
513
|
+
pushIssue(issues, "invalid-type", path, "Expected an array");
|
|
514
|
+
return [];
|
|
515
|
+
}
|
|
516
|
+
return value;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function collectCatalog<T extends { readonly id: string }>(
|
|
520
|
+
values: readonly T[],
|
|
521
|
+
path: string,
|
|
522
|
+
issues: DiagnosticValidationIssue[],
|
|
523
|
+
): Map<string, T> {
|
|
524
|
+
const result = new Map<string, T>();
|
|
525
|
+
values.forEach((value, index) => {
|
|
526
|
+
const idPath = `${path}[${index}].id`;
|
|
527
|
+
if (!validateToken(value?.id, idPath, "ID", issues)) return;
|
|
528
|
+
if (result.has(value.id)) {
|
|
529
|
+
pushIssue(issues, "duplicate-id", idPath, `Duplicate ID ${value.id}`);
|
|
530
|
+
} else result.set(value.id, value);
|
|
531
|
+
});
|
|
532
|
+
return result;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function semanticReference(measure: DiagnosticMeasureDefinition): string {
|
|
536
|
+
if (measure.kind === "amount") return `amount:${measure.basisId ?? ""}`;
|
|
537
|
+
if (measure.kind === "count")
|
|
538
|
+
return `count:${measure.countPopulationId ?? ""}`;
|
|
539
|
+
return `exposure:${measure.exposureBasisId ?? ""}`;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function transitiveMeasureIds(
|
|
543
|
+
roots: readonly string[],
|
|
544
|
+
derivations: ReadonlyMap<string, DiagnosticDerivedMeasureDefinition>,
|
|
545
|
+
): readonly string[] {
|
|
546
|
+
const result = new Set<string>();
|
|
547
|
+
const stack = [...roots];
|
|
548
|
+
while (stack.length > 0) {
|
|
549
|
+
const id = stack.pop()!;
|
|
550
|
+
if (result.has(id)) continue;
|
|
551
|
+
result.add(id);
|
|
552
|
+
const derivation = derivations.get(id);
|
|
553
|
+
if (derivation) {
|
|
554
|
+
stack.push(
|
|
555
|
+
...walkDiagnosticExpression(derivation.expression, "claim", "$", [])
|
|
556
|
+
.dependencies,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return [...result].sort();
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
interface ProjectedAmountBasis {
|
|
564
|
+
readonly currency: string;
|
|
565
|
+
readonly perspective: AmountPerspective;
|
|
566
|
+
readonly components: readonly AmountBasisComponent[];
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
interface ProjectedClaimSemantics {
|
|
570
|
+
readonly kind: DiagnosticMeasureKind;
|
|
571
|
+
readonly unit: string;
|
|
572
|
+
readonly reference: string;
|
|
573
|
+
readonly amountBasis?: ProjectedAmountBasis;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function projectedAmountBasis(
|
|
577
|
+
basis: AmountBasisDefinition,
|
|
578
|
+
): ProjectedAmountBasis {
|
|
579
|
+
return {
|
|
580
|
+
currency: basis.currency,
|
|
581
|
+
perspective: basis.perspective,
|
|
582
|
+
components: [...basis.components].sort((left, right) =>
|
|
583
|
+
left.id < right.id ? -1 : left.id > right.id ? 1 : 0,
|
|
584
|
+
),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function projectedBasisKey(basis: ProjectedAmountBasis): string {
|
|
589
|
+
return canonicalJson(basis);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function projectClaimExpression(
|
|
593
|
+
expression: DiagnosticClaimExpression,
|
|
594
|
+
path: string,
|
|
595
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
596
|
+
amountBases: ReadonlyMap<string, AmountBasisDefinition>,
|
|
597
|
+
issues: DiagnosticValidationIssue[],
|
|
598
|
+
): ProjectedClaimSemantics | null {
|
|
599
|
+
if (expression.op === "measure") {
|
|
600
|
+
const measure = measures.get(expression.measureId);
|
|
601
|
+
if (!measure) return null;
|
|
602
|
+
if (measure.kind !== "amount") {
|
|
603
|
+
return {
|
|
604
|
+
kind: measure.kind,
|
|
605
|
+
unit: measure.unit,
|
|
606
|
+
reference: semanticReference(measure),
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
const basis = measure.basisId
|
|
610
|
+
? amountBases.get(measure.basisId)
|
|
611
|
+
: undefined;
|
|
612
|
+
return basis
|
|
613
|
+
? {
|
|
614
|
+
kind: "amount",
|
|
615
|
+
unit: measure.unit,
|
|
616
|
+
reference: `amount:${measure.basisId}`,
|
|
617
|
+
amountBasis: projectedAmountBasis(basis),
|
|
618
|
+
}
|
|
619
|
+
: null;
|
|
620
|
+
}
|
|
621
|
+
if (expression.op === "claim-layer") {
|
|
622
|
+
const measure = measures.get(expression.measureId);
|
|
623
|
+
if (!measure || measure.kind !== "amount" || !measure.basisId) {
|
|
624
|
+
pushIssue(
|
|
625
|
+
issues,
|
|
626
|
+
"incompatible-semantics",
|
|
627
|
+
path,
|
|
628
|
+
"Claim layers require an amount measure",
|
|
629
|
+
);
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
const basis = amountBases.get(measure.basisId);
|
|
633
|
+
const component = basis?.components[0];
|
|
634
|
+
if (
|
|
635
|
+
!basis ||
|
|
636
|
+
basis.components.length !== 1 ||
|
|
637
|
+
component?.treatment !== "included" ||
|
|
638
|
+
component.limitation.kind !== "unlimited"
|
|
639
|
+
) {
|
|
640
|
+
pushIssue(
|
|
641
|
+
issues,
|
|
642
|
+
"incompatible-semantics",
|
|
643
|
+
path,
|
|
644
|
+
"Claim layers require one included unlimited source component",
|
|
645
|
+
);
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return {
|
|
649
|
+
kind: "amount",
|
|
650
|
+
unit: measure.unit,
|
|
651
|
+
reference: "amount:projected",
|
|
652
|
+
amountBasis: {
|
|
653
|
+
currency: basis.currency,
|
|
654
|
+
perspective: basis.perspective,
|
|
655
|
+
components: [
|
|
656
|
+
{
|
|
657
|
+
id: component.id,
|
|
658
|
+
treatment: "included",
|
|
659
|
+
limitation: {
|
|
660
|
+
kind: "layer",
|
|
661
|
+
attachment: expression.attachment,
|
|
662
|
+
limit: expression.limit,
|
|
663
|
+
application: "claim",
|
|
664
|
+
derivation: { kind: "sdk" },
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
],
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const children =
|
|
672
|
+
expression.op === "add"
|
|
673
|
+
? expression.terms
|
|
674
|
+
: [expression.left, expression.right];
|
|
675
|
+
const projected = children.map((child, index) =>
|
|
676
|
+
projectClaimExpression(
|
|
677
|
+
child,
|
|
678
|
+
expression.op === "add"
|
|
679
|
+
? `${path}.terms[${index}]`
|
|
680
|
+
: `${path}.${index === 0 ? "left" : "right"}`,
|
|
681
|
+
measures,
|
|
682
|
+
amountBases,
|
|
683
|
+
issues,
|
|
684
|
+
),
|
|
685
|
+
);
|
|
686
|
+
if (projected.some((item) => item === null)) return null;
|
|
687
|
+
const present = projected as ProjectedClaimSemantics[];
|
|
688
|
+
if (expression.op === "subtract") {
|
|
689
|
+
const [left, right] = present;
|
|
690
|
+
if (
|
|
691
|
+
left!.kind !== right!.kind ||
|
|
692
|
+
left!.unit !== right!.unit ||
|
|
693
|
+
(left!.kind === "amount"
|
|
694
|
+
? projectedBasisKey(left!.amountBasis!) !==
|
|
695
|
+
projectedBasisKey(right!.amountBasis!)
|
|
696
|
+
: left!.reference !== right!.reference)
|
|
697
|
+
) {
|
|
698
|
+
pushIssue(
|
|
699
|
+
issues,
|
|
700
|
+
"incompatible-semantics",
|
|
701
|
+
path,
|
|
702
|
+
"Claim subtraction requires one exact semantic basis",
|
|
703
|
+
);
|
|
704
|
+
return null;
|
|
705
|
+
}
|
|
706
|
+
return left!;
|
|
707
|
+
}
|
|
708
|
+
const first = present[0]!;
|
|
709
|
+
if (
|
|
710
|
+
present.some((item) => item.kind !== first.kind || item.unit !== first.unit)
|
|
711
|
+
) {
|
|
712
|
+
pushIssue(
|
|
713
|
+
issues,
|
|
714
|
+
"incompatible-semantics",
|
|
715
|
+
path,
|
|
716
|
+
"Claim addition requires one kind and unit",
|
|
717
|
+
);
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
if (first.kind !== "amount") {
|
|
721
|
+
if (present.some((item) => item.reference !== first.reference)) {
|
|
722
|
+
pushIssue(
|
|
723
|
+
issues,
|
|
724
|
+
"incompatible-semantics",
|
|
725
|
+
path,
|
|
726
|
+
"Claim addition requires one exact semantic basis",
|
|
727
|
+
);
|
|
728
|
+
return null;
|
|
729
|
+
}
|
|
730
|
+
return first;
|
|
731
|
+
}
|
|
732
|
+
const bases = present.map((item) => item.amountBasis!);
|
|
733
|
+
if (
|
|
734
|
+
bases.some(
|
|
735
|
+
(basis) =>
|
|
736
|
+
basis.currency !== bases[0]!.currency ||
|
|
737
|
+
basis.perspective !== bases[0]!.perspective,
|
|
738
|
+
)
|
|
739
|
+
) {
|
|
740
|
+
pushIssue(
|
|
741
|
+
issues,
|
|
742
|
+
"incompatible-semantics",
|
|
743
|
+
path,
|
|
744
|
+
"Amount addition cannot mix currency or perspective",
|
|
745
|
+
);
|
|
746
|
+
return null;
|
|
747
|
+
}
|
|
748
|
+
const componentIds = new Set<string>();
|
|
749
|
+
const components: AmountBasisComponent[] = [];
|
|
750
|
+
for (const basis of bases) {
|
|
751
|
+
for (const component of basis.components) {
|
|
752
|
+
if (componentIds.has(component.id)) {
|
|
753
|
+
pushIssue(
|
|
754
|
+
issues,
|
|
755
|
+
"incompatible-semantics",
|
|
756
|
+
path,
|
|
757
|
+
`Amount addition overlaps component ${component.id}`,
|
|
758
|
+
);
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
componentIds.add(component.id);
|
|
762
|
+
components.push(component);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const amountBasis = {
|
|
766
|
+
currency: bases[0]!.currency,
|
|
767
|
+
perspective: bases[0]!.perspective,
|
|
768
|
+
components: components.sort((left, right) =>
|
|
769
|
+
left.id < right.id ? -1 : left.id > right.id ? 1 : 0,
|
|
770
|
+
),
|
|
771
|
+
};
|
|
772
|
+
return {
|
|
773
|
+
kind: "amount",
|
|
774
|
+
unit: first.unit,
|
|
775
|
+
reference: "amount:projected",
|
|
776
|
+
amountBasis,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function expressionSemantics(
|
|
781
|
+
expression: DiagnosticMeasureExpression,
|
|
782
|
+
path: string,
|
|
783
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
784
|
+
issues: DiagnosticValidationIssue[],
|
|
785
|
+
initialDepth = 1,
|
|
786
|
+
): {
|
|
787
|
+
readonly measureIds: readonly string[];
|
|
788
|
+
readonly signature: string | null;
|
|
789
|
+
} {
|
|
790
|
+
const walked = walkDiagnosticExpression(
|
|
791
|
+
expression,
|
|
792
|
+
"measure",
|
|
793
|
+
path,
|
|
794
|
+
issues,
|
|
795
|
+
initialDepth,
|
|
796
|
+
);
|
|
797
|
+
const found = walked.dependencies
|
|
798
|
+
.map((id) => measures.get(id))
|
|
799
|
+
.filter(
|
|
800
|
+
(value): value is DiagnosticMeasureDefinition => value !== undefined,
|
|
801
|
+
);
|
|
802
|
+
for (const id of walked.dependencies) {
|
|
803
|
+
if (!measures.has(id)) {
|
|
804
|
+
pushIssue(issues, "unknown-reference", path, `Unknown measure ${id}`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const signatures = new Set(
|
|
808
|
+
found.map((measure) =>
|
|
809
|
+
canonicalJson([measure.kind, measure.unit, semanticReference(measure)]),
|
|
810
|
+
),
|
|
811
|
+
);
|
|
812
|
+
if (signatures.size > 1) {
|
|
813
|
+
pushIssue(
|
|
814
|
+
issues,
|
|
815
|
+
"incompatible-semantics",
|
|
816
|
+
path,
|
|
817
|
+
"Expression combines incompatible measure semantics",
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
measureIds: walked.dependencies,
|
|
822
|
+
signature: signatures.size === 1 ? [...signatures][0]! : null,
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function validateTolerance(
|
|
827
|
+
value: { absolute?: number; relative?: number } | undefined,
|
|
828
|
+
path: string,
|
|
829
|
+
issues: DiagnosticValidationIssue[],
|
|
830
|
+
): void {
|
|
831
|
+
for (const key of ["absolute", "relative"] as const) {
|
|
832
|
+
const component = value?.[key];
|
|
833
|
+
if (component === undefined) continue;
|
|
834
|
+
if (
|
|
835
|
+
!validateFinite(component, `${path}.${key}`, `${key} tolerance`, issues)
|
|
836
|
+
)
|
|
837
|
+
continue;
|
|
838
|
+
if (component < 0) {
|
|
839
|
+
pushIssue(
|
|
840
|
+
issues,
|
|
841
|
+
"invalid-number",
|
|
842
|
+
`${path}.${key}`,
|
|
843
|
+
`${key} tolerance must be nonnegative`,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function validateReviewFilter(
|
|
850
|
+
value: unknown,
|
|
851
|
+
path: string,
|
|
852
|
+
axis: DiagnosticPeriodAxis,
|
|
853
|
+
issues: DiagnosticValidationIssue[],
|
|
854
|
+
): void {
|
|
855
|
+
if (!isPlainRecord(value)) {
|
|
856
|
+
pushIssue(issues, "invalid-type", path, "Review filter must be an object");
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const periodCoordinates = new Map<string, number>();
|
|
860
|
+
const validatePeriods = (
|
|
861
|
+
key: "origins" | "valuations",
|
|
862
|
+
side: "origin" | "valuation",
|
|
863
|
+
) => {
|
|
864
|
+
const raw = value[key];
|
|
865
|
+
if (raw === undefined) return;
|
|
866
|
+
const entries = requireArray(raw, `${path}.${key}`, issues);
|
|
867
|
+
entries.forEach((entry, index) => {
|
|
868
|
+
const entryPath = `${path}.${key}[${index}]`;
|
|
869
|
+
if (!validateToken(entry, entryPath, `${side} period`, issues)) return;
|
|
870
|
+
const normalized = normalizeDiagnosticPeriodWithAxis(axis, side, entry);
|
|
871
|
+
if (!normalized)
|
|
872
|
+
pushIssue(
|
|
873
|
+
issues,
|
|
874
|
+
"invalid-period",
|
|
875
|
+
entryPath,
|
|
876
|
+
`Unknown ${side} period ${JSON.stringify(entry)}`,
|
|
877
|
+
);
|
|
878
|
+
});
|
|
879
|
+
};
|
|
880
|
+
const validateEndpoint = (
|
|
881
|
+
key: "originFrom" | "originThrough" | "valuationFrom" | "valuationThrough",
|
|
882
|
+
side: "origin" | "valuation",
|
|
883
|
+
) => {
|
|
884
|
+
const raw = value[key];
|
|
885
|
+
if (raw === undefined) return;
|
|
886
|
+
if (!validateToken(raw, `${path}.${key}`, `${side} period`, issues)) return;
|
|
887
|
+
const normalized = normalizeDiagnosticPeriodWithAxis(axis, side, raw);
|
|
888
|
+
if (!normalized)
|
|
889
|
+
pushIssue(
|
|
890
|
+
issues,
|
|
891
|
+
"invalid-period",
|
|
892
|
+
`${path}.${key}`,
|
|
893
|
+
`Unknown ${side} period ${JSON.stringify(raw)}`,
|
|
894
|
+
);
|
|
895
|
+
else periodCoordinates.set(key, normalized.coordinate);
|
|
896
|
+
};
|
|
897
|
+
if (value.sourceGroups !== undefined)
|
|
898
|
+
requireArray(value.sourceGroups, `${path}.sourceGroups`, issues).forEach(
|
|
899
|
+
(item, index) => {
|
|
900
|
+
validateToken(
|
|
901
|
+
item,
|
|
902
|
+
`${path}.sourceGroups[${index}]`,
|
|
903
|
+
"Source group",
|
|
904
|
+
issues,
|
|
905
|
+
);
|
|
906
|
+
},
|
|
907
|
+
);
|
|
908
|
+
validatePeriods("origins", "origin");
|
|
909
|
+
validateEndpoint("originFrom", "origin");
|
|
910
|
+
validateEndpoint("originThrough", "origin");
|
|
911
|
+
validatePeriods("valuations", "valuation");
|
|
912
|
+
validateEndpoint("valuationFrom", "valuation");
|
|
913
|
+
validateEndpoint("valuationThrough", "valuation");
|
|
914
|
+
for (const key of ["minDevelopmentAge", "maxDevelopmentAge"] as const) {
|
|
915
|
+
const raw = value[key];
|
|
916
|
+
if (raw === undefined) continue;
|
|
917
|
+
if (
|
|
918
|
+
!validateFinite(raw, `${path}.${key}`, key, issues) ||
|
|
919
|
+
!Number.isSafeInteger(raw) ||
|
|
920
|
+
raw < 0
|
|
921
|
+
) {
|
|
922
|
+
pushIssue(
|
|
923
|
+
issues,
|
|
924
|
+
"invalid-number",
|
|
925
|
+
`${path}.${key}`,
|
|
926
|
+
`${key} must be a nonnegative safe integer`,
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
if (
|
|
931
|
+
(periodCoordinates.get("originFrom") ?? -Infinity) >
|
|
932
|
+
(periodCoordinates.get("originThrough") ?? Infinity)
|
|
933
|
+
) {
|
|
934
|
+
pushIssue(
|
|
935
|
+
issues,
|
|
936
|
+
"invalid-input-relationship",
|
|
937
|
+
path,
|
|
938
|
+
"originFrom must not follow originThrough",
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
if (
|
|
942
|
+
(periodCoordinates.get("valuationFrom") ?? -Infinity) >
|
|
943
|
+
(periodCoordinates.get("valuationThrough") ?? Infinity)
|
|
944
|
+
) {
|
|
945
|
+
pushIssue(
|
|
946
|
+
issues,
|
|
947
|
+
"invalid-input-relationship",
|
|
948
|
+
path,
|
|
949
|
+
"valuationFrom must not follow valuationThrough",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
if (
|
|
953
|
+
typeof value.minDevelopmentAge === "number" &&
|
|
954
|
+
typeof value.maxDevelopmentAge === "number" &&
|
|
955
|
+
value.minDevelopmentAge > value.maxDevelopmentAge
|
|
956
|
+
) {
|
|
957
|
+
pushIssue(
|
|
958
|
+
issues,
|
|
959
|
+
"invalid-input-relationship",
|
|
960
|
+
path,
|
|
961
|
+
"minDevelopmentAge must not exceed maxDevelopmentAge",
|
|
962
|
+
);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function measureExpressionBasis(
|
|
967
|
+
expression: DiagnosticMeasureExpression,
|
|
968
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
969
|
+
amountBases: ReadonlyMap<string, AmountBasisDefinition>,
|
|
970
|
+
): AmountBasisDefinition | null {
|
|
971
|
+
const ids = walkDiagnosticExpression(
|
|
972
|
+
expression,
|
|
973
|
+
"measure",
|
|
974
|
+
"$",
|
|
975
|
+
[],
|
|
976
|
+
).dependencies;
|
|
977
|
+
const found = ids.map((id) => measures.get(id));
|
|
978
|
+
if (
|
|
979
|
+
found.length === 0 ||
|
|
980
|
+
found.some((measure) => measure?.kind !== "amount" || !measure.basisId)
|
|
981
|
+
)
|
|
982
|
+
return null;
|
|
983
|
+
const basisIds = new Set(found.map((measure) => measure!.basisId!));
|
|
984
|
+
return basisIds.size === 1
|
|
985
|
+
? (amountBases.get([...basisIds][0]!) ?? null)
|
|
986
|
+
: null;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
interface LayerComponentTrace {
|
|
990
|
+
readonly rawMeasureId: string;
|
|
991
|
+
readonly hasLayer: boolean;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function traceLayerComponents(
|
|
995
|
+
measureId: string,
|
|
996
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
997
|
+
amountBases: ReadonlyMap<string, AmountBasisDefinition>,
|
|
998
|
+
derivations: ReadonlyMap<string, DiagnosticDerivedMeasureDefinition>,
|
|
999
|
+
seen = new Set<string>(),
|
|
1000
|
+
): ReadonlyMap<string, LayerComponentTrace> | null {
|
|
1001
|
+
if (seen.has(measureId)) return null;
|
|
1002
|
+
const derivation = derivations.get(measureId);
|
|
1003
|
+
if (!derivation) {
|
|
1004
|
+
const measure = measures.get(measureId);
|
|
1005
|
+
const basis =
|
|
1006
|
+
measure?.kind === "amount" && measure.basisId
|
|
1007
|
+
? amountBases.get(measure.basisId)
|
|
1008
|
+
: undefined;
|
|
1009
|
+
return basis
|
|
1010
|
+
? new Map(
|
|
1011
|
+
basis.components.map((component) => [
|
|
1012
|
+
component.id,
|
|
1013
|
+
{ rawMeasureId: measureId, hasLayer: false },
|
|
1014
|
+
]),
|
|
1015
|
+
)
|
|
1016
|
+
: null;
|
|
1017
|
+
}
|
|
1018
|
+
const nextSeen = new Set(seen).add(measureId);
|
|
1019
|
+
const visit = (
|
|
1020
|
+
expression: DiagnosticClaimExpression,
|
|
1021
|
+
): ReadonlyMap<string, LayerComponentTrace> | null => {
|
|
1022
|
+
if (expression.op === "measure")
|
|
1023
|
+
return traceLayerComponents(
|
|
1024
|
+
expression.measureId,
|
|
1025
|
+
measures,
|
|
1026
|
+
amountBases,
|
|
1027
|
+
derivations,
|
|
1028
|
+
nextSeen,
|
|
1029
|
+
);
|
|
1030
|
+
if (expression.op === "claim-layer") {
|
|
1031
|
+
const nested = traceLayerComponents(
|
|
1032
|
+
expression.measureId,
|
|
1033
|
+
measures,
|
|
1034
|
+
amountBases,
|
|
1035
|
+
derivations,
|
|
1036
|
+
nextSeen,
|
|
1037
|
+
);
|
|
1038
|
+
if (!nested || nested.size !== 1) return null;
|
|
1039
|
+
return new Map(
|
|
1040
|
+
[...nested].map(([componentId, trace]) => [
|
|
1041
|
+
componentId,
|
|
1042
|
+
{ ...trace, hasLayer: true },
|
|
1043
|
+
]),
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
if (expression.op === "subtract") return null;
|
|
1047
|
+
const result = new Map<string, LayerComponentTrace>();
|
|
1048
|
+
for (const term of expression.terms) {
|
|
1049
|
+
const traced = visit(term);
|
|
1050
|
+
if (!traced) return null;
|
|
1051
|
+
for (const [componentId, trace] of traced) {
|
|
1052
|
+
if (result.has(componentId)) return null;
|
|
1053
|
+
result.set(componentId, trace);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return result;
|
|
1057
|
+
};
|
|
1058
|
+
return visit(derivation.expression);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
function limitationInterval(limitation: AmountLimitation): {
|
|
1062
|
+
readonly attachment: number;
|
|
1063
|
+
readonly exhaustion: number;
|
|
1064
|
+
readonly application: "claim" | "occurrence" | "policy" | null;
|
|
1065
|
+
} | null {
|
|
1066
|
+
if (limitation.kind === "unlimited")
|
|
1067
|
+
return { attachment: 0, exhaustion: Infinity, application: null };
|
|
1068
|
+
if (
|
|
1069
|
+
limitation.kind !== "layer" ||
|
|
1070
|
+
limitation.derivation.kind !== "sdk" ||
|
|
1071
|
+
limitation.application === "source-defined"
|
|
1072
|
+
)
|
|
1073
|
+
return null;
|
|
1074
|
+
return {
|
|
1075
|
+
attachment: limitation.attachment,
|
|
1076
|
+
exhaustion:
|
|
1077
|
+
limitation.limit === null
|
|
1078
|
+
? Infinity
|
|
1079
|
+
: limitation.attachment + limitation.limit,
|
|
1080
|
+
application: limitation.application,
|
|
1081
|
+
};
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function validateLayerComparability(
|
|
1085
|
+
rule: Extract<DiagnosticReviewRule, { kind: "layer-order" }>,
|
|
1086
|
+
path: string,
|
|
1087
|
+
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
1088
|
+
amountBases: ReadonlyMap<string, AmountBasisDefinition>,
|
|
1089
|
+
derivations: ReadonlyMap<string, DiagnosticDerivedMeasureDefinition>,
|
|
1090
|
+
issues: DiagnosticValidationIssue[],
|
|
1091
|
+
): void {
|
|
1092
|
+
const narrowerBasis = measureExpressionBasis(
|
|
1093
|
+
rule.narrower,
|
|
1094
|
+
measures,
|
|
1095
|
+
amountBases,
|
|
1096
|
+
);
|
|
1097
|
+
const broaderBasis = measureExpressionBasis(
|
|
1098
|
+
rule.broader,
|
|
1099
|
+
measures,
|
|
1100
|
+
amountBases,
|
|
1101
|
+
);
|
|
1102
|
+
if (
|
|
1103
|
+
!narrowerBasis ||
|
|
1104
|
+
!broaderBasis ||
|
|
1105
|
+
narrowerBasis.currency !== broaderBasis.currency
|
|
1106
|
+
) {
|
|
1107
|
+
pushIssue(
|
|
1108
|
+
issues,
|
|
1109
|
+
"incompatible-semantics",
|
|
1110
|
+
path,
|
|
1111
|
+
"Layer-order operands must be amounts with the same currency",
|
|
1112
|
+
);
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
if (rule.comparability.kind === "caller-asserted") return;
|
|
1116
|
+
if (narrowerBasis.perspective !== broaderBasis.perspective) {
|
|
1117
|
+
pushIssue(
|
|
1118
|
+
issues,
|
|
1119
|
+
"incompatible-semantics",
|
|
1120
|
+
path,
|
|
1121
|
+
"Compiler-proven layer order requires the same amount perspective",
|
|
1122
|
+
);
|
|
1123
|
+
return;
|
|
1124
|
+
}
|
|
1125
|
+
const proveMeasures = (
|
|
1126
|
+
narrowerMeasureId: string,
|
|
1127
|
+
broaderMeasureId: string,
|
|
1128
|
+
): { readonly valid: boolean; readonly hasLayer: boolean } => {
|
|
1129
|
+
const narrowerMeasure = measures.get(narrowerMeasureId);
|
|
1130
|
+
const broaderMeasure = measures.get(broaderMeasureId);
|
|
1131
|
+
const narrowBasis =
|
|
1132
|
+
narrowerMeasure?.kind === "amount" && narrowerMeasure.basisId
|
|
1133
|
+
? amountBases.get(narrowerMeasure.basisId)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
const broadBasis =
|
|
1136
|
+
broaderMeasure?.kind === "amount" && broaderMeasure.basisId
|
|
1137
|
+
? amountBases.get(broaderMeasure.basisId)
|
|
1138
|
+
: undefined;
|
|
1139
|
+
if (
|
|
1140
|
+
!narrowBasis ||
|
|
1141
|
+
!broadBasis ||
|
|
1142
|
+
narrowBasis.currency !== broadBasis.currency ||
|
|
1143
|
+
narrowBasis.perspective !== broadBasis.perspective
|
|
1144
|
+
)
|
|
1145
|
+
return { valid: false, hasLayer: false };
|
|
1146
|
+
const narrowComponents = [...narrowBasis.components].sort((a, b) =>
|
|
1147
|
+
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
1148
|
+
);
|
|
1149
|
+
const broadComponents = [...broadBasis.components].sort((a, b) =>
|
|
1150
|
+
a.id < b.id ? -1 : a.id > b.id ? 1 : 0,
|
|
1151
|
+
);
|
|
1152
|
+
const narrowTraces = traceLayerComponents(
|
|
1153
|
+
narrowerMeasureId,
|
|
1154
|
+
measures,
|
|
1155
|
+
amountBases,
|
|
1156
|
+
derivations,
|
|
1157
|
+
);
|
|
1158
|
+
const broadTraces = traceLayerComponents(
|
|
1159
|
+
broaderMeasureId,
|
|
1160
|
+
measures,
|
|
1161
|
+
amountBases,
|
|
1162
|
+
derivations,
|
|
1163
|
+
);
|
|
1164
|
+
if (
|
|
1165
|
+
!narrowTraces ||
|
|
1166
|
+
!broadTraces ||
|
|
1167
|
+
narrowComponents.length !== broadComponents.length
|
|
1168
|
+
)
|
|
1169
|
+
return { valid: false, hasLayer: false };
|
|
1170
|
+
let hasLayer = false;
|
|
1171
|
+
for (let index = 0; index < narrowComponents.length; index++) {
|
|
1172
|
+
const narrower = narrowComponents[index]!;
|
|
1173
|
+
const broader = broadComponents[index]!;
|
|
1174
|
+
const narrowerInterval = limitationInterval(narrower.limitation);
|
|
1175
|
+
const broaderInterval = limitationInterval(broader.limitation);
|
|
1176
|
+
const narrowerTrace = narrowTraces.get(narrower.id);
|
|
1177
|
+
const broaderTrace = broadTraces.get(broader.id);
|
|
1178
|
+
if (
|
|
1179
|
+
narrower.id !== broader.id ||
|
|
1180
|
+
narrower.treatment !== broader.treatment ||
|
|
1181
|
+
narrower.treatment === "unknown" ||
|
|
1182
|
+
!narrowerInterval ||
|
|
1183
|
+
!broaderInterval ||
|
|
1184
|
+
!narrowerTrace ||
|
|
1185
|
+
!broaderTrace ||
|
|
1186
|
+
narrowerTrace.rawMeasureId !== broaderTrace.rawMeasureId ||
|
|
1187
|
+
(broaderInterval.application !== null &&
|
|
1188
|
+
narrowerInterval.application !== broaderInterval.application) ||
|
|
1189
|
+
narrowerInterval.attachment < broaderInterval.attachment ||
|
|
1190
|
+
narrowerInterval.exhaustion > broaderInterval.exhaustion
|
|
1191
|
+
)
|
|
1192
|
+
return { valid: false, hasLayer: false };
|
|
1193
|
+
hasLayer ||= narrowerTrace.hasLayer || broaderTrace.hasLayer;
|
|
1194
|
+
}
|
|
1195
|
+
return { valid: true, hasLayer };
|
|
1196
|
+
};
|
|
1197
|
+
const proveExpressions = (
|
|
1198
|
+
narrower: DiagnosticMeasureExpression,
|
|
1199
|
+
broader: DiagnosticMeasureExpression,
|
|
1200
|
+
): { readonly valid: boolean; readonly hasLayer: boolean } => {
|
|
1201
|
+
if (narrower.op === "measure" && broader.op === "measure")
|
|
1202
|
+
return proveMeasures(narrower.measureId, broader.measureId);
|
|
1203
|
+
if (
|
|
1204
|
+
narrower.op !== "add" ||
|
|
1205
|
+
broader.op !== "add" ||
|
|
1206
|
+
narrower.terms.length !== broader.terms.length
|
|
1207
|
+
)
|
|
1208
|
+
return { valid: false, hasLayer: false };
|
|
1209
|
+
const children = narrower.terms.map((term, index) =>
|
|
1210
|
+
proveExpressions(term, broader.terms[index]!),
|
|
1211
|
+
);
|
|
1212
|
+
return {
|
|
1213
|
+
valid: children.every((item) => item.valid),
|
|
1214
|
+
hasLayer: children.some((item) => item.hasLayer),
|
|
1215
|
+
};
|
|
1216
|
+
};
|
|
1217
|
+
const proof = proveExpressions(rule.narrower, rule.broader);
|
|
1218
|
+
if (!proof.valid || !proof.hasLayer) {
|
|
1219
|
+
pushIssue(
|
|
1220
|
+
issues,
|
|
1221
|
+
"incompatible-semantics",
|
|
1222
|
+
path,
|
|
1223
|
+
"Compiler metadata does not prove that the narrower layer is contained by the broader layer",
|
|
1224
|
+
);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function validatePeriodAxis(
|
|
1229
|
+
axis: DiagnosticPeriodAxis,
|
|
1230
|
+
issues: DiagnosticValidationIssue[],
|
|
1231
|
+
): void {
|
|
1232
|
+
if (!isPlainRecord(axis)) {
|
|
1233
|
+
pushIssue(
|
|
1234
|
+
issues,
|
|
1235
|
+
"invalid-type",
|
|
1236
|
+
"$.periodAxis",
|
|
1237
|
+
"Period axis must be an object",
|
|
1238
|
+
);
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
if (
|
|
1242
|
+
!validateFinite(
|
|
1243
|
+
axis.ageOffset,
|
|
1244
|
+
"$.periodAxis.ageOffset",
|
|
1245
|
+
"Period age offset",
|
|
1246
|
+
issues,
|
|
1247
|
+
) ||
|
|
1248
|
+
!Number.isSafeInteger(axis.ageOffset)
|
|
1249
|
+
) {
|
|
1250
|
+
pushIssue(
|
|
1251
|
+
issues,
|
|
1252
|
+
"invalid-number",
|
|
1253
|
+
"$.periodAxis.ageOffset",
|
|
1254
|
+
"Period age offset must be a safe integer",
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
if (axis.kind === "calendar") {
|
|
1258
|
+
validateAllowedKeys(
|
|
1259
|
+
axis,
|
|
1260
|
+
[
|
|
1261
|
+
"kind",
|
|
1262
|
+
"originCadence",
|
|
1263
|
+
"valuationCadence",
|
|
1264
|
+
"originAnchor",
|
|
1265
|
+
"valuationAnchor",
|
|
1266
|
+
"ageUnit",
|
|
1267
|
+
"ageOffset",
|
|
1268
|
+
],
|
|
1269
|
+
"$.periodAxis",
|
|
1270
|
+
issues,
|
|
1271
|
+
);
|
|
1272
|
+
if (!["month", "quarter", "year"].includes(axis.originCadence)) {
|
|
1273
|
+
pushIssue(
|
|
1274
|
+
issues,
|
|
1275
|
+
"invalid-period",
|
|
1276
|
+
"$.periodAxis.originCadence",
|
|
1277
|
+
"Unknown origin cadence",
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
if (!["month", "quarter", "year"].includes(axis.valuationCadence)) {
|
|
1281
|
+
pushIssue(
|
|
1282
|
+
issues,
|
|
1283
|
+
"invalid-period",
|
|
1284
|
+
"$.periodAxis.valuationCadence",
|
|
1285
|
+
"Unknown valuation cadence",
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
if (axis.originAnchor !== "start" && axis.originAnchor !== "end") {
|
|
1289
|
+
pushIssue(
|
|
1290
|
+
issues,
|
|
1291
|
+
"invalid-period",
|
|
1292
|
+
"$.periodAxis.originAnchor",
|
|
1293
|
+
"Unknown origin anchor",
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
if (axis.valuationAnchor !== "start" && axis.valuationAnchor !== "end") {
|
|
1297
|
+
pushIssue(
|
|
1298
|
+
issues,
|
|
1299
|
+
"invalid-period",
|
|
1300
|
+
"$.periodAxis.valuationAnchor",
|
|
1301
|
+
"Unknown valuation anchor",
|
|
1302
|
+
);
|
|
1303
|
+
}
|
|
1304
|
+
if (axis.ageUnit !== "month") {
|
|
1305
|
+
pushIssue(
|
|
1306
|
+
issues,
|
|
1307
|
+
"invalid-period",
|
|
1308
|
+
"$.periodAxis.ageUnit",
|
|
1309
|
+
"Calendar axes use month age units",
|
|
1310
|
+
);
|
|
1311
|
+
}
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
validateAllowedKeys(
|
|
1315
|
+
axis,
|
|
1316
|
+
["kind", "id", "version", "ageUnit", "ageOffset", "origins", "valuations"],
|
|
1317
|
+
"$.periodAxis",
|
|
1318
|
+
issues,
|
|
1319
|
+
);
|
|
1320
|
+
if (axis.kind !== "ordered") {
|
|
1321
|
+
pushIssue(
|
|
1322
|
+
issues,
|
|
1323
|
+
"invalid-period",
|
|
1324
|
+
"$.periodAxis.kind",
|
|
1325
|
+
"Unknown period-axis kind",
|
|
1326
|
+
);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
validateToken(axis.id, "$.periodAxis.id", "Ordered-axis ID", issues);
|
|
1330
|
+
validateToken(
|
|
1331
|
+
axis.version,
|
|
1332
|
+
"$.periodAxis.version",
|
|
1333
|
+
"Ordered-axis version",
|
|
1334
|
+
issues,
|
|
1335
|
+
);
|
|
1336
|
+
validateToken(
|
|
1337
|
+
axis.ageUnit,
|
|
1338
|
+
"$.periodAxis.ageUnit",
|
|
1339
|
+
"Ordered-axis age unit",
|
|
1340
|
+
issues,
|
|
1341
|
+
);
|
|
1342
|
+
for (const side of ["origins", "valuations"] as const) {
|
|
1343
|
+
const coordinates = requireArray(
|
|
1344
|
+
axis[side],
|
|
1345
|
+
`$.periodAxis.${side}`,
|
|
1346
|
+
issues,
|
|
1347
|
+
) as readonly DiagnosticPeriodCoordinate[];
|
|
1348
|
+
const names = new Set<string>();
|
|
1349
|
+
const coordinateValues = new Set<number>();
|
|
1350
|
+
coordinates.forEach((coordinate, index) => {
|
|
1351
|
+
const base = `$.periodAxis.${side}[${index}]`;
|
|
1352
|
+
if (!isPlainRecord(coordinate)) {
|
|
1353
|
+
pushIssue(
|
|
1354
|
+
issues,
|
|
1355
|
+
"invalid-type",
|
|
1356
|
+
base,
|
|
1357
|
+
"Period coordinate must be an object",
|
|
1358
|
+
);
|
|
1359
|
+
return;
|
|
1360
|
+
}
|
|
1361
|
+
validateAllowedKeys(
|
|
1362
|
+
coordinate,
|
|
1363
|
+
["label", "aliases", "coordinate"],
|
|
1364
|
+
base,
|
|
1365
|
+
issues,
|
|
1366
|
+
);
|
|
1367
|
+
if (
|
|
1368
|
+
validateToken(
|
|
1369
|
+
coordinate?.label,
|
|
1370
|
+
`${base}.label`,
|
|
1371
|
+
"Period label",
|
|
1372
|
+
issues,
|
|
1373
|
+
)
|
|
1374
|
+
) {
|
|
1375
|
+
if (names.has(coordinate.label))
|
|
1376
|
+
pushIssue(
|
|
1377
|
+
issues,
|
|
1378
|
+
"duplicate-id",
|
|
1379
|
+
`${base}.label`,
|
|
1380
|
+
`Duplicate period label or alias ${coordinate.label}`,
|
|
1381
|
+
);
|
|
1382
|
+
names.add(coordinate.label);
|
|
1383
|
+
}
|
|
1384
|
+
if (
|
|
1385
|
+
!validateFinite(
|
|
1386
|
+
coordinate?.coordinate,
|
|
1387
|
+
`${base}.coordinate`,
|
|
1388
|
+
"Period coordinate",
|
|
1389
|
+
issues,
|
|
1390
|
+
) ||
|
|
1391
|
+
!Number.isSafeInteger(coordinate.coordinate)
|
|
1392
|
+
) {
|
|
1393
|
+
pushIssue(
|
|
1394
|
+
issues,
|
|
1395
|
+
"invalid-number",
|
|
1396
|
+
`${base}.coordinate`,
|
|
1397
|
+
"Period coordinate must be a safe integer",
|
|
1398
|
+
);
|
|
1399
|
+
} else if (coordinateValues.has(coordinate.coordinate)) {
|
|
1400
|
+
pushIssue(
|
|
1401
|
+
issues,
|
|
1402
|
+
"duplicate-id",
|
|
1403
|
+
`${base}.coordinate`,
|
|
1404
|
+
`Duplicate period coordinate ${coordinate.coordinate}`,
|
|
1405
|
+
);
|
|
1406
|
+
} else coordinateValues.add(coordinate.coordinate);
|
|
1407
|
+
const aliases =
|
|
1408
|
+
coordinate.aliases === undefined
|
|
1409
|
+
? []
|
|
1410
|
+
: requireArray(coordinate.aliases, `${base}.aliases`, issues);
|
|
1411
|
+
for (const [aliasIndex, alias] of aliases.entries()) {
|
|
1412
|
+
if (
|
|
1413
|
+
!validateToken(
|
|
1414
|
+
alias,
|
|
1415
|
+
`${base}.aliases[${aliasIndex}]`,
|
|
1416
|
+
"Period alias",
|
|
1417
|
+
issues,
|
|
1418
|
+
)
|
|
1419
|
+
)
|
|
1420
|
+
continue;
|
|
1421
|
+
if (names.has(alias))
|
|
1422
|
+
pushIssue(
|
|
1423
|
+
issues,
|
|
1424
|
+
"duplicate-id",
|
|
1425
|
+
`${base}.aliases[${aliasIndex}]`,
|
|
1426
|
+
`Duplicate period label or alias ${alias}`,
|
|
1427
|
+
);
|
|
1428
|
+
names.add(alias);
|
|
1429
|
+
}
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
1435
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
1436
|
+
validateJsonValue(value, issues);
|
|
1437
|
+
if (!isPlainRecord(value)) {
|
|
1438
|
+
pushIssue(
|
|
1439
|
+
issues,
|
|
1440
|
+
"invalid-type",
|
|
1441
|
+
"$",
|
|
1442
|
+
"Diagnostic definition must be a plain object",
|
|
1443
|
+
);
|
|
1444
|
+
throw new DiagnosticValidationError(issues);
|
|
1445
|
+
}
|
|
1446
|
+
const definition = value as unknown as DiagnosticDefinition;
|
|
1447
|
+
validateAllowedKeys(
|
|
1448
|
+
value,
|
|
1449
|
+
[
|
|
1450
|
+
"diagnosticDefinitionVersion",
|
|
1451
|
+
"id",
|
|
1452
|
+
"version",
|
|
1453
|
+
"lossRowGrain",
|
|
1454
|
+
"measures",
|
|
1455
|
+
"countPopulations",
|
|
1456
|
+
"exposureBases",
|
|
1457
|
+
"amountBases",
|
|
1458
|
+
"derivedMeasures",
|
|
1459
|
+
"formulas",
|
|
1460
|
+
"instances",
|
|
1461
|
+
"reviewRules",
|
|
1462
|
+
"periodAxis",
|
|
1463
|
+
],
|
|
1464
|
+
"$",
|
|
1465
|
+
issues,
|
|
1466
|
+
);
|
|
1467
|
+
if (definition.diagnosticDefinitionVersion !== "1.0.0") {
|
|
1468
|
+
pushIssue(
|
|
1469
|
+
issues,
|
|
1470
|
+
"invalid-type",
|
|
1471
|
+
"$.diagnosticDefinitionVersion",
|
|
1472
|
+
"Diagnostic definition version must be 1.0.0",
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
validateToken(definition.id, "$.id", "Definition ID", issues);
|
|
1476
|
+
validateToken(definition.version, "$.version", "Definition version", issues);
|
|
1477
|
+
if (
|
|
1478
|
+
definition.lossRowGrain !== "claim" &&
|
|
1479
|
+
definition.lossRowGrain !== "aggregate"
|
|
1480
|
+
) {
|
|
1481
|
+
pushIssue(
|
|
1482
|
+
issues,
|
|
1483
|
+
"invalid-type",
|
|
1484
|
+
"$.lossRowGrain",
|
|
1485
|
+
"Loss row grain must be claim or aggregate",
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
const measures = requireArray(
|
|
1490
|
+
definition.measures,
|
|
1491
|
+
"$.measures",
|
|
1492
|
+
issues,
|
|
1493
|
+
) as readonly DiagnosticMeasureDefinition[];
|
|
1494
|
+
const populations = requireArray(
|
|
1495
|
+
definition.countPopulations,
|
|
1496
|
+
"$.countPopulations",
|
|
1497
|
+
issues,
|
|
1498
|
+
) as readonly DiagnosticCountPopulationDefinition[];
|
|
1499
|
+
const exposureBases = requireArray(
|
|
1500
|
+
definition.exposureBases,
|
|
1501
|
+
"$.exposureBases",
|
|
1502
|
+
issues,
|
|
1503
|
+
) as readonly DiagnosticExposureBasisDefinition[];
|
|
1504
|
+
const amountBases = requireArray(
|
|
1505
|
+
definition.amountBases,
|
|
1506
|
+
"$.amountBases",
|
|
1507
|
+
issues,
|
|
1508
|
+
) as readonly AmountBasisDefinition[];
|
|
1509
|
+
const derivations = requireArray(
|
|
1510
|
+
definition.derivedMeasures,
|
|
1511
|
+
"$.derivedMeasures",
|
|
1512
|
+
issues,
|
|
1513
|
+
) as readonly DiagnosticDerivedMeasureDefinition[];
|
|
1514
|
+
const formulas = requireArray(
|
|
1515
|
+
definition.formulas,
|
|
1516
|
+
"$.formulas",
|
|
1517
|
+
issues,
|
|
1518
|
+
) as readonly DiagnosticFormulaTemplate[];
|
|
1519
|
+
const instances = requireArray(
|
|
1520
|
+
definition.instances,
|
|
1521
|
+
"$.instances",
|
|
1522
|
+
issues,
|
|
1523
|
+
) as readonly DiagnosticMetricInstance[];
|
|
1524
|
+
const reviewRules = requireArray(
|
|
1525
|
+
definition.reviewRules,
|
|
1526
|
+
"$.reviewRules",
|
|
1527
|
+
issues,
|
|
1528
|
+
) as readonly DiagnosticReviewRule[];
|
|
1529
|
+
|
|
1530
|
+
const measureMap = collectCatalog(measures, "$.measures", issues);
|
|
1531
|
+
const populationMap = collectCatalog(
|
|
1532
|
+
populations,
|
|
1533
|
+
"$.countPopulations",
|
|
1534
|
+
issues,
|
|
1535
|
+
);
|
|
1536
|
+
const exposureBasisMap = collectCatalog(
|
|
1537
|
+
exposureBases,
|
|
1538
|
+
"$.exposureBases",
|
|
1539
|
+
issues,
|
|
1540
|
+
);
|
|
1541
|
+
const amountBasisMap = collectCatalog(amountBases, "$.amountBases", issues);
|
|
1542
|
+
collectCatalog(derivations, "$.derivedMeasures", issues);
|
|
1543
|
+
const derivationsByOutput = new Map<
|
|
1544
|
+
string,
|
|
1545
|
+
DiagnosticDerivedMeasureDefinition
|
|
1546
|
+
>();
|
|
1547
|
+
for (const derivation of derivations) {
|
|
1548
|
+
if (
|
|
1549
|
+
isPlainRecord(derivation) &&
|
|
1550
|
+
typeof derivation.outputMeasureId === "string" &&
|
|
1551
|
+
!derivationsByOutput.has(derivation.outputMeasureId)
|
|
1552
|
+
) {
|
|
1553
|
+
derivationsByOutput.set(derivation.outputMeasureId, derivation);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
const formulaMap = collectCatalog(formulas, "$.formulas", issues);
|
|
1557
|
+
collectCatalog(instances, "$.instances", issues);
|
|
1558
|
+
const reviewRuleMap = collectCatalog(reviewRules, "$.reviewRules", issues);
|
|
1559
|
+
|
|
1560
|
+
populations.forEach((population, index) => {
|
|
1561
|
+
const base = `$.countPopulations[${index}]`;
|
|
1562
|
+
if (!isPlainRecord(population)) {
|
|
1563
|
+
pushIssue(
|
|
1564
|
+
issues,
|
|
1565
|
+
"invalid-type",
|
|
1566
|
+
base,
|
|
1567
|
+
"Count population must be an object",
|
|
1568
|
+
);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
validateAllowedKeys(
|
|
1572
|
+
population,
|
|
1573
|
+
["id", "displayName", "subject", "unit", "description", "attributes"],
|
|
1574
|
+
base,
|
|
1575
|
+
issues,
|
|
1576
|
+
);
|
|
1577
|
+
validateToken(
|
|
1578
|
+
population.displayName,
|
|
1579
|
+
`${base}.displayName`,
|
|
1580
|
+
"Population display name",
|
|
1581
|
+
issues,
|
|
1582
|
+
);
|
|
1583
|
+
validateToken(
|
|
1584
|
+
population.description,
|
|
1585
|
+
`${base}.description`,
|
|
1586
|
+
"Population description",
|
|
1587
|
+
issues,
|
|
1588
|
+
);
|
|
1589
|
+
validateToken(population.unit, `${base}.unit`, "Population unit", issues);
|
|
1590
|
+
if (
|
|
1591
|
+
![
|
|
1592
|
+
"claim",
|
|
1593
|
+
"claimant",
|
|
1594
|
+
"policy",
|
|
1595
|
+
"occurrence",
|
|
1596
|
+
"other",
|
|
1597
|
+
"unknown",
|
|
1598
|
+
].includes(population.subject)
|
|
1599
|
+
) {
|
|
1600
|
+
pushIssue(
|
|
1601
|
+
issues,
|
|
1602
|
+
"invalid-type",
|
|
1603
|
+
`${base}.subject`,
|
|
1604
|
+
"Unknown count-population subject",
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
validateScalarAttributes(
|
|
1608
|
+
population.attributes,
|
|
1609
|
+
`${base}.attributes`,
|
|
1610
|
+
issues,
|
|
1611
|
+
);
|
|
1612
|
+
});
|
|
1613
|
+
|
|
1614
|
+
exposureBases.forEach((basis, index) => {
|
|
1615
|
+
const base = `$.exposureBases[${index}]`;
|
|
1616
|
+
if (!isPlainRecord(basis)) {
|
|
1617
|
+
pushIssue(
|
|
1618
|
+
issues,
|
|
1619
|
+
"invalid-type",
|
|
1620
|
+
base,
|
|
1621
|
+
"Exposure basis must be an object",
|
|
1622
|
+
);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
validateAllowedKeys(
|
|
1626
|
+
basis,
|
|
1627
|
+
[
|
|
1628
|
+
"id",
|
|
1629
|
+
"displayName",
|
|
1630
|
+
"basis",
|
|
1631
|
+
"unit",
|
|
1632
|
+
"description",
|
|
1633
|
+
"sourceDescription",
|
|
1634
|
+
"attributes",
|
|
1635
|
+
],
|
|
1636
|
+
base,
|
|
1637
|
+
issues,
|
|
1638
|
+
);
|
|
1639
|
+
validateToken(
|
|
1640
|
+
basis.displayName,
|
|
1641
|
+
`${base}.displayName`,
|
|
1642
|
+
"Exposure-basis display name",
|
|
1643
|
+
issues,
|
|
1644
|
+
);
|
|
1645
|
+
validateToken(
|
|
1646
|
+
basis.description,
|
|
1647
|
+
`${base}.description`,
|
|
1648
|
+
"Exposure-basis description",
|
|
1649
|
+
issues,
|
|
1650
|
+
);
|
|
1651
|
+
validateToken(basis.unit, `${base}.unit`, "Exposure-basis unit", issues);
|
|
1652
|
+
if (basis.sourceDescription !== undefined)
|
|
1653
|
+
validateToken(
|
|
1654
|
+
basis.sourceDescription,
|
|
1655
|
+
`${base}.sourceDescription`,
|
|
1656
|
+
"Exposure source description",
|
|
1657
|
+
issues,
|
|
1658
|
+
);
|
|
1659
|
+
if (
|
|
1660
|
+
!["earned", "written", "in-force", "other", "unknown"].includes(
|
|
1661
|
+
basis.basis,
|
|
1662
|
+
)
|
|
1663
|
+
) {
|
|
1664
|
+
pushIssue(
|
|
1665
|
+
issues,
|
|
1666
|
+
"invalid-type",
|
|
1667
|
+
`${base}.basis`,
|
|
1668
|
+
"Unknown exposure basis",
|
|
1669
|
+
);
|
|
1670
|
+
}
|
|
1671
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
amountBases.forEach((basis, index) => {
|
|
1675
|
+
const base = `$.amountBases[${index}]`;
|
|
1676
|
+
if (!isPlainRecord(basis)) {
|
|
1677
|
+
pushIssue(issues, "invalid-type", base, "Amount basis must be an object");
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
validateAllowedKeys(
|
|
1681
|
+
basis,
|
|
1682
|
+
[
|
|
1683
|
+
"id",
|
|
1684
|
+
"displayName",
|
|
1685
|
+
"currency",
|
|
1686
|
+
"perspective",
|
|
1687
|
+
"components",
|
|
1688
|
+
"sourceDescription",
|
|
1689
|
+
"attributes",
|
|
1690
|
+
],
|
|
1691
|
+
base,
|
|
1692
|
+
issues,
|
|
1693
|
+
);
|
|
1694
|
+
validateToken(
|
|
1695
|
+
basis.displayName,
|
|
1696
|
+
`${base}.displayName`,
|
|
1697
|
+
"Amount-basis display name",
|
|
1698
|
+
issues,
|
|
1699
|
+
);
|
|
1700
|
+
validateToken(
|
|
1701
|
+
basis.currency,
|
|
1702
|
+
`${base}.currency`,
|
|
1703
|
+
"Amount-basis currency",
|
|
1704
|
+
issues,
|
|
1705
|
+
);
|
|
1706
|
+
if (basis.sourceDescription !== undefined)
|
|
1707
|
+
validateToken(
|
|
1708
|
+
basis.sourceDescription,
|
|
1709
|
+
`${base}.sourceDescription`,
|
|
1710
|
+
"Amount source description",
|
|
1711
|
+
issues,
|
|
1712
|
+
);
|
|
1713
|
+
if (
|
|
1714
|
+
!["gross", "net", "ceded", "other", "unknown"].includes(basis.perspective)
|
|
1715
|
+
) {
|
|
1716
|
+
pushIssue(
|
|
1717
|
+
issues,
|
|
1718
|
+
"invalid-type",
|
|
1719
|
+
`${base}.perspective`,
|
|
1720
|
+
"Unknown amount perspective",
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
1724
|
+
const components = requireArray(
|
|
1725
|
+
basis.components,
|
|
1726
|
+
`${base}.components`,
|
|
1727
|
+
issues,
|
|
1728
|
+
) as readonly AmountBasisComponent[];
|
|
1729
|
+
if (components.length === 0)
|
|
1730
|
+
pushIssue(
|
|
1731
|
+
issues,
|
|
1732
|
+
"missing-required",
|
|
1733
|
+
`${base}.components`,
|
|
1734
|
+
"Amount basis requires at least one component",
|
|
1735
|
+
);
|
|
1736
|
+
const componentIds = collectCatalog(
|
|
1737
|
+
components,
|
|
1738
|
+
`${base}.components`,
|
|
1739
|
+
issues,
|
|
1740
|
+
);
|
|
1741
|
+
if (
|
|
1742
|
+
componentIds.size > 0 &&
|
|
1743
|
+
components.every((component) => component.treatment === "excluded")
|
|
1744
|
+
) {
|
|
1745
|
+
pushIssue(
|
|
1746
|
+
issues,
|
|
1747
|
+
"incompatible-semantics",
|
|
1748
|
+
`${base}.components`,
|
|
1749
|
+
"Amount basis cannot exclude every component",
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
components.forEach((component, componentIndex) => {
|
|
1753
|
+
const componentBase = `${base}.components[${componentIndex}]`;
|
|
1754
|
+
if (!isPlainRecord(component)) {
|
|
1755
|
+
pushIssue(
|
|
1756
|
+
issues,
|
|
1757
|
+
"invalid-type",
|
|
1758
|
+
componentBase,
|
|
1759
|
+
"Amount-basis component must be an object",
|
|
1760
|
+
);
|
|
1761
|
+
return;
|
|
1762
|
+
}
|
|
1763
|
+
validateAllowedKeys(
|
|
1764
|
+
component,
|
|
1765
|
+
["id", "treatment", "limitation"],
|
|
1766
|
+
componentBase,
|
|
1767
|
+
issues,
|
|
1768
|
+
);
|
|
1769
|
+
if (!["included", "excluded", "unknown"].includes(component.treatment)) {
|
|
1770
|
+
pushIssue(
|
|
1771
|
+
issues,
|
|
1772
|
+
"invalid-type",
|
|
1773
|
+
`${componentBase}.treatment`,
|
|
1774
|
+
"Unknown component treatment",
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
const limitation = component.limitation;
|
|
1778
|
+
if (!isPlainRecord(limitation)) {
|
|
1779
|
+
pushIssue(
|
|
1780
|
+
issues,
|
|
1781
|
+
"invalid-type",
|
|
1782
|
+
`${componentBase}.limitation`,
|
|
1783
|
+
"Amount limitation must be an object",
|
|
1784
|
+
);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (limitation.kind === "unknown") {
|
|
1788
|
+
validateAllowedKeys(
|
|
1789
|
+
limitation,
|
|
1790
|
+
["kind", "description"],
|
|
1791
|
+
`${componentBase}.limitation`,
|
|
1792
|
+
issues,
|
|
1793
|
+
);
|
|
1794
|
+
if (limitation.description !== undefined)
|
|
1795
|
+
validateToken(
|
|
1796
|
+
limitation.description,
|
|
1797
|
+
`${componentBase}.limitation.description`,
|
|
1798
|
+
"Unknown limitation description",
|
|
1799
|
+
issues,
|
|
1800
|
+
);
|
|
1801
|
+
} else if (
|
|
1802
|
+
limitation.kind === "layer" ||
|
|
1803
|
+
limitation.kind === "pre-limited"
|
|
1804
|
+
) {
|
|
1805
|
+
validateAllowedKeys(
|
|
1806
|
+
limitation,
|
|
1807
|
+
["kind", "attachment", "limit", "application", "derivation"],
|
|
1808
|
+
`${componentBase}.limitation`,
|
|
1809
|
+
issues,
|
|
1810
|
+
);
|
|
1811
|
+
if (
|
|
1812
|
+
!["claim", "occurrence", "policy", "source-defined"].includes(
|
|
1813
|
+
limitation.application,
|
|
1814
|
+
)
|
|
1815
|
+
) {
|
|
1816
|
+
pushIssue(
|
|
1817
|
+
issues,
|
|
1818
|
+
"invalid-type",
|
|
1819
|
+
`${componentBase}.limitation.application`,
|
|
1820
|
+
"Unknown limitation application",
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
if (
|
|
1824
|
+
validateFinite(
|
|
1825
|
+
limitation.attachment,
|
|
1826
|
+
`${componentBase}.limitation.attachment`,
|
|
1827
|
+
"Attachment",
|
|
1828
|
+
issues,
|
|
1829
|
+
) &&
|
|
1830
|
+
limitation.attachment < 0
|
|
1831
|
+
) {
|
|
1832
|
+
pushIssue(
|
|
1833
|
+
issues,
|
|
1834
|
+
"invalid-number",
|
|
1835
|
+
`${componentBase}.limitation.attachment`,
|
|
1836
|
+
"Attachment must be nonnegative",
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
if (limitation.limit !== null) {
|
|
1840
|
+
if (
|
|
1841
|
+
validateFinite(
|
|
1842
|
+
limitation.limit,
|
|
1843
|
+
`${componentBase}.limitation.limit`,
|
|
1844
|
+
"Layer width",
|
|
1845
|
+
issues,
|
|
1846
|
+
) &&
|
|
1847
|
+
limitation.limit <= 0
|
|
1848
|
+
) {
|
|
1849
|
+
pushIssue(
|
|
1850
|
+
issues,
|
|
1851
|
+
"invalid-number",
|
|
1852
|
+
`${componentBase}.limitation.limit`,
|
|
1853
|
+
"Layer width must be positive",
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
if (
|
|
1857
|
+
Number.isFinite(limitation.attachment) &&
|
|
1858
|
+
Number.isFinite(limitation.limit) &&
|
|
1859
|
+
!Number.isFinite(limitation.attachment + limitation.limit)
|
|
1860
|
+
) {
|
|
1861
|
+
pushIssue(
|
|
1862
|
+
issues,
|
|
1863
|
+
"invalid-number",
|
|
1864
|
+
`${componentBase}.limitation.limit`,
|
|
1865
|
+
"Attachment plus layer width must be finite",
|
|
1866
|
+
);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (limitation.derivation?.kind === "sdk") {
|
|
1870
|
+
if (isPlainRecord(limitation.derivation))
|
|
1871
|
+
validateAllowedKeys(
|
|
1872
|
+
limitation.derivation,
|
|
1873
|
+
["kind"],
|
|
1874
|
+
`${componentBase}.limitation.derivation`,
|
|
1875
|
+
issues,
|
|
1876
|
+
);
|
|
1877
|
+
if (
|
|
1878
|
+
limitation.kind !== "layer" ||
|
|
1879
|
+
limitation.application !== "claim"
|
|
1880
|
+
) {
|
|
1881
|
+
pushIssue(
|
|
1882
|
+
issues,
|
|
1883
|
+
"incompatible-semantics",
|
|
1884
|
+
`${componentBase}.limitation.derivation`,
|
|
1885
|
+
"SDK limitations require a claim layer",
|
|
1886
|
+
);
|
|
1887
|
+
}
|
|
1888
|
+
} else if (limitation.derivation?.kind === "external") {
|
|
1889
|
+
if (isPlainRecord(limitation.derivation))
|
|
1890
|
+
validateAllowedKeys(
|
|
1891
|
+
limitation.derivation,
|
|
1892
|
+
["kind", "actor", "transformationRef"],
|
|
1893
|
+
`${componentBase}.limitation.derivation`,
|
|
1894
|
+
issues,
|
|
1895
|
+
);
|
|
1896
|
+
if (
|
|
1897
|
+
limitation.derivation.actor !== "caller" &&
|
|
1898
|
+
limitation.derivation.actor !== "source"
|
|
1899
|
+
) {
|
|
1900
|
+
pushIssue(
|
|
1901
|
+
issues,
|
|
1902
|
+
"invalid-type",
|
|
1903
|
+
`${componentBase}.limitation.derivation.actor`,
|
|
1904
|
+
"External derivation actor must be caller or source",
|
|
1905
|
+
);
|
|
1906
|
+
}
|
|
1907
|
+
validateToken(
|
|
1908
|
+
limitation.derivation.transformationRef,
|
|
1909
|
+
`${componentBase}.limitation.derivation.transformationRef`,
|
|
1910
|
+
"Transformation reference",
|
|
1911
|
+
issues,
|
|
1912
|
+
);
|
|
1913
|
+
} else {
|
|
1914
|
+
pushIssue(
|
|
1915
|
+
issues,
|
|
1916
|
+
"invalid-type",
|
|
1917
|
+
`${componentBase}.limitation.derivation`,
|
|
1918
|
+
"Limited basis requires a derivation record",
|
|
1919
|
+
);
|
|
1920
|
+
}
|
|
1921
|
+
} else if (limitation.kind !== "unlimited") {
|
|
1922
|
+
pushIssue(
|
|
1923
|
+
issues,
|
|
1924
|
+
"invalid-type",
|
|
1925
|
+
`${componentBase}.limitation.kind`,
|
|
1926
|
+
"Unknown amount limitation kind",
|
|
1927
|
+
);
|
|
1928
|
+
} else
|
|
1929
|
+
validateAllowedKeys(
|
|
1930
|
+
limitation,
|
|
1931
|
+
["kind"],
|
|
1932
|
+
`${componentBase}.limitation`,
|
|
1933
|
+
issues,
|
|
1934
|
+
);
|
|
1935
|
+
});
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
measures.forEach((measure, index) => {
|
|
1939
|
+
const base = `$.measures[${index}]`;
|
|
1940
|
+
if (!isPlainRecord(measure)) {
|
|
1941
|
+
pushIssue(issues, "invalid-type", base, "Measure must be an object");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
validateAllowedKeys(
|
|
1945
|
+
measure,
|
|
1946
|
+
[
|
|
1947
|
+
"id",
|
|
1948
|
+
"displayName",
|
|
1949
|
+
"description",
|
|
1950
|
+
"source",
|
|
1951
|
+
"kind",
|
|
1952
|
+
"unit",
|
|
1953
|
+
"developmentSemantics",
|
|
1954
|
+
"aggregation",
|
|
1955
|
+
"missing",
|
|
1956
|
+
"basisId",
|
|
1957
|
+
"countPopulationId",
|
|
1958
|
+
"exposureBasisId",
|
|
1959
|
+
"exposureTiming",
|
|
1960
|
+
],
|
|
1961
|
+
base,
|
|
1962
|
+
issues,
|
|
1963
|
+
);
|
|
1964
|
+
validateToken(
|
|
1965
|
+
measure.displayName,
|
|
1966
|
+
`${base}.displayName`,
|
|
1967
|
+
"Measure display name",
|
|
1968
|
+
issues,
|
|
1969
|
+
);
|
|
1970
|
+
validateToken(
|
|
1971
|
+
measure.description,
|
|
1972
|
+
`${base}.description`,
|
|
1973
|
+
"Measure description",
|
|
1974
|
+
issues,
|
|
1975
|
+
);
|
|
1976
|
+
validateToken(measure.unit, `${base}.unit`, "Measure unit", issues);
|
|
1977
|
+
if (measure.aggregation !== "sum")
|
|
1978
|
+
pushIssue(
|
|
1979
|
+
issues,
|
|
1980
|
+
"incompatible-semantics",
|
|
1981
|
+
`${base}.aggregation`,
|
|
1982
|
+
"Only sum aggregation is supported",
|
|
1983
|
+
);
|
|
1984
|
+
if (measure.missing !== "unknown" && measure.missing !== "zero")
|
|
1985
|
+
pushIssue(
|
|
1986
|
+
issues,
|
|
1987
|
+
"invalid-type",
|
|
1988
|
+
`${base}.missing`,
|
|
1989
|
+
"Unknown missing-value policy",
|
|
1990
|
+
);
|
|
1991
|
+
if (
|
|
1992
|
+
!["cumulative", "incremental", "point-in-time", "unknown"].includes(
|
|
1993
|
+
measure.developmentSemantics,
|
|
1994
|
+
)
|
|
1995
|
+
) {
|
|
1996
|
+
pushIssue(
|
|
1997
|
+
issues,
|
|
1998
|
+
"invalid-type",
|
|
1999
|
+
`${base}.developmentSemantics`,
|
|
2000
|
+
"Unknown development semantics",
|
|
2001
|
+
);
|
|
2002
|
+
}
|
|
2003
|
+
if (measure.kind === "amount") {
|
|
2004
|
+
const basis = measure.basisId
|
|
2005
|
+
? amountBasisMap.get(measure.basisId)
|
|
2006
|
+
: undefined;
|
|
2007
|
+
if (!basis)
|
|
2008
|
+
pushIssue(
|
|
2009
|
+
issues,
|
|
2010
|
+
"unknown-reference",
|
|
2011
|
+
`${base}.basisId`,
|
|
2012
|
+
"Amount measure requires an existing amount basis",
|
|
2013
|
+
);
|
|
2014
|
+
else if (measure.unit !== basis.currency)
|
|
2015
|
+
pushIssue(
|
|
2016
|
+
issues,
|
|
2017
|
+
"incompatible-semantics",
|
|
2018
|
+
`${base}.unit`,
|
|
2019
|
+
"Amount measure unit must equal basis currency",
|
|
2020
|
+
);
|
|
2021
|
+
if (
|
|
2022
|
+
measure.countPopulationId !== undefined ||
|
|
2023
|
+
measure.exposureBasisId !== undefined ||
|
|
2024
|
+
measure.exposureTiming !== undefined
|
|
2025
|
+
) {
|
|
2026
|
+
pushIssue(
|
|
2027
|
+
issues,
|
|
2028
|
+
"incompatible-semantics",
|
|
2029
|
+
base,
|
|
2030
|
+
"Amount measure has inapplicable population or exposure semantics",
|
|
2031
|
+
);
|
|
2032
|
+
}
|
|
2033
|
+
} else if (measure.kind === "count") {
|
|
2034
|
+
const population = measure.countPopulationId
|
|
2035
|
+
? populationMap.get(measure.countPopulationId)
|
|
2036
|
+
: undefined;
|
|
2037
|
+
if (!population)
|
|
2038
|
+
pushIssue(
|
|
2039
|
+
issues,
|
|
2040
|
+
"unknown-reference",
|
|
2041
|
+
`${base}.countPopulationId`,
|
|
2042
|
+
"Count measure requires an existing count population",
|
|
2043
|
+
);
|
|
2044
|
+
else if (measure.unit !== population.unit)
|
|
2045
|
+
pushIssue(
|
|
2046
|
+
issues,
|
|
2047
|
+
"incompatible-semantics",
|
|
2048
|
+
`${base}.unit`,
|
|
2049
|
+
"Count measure unit must equal population unit",
|
|
2050
|
+
);
|
|
2051
|
+
if (
|
|
2052
|
+
measure.basisId !== undefined ||
|
|
2053
|
+
measure.exposureBasisId !== undefined ||
|
|
2054
|
+
measure.exposureTiming !== undefined
|
|
2055
|
+
) {
|
|
2056
|
+
pushIssue(
|
|
2057
|
+
issues,
|
|
2058
|
+
"incompatible-semantics",
|
|
2059
|
+
base,
|
|
2060
|
+
"Count measure has inapplicable amount or exposure semantics",
|
|
2061
|
+
);
|
|
2062
|
+
}
|
|
2063
|
+
} else if (measure.kind === "exposure") {
|
|
2064
|
+
const basis = measure.exposureBasisId
|
|
2065
|
+
? exposureBasisMap.get(measure.exposureBasisId)
|
|
2066
|
+
: undefined;
|
|
2067
|
+
if (!basis)
|
|
2068
|
+
pushIssue(
|
|
2069
|
+
issues,
|
|
2070
|
+
"unknown-reference",
|
|
2071
|
+
`${base}.exposureBasisId`,
|
|
2072
|
+
"Exposure measure requires an existing exposure basis",
|
|
2073
|
+
);
|
|
2074
|
+
else if (measure.unit !== basis.unit)
|
|
2075
|
+
pushIssue(
|
|
2076
|
+
issues,
|
|
2077
|
+
"incompatible-semantics",
|
|
2078
|
+
`${base}.unit`,
|
|
2079
|
+
"Exposure measure unit must equal exposure-basis unit",
|
|
2080
|
+
);
|
|
2081
|
+
if (measure.source !== "exposure")
|
|
2082
|
+
pushIssue(
|
|
2083
|
+
issues,
|
|
2084
|
+
"incompatible-semantics",
|
|
2085
|
+
`${base}.source`,
|
|
2086
|
+
"Exposure measure source must be exposure",
|
|
2087
|
+
);
|
|
2088
|
+
if (
|
|
2089
|
+
measure.exposureTiming !== "origin-static" &&
|
|
2090
|
+
measure.exposureTiming !== "valuation-specific"
|
|
2091
|
+
) {
|
|
2092
|
+
pushIssue(
|
|
2093
|
+
issues,
|
|
2094
|
+
"missing-required",
|
|
2095
|
+
`${base}.exposureTiming`,
|
|
2096
|
+
"Exposure measure requires timing",
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
if (measure.missing !== "unknown")
|
|
2100
|
+
pushIssue(
|
|
2101
|
+
issues,
|
|
2102
|
+
"incompatible-semantics",
|
|
2103
|
+
`${base}.missing`,
|
|
2104
|
+
"Exposure missingness must remain unknown",
|
|
2105
|
+
);
|
|
2106
|
+
if (
|
|
2107
|
+
measure.basisId !== undefined ||
|
|
2108
|
+
measure.countPopulationId !== undefined
|
|
2109
|
+
) {
|
|
2110
|
+
pushIssue(
|
|
2111
|
+
issues,
|
|
2112
|
+
"incompatible-semantics",
|
|
2113
|
+
base,
|
|
2114
|
+
"Exposure measure has inapplicable amount or population semantics",
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
} else {
|
|
2118
|
+
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown measure kind");
|
|
2119
|
+
}
|
|
2120
|
+
if (
|
|
2121
|
+
measure.kind !== "exposure" &&
|
|
2122
|
+
measure.source !== "loss" &&
|
|
2123
|
+
measure.source !== "derived"
|
|
2124
|
+
) {
|
|
2125
|
+
pushIssue(
|
|
2126
|
+
issues,
|
|
2127
|
+
"invalid-type",
|
|
2128
|
+
`${base}.source`,
|
|
2129
|
+
"Non-exposure measure source must be loss or derived",
|
|
2130
|
+
);
|
|
2131
|
+
}
|
|
2132
|
+
if (measure.kind !== "exposure" && measure.source === "exposure") {
|
|
2133
|
+
pushIssue(
|
|
2134
|
+
issues,
|
|
2135
|
+
"incompatible-semantics",
|
|
2136
|
+
`${base}.source`,
|
|
2137
|
+
"Only exposure measures may use exposure source",
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (measure.kind === "amount" && measure.basisId) {
|
|
2141
|
+
const basis = amountBasisMap.get(measure.basisId);
|
|
2142
|
+
const hasSdkLimitation =
|
|
2143
|
+
basis?.components.some(
|
|
2144
|
+
(component) =>
|
|
2145
|
+
(component.limitation.kind === "layer" ||
|
|
2146
|
+
component.limitation.kind === "pre-limited") &&
|
|
2147
|
+
component.limitation.derivation.kind === "sdk",
|
|
2148
|
+
) ?? false;
|
|
2149
|
+
if (hasSdkLimitation && measure.source !== "derived") {
|
|
2150
|
+
pushIssue(
|
|
2151
|
+
issues,
|
|
2152
|
+
"incompatible-semantics",
|
|
2153
|
+
`${base}.source`,
|
|
2154
|
+
"SDK-limited amount measures must be derived",
|
|
2155
|
+
);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
|
|
2160
|
+
let definitionExpressionNodes = 0;
|
|
2161
|
+
const formulaRoleUsage = new Map<string, readonly string[]>();
|
|
2162
|
+
formulas.forEach((formula, index) => {
|
|
2163
|
+
const base = `$.formulas[${index}]`;
|
|
2164
|
+
if (!isPlainRecord(formula)) {
|
|
2165
|
+
pushIssue(issues, "invalid-type", base, "Formula must be an object");
|
|
2166
|
+
return;
|
|
2167
|
+
}
|
|
2168
|
+
validateAllowedKeys(
|
|
2169
|
+
formula,
|
|
2170
|
+
[
|
|
2171
|
+
"id",
|
|
2172
|
+
"version",
|
|
2173
|
+
"roles",
|
|
2174
|
+
"numerator",
|
|
2175
|
+
"denominator",
|
|
2176
|
+
"denominatorPolicy",
|
|
2177
|
+
],
|
|
2178
|
+
base,
|
|
2179
|
+
issues,
|
|
2180
|
+
);
|
|
2181
|
+
validateToken(
|
|
2182
|
+
formula.version,
|
|
2183
|
+
`${base}.version`,
|
|
2184
|
+
"Formula version",
|
|
2185
|
+
issues,
|
|
2186
|
+
);
|
|
2187
|
+
if (formula.denominatorPolicy !== "positive-or-null") {
|
|
2188
|
+
pushIssue(
|
|
2189
|
+
issues,
|
|
2190
|
+
"incompatible-semantics",
|
|
2191
|
+
`${base}.denominatorPolicy`,
|
|
2192
|
+
"Unknown denominator policy",
|
|
2193
|
+
);
|
|
2194
|
+
}
|
|
2195
|
+
if (!isPlainRecord(formula.roles)) {
|
|
2196
|
+
pushIssue(
|
|
2197
|
+
issues,
|
|
2198
|
+
"invalid-type",
|
|
2199
|
+
`${base}.roles`,
|
|
2200
|
+
"Formula roles must be an object",
|
|
2201
|
+
);
|
|
2202
|
+
return;
|
|
2203
|
+
}
|
|
2204
|
+
for (const [roleName, role] of Object.entries(formula.roles)) {
|
|
2205
|
+
validateToken(
|
|
2206
|
+
roleName,
|
|
2207
|
+
propertyPath(`${base}.roles`, roleName),
|
|
2208
|
+
"Formula role",
|
|
2209
|
+
issues,
|
|
2210
|
+
);
|
|
2211
|
+
if (
|
|
2212
|
+
!isPlainRecord(role) ||
|
|
2213
|
+
!["count", "amount", "exposure"].includes(role.kind)
|
|
2214
|
+
) {
|
|
2215
|
+
pushIssue(
|
|
2216
|
+
issues,
|
|
2217
|
+
"invalid-type",
|
|
2218
|
+
propertyPath(`${base}.roles`, roleName),
|
|
2219
|
+
"Formula role has an invalid kind",
|
|
2220
|
+
);
|
|
2221
|
+
} else {
|
|
2222
|
+
const rolePath = propertyPath(`${base}.roles`, roleName);
|
|
2223
|
+
validateAllowedKeys(
|
|
2224
|
+
role,
|
|
2225
|
+
["kind", "compatibilityGroup", "developmentSemantics"],
|
|
2226
|
+
rolePath,
|
|
2227
|
+
issues,
|
|
2228
|
+
);
|
|
2229
|
+
if (
|
|
2230
|
+
role.developmentSemantics !== undefined &&
|
|
2231
|
+
!["cumulative", "incremental", "point-in-time", "unknown"].includes(
|
|
2232
|
+
role.developmentSemantics as string,
|
|
2233
|
+
)
|
|
2234
|
+
) {
|
|
2235
|
+
pushIssue(
|
|
2236
|
+
issues,
|
|
2237
|
+
"invalid-type",
|
|
2238
|
+
`${rolePath}.developmentSemantics`,
|
|
2239
|
+
"Unknown formula-role development semantics",
|
|
2240
|
+
);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
if (role.compatibilityGroup !== undefined)
|
|
2244
|
+
validateToken(
|
|
2245
|
+
role.compatibilityGroup,
|
|
2246
|
+
`${propertyPath(`${base}.roles`, roleName)}.compatibilityGroup`,
|
|
2247
|
+
"Compatibility group",
|
|
2248
|
+
issues,
|
|
2249
|
+
);
|
|
2250
|
+
}
|
|
2251
|
+
const numerator = walkDiagnosticExpression(
|
|
2252
|
+
formula.numerator,
|
|
2253
|
+
"role",
|
|
2254
|
+
`${base}.numerator`,
|
|
2255
|
+
issues,
|
|
2256
|
+
);
|
|
2257
|
+
const denominator = walkDiagnosticExpression(
|
|
2258
|
+
formula.denominator,
|
|
2259
|
+
"role",
|
|
2260
|
+
`${base}.denominator`,
|
|
2261
|
+
issues,
|
|
2262
|
+
);
|
|
2263
|
+
definitionExpressionNodes += numerator.nodeCount + denominator.nodeCount;
|
|
2264
|
+
const used = [
|
|
2265
|
+
...new Set([...numerator.dependencies, ...denominator.dependencies]),
|
|
2266
|
+
].sort();
|
|
2267
|
+
formulaRoleUsage.set(formula.id, used);
|
|
2268
|
+
for (const role of used) {
|
|
2269
|
+
if (!hasDiagnosticOwn(formula.roles, role)) {
|
|
2270
|
+
pushIssue(
|
|
2271
|
+
issues,
|
|
2272
|
+
"unknown-reference",
|
|
2273
|
+
base,
|
|
2274
|
+
`Formula ${formula.id} references unknown role ${role}`,
|
|
2275
|
+
);
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
for (const role of Object.keys(formula.roles)) {
|
|
2279
|
+
if (!used.includes(role))
|
|
2280
|
+
pushIssue(
|
|
2281
|
+
issues,
|
|
2282
|
+
"incompatible-semantics",
|
|
2283
|
+
propertyPath(`${base}.roles`, role),
|
|
2284
|
+
`Formula role ${role} is unused`,
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
for (const [side, walked] of [
|
|
2288
|
+
["numerator", numerator],
|
|
2289
|
+
["denominator", denominator],
|
|
2290
|
+
] as const) {
|
|
2291
|
+
const kinds = new Set(
|
|
2292
|
+
walked.dependencies
|
|
2293
|
+
.map((role) => formula.roles[role]?.kind)
|
|
2294
|
+
.filter(Boolean),
|
|
2295
|
+
);
|
|
2296
|
+
if (kinds.size > 1)
|
|
2297
|
+
pushIssue(
|
|
2298
|
+
issues,
|
|
2299
|
+
"incompatible-semantics",
|
|
2300
|
+
`${base}.${side}`,
|
|
2301
|
+
"Formula arithmetic combines different role kinds",
|
|
2302
|
+
);
|
|
2303
|
+
}
|
|
2304
|
+
});
|
|
2305
|
+
|
|
2306
|
+
const instanceRuleIds = new Set<string>();
|
|
2307
|
+
instances.forEach((instance, index) => {
|
|
2308
|
+
const base = `$.instances[${index}]`;
|
|
2309
|
+
if (!isPlainRecord(instance)) {
|
|
2310
|
+
pushIssue(
|
|
2311
|
+
issues,
|
|
2312
|
+
"invalid-type",
|
|
2313
|
+
base,
|
|
2314
|
+
"Metric instance must be an object",
|
|
2315
|
+
);
|
|
2316
|
+
return;
|
|
2317
|
+
}
|
|
2318
|
+
validateAllowedKeys(
|
|
2319
|
+
instance,
|
|
2320
|
+
["id", "version", "formulaId", "bindings", "presentation", "rules"],
|
|
2321
|
+
base,
|
|
2322
|
+
issues,
|
|
2323
|
+
);
|
|
2324
|
+
validateToken(
|
|
2325
|
+
instance.version,
|
|
2326
|
+
`${base}.version`,
|
|
2327
|
+
"Instance version",
|
|
2328
|
+
issues,
|
|
2329
|
+
);
|
|
2330
|
+
validateToken(
|
|
2331
|
+
instance.formulaId,
|
|
2332
|
+
`${base}.formulaId`,
|
|
2333
|
+
"Formula reference",
|
|
2334
|
+
issues,
|
|
2335
|
+
);
|
|
2336
|
+
const formula = formulaMap.get(instance.formulaId);
|
|
2337
|
+
if (!formula) {
|
|
2338
|
+
pushIssue(
|
|
2339
|
+
issues,
|
|
2340
|
+
"unknown-reference",
|
|
2341
|
+
`${base}.formulaId`,
|
|
2342
|
+
`Unknown formula ${instance.formulaId}`,
|
|
2343
|
+
);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
if (!isPlainRecord(instance.bindings)) {
|
|
2347
|
+
pushIssue(
|
|
2348
|
+
issues,
|
|
2349
|
+
"invalid-type",
|
|
2350
|
+
`${base}.bindings`,
|
|
2351
|
+
"Instance bindings must be an object",
|
|
2352
|
+
);
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
const usedRoles = formulaRoleUsage.get(formula.id) ?? [];
|
|
2356
|
+
const bindingSemantics = new Map<
|
|
2357
|
+
string,
|
|
2358
|
+
ReturnType<typeof expressionSemantics>
|
|
2359
|
+
>();
|
|
2360
|
+
for (const roleName of usedRoles) {
|
|
2361
|
+
if (!hasDiagnosticOwn(instance.bindings, roleName)) {
|
|
2362
|
+
pushIssue(
|
|
2363
|
+
issues,
|
|
2364
|
+
"missing-required",
|
|
2365
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2366
|
+
`Missing binding for role ${roleName}`,
|
|
2367
|
+
);
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
2370
|
+
const expression = instance.bindings[roleName]!;
|
|
2371
|
+
const semantics = expressionSemantics(
|
|
2372
|
+
expression,
|
|
2373
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2374
|
+
measureMap,
|
|
2375
|
+
issues,
|
|
2376
|
+
);
|
|
2377
|
+
bindingSemantics.set(roleName, semantics);
|
|
2378
|
+
definitionExpressionNodes += walkDiagnosticExpression(
|
|
2379
|
+
expression,
|
|
2380
|
+
"measure",
|
|
2381
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2382
|
+
[],
|
|
2383
|
+
).nodeCount;
|
|
2384
|
+
const role = formula.roles[roleName]!;
|
|
2385
|
+
const boundMeasures = semantics.measureIds
|
|
2386
|
+
.map((id) => measureMap.get(id))
|
|
2387
|
+
.filter(
|
|
2388
|
+
(item): item is DiagnosticMeasureDefinition => item !== undefined,
|
|
2389
|
+
);
|
|
2390
|
+
if (boundMeasures.some((measure) => measure.kind !== role.kind)) {
|
|
2391
|
+
pushIssue(
|
|
2392
|
+
issues,
|
|
2393
|
+
"incompatible-semantics",
|
|
2394
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2395
|
+
`Binding kind does not match role ${roleName}`,
|
|
2396
|
+
);
|
|
2397
|
+
}
|
|
2398
|
+
if (role.developmentSemantics !== undefined) {
|
|
2399
|
+
const leaves = transitiveMeasureIds(
|
|
2400
|
+
semantics.measureIds,
|
|
2401
|
+
derivationsByOutput,
|
|
2402
|
+
)
|
|
2403
|
+
.map((id) => measureMap.get(id))
|
|
2404
|
+
.filter(
|
|
2405
|
+
(measure): measure is DiagnosticMeasureDefinition =>
|
|
2406
|
+
measure !== undefined,
|
|
2407
|
+
);
|
|
2408
|
+
if (
|
|
2409
|
+
leaves.some(
|
|
2410
|
+
(measure) =>
|
|
2411
|
+
measure.developmentSemantics !== role.developmentSemantics,
|
|
2412
|
+
)
|
|
2413
|
+
) {
|
|
2414
|
+
pushIssue(
|
|
2415
|
+
issues,
|
|
2416
|
+
"incompatible-semantics",
|
|
2417
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2418
|
+
`Transitive binding development semantics do not match role ${roleName}`,
|
|
2419
|
+
);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
const calculationSignatures = new Map<
|
|
2424
|
+
"numerator" | "denominator",
|
|
2425
|
+
string | null
|
|
2426
|
+
>();
|
|
2427
|
+
for (const [side, roleExpression] of [
|
|
2428
|
+
["numerator", formula.numerator],
|
|
2429
|
+
["denominator", formula.denominator],
|
|
2430
|
+
] as const) {
|
|
2431
|
+
const roleIds = walkDiagnosticExpression(
|
|
2432
|
+
roleExpression,
|
|
2433
|
+
"role",
|
|
2434
|
+
`${base}.${side}`,
|
|
2435
|
+
[],
|
|
2436
|
+
).dependencies;
|
|
2437
|
+
const signatures = new Set(
|
|
2438
|
+
roleIds
|
|
2439
|
+
.map((roleId) => bindingSemantics.get(roleId)?.signature)
|
|
2440
|
+
.filter(
|
|
2441
|
+
(signature): signature is string =>
|
|
2442
|
+
signature !== null && signature !== undefined,
|
|
2443
|
+
),
|
|
2444
|
+
);
|
|
2445
|
+
calculationSignatures.set(
|
|
2446
|
+
side,
|
|
2447
|
+
signatures.size === 1 ? [...signatures][0]! : null,
|
|
2448
|
+
);
|
|
2449
|
+
if (signatures.size > 1) {
|
|
2450
|
+
pushIssue(
|
|
2451
|
+
issues,
|
|
2452
|
+
"incompatible-semantics",
|
|
2453
|
+
`${base}.bindings`,
|
|
2454
|
+
`Bound ${side} arithmetic combines incompatible semantics`,
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
for (const binding of Object.keys(instance.bindings)) {
|
|
2459
|
+
if (!usedRoles.includes(binding)) {
|
|
2460
|
+
pushIssue(
|
|
2461
|
+
issues,
|
|
2462
|
+
"unknown-reference",
|
|
2463
|
+
propertyPath(`${base}.bindings`, binding),
|
|
2464
|
+
`Unexpected binding for role ${binding}`,
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
const compatibility = new Map<string, string>();
|
|
2469
|
+
for (const roleName of usedRoles) {
|
|
2470
|
+
const role = formula.roles[roleName]!;
|
|
2471
|
+
const binding = instance.bindings[roleName];
|
|
2472
|
+
if (!role.compatibilityGroup || !binding) continue;
|
|
2473
|
+
const semantics = expressionSemantics(
|
|
2474
|
+
binding,
|
|
2475
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2476
|
+
measureMap,
|
|
2477
|
+
[],
|
|
2478
|
+
);
|
|
2479
|
+
if (semantics.signature === null) continue;
|
|
2480
|
+
const previous = compatibility.get(role.compatibilityGroup);
|
|
2481
|
+
if (previous !== undefined && previous !== semantics.signature) {
|
|
2482
|
+
pushIssue(
|
|
2483
|
+
issues,
|
|
2484
|
+
"incompatible-semantics",
|
|
2485
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2486
|
+
`Compatibility group ${role.compatibilityGroup} binds different semantics`,
|
|
2487
|
+
);
|
|
2488
|
+
} else compatibility.set(role.compatibilityGroup, semantics.signature);
|
|
2489
|
+
}
|
|
2490
|
+
if (!isPlainRecord(instance.presentation)) {
|
|
2491
|
+
pushIssue(
|
|
2492
|
+
issues,
|
|
2493
|
+
"invalid-type",
|
|
2494
|
+
`${base}.presentation`,
|
|
2495
|
+
"Metric presentation must be an object",
|
|
2496
|
+
);
|
|
2497
|
+
} else
|
|
2498
|
+
validateAllowedKeys(
|
|
2499
|
+
instance.presentation,
|
|
2500
|
+
[
|
|
2501
|
+
"displayName",
|
|
2502
|
+
"description",
|
|
2503
|
+
"displayUnit",
|
|
2504
|
+
"scale",
|
|
2505
|
+
"numeratorLabel",
|
|
2506
|
+
"denominatorLabel",
|
|
2507
|
+
],
|
|
2508
|
+
`${base}.presentation`,
|
|
2509
|
+
issues,
|
|
2510
|
+
);
|
|
2511
|
+
for (const [key, text] of [
|
|
2512
|
+
["displayName", instance.presentation?.displayName],
|
|
2513
|
+
["description", instance.presentation?.description],
|
|
2514
|
+
["displayUnit", instance.presentation?.displayUnit],
|
|
2515
|
+
["numeratorLabel", instance.presentation?.numeratorLabel],
|
|
2516
|
+
["denominatorLabel", instance.presentation?.denominatorLabel],
|
|
2517
|
+
] as const)
|
|
2518
|
+
validateToken(
|
|
2519
|
+
text,
|
|
2520
|
+
`${base}.presentation.${key}`,
|
|
2521
|
+
`Presentation ${key}`,
|
|
2522
|
+
issues,
|
|
2523
|
+
);
|
|
2524
|
+
if (
|
|
2525
|
+
!validateFinite(
|
|
2526
|
+
instance.presentation?.scale,
|
|
2527
|
+
`${base}.presentation.scale`,
|
|
2528
|
+
"Presentation scale",
|
|
2529
|
+
issues,
|
|
2530
|
+
) ||
|
|
2531
|
+
instance.presentation.scale <= 0
|
|
2532
|
+
) {
|
|
2533
|
+
pushIssue(
|
|
2534
|
+
issues,
|
|
2535
|
+
"invalid-number",
|
|
2536
|
+
`${base}.presentation.scale`,
|
|
2537
|
+
"Presentation scale must be positive",
|
|
2538
|
+
);
|
|
2539
|
+
}
|
|
2540
|
+
requireArray(instance.rules, `${base}.rules`, issues).forEach(
|
|
2541
|
+
(rawRule, ruleIndex) => {
|
|
2542
|
+
const ruleBase = `${base}.rules[${ruleIndex}]`;
|
|
2543
|
+
if (!isPlainRecord(rawRule)) {
|
|
2544
|
+
pushIssue(
|
|
2545
|
+
issues,
|
|
2546
|
+
"invalid-type",
|
|
2547
|
+
ruleBase,
|
|
2548
|
+
"Metric rule must be an object",
|
|
2549
|
+
);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
const rule = rawRule as unknown as DiagnosticComparisonRule;
|
|
2553
|
+
validateAllowedKeys(
|
|
2554
|
+
rawRule,
|
|
2555
|
+
["id", "code", "message", "severity", "when"],
|
|
2556
|
+
ruleBase,
|
|
2557
|
+
issues,
|
|
2558
|
+
);
|
|
2559
|
+
if (
|
|
2560
|
+
validateToken(rule.id, `${ruleBase}.id`, "Metric rule ID", issues)
|
|
2561
|
+
) {
|
|
2562
|
+
if (rule.id.startsWith("diagnostic/structural/"))
|
|
2563
|
+
pushIssue(
|
|
2564
|
+
issues,
|
|
2565
|
+
"incompatible-semantics",
|
|
2566
|
+
`${ruleBase}.id`,
|
|
2567
|
+
"Metric rule ID uses the reserved structural prefix",
|
|
2568
|
+
);
|
|
2569
|
+
if (instanceRuleIds.has(rule.id) || reviewRuleMap.has(rule.id))
|
|
2570
|
+
pushIssue(
|
|
2571
|
+
issues,
|
|
2572
|
+
"duplicate-id",
|
|
2573
|
+
`${ruleBase}.id`,
|
|
2574
|
+
`Duplicate rule ID ${rule.id}`,
|
|
2575
|
+
);
|
|
2576
|
+
instanceRuleIds.add(rule.id);
|
|
2577
|
+
}
|
|
2578
|
+
validateToken(
|
|
2579
|
+
rule.code,
|
|
2580
|
+
`${ruleBase}.code`,
|
|
2581
|
+
"Metric rule code",
|
|
2582
|
+
issues,
|
|
2583
|
+
);
|
|
2584
|
+
validateToken(
|
|
2585
|
+
rule.message,
|
|
2586
|
+
`${ruleBase}.message`,
|
|
2587
|
+
"Metric rule message",
|
|
2588
|
+
issues,
|
|
2589
|
+
);
|
|
2590
|
+
if (rule.severity !== "warning" && rule.severity !== "fail")
|
|
2591
|
+
pushIssue(
|
|
2592
|
+
issues,
|
|
2593
|
+
"invalid-type",
|
|
2594
|
+
`${ruleBase}.severity`,
|
|
2595
|
+
"Metric rule severity must be warning or fail",
|
|
2596
|
+
);
|
|
2597
|
+
if (!isPlainRecord(rule.when)) {
|
|
2598
|
+
pushIssue(
|
|
2599
|
+
issues,
|
|
2600
|
+
"invalid-type",
|
|
2601
|
+
`${ruleBase}.when`,
|
|
2602
|
+
"Metric rule predicate must be an object",
|
|
2603
|
+
);
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
validateAllowedKeys(
|
|
2607
|
+
rule.when,
|
|
2608
|
+
["left", "operator", "right", "tolerance"],
|
|
2609
|
+
`${ruleBase}.when`,
|
|
2610
|
+
issues,
|
|
2611
|
+
);
|
|
2612
|
+
if (rule.when.tolerance !== undefined) {
|
|
2613
|
+
if (!isPlainRecord(rule.when.tolerance))
|
|
2614
|
+
pushIssue(
|
|
2615
|
+
issues,
|
|
2616
|
+
"invalid-type",
|
|
2617
|
+
`${ruleBase}.when.tolerance`,
|
|
2618
|
+
"Tolerance must be an object",
|
|
2619
|
+
);
|
|
2620
|
+
else
|
|
2621
|
+
validateAllowedKeys(
|
|
2622
|
+
rule.when.tolerance,
|
|
2623
|
+
["absolute", "relative"],
|
|
2624
|
+
`${ruleBase}.when.tolerance`,
|
|
2625
|
+
issues,
|
|
2626
|
+
);
|
|
2627
|
+
}
|
|
2628
|
+
if (
|
|
2629
|
+
!["lt", "lte", "eq", "neq", "gte", "gt"].includes(rule.when.operator)
|
|
2630
|
+
)
|
|
2631
|
+
pushIssue(
|
|
2632
|
+
issues,
|
|
2633
|
+
"invalid-type",
|
|
2634
|
+
`${ruleBase}.when.operator`,
|
|
2635
|
+
"Unknown metric comparison operator",
|
|
2636
|
+
);
|
|
2637
|
+
validateTolerance(
|
|
2638
|
+
rule.when?.tolerance,
|
|
2639
|
+
`${ruleBase}.when.tolerance`,
|
|
2640
|
+
issues,
|
|
2641
|
+
);
|
|
2642
|
+
const operandSignatures = new Map<
|
|
2643
|
+
"left" | "right",
|
|
2644
|
+
string | null | undefined
|
|
2645
|
+
>();
|
|
2646
|
+
for (const [side, operand] of [
|
|
2647
|
+
["left", rule.when?.left],
|
|
2648
|
+
["right", rule.when?.right],
|
|
2649
|
+
] as const) {
|
|
2650
|
+
const operandPath = `${ruleBase}.when.${side}`;
|
|
2651
|
+
if (isPlainRecord(operand)) {
|
|
2652
|
+
const allowed =
|
|
2653
|
+
operand.source === "measure"
|
|
2654
|
+
? ["source", "expression"]
|
|
2655
|
+
: operand.source === "calculation"
|
|
2656
|
+
? ["source", "field"]
|
|
2657
|
+
: operand.source === "constant"
|
|
2658
|
+
? ["source", "value"]
|
|
2659
|
+
: ["source"];
|
|
2660
|
+
validateAllowedKeys(operand, allowed, operandPath, issues);
|
|
2661
|
+
}
|
|
2662
|
+
if (operand?.source === "measure") {
|
|
2663
|
+
const semantic = expressionSemantics(
|
|
2664
|
+
operand.expression,
|
|
2665
|
+
`${operandPath}.expression`,
|
|
2666
|
+
measureMap,
|
|
2667
|
+
issues,
|
|
2668
|
+
2,
|
|
2669
|
+
);
|
|
2670
|
+
const operandNodes =
|
|
2671
|
+
1 +
|
|
2672
|
+
walkDiagnosticExpression(
|
|
2673
|
+
operand.expression,
|
|
2674
|
+
"measure",
|
|
2675
|
+
`${operandPath}.expression`,
|
|
2676
|
+
[],
|
|
2677
|
+
).nodeCount;
|
|
2678
|
+
definitionExpressionNodes += operandNodes;
|
|
2679
|
+
if (operandNodes > MAX_DIAGNOSTIC_EXPRESSION_NODES) {
|
|
2680
|
+
pushIssue(
|
|
2681
|
+
issues,
|
|
2682
|
+
"expression-limit",
|
|
2683
|
+
operandPath,
|
|
2684
|
+
`Metric rule operand node count exceeds ${MAX_DIAGNOSTIC_EXPRESSION_NODES}`,
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
operandSignatures.set(side, semantic.signature);
|
|
2688
|
+
} else if (operand?.source === "calculation") {
|
|
2689
|
+
definitionExpressionNodes += 1;
|
|
2690
|
+
if (
|
|
2691
|
+
operand.field !== "numerator" &&
|
|
2692
|
+
operand.field !== "denominator"
|
|
2693
|
+
)
|
|
2694
|
+
pushIssue(
|
|
2695
|
+
issues,
|
|
2696
|
+
"invalid-type",
|
|
2697
|
+
`${operandPath}.field`,
|
|
2698
|
+
"Unknown calculation field",
|
|
2699
|
+
);
|
|
2700
|
+
else
|
|
2701
|
+
operandSignatures.set(
|
|
2702
|
+
side,
|
|
2703
|
+
calculationSignatures.get(operand.field),
|
|
2704
|
+
);
|
|
2705
|
+
} else if (operand?.source === "constant") {
|
|
2706
|
+
definitionExpressionNodes += 1;
|
|
2707
|
+
operandSignatures.set(side, undefined);
|
|
2708
|
+
validateFinite(
|
|
2709
|
+
operand.value,
|
|
2710
|
+
`${operandPath}.value`,
|
|
2711
|
+
"Rule constant",
|
|
2712
|
+
issues,
|
|
2713
|
+
);
|
|
2714
|
+
} else
|
|
2715
|
+
pushIssue(
|
|
2716
|
+
issues,
|
|
2717
|
+
"invalid-type",
|
|
2718
|
+
operandPath,
|
|
2719
|
+
"Unknown metric rule operand",
|
|
2720
|
+
);
|
|
2721
|
+
}
|
|
2722
|
+
const leftSignature = operandSignatures.get("left");
|
|
2723
|
+
const rightSignature = operandSignatures.get("right");
|
|
2724
|
+
if (
|
|
2725
|
+
leftSignature !== undefined &&
|
|
2726
|
+
rightSignature !== undefined &&
|
|
2727
|
+
leftSignature !== null &&
|
|
2728
|
+
rightSignature !== null &&
|
|
2729
|
+
leftSignature !== rightSignature
|
|
2730
|
+
) {
|
|
2731
|
+
pushIssue(
|
|
2732
|
+
issues,
|
|
2733
|
+
"incompatible-semantics",
|
|
2734
|
+
`${ruleBase}.when`,
|
|
2735
|
+
"Metric comparison operands must have identical quantity semantics",
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
},
|
|
2739
|
+
);
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
const derivedOutputs = new Map<string, DiagnosticDerivedMeasureDefinition>();
|
|
2743
|
+
derivations.forEach((derivation, index) => {
|
|
2744
|
+
const base = `$.derivedMeasures[${index}]`;
|
|
2745
|
+
if (!isPlainRecord(derivation)) {
|
|
2746
|
+
pushIssue(
|
|
2747
|
+
issues,
|
|
2748
|
+
"invalid-type",
|
|
2749
|
+
base,
|
|
2750
|
+
"Derived measure must be an object",
|
|
2751
|
+
);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
validateAllowedKeys(
|
|
2755
|
+
derivation,
|
|
2756
|
+
["id", "outputMeasureId", "expression"],
|
|
2757
|
+
base,
|
|
2758
|
+
issues,
|
|
2759
|
+
);
|
|
2760
|
+
validateToken(
|
|
2761
|
+
derivation.outputMeasureId,
|
|
2762
|
+
`${base}.outputMeasureId`,
|
|
2763
|
+
"Derived output measure",
|
|
2764
|
+
issues,
|
|
2765
|
+
);
|
|
2766
|
+
const output = measureMap.get(derivation.outputMeasureId);
|
|
2767
|
+
if (!output)
|
|
2768
|
+
pushIssue(
|
|
2769
|
+
issues,
|
|
2770
|
+
"unknown-reference",
|
|
2771
|
+
`${base}.outputMeasureId`,
|
|
2772
|
+
`Unknown derived output ${derivation.outputMeasureId}`,
|
|
2773
|
+
);
|
|
2774
|
+
else if (output.source !== "derived")
|
|
2775
|
+
pushIssue(
|
|
2776
|
+
issues,
|
|
2777
|
+
"incompatible-semantics",
|
|
2778
|
+
`${base}.outputMeasureId`,
|
|
2779
|
+
"Derivation output must declare source derived",
|
|
2780
|
+
);
|
|
2781
|
+
if (derivedOutputs.has(derivation.outputMeasureId))
|
|
2782
|
+
pushIssue(
|
|
2783
|
+
issues,
|
|
2784
|
+
"duplicate-id",
|
|
2785
|
+
`${base}.outputMeasureId`,
|
|
2786
|
+
`Duplicate derived output ${derivation.outputMeasureId}`,
|
|
2787
|
+
);
|
|
2788
|
+
derivedOutputs.set(derivation.outputMeasureId, derivation);
|
|
2789
|
+
const walked = walkDiagnosticExpression(
|
|
2790
|
+
derivation.expression,
|
|
2791
|
+
"claim",
|
|
2792
|
+
`${base}.expression`,
|
|
2793
|
+
issues,
|
|
2794
|
+
);
|
|
2795
|
+
definitionExpressionNodes += walked.nodeCount;
|
|
2796
|
+
for (const id of walked.dependencies)
|
|
2797
|
+
if (!measureMap.has(id))
|
|
2798
|
+
pushIssue(
|
|
2799
|
+
issues,
|
|
2800
|
+
"unknown-reference",
|
|
2801
|
+
`${base}.expression`,
|
|
2802
|
+
`Unknown derivation measure ${id}`,
|
|
2803
|
+
);
|
|
2804
|
+
const transitiveIds = transitiveMeasureIds(
|
|
2805
|
+
walked.dependencies,
|
|
2806
|
+
derivationsByOutput,
|
|
2807
|
+
);
|
|
2808
|
+
const transitiveInputs = transitiveIds
|
|
2809
|
+
.map((id) => measureMap.get(id))
|
|
2810
|
+
.filter(
|
|
2811
|
+
(item): item is DiagnosticMeasureDefinition => item !== undefined,
|
|
2812
|
+
);
|
|
2813
|
+
if (
|
|
2814
|
+
output &&
|
|
2815
|
+
transitiveInputs.some(
|
|
2816
|
+
(input) => input.developmentSemantics !== output.developmentSemantics,
|
|
2817
|
+
)
|
|
2818
|
+
) {
|
|
2819
|
+
pushIssue(
|
|
2820
|
+
issues,
|
|
2821
|
+
"incompatible-semantics",
|
|
2822
|
+
`${base}.expression`,
|
|
2823
|
+
"Derived output and inputs must share development semantics",
|
|
2824
|
+
);
|
|
2825
|
+
}
|
|
2826
|
+
if (definition.lossRowGrain !== "claim") {
|
|
2827
|
+
pushIssue(
|
|
2828
|
+
issues,
|
|
2829
|
+
"incompatible-semantics",
|
|
2830
|
+
base,
|
|
2831
|
+
"Derived measures require claim-grain input",
|
|
2832
|
+
);
|
|
2833
|
+
}
|
|
2834
|
+
const expressionStack: unknown[] = [derivation.expression];
|
|
2835
|
+
while (expressionStack.length > 0) {
|
|
2836
|
+
const current = expressionStack.pop();
|
|
2837
|
+
if (!isPlainRecord(current)) continue;
|
|
2838
|
+
if (current.op === "claim-layer") {
|
|
2839
|
+
const attachment = current.attachment;
|
|
2840
|
+
const limit = current.limit;
|
|
2841
|
+
const attachmentOk = validateFinite(
|
|
2842
|
+
attachment,
|
|
2843
|
+
`${base}.expression.attachment`,
|
|
2844
|
+
"Claim-layer attachment",
|
|
2845
|
+
issues,
|
|
2846
|
+
);
|
|
2847
|
+
if (attachmentOk && attachment < 0)
|
|
2848
|
+
pushIssue(
|
|
2849
|
+
issues,
|
|
2850
|
+
"invalid-number",
|
|
2851
|
+
`${base}.expression.attachment`,
|
|
2852
|
+
"Claim-layer attachment must be nonnegative",
|
|
2853
|
+
);
|
|
2854
|
+
if (limit !== null) {
|
|
2855
|
+
const limitOk = validateFinite(
|
|
2856
|
+
limit,
|
|
2857
|
+
`${base}.expression.limit`,
|
|
2858
|
+
"Claim-layer width",
|
|
2859
|
+
issues,
|
|
2860
|
+
);
|
|
2861
|
+
if (limitOk && limit <= 0)
|
|
2862
|
+
pushIssue(
|
|
2863
|
+
issues,
|
|
2864
|
+
"invalid-number",
|
|
2865
|
+
`${base}.expression.limit`,
|
|
2866
|
+
"Claim-layer width must be positive",
|
|
2867
|
+
);
|
|
2868
|
+
if (attachmentOk && limitOk && !Number.isFinite(attachment + limit)) {
|
|
2869
|
+
pushIssue(
|
|
2870
|
+
issues,
|
|
2871
|
+
"invalid-number",
|
|
2872
|
+
`${base}.expression.limit`,
|
|
2873
|
+
"Attachment plus claim-layer width must be finite",
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
}
|
|
2877
|
+
} else if (current.op === "add" && Array.isArray(current.terms))
|
|
2878
|
+
expressionStack.push(...current.terms);
|
|
2879
|
+
else if (current.op === "subtract")
|
|
2880
|
+
expressionStack.push(current.left, current.right);
|
|
2881
|
+
}
|
|
2882
|
+
const projection = projectClaimExpression(
|
|
2883
|
+
derivation.expression,
|
|
2884
|
+
`${base}.expression`,
|
|
2885
|
+
measureMap,
|
|
2886
|
+
amountBasisMap,
|
|
2887
|
+
issues,
|
|
2888
|
+
);
|
|
2889
|
+
if (output && projection) {
|
|
2890
|
+
const expected =
|
|
2891
|
+
output.kind === "amount" && output.basisId
|
|
2892
|
+
? amountBasisMap.get(output.basisId)
|
|
2893
|
+
: undefined;
|
|
2894
|
+
const matches =
|
|
2895
|
+
output.kind === projection.kind &&
|
|
2896
|
+
output.unit === projection.unit &&
|
|
2897
|
+
(output.kind === "amount"
|
|
2898
|
+
? expected !== undefined &&
|
|
2899
|
+
projectedBasisKey(projectedAmountBasis(expected)) ===
|
|
2900
|
+
projectedBasisKey(projection.amountBasis!)
|
|
2901
|
+
: semanticReference(output) === projection.reference);
|
|
2902
|
+
if (!matches)
|
|
2903
|
+
pushIssue(
|
|
2904
|
+
issues,
|
|
2905
|
+
"incompatible-semantics",
|
|
2906
|
+
`${base}.expression`,
|
|
2907
|
+
"Derived expression does not project the output measure semantics",
|
|
2908
|
+
);
|
|
2909
|
+
}
|
|
2910
|
+
});
|
|
2911
|
+
for (const [measureId, measure] of measureMap) {
|
|
2912
|
+
if (measure.source === "derived" && !derivedOutputs.has(measureId)) {
|
|
2913
|
+
pushIssue(
|
|
2914
|
+
issues,
|
|
2915
|
+
"missing-required",
|
|
2916
|
+
"$.derivedMeasures",
|
|
2917
|
+
`Derived measure ${measureId} requires exactly one derivation`,
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
}
|
|
2921
|
+
const visiting = new Set<string>();
|
|
2922
|
+
const visited = new Set<string>();
|
|
2923
|
+
const visit = (id: string): void => {
|
|
2924
|
+
if (visited.has(id)) return;
|
|
2925
|
+
if (visiting.has(id)) {
|
|
2926
|
+
pushIssue(
|
|
2927
|
+
issues,
|
|
2928
|
+
"cycle",
|
|
2929
|
+
"$.derivedMeasures",
|
|
2930
|
+
`Derived measure graph contains a cycle at ${id}`,
|
|
2931
|
+
);
|
|
2932
|
+
return;
|
|
2933
|
+
}
|
|
2934
|
+
visiting.add(id);
|
|
2935
|
+
const derivation = derivedOutputs.get(id);
|
|
2936
|
+
if (derivation) {
|
|
2937
|
+
const deps = walkDiagnosticExpression(
|
|
2938
|
+
derivation.expression,
|
|
2939
|
+
"claim",
|
|
2940
|
+
"$.derivedMeasures",
|
|
2941
|
+
[],
|
|
2942
|
+
).dependencies;
|
|
2943
|
+
for (const dependency of deps)
|
|
2944
|
+
if (derivedOutputs.has(dependency)) visit(dependency);
|
|
2945
|
+
}
|
|
2946
|
+
visiting.delete(id);
|
|
2947
|
+
visited.add(id);
|
|
2948
|
+
};
|
|
2949
|
+
for (const id of derivedOutputs.keys()) visit(id);
|
|
2950
|
+
|
|
2951
|
+
reviewRules.forEach((rule, index) => {
|
|
2952
|
+
const base = `$.reviewRules[${index}]`;
|
|
2953
|
+
if (!isPlainRecord(rule)) {
|
|
2954
|
+
pushIssue(issues, "invalid-type", base, "Review rule must be an object");
|
|
2955
|
+
return;
|
|
2956
|
+
}
|
|
2957
|
+
const commonKeys = [
|
|
2958
|
+
"kind",
|
|
2959
|
+
"id",
|
|
2960
|
+
"code",
|
|
2961
|
+
"description",
|
|
2962
|
+
"severity",
|
|
2963
|
+
"tolerance",
|
|
2964
|
+
"missingInput",
|
|
2965
|
+
];
|
|
2966
|
+
const branchKeys =
|
|
2967
|
+
rule.kind === "compare"
|
|
2968
|
+
? ["when"]
|
|
2969
|
+
: rule.kind === "reconcile"
|
|
2970
|
+
? ["actual", "expected"]
|
|
2971
|
+
: rule.kind === "monotonic"
|
|
2972
|
+
? ["expression", "direction"]
|
|
2973
|
+
: rule.kind === "layer-order"
|
|
2974
|
+
? ["narrower", "broader", "comparability"]
|
|
2975
|
+
: rule.kind === "control-total"
|
|
2976
|
+
? ["expression", "expected", "filter", "projection"]
|
|
2977
|
+
: [];
|
|
2978
|
+
validateAllowedKeys(rule, [...commonKeys, ...branchKeys], base, issues);
|
|
2979
|
+
if (rule.tolerance !== undefined) {
|
|
2980
|
+
if (!isPlainRecord(rule.tolerance))
|
|
2981
|
+
pushIssue(
|
|
2982
|
+
issues,
|
|
2983
|
+
"invalid-type",
|
|
2984
|
+
`${base}.tolerance`,
|
|
2985
|
+
"Tolerance must be an object",
|
|
2986
|
+
);
|
|
2987
|
+
else
|
|
2988
|
+
validateAllowedKeys(
|
|
2989
|
+
rule.tolerance,
|
|
2990
|
+
["absolute", "relative"],
|
|
2991
|
+
`${base}.tolerance`,
|
|
2992
|
+
issues,
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
validateToken(rule.code, `${base}.code`, "Review rule code", issues);
|
|
2996
|
+
validateToken(
|
|
2997
|
+
rule.description,
|
|
2998
|
+
`${base}.description`,
|
|
2999
|
+
"Review rule description",
|
|
3000
|
+
issues,
|
|
3001
|
+
);
|
|
3002
|
+
if (
|
|
3003
|
+
typeof rule.id === "string" &&
|
|
3004
|
+
rule.id.startsWith("diagnostic/structural/")
|
|
3005
|
+
)
|
|
3006
|
+
pushIssue(
|
|
3007
|
+
issues,
|
|
3008
|
+
"incompatible-semantics",
|
|
3009
|
+
`${base}.id`,
|
|
3010
|
+
"Review rule ID uses the reserved structural prefix",
|
|
3011
|
+
);
|
|
3012
|
+
if (rule.severity !== "warning" && rule.severity !== "fail")
|
|
3013
|
+
pushIssue(
|
|
3014
|
+
issues,
|
|
3015
|
+
"invalid-type",
|
|
3016
|
+
`${base}.severity`,
|
|
3017
|
+
"Review severity must be warning or fail",
|
|
3018
|
+
);
|
|
3019
|
+
if (
|
|
3020
|
+
rule.missingInput !== "not-evaluated" &&
|
|
3021
|
+
rule.missingInput !== "finding"
|
|
3022
|
+
)
|
|
3023
|
+
pushIssue(
|
|
3024
|
+
issues,
|
|
3025
|
+
"invalid-type",
|
|
3026
|
+
`${base}.missingInput`,
|
|
3027
|
+
"Review missing-input policy must be not-evaluated or finding",
|
|
3028
|
+
);
|
|
3029
|
+
validateTolerance(rule.tolerance, `${base}.tolerance`, issues);
|
|
3030
|
+
const roots: {
|
|
3031
|
+
readonly expression: DiagnosticMeasureExpression;
|
|
3032
|
+
readonly path: string;
|
|
3033
|
+
}[] = [];
|
|
3034
|
+
const operand = (value: unknown, path: string): void => {
|
|
3035
|
+
if (isPlainRecord(value) && value.op === "constant") {
|
|
3036
|
+
validateAllowedKeys(value, ["op", "value"], path, issues);
|
|
3037
|
+
validateFinite(value.value, `${path}.value`, "Review constant", issues);
|
|
3038
|
+
definitionExpressionNodes++;
|
|
3039
|
+
} else
|
|
3040
|
+
roots.push({ expression: value as DiagnosticMeasureExpression, path });
|
|
3041
|
+
};
|
|
3042
|
+
if (rule.kind === "compare") {
|
|
3043
|
+
if (!isPlainRecord(rule.when))
|
|
3044
|
+
pushIssue(
|
|
3045
|
+
issues,
|
|
3046
|
+
"invalid-type",
|
|
3047
|
+
`${base}.when`,
|
|
3048
|
+
"Compare rule predicate must be an object",
|
|
3049
|
+
);
|
|
3050
|
+
else {
|
|
3051
|
+
validateAllowedKeys(
|
|
3052
|
+
rule.when,
|
|
3053
|
+
["left", "operator", "right"],
|
|
3054
|
+
`${base}.when`,
|
|
3055
|
+
issues,
|
|
3056
|
+
);
|
|
3057
|
+
operand(rule.when.left, `${base}.when.left`);
|
|
3058
|
+
operand(rule.when.right, `${base}.when.right`);
|
|
3059
|
+
if (
|
|
3060
|
+
!["lt", "lte", "eq", "neq", "gte", "gt"].includes(rule.when.operator)
|
|
3061
|
+
)
|
|
3062
|
+
pushIssue(
|
|
3063
|
+
issues,
|
|
3064
|
+
"invalid-type",
|
|
3065
|
+
`${base}.when.operator`,
|
|
3066
|
+
"Unknown review comparison operator",
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
} else if (rule.kind === "reconcile") {
|
|
3070
|
+
roots.push({ expression: rule.actual, path: `${base}.actual` });
|
|
3071
|
+
operand(rule.expected, `${base}.expected`);
|
|
3072
|
+
} else if (rule.kind === "monotonic") {
|
|
3073
|
+
roots.push({ expression: rule.expression, path: `${base}.expression` });
|
|
3074
|
+
if (
|
|
3075
|
+
rule.direction !== "nondecreasing" &&
|
|
3076
|
+
rule.direction !== "nonincreasing"
|
|
3077
|
+
)
|
|
3078
|
+
pushIssue(
|
|
3079
|
+
issues,
|
|
3080
|
+
"invalid-type",
|
|
3081
|
+
`${base}.direction`,
|
|
3082
|
+
"Unknown monotonic direction",
|
|
3083
|
+
);
|
|
3084
|
+
} else if (rule.kind === "layer-order") {
|
|
3085
|
+
roots.push(
|
|
3086
|
+
{ expression: rule.narrower, path: `${base}.narrower` },
|
|
3087
|
+
{ expression: rule.broader, path: `${base}.broader` },
|
|
3088
|
+
);
|
|
3089
|
+
if (
|
|
3090
|
+
!isPlainRecord(rule.comparability) ||
|
|
3091
|
+
(rule.comparability.kind !== "compiler-proven" &&
|
|
3092
|
+
rule.comparability.kind !== "caller-asserted")
|
|
3093
|
+
) {
|
|
3094
|
+
pushIssue(
|
|
3095
|
+
issues,
|
|
3096
|
+
"invalid-type",
|
|
3097
|
+
`${base}.comparability`,
|
|
3098
|
+
"Unknown layer comparability evidence",
|
|
3099
|
+
);
|
|
3100
|
+
} else if (rule.comparability.kind === "caller-asserted") {
|
|
3101
|
+
validateAllowedKeys(
|
|
3102
|
+
rule.comparability,
|
|
3103
|
+
["kind", "rationaleArtifactId"],
|
|
3104
|
+
`${base}.comparability`,
|
|
3105
|
+
issues,
|
|
3106
|
+
);
|
|
3107
|
+
validateToken(
|
|
3108
|
+
rule.comparability.rationaleArtifactId,
|
|
3109
|
+
`${base}.comparability.rationaleArtifactId`,
|
|
3110
|
+
"Rationale artifact ID",
|
|
3111
|
+
issues,
|
|
3112
|
+
);
|
|
3113
|
+
}
|
|
3114
|
+
if (
|
|
3115
|
+
isPlainRecord(rule.comparability) &&
|
|
3116
|
+
rule.comparability.kind === "compiler-proven"
|
|
3117
|
+
)
|
|
3118
|
+
validateAllowedKeys(
|
|
3119
|
+
rule.comparability,
|
|
3120
|
+
["kind"],
|
|
3121
|
+
`${base}.comparability`,
|
|
3122
|
+
issues,
|
|
3123
|
+
);
|
|
3124
|
+
if (
|
|
3125
|
+
isPlainRecord(rule.comparability) &&
|
|
3126
|
+
(rule.comparability.kind === "compiler-proven" ||
|
|
3127
|
+
rule.comparability.kind === "caller-asserted")
|
|
3128
|
+
) {
|
|
3129
|
+
validateLayerComparability(
|
|
3130
|
+
rule,
|
|
3131
|
+
`${base}.comparability`,
|
|
3132
|
+
measureMap,
|
|
3133
|
+
amountBasisMap,
|
|
3134
|
+
derivationsByOutput,
|
|
3135
|
+
issues,
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
} else if (rule.kind === "control-total") {
|
|
3139
|
+
roots.push({ expression: rule.expression, path: `${base}.expression` });
|
|
3140
|
+
validateFinite(
|
|
3141
|
+
rule.expected,
|
|
3142
|
+
`${base}.expected`,
|
|
3143
|
+
"Control total",
|
|
3144
|
+
issues,
|
|
3145
|
+
);
|
|
3146
|
+
if (rule.filter !== undefined)
|
|
3147
|
+
validateReviewFilter(
|
|
3148
|
+
rule.filter,
|
|
3149
|
+
`${base}.filter`,
|
|
3150
|
+
definition.periodAxis,
|
|
3151
|
+
issues,
|
|
3152
|
+
);
|
|
3153
|
+
if (
|
|
3154
|
+
!isPlainRecord(rule.projection) ||
|
|
3155
|
+
!["valuation", "latest-valuation-per-origin", "all-cells"].includes(
|
|
3156
|
+
rule.projection.kind,
|
|
3157
|
+
)
|
|
3158
|
+
) {
|
|
3159
|
+
pushIssue(
|
|
3160
|
+
issues,
|
|
3161
|
+
"invalid-type",
|
|
3162
|
+
`${base}.projection`,
|
|
3163
|
+
"Unknown control-total projection",
|
|
3164
|
+
);
|
|
3165
|
+
} else if (rule.projection.kind === "valuation") {
|
|
3166
|
+
validateAllowedKeys(
|
|
3167
|
+
rule.projection,
|
|
3168
|
+
["kind", "valuation"],
|
|
3169
|
+
`${base}.projection`,
|
|
3170
|
+
issues,
|
|
3171
|
+
);
|
|
3172
|
+
if (
|
|
3173
|
+
validateToken(
|
|
3174
|
+
rule.projection.valuation,
|
|
3175
|
+
`${base}.projection.valuation`,
|
|
3176
|
+
"Control-total valuation",
|
|
3177
|
+
issues,
|
|
3178
|
+
) &&
|
|
3179
|
+
!normalizeDiagnosticPeriodWithAxis(
|
|
3180
|
+
definition.periodAxis,
|
|
3181
|
+
"valuation",
|
|
3182
|
+
rule.projection.valuation,
|
|
3183
|
+
)
|
|
3184
|
+
) {
|
|
3185
|
+
pushIssue(
|
|
3186
|
+
issues,
|
|
3187
|
+
"invalid-period",
|
|
3188
|
+
`${base}.projection.valuation`,
|
|
3189
|
+
`Unknown valuation period ${JSON.stringify(rule.projection.valuation)}`,
|
|
3190
|
+
);
|
|
3191
|
+
}
|
|
3192
|
+
} else
|
|
3193
|
+
validateAllowedKeys(
|
|
3194
|
+
rule.projection,
|
|
3195
|
+
["kind"],
|
|
3196
|
+
`${base}.projection`,
|
|
3197
|
+
issues,
|
|
3198
|
+
);
|
|
3199
|
+
} else
|
|
3200
|
+
pushIssue(
|
|
3201
|
+
issues,
|
|
3202
|
+
"invalid-type",
|
|
3203
|
+
`${base}.kind`,
|
|
3204
|
+
"Unknown review-rule kind",
|
|
3205
|
+
);
|
|
3206
|
+
const compatibleOperands = (
|
|
3207
|
+
left: DiagnosticReviewOperand,
|
|
3208
|
+
right: DiagnosticReviewOperand,
|
|
3209
|
+
path: string,
|
|
3210
|
+
): void => {
|
|
3211
|
+
if (!isPlainRecord(left) || !isPlainRecord(right)) return;
|
|
3212
|
+
if (left.op === "constant" || right.op === "constant") return;
|
|
3213
|
+
const leftSignature = expressionSemantics(
|
|
3214
|
+
left,
|
|
3215
|
+
`${path}.left`,
|
|
3216
|
+
measureMap,
|
|
3217
|
+
[],
|
|
3218
|
+
).signature;
|
|
3219
|
+
const rightSignature = expressionSemantics(
|
|
3220
|
+
right,
|
|
3221
|
+
`${path}.right`,
|
|
3222
|
+
measureMap,
|
|
3223
|
+
[],
|
|
3224
|
+
).signature;
|
|
3225
|
+
if (
|
|
3226
|
+
leftSignature !== null &&
|
|
3227
|
+
rightSignature !== null &&
|
|
3228
|
+
leftSignature !== rightSignature
|
|
3229
|
+
) {
|
|
3230
|
+
pushIssue(
|
|
3231
|
+
issues,
|
|
3232
|
+
"incompatible-semantics",
|
|
3233
|
+
path,
|
|
3234
|
+
"Review comparison operands must have identical quantity semantics",
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
};
|
|
3238
|
+
if (rule.kind === "compare" && isPlainRecord(rule.when)) {
|
|
3239
|
+
compatibleOperands(rule.when.left, rule.when.right, `${base}.when`);
|
|
3240
|
+
} else if (rule.kind === "reconcile") {
|
|
3241
|
+
compatibleOperands(rule.actual, rule.expected, base);
|
|
3242
|
+
}
|
|
3243
|
+
roots.forEach(({ expression, path }) => {
|
|
3244
|
+
const semantics = expressionSemantics(
|
|
3245
|
+
expression,
|
|
3246
|
+
path,
|
|
3247
|
+
measureMap,
|
|
3248
|
+
issues,
|
|
3249
|
+
);
|
|
3250
|
+
definitionExpressionNodes += walkDiagnosticExpression(
|
|
3251
|
+
expression,
|
|
3252
|
+
"measure",
|
|
3253
|
+
path,
|
|
3254
|
+
[],
|
|
3255
|
+
).nodeCount;
|
|
3256
|
+
if (rule.kind === "monotonic") {
|
|
3257
|
+
const leaves = transitiveMeasureIds(
|
|
3258
|
+
semantics.measureIds,
|
|
3259
|
+
derivationsByOutput,
|
|
3260
|
+
)
|
|
3261
|
+
.map((id) => measureMap.get(id))
|
|
3262
|
+
.filter(
|
|
3263
|
+
(item): item is DiagnosticMeasureDefinition => item !== undefined,
|
|
3264
|
+
);
|
|
3265
|
+
if (
|
|
3266
|
+
leaves.some(
|
|
3267
|
+
(measure) =>
|
|
3268
|
+
measure.developmentSemantics !== "cumulative" &&
|
|
3269
|
+
measure.developmentSemantics !== "point-in-time",
|
|
3270
|
+
)
|
|
3271
|
+
) {
|
|
3272
|
+
pushIssue(
|
|
3273
|
+
issues,
|
|
3274
|
+
"incompatible-semantics",
|
|
3275
|
+
path,
|
|
3276
|
+
"Monotonic review requires cumulative or point-in-time leaves",
|
|
3277
|
+
);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (
|
|
3281
|
+
rule.kind === "control-total" &&
|
|
3282
|
+
rule.projection?.kind === "all-cells"
|
|
3283
|
+
) {
|
|
3284
|
+
const leaves = transitiveMeasureIds(
|
|
3285
|
+
semantics.measureIds,
|
|
3286
|
+
derivationsByOutput,
|
|
3287
|
+
)
|
|
3288
|
+
.map((id) => measureMap.get(id))
|
|
3289
|
+
.filter(
|
|
3290
|
+
(item): item is DiagnosticMeasureDefinition => item !== undefined,
|
|
3291
|
+
);
|
|
3292
|
+
if (
|
|
3293
|
+
leaves.some(
|
|
3294
|
+
(measure) =>
|
|
3295
|
+
measure.developmentSemantics === "cumulative" ||
|
|
3296
|
+
measure.exposureTiming === "origin-static",
|
|
3297
|
+
)
|
|
3298
|
+
) {
|
|
3299
|
+
pushIssue(
|
|
3300
|
+
issues,
|
|
3301
|
+
"incompatible-semantics",
|
|
3302
|
+
`${base}.projection`,
|
|
3303
|
+
"All-cells control totals cannot repeat cumulative or origin-static measures",
|
|
3304
|
+
);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
});
|
|
3309
|
+
|
|
3310
|
+
if (definitionExpressionNodes > MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES) {
|
|
3311
|
+
pushIssue(
|
|
3312
|
+
issues,
|
|
3313
|
+
"expression-limit",
|
|
3314
|
+
"$",
|
|
3315
|
+
`Definition expression node count exceeds ${MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES}`,
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
validatePeriodAxis(definition.periodAxis, issues);
|
|
3319
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
3320
|
+
return definition;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
/** Internal boundary reuse: direct helpers use the compiler's exact axis contract. */
|
|
3324
|
+
export function validateDiagnosticPeriodAxisInput(
|
|
3325
|
+
value: unknown,
|
|
3326
|
+
): readonly DiagnosticValidationIssue[] {
|
|
3327
|
+
const preflight = diagnosticJsonPreflight(value, "configuration").map((issue) => ({
|
|
3328
|
+
...issue,
|
|
3329
|
+
path: `$.periodAxis${issue.path.slice(1)}`,
|
|
3330
|
+
}));
|
|
3331
|
+
if (preflight.length > 0) return preflight;
|
|
3332
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
3333
|
+
validatePeriodAxis(value as DiagnosticPeriodAxis, issues);
|
|
3334
|
+
return issues.map((issue) => ({ ...issue, domain: "configuration" as const }));
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
function optionalFromNullable<T>(value: T | null | undefined): T | undefined {
|
|
3338
|
+
return value === null || value === undefined ? undefined : value;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function omitProperties<T extends object>(
|
|
3342
|
+
value: T,
|
|
3343
|
+
keys: readonly string[],
|
|
3344
|
+
): Record<string, unknown> {
|
|
3345
|
+
const result = { ...value } as Record<string, unknown>;
|
|
3346
|
+
for (const key of keys) delete result[key];
|
|
3347
|
+
return result;
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
/**
|
|
3351
|
+
* The normalized wire projection materializes optional semantic fields as
|
|
3352
|
+
* null. Convert only those documented defaults back to the authored view so
|
|
3353
|
+
* both accepted inputs travel through the exact same semantic compiler.
|
|
3354
|
+
*/
|
|
3355
|
+
function authoredView(
|
|
3356
|
+
value: DiagnosticDefinition | NormalizedDiagnosticDefinitionIdentity,
|
|
3357
|
+
): DiagnosticDefinition {
|
|
3358
|
+
const definition = value as NormalizedDiagnosticDefinitionIdentity;
|
|
3359
|
+
const reviewRules = definition.reviewRules.map((rule) => {
|
|
3360
|
+
const tolerance = { ...rule.tolerance };
|
|
3361
|
+
if (rule.kind !== "control-total") return { ...rule, tolerance };
|
|
3362
|
+
const filter =
|
|
3363
|
+
rule.filter === null || rule.filter === undefined
|
|
3364
|
+
? undefined
|
|
3365
|
+
: (Object.fromEntries(
|
|
3366
|
+
Object.entries({
|
|
3367
|
+
sourceGroups: optionalFromNullable(rule.filter.sourceGroups),
|
|
3368
|
+
origins: optionalFromNullable(rule.filter.origins),
|
|
3369
|
+
originFrom: optionalFromNullable(rule.filter.originFrom),
|
|
3370
|
+
originThrough: optionalFromNullable(rule.filter.originThrough),
|
|
3371
|
+
valuations: optionalFromNullable(rule.filter.valuations),
|
|
3372
|
+
valuationFrom: optionalFromNullable(rule.filter.valuationFrom),
|
|
3373
|
+
valuationThrough: optionalFromNullable(
|
|
3374
|
+
rule.filter.valuationThrough,
|
|
3375
|
+
),
|
|
3376
|
+
minDevelopmentAge: optionalFromNullable(
|
|
3377
|
+
rule.filter.minDevelopmentAge,
|
|
3378
|
+
),
|
|
3379
|
+
maxDevelopmentAge: optionalFromNullable(
|
|
3380
|
+
rule.filter.maxDevelopmentAge,
|
|
3381
|
+
),
|
|
3382
|
+
}).filter(([, item]) => item !== undefined),
|
|
3383
|
+
) as DiagnosticReviewFilter);
|
|
3384
|
+
return {
|
|
3385
|
+
...omitProperties(rule, ["filter"]),
|
|
3386
|
+
tolerance,
|
|
3387
|
+
...(filter === undefined ? {} : { filter }),
|
|
3388
|
+
} as DiagnosticReviewRule;
|
|
3389
|
+
}) as readonly DiagnosticReviewRule[];
|
|
3390
|
+
return {
|
|
3391
|
+
...definition,
|
|
3392
|
+
diagnosticDefinitionVersion: definition.diagnosticDefinitionVersion,
|
|
3393
|
+
id: definition.id,
|
|
3394
|
+
version: definition.version,
|
|
3395
|
+
lossRowGrain: definition.lossRowGrain,
|
|
3396
|
+
measures: definition.measures.map((measure) => ({
|
|
3397
|
+
...omitProperties(measure, [
|
|
3398
|
+
"basisId",
|
|
3399
|
+
"countPopulationId",
|
|
3400
|
+
"exposureBasisId",
|
|
3401
|
+
"exposureTiming",
|
|
3402
|
+
]),
|
|
3403
|
+
id: measure.id,
|
|
3404
|
+
displayName: measure.displayName,
|
|
3405
|
+
description: measure.description,
|
|
3406
|
+
source: measure.source,
|
|
3407
|
+
kind: measure.kind,
|
|
3408
|
+
unit: measure.unit,
|
|
3409
|
+
developmentSemantics: measure.developmentSemantics,
|
|
3410
|
+
aggregation: measure.aggregation,
|
|
3411
|
+
missing: measure.missing,
|
|
3412
|
+
...(measure.basisId === null || measure.basisId === undefined
|
|
3413
|
+
? {}
|
|
3414
|
+
: { basisId: measure.basisId }),
|
|
3415
|
+
...(measure.countPopulationId === null ||
|
|
3416
|
+
measure.countPopulationId === undefined
|
|
3417
|
+
? {}
|
|
3418
|
+
: { countPopulationId: measure.countPopulationId }),
|
|
3419
|
+
...(measure.exposureBasisId === null ||
|
|
3420
|
+
measure.exposureBasisId === undefined
|
|
3421
|
+
? {}
|
|
3422
|
+
: { exposureBasisId: measure.exposureBasisId }),
|
|
3423
|
+
...(measure.exposureTiming === null ||
|
|
3424
|
+
measure.exposureTiming === undefined
|
|
3425
|
+
? {}
|
|
3426
|
+
: { exposureTiming: measure.exposureTiming }),
|
|
3427
|
+
})),
|
|
3428
|
+
countPopulations: definition.countPopulations.map((population) => ({
|
|
3429
|
+
...population,
|
|
3430
|
+
})),
|
|
3431
|
+
exposureBases: definition.exposureBases.map((basis) => ({
|
|
3432
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
3433
|
+
id: basis.id,
|
|
3434
|
+
displayName: basis.displayName,
|
|
3435
|
+
basis: basis.basis,
|
|
3436
|
+
unit: basis.unit,
|
|
3437
|
+
description: basis.description,
|
|
3438
|
+
...(basis.sourceDescription === null ||
|
|
3439
|
+
basis.sourceDescription === undefined
|
|
3440
|
+
? {}
|
|
3441
|
+
: { sourceDescription: basis.sourceDescription }),
|
|
3442
|
+
...(basis.attributes === undefined
|
|
3443
|
+
? {}
|
|
3444
|
+
: { attributes: basis.attributes }),
|
|
3445
|
+
})),
|
|
3446
|
+
amountBases: definition.amountBases.map((basis) => ({
|
|
3447
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
3448
|
+
id: basis.id,
|
|
3449
|
+
displayName: basis.displayName,
|
|
3450
|
+
currency: basis.currency,
|
|
3451
|
+
perspective: basis.perspective,
|
|
3452
|
+
components: basis.components.map((component) => ({
|
|
3453
|
+
...component,
|
|
3454
|
+
id: component.id,
|
|
3455
|
+
treatment: component.treatment,
|
|
3456
|
+
limitation:
|
|
3457
|
+
component.limitation.kind === "unknown"
|
|
3458
|
+
? {
|
|
3459
|
+
...omitProperties(component.limitation, ["description"]),
|
|
3460
|
+
kind: "unknown" as const,
|
|
3461
|
+
...(component.limitation.description === null ||
|
|
3462
|
+
component.limitation.description === undefined
|
|
3463
|
+
? {}
|
|
3464
|
+
: { description: component.limitation.description }),
|
|
3465
|
+
}
|
|
3466
|
+
: component.limitation,
|
|
3467
|
+
})),
|
|
3468
|
+
...(basis.sourceDescription === null ||
|
|
3469
|
+
basis.sourceDescription === undefined
|
|
3470
|
+
? {}
|
|
3471
|
+
: { sourceDescription: basis.sourceDescription }),
|
|
3472
|
+
...(basis.attributes === undefined
|
|
3473
|
+
? {}
|
|
3474
|
+
: { attributes: basis.attributes }),
|
|
3475
|
+
})),
|
|
3476
|
+
derivedMeasures: definition.derivedMeasures.map((derivation) => ({
|
|
3477
|
+
...derivation,
|
|
3478
|
+
})),
|
|
3479
|
+
formulas: definition.formulas.map((formula) => ({
|
|
3480
|
+
...formula,
|
|
3481
|
+
id: formula.id,
|
|
3482
|
+
version: formula.version,
|
|
3483
|
+
roles: Object.fromEntries(
|
|
3484
|
+
Object.entries(formula.roles).map(([name, role]) => [
|
|
3485
|
+
name,
|
|
3486
|
+
{
|
|
3487
|
+
...omitProperties(role, [
|
|
3488
|
+
"compatibilityGroup",
|
|
3489
|
+
"developmentSemantics",
|
|
3490
|
+
]),
|
|
3491
|
+
kind: role.kind,
|
|
3492
|
+
...(role.compatibilityGroup === null ||
|
|
3493
|
+
role.compatibilityGroup === undefined
|
|
3494
|
+
? {}
|
|
3495
|
+
: { compatibilityGroup: role.compatibilityGroup }),
|
|
3496
|
+
...(role.developmentSemantics === null ||
|
|
3497
|
+
role.developmentSemantics === undefined
|
|
3498
|
+
? {}
|
|
3499
|
+
: { developmentSemantics: role.developmentSemantics }),
|
|
3500
|
+
},
|
|
3501
|
+
]),
|
|
3502
|
+
),
|
|
3503
|
+
numerator: formula.numerator,
|
|
3504
|
+
denominator: formula.denominator,
|
|
3505
|
+
denominatorPolicy: formula.denominatorPolicy,
|
|
3506
|
+
})),
|
|
3507
|
+
instances: definition.instances.map((instance) => ({
|
|
3508
|
+
...instance,
|
|
3509
|
+
id: instance.id,
|
|
3510
|
+
version: instance.version,
|
|
3511
|
+
formulaId: instance.formulaId,
|
|
3512
|
+
bindings: instance.bindings,
|
|
3513
|
+
presentation: instance.presentation,
|
|
3514
|
+
rules: instance.rules.map((rule) => ({
|
|
3515
|
+
...rule,
|
|
3516
|
+
when: { ...rule.when, tolerance: { ...rule.when.tolerance } },
|
|
3517
|
+
})),
|
|
3518
|
+
})),
|
|
3519
|
+
reviewRules,
|
|
3520
|
+
periodAxis: definition.periodAxis,
|
|
3521
|
+
};
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
/** Compiles, validates, normalizes, fingerprints, and freezes a diagnostic definition. */
|
|
3525
|
+
export function compileDiagnosticDefinition(
|
|
3526
|
+
value: DiagnosticDefinition | NormalizedDiagnosticDefinitionIdentity,
|
|
3527
|
+
): CompiledDiagnosticDefinition {
|
|
3528
|
+
const boundaryIssues: DiagnosticValidationIssue[] = [];
|
|
3529
|
+
validateJsonValue(value, boundaryIssues);
|
|
3530
|
+
if (boundaryIssues.length > 0)
|
|
3531
|
+
throw new DiagnosticValidationError(boundaryIssues);
|
|
3532
|
+
let authored: DiagnosticDefinition;
|
|
3533
|
+
try {
|
|
3534
|
+
authored = authoredView(value);
|
|
3535
|
+
} catch {
|
|
3536
|
+
throw new DiagnosticValidationError([
|
|
3537
|
+
{
|
|
3538
|
+
domain: "definition",
|
|
3539
|
+
code: "invalid-type",
|
|
3540
|
+
path: "$",
|
|
3541
|
+
message:
|
|
3542
|
+
"Diagnostic definition does not have the required object and array structure",
|
|
3543
|
+
},
|
|
3544
|
+
]);
|
|
3545
|
+
}
|
|
3546
|
+
let definition: DiagnosticDefinition;
|
|
3547
|
+
try {
|
|
3548
|
+
definition = validateDefinition(authored);
|
|
3549
|
+
} catch (error) {
|
|
3550
|
+
if (error instanceof DiagnosticValidationError) throw error;
|
|
3551
|
+
throw new DiagnosticValidationError([
|
|
3552
|
+
{
|
|
3553
|
+
domain: "definition",
|
|
3554
|
+
code: "invalid-type",
|
|
3555
|
+
path: "$",
|
|
3556
|
+
message: "Diagnostic definition contains a malformed nested value",
|
|
3557
|
+
},
|
|
3558
|
+
]);
|
|
3559
|
+
}
|
|
3560
|
+
const normalized = normalizeDiagnosticDefinition(definition);
|
|
3561
|
+
const identities = buildDiagnosticIdentities(normalized);
|
|
3562
|
+
const compiledBase = {
|
|
3563
|
+
definition: normalized,
|
|
3564
|
+
formulaFingerprints: identities.formulaFingerprints,
|
|
3565
|
+
calculationFingerprints: identities.calculationFingerprints,
|
|
3566
|
+
definitionIntegrity: identities.definitionIntegrity,
|
|
3567
|
+
};
|
|
3568
|
+
Object.defineProperty(compiledBase, compiledDiagnosticDefinitionBrand, {
|
|
3569
|
+
value: true,
|
|
3570
|
+
enumerable: false,
|
|
3571
|
+
configurable: false,
|
|
3572
|
+
writable: false,
|
|
3573
|
+
});
|
|
3574
|
+
const compiled = compiledBase as CompiledDiagnosticDefinition;
|
|
3575
|
+
Object.freeze(compiled);
|
|
3576
|
+
authenticCompiledDefinitions.add(compiled);
|
|
3577
|
+
compiledInternals.set(compiled, {
|
|
3578
|
+
formulasById: new Map(
|
|
3579
|
+
normalized.formulas.map((formula) => [formula.id, formula]),
|
|
3580
|
+
),
|
|
3581
|
+
measuresById: new Map(
|
|
3582
|
+
definition.measures.map((measure) => [measure.id, measure]),
|
|
3583
|
+
),
|
|
3584
|
+
calculationScopesByInstanceId: identities.calculationScopesByInstanceId,
|
|
3585
|
+
calculationDependenciesByInstanceId:
|
|
3586
|
+
identities.calculationDependenciesByInstanceId,
|
|
3587
|
+
evaluationDependenciesByInstanceId:
|
|
3588
|
+
identities.evaluationDependenciesByInstanceId,
|
|
3589
|
+
derivationsByOutputMeasureId: new Map(
|
|
3590
|
+
normalized.derivedMeasures.map((derivation) => [
|
|
3591
|
+
derivation.outputMeasureId,
|
|
3592
|
+
derivation as DiagnosticDerivedMeasureDefinition,
|
|
3593
|
+
]),
|
|
3594
|
+
),
|
|
3595
|
+
});
|
|
3596
|
+
return compiled;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
export function assertCompiledDiagnosticDefinition(
|
|
3600
|
+
value: unknown,
|
|
3601
|
+
): asserts value is CompiledDiagnosticDefinition {
|
|
3602
|
+
if (
|
|
3603
|
+
(typeof value !== "object" && typeof value !== "function") ||
|
|
3604
|
+
value === null ||
|
|
3605
|
+
!authenticCompiledDefinitions.has(value)
|
|
3606
|
+
) {
|
|
3607
|
+
throw new DiagnosticValidationError([
|
|
3608
|
+
{
|
|
3609
|
+
domain: "definition",
|
|
3610
|
+
code: "invalid-input-relationship",
|
|
3611
|
+
path: "$",
|
|
3612
|
+
message: "Value is not an authentic compiled diagnostic definition",
|
|
3613
|
+
},
|
|
3614
|
+
]);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
/** @internal Shared only by core diagnostic runtime modules; not re-exported. */
|
|
3619
|
+
export function getCompiledDiagnosticDefinitionInternals(
|
|
3620
|
+
value: CompiledDiagnosticDefinition,
|
|
3621
|
+
): CompiledDiagnosticDefinitionInternals {
|
|
3622
|
+
assertCompiledDiagnosticDefinition(value);
|
|
3623
|
+
return compiledInternals.get(value)!;
|
|
3624
|
+
}
|