@actuarial-ts/core 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -3
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +3 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +30 -7
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticDefinitions.d.ts +3 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -1
- package/dist/diagnosticDefinitions.js +844 -182
- package/dist/diagnosticDefinitions.js.map +1 -1
- package/dist/diagnosticDerivations.d.ts +23 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -1
- package/dist/diagnosticDerivations.js +187 -43
- package/dist/diagnosticDerivations.js.map +1 -1
- package/dist/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +319 -21
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticExpressions.d.ts +1 -1
- package/dist/diagnosticExpressions.d.ts.map +1 -1
- package/dist/diagnosticExpressions.js +35 -15
- package/dist/diagnosticExpressions.js.map +1 -1
- package/dist/diagnosticFormulas.d.ts +3 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +160 -26
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts +15 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +321 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.js.map +1 -0
- package/dist/diagnosticOrdering.d.ts +8 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -0
- package/dist/diagnosticOrdering.js +73 -0
- package/dist/diagnosticOrdering.js.map +1 -0
- package/dist/diagnosticPeriodAxis.d.ts +9 -0
- package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
- package/dist/diagnosticPeriodAxis.js +69 -0
- package/dist/diagnosticPeriodAxis.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +2 -3
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +20 -68
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +75 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1606 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +40 -15
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +600 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +27 -2
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +764 -70
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts +26 -0
- package/dist/diagnosticRuntime.d.ts.map +1 -0
- package/dist/diagnosticRuntime.js +243 -0
- package/dist/diagnosticRuntime.js.map +1 -0
- package/dist/diagnosticSourceOrdering.d.ts +6 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
- package/dist/diagnosticSourceOrdering.js +38 -0
- package/dist/diagnosticSourceOrdering.js.map +1 -0
- package/dist/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts +4 -0
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +32 -4
- package/dist/mack.js.map +1 -1
- package/dist/triangle.d.ts.map +1 -1
- package/dist/triangle.js +59 -9
- package/dist/triangle.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/canonical.ts +144 -26
- package/src/capping.ts +109 -18
- package/src/diagnosticAggregation.ts +69 -20
- package/src/diagnosticDefinitions.ts +2675 -426
- package/src/diagnosticDerivations.ts +281 -49
- package/src/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +550 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +700 -191
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2558 -132
- package/src/diagnosticReview.ts +1072 -58
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +1273 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +69 -4
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- package/src/version.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
MAX_DIAGNOSTIC_DEFINITION_EXPRESSION_NODES,
|
|
3
|
+
MAX_DIAGNOSTIC_EXPRESSION_NODES,
|
|
3
4
|
walkDiagnosticExpression,
|
|
4
5
|
type DiagnosticClaimExpression,
|
|
5
6
|
type DiagnosticMeasureExpression,
|
|
@@ -18,6 +19,14 @@ import {
|
|
|
18
19
|
type DiagnosticValidationIssueCode,
|
|
19
20
|
} from "./types.js";
|
|
20
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";
|
|
21
30
|
|
|
22
31
|
export type DiagnosticMeasureKind = "count" | "amount" | "exposure";
|
|
23
32
|
export type DiagnosticMeasureSource = "loss" | "exposure" | "derived";
|
|
@@ -38,12 +47,11 @@ export type JsonValue =
|
|
|
38
47
|
| readonly JsonValue[]
|
|
39
48
|
| { readonly [key: string]: JsonValue };
|
|
40
49
|
|
|
41
|
-
export type DiagnosticDeepReadonly<T> =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
: T
|
|
45
|
-
|
|
46
|
-
: T;
|
|
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;
|
|
47
55
|
|
|
48
56
|
export interface DiagnosticMeasureDefinition {
|
|
49
57
|
id: string;
|
|
@@ -238,7 +246,10 @@ export type DiagnosticControlTotalProjection =
|
|
|
238
246
|
| { kind: "latest-valuation-per-origin" }
|
|
239
247
|
| { kind: "all-cells" };
|
|
240
248
|
|
|
241
|
-
export type DiagnosticReviewFilter = Omit<
|
|
249
|
+
export type DiagnosticReviewFilter = Omit<
|
|
250
|
+
DiagnosticsFilter,
|
|
251
|
+
"outputGroups" | "instanceIds"
|
|
252
|
+
>;
|
|
242
253
|
|
|
243
254
|
export type DiagnosticReviewRule =
|
|
244
255
|
| (DiagnosticReviewRuleBase & {
|
|
@@ -306,7 +317,9 @@ export interface DiagnosticMeasureStats {
|
|
|
306
317
|
readonly structural: number;
|
|
307
318
|
}
|
|
308
319
|
|
|
309
|
-
const compiledDiagnosticDefinitionBrand: unique symbol = Symbol(
|
|
320
|
+
const compiledDiagnosticDefinitionBrand: unique symbol = Symbol(
|
|
321
|
+
"compiled-diagnostic-definition",
|
|
322
|
+
);
|
|
310
323
|
|
|
311
324
|
export interface CompiledDiagnosticDefinition {
|
|
312
325
|
readonly [compiledDiagnosticDefinitionBrand]: true;
|
|
@@ -317,21 +330,37 @@ export interface CompiledDiagnosticDefinition {
|
|
|
317
330
|
}
|
|
318
331
|
|
|
319
332
|
export interface CompiledDiagnosticDefinitionInternals {
|
|
320
|
-
readonly formulasById: ReadonlyMap<
|
|
333
|
+
readonly formulasById: ReadonlyMap<
|
|
334
|
+
string,
|
|
335
|
+
NormalizedDiagnosticFormulaIdentity
|
|
336
|
+
>;
|
|
321
337
|
readonly measuresById: ReadonlyMap<string, DiagnosticMeasureDefinition>;
|
|
322
|
-
readonly calculationScopesByInstanceId: ReadonlyMap<
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
+
>;
|
|
326
354
|
}
|
|
327
355
|
|
|
328
356
|
const authenticCompiledDefinitions = new WeakSet<object>();
|
|
329
|
-
const compiledInternals = new WeakMap<
|
|
357
|
+
const compiledInternals = new WeakMap<
|
|
358
|
+
object,
|
|
359
|
+
CompiledDiagnosticDefinitionInternals
|
|
360
|
+
>();
|
|
330
361
|
|
|
331
362
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
332
|
-
|
|
333
|
-
const prototype: unknown = Object.getPrototypeOf(value);
|
|
334
|
-
return prototype === Object.prototype || prototype === null;
|
|
363
|
+
return isDiagnosticPlainRecord(value);
|
|
335
364
|
}
|
|
336
365
|
|
|
337
366
|
function propertyPath(parent: string, key: string): string {
|
|
@@ -349,17 +378,79 @@ function pushIssue(
|
|
|
349
378
|
issues.push({ domain: "definition", code, path, message });
|
|
350
379
|
}
|
|
351
380
|
|
|
352
|
-
function
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
+
}
|
|
361
453
|
}
|
|
362
|
-
return true;
|
|
363
454
|
}
|
|
364
455
|
|
|
365
456
|
function validateToken(
|
|
@@ -372,12 +463,22 @@ function validateToken(
|
|
|
372
463
|
pushIssue(issues, "invalid-type", path, `${label} must be a string`);
|
|
373
464
|
return false;
|
|
374
465
|
}
|
|
375
|
-
if (!
|
|
376
|
-
pushIssue(
|
|
466
|
+
if (!isWellFormedDiagnosticString(value)) {
|
|
467
|
+
pushIssue(
|
|
468
|
+
issues,
|
|
469
|
+
"invalid-string",
|
|
470
|
+
path,
|
|
471
|
+
`${label} contains an invalid Unicode string`,
|
|
472
|
+
);
|
|
377
473
|
return false;
|
|
378
474
|
}
|
|
379
|
-
if (
|
|
380
|
-
pushIssue(
|
|
475
|
+
if (!isDiagnosticToken(value)) {
|
|
476
|
+
pushIssue(
|
|
477
|
+
issues,
|
|
478
|
+
"invalid-string",
|
|
479
|
+
path,
|
|
480
|
+
`${label} must be nonempty without surrounding ASCII whitespace`,
|
|
481
|
+
);
|
|
381
482
|
return false;
|
|
382
483
|
}
|
|
383
484
|
return true;
|
|
@@ -396,67 +497,11 @@ function validateFinite(
|
|
|
396
497
|
return true;
|
|
397
498
|
}
|
|
398
499
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
function validateJsonValue(value: unknown, issues: DiagnosticValidationIssue[]): void {
|
|
406
|
-
const active = new WeakSet<object>();
|
|
407
|
-
const stack: JsonFrame[] = [{ value, path: "$", exiting: false }];
|
|
408
|
-
while (stack.length > 0) {
|
|
409
|
-
const frame = stack.pop()!;
|
|
410
|
-
if (frame.value === null || typeof frame.value === "boolean") continue;
|
|
411
|
-
if (typeof frame.value === "string") {
|
|
412
|
-
if (!hasWellFormedUtf16(frame.value)) {
|
|
413
|
-
pushIssue(issues, "invalid-json-value", frame.path, "String is not well-formed UTF-16 or contains U+0000");
|
|
414
|
-
}
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
if (typeof frame.value === "number") {
|
|
418
|
-
if (!Number.isFinite(frame.value)) {
|
|
419
|
-
pushIssue(issues, "invalid-json-value", frame.path, "JSON numeric value must be finite");
|
|
420
|
-
}
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
if (typeof frame.value !== "object" || frame.value === undefined) {
|
|
424
|
-
pushIssue(issues, "invalid-json-value", frame.path, "Value is not plain JSON data");
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
const object = frame.value as object;
|
|
428
|
-
if (frame.exiting) {
|
|
429
|
-
active.delete(object);
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (active.has(object)) {
|
|
433
|
-
pushIssue(issues, "cycle", frame.path, "JSON value contains a cycle");
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
if (!Array.isArray(object) && !isPlainRecord(object)) {
|
|
437
|
-
pushIssue(issues, "invalid-json-value", frame.path, "Value must use a plain object or array prototype");
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
active.add(object);
|
|
441
|
-
stack.push({ value: object, path: frame.path, exiting: true });
|
|
442
|
-
if (Array.isArray(object)) {
|
|
443
|
-
for (let index = object.length - 1; index >= 0; index--) {
|
|
444
|
-
stack.push({ value: object[index], path: `${frame.path}[${index}]`, exiting: false });
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
for (const key of Object.keys(object).sort().reverse()) {
|
|
448
|
-
if (!hasWellFormedUtf16(key)) {
|
|
449
|
-
pushIssue(issues, "invalid-json-value", propertyPath(frame.path, key), "Object key is not well-formed UTF-16 or contains U+0000");
|
|
450
|
-
}
|
|
451
|
-
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
452
|
-
if (!descriptor || !("value" in descriptor)) {
|
|
453
|
-
pushIssue(issues, "invalid-json-value", propertyPath(frame.path, key), "JSON objects may contain only data properties");
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
stack.push({ value: descriptor.value, path: propertyPath(frame.path, key), exiting: false });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
}
|
|
500
|
+
function validateJsonValue(
|
|
501
|
+
value: unknown,
|
|
502
|
+
issues: DiagnosticValidationIssue[],
|
|
503
|
+
): void {
|
|
504
|
+
issues.push(...diagnosticJsonPreflight(value, "definition"));
|
|
460
505
|
}
|
|
461
506
|
|
|
462
507
|
function requireArray(
|
|
@@ -489,7 +534,8 @@ function collectCatalog<T extends { readonly id: string }>(
|
|
|
489
534
|
|
|
490
535
|
function semanticReference(measure: DiagnosticMeasureDefinition): string {
|
|
491
536
|
if (measure.kind === "amount") return `amount:${measure.basisId ?? ""}`;
|
|
492
|
-
if (measure.kind === "count")
|
|
537
|
+
if (measure.kind === "count")
|
|
538
|
+
return `count:${measure.countPopulationId ?? ""}`;
|
|
493
539
|
return `exposure:${measure.exposureBasisId ?? ""}`;
|
|
494
540
|
}
|
|
495
541
|
|
|
@@ -505,7 +551,10 @@ function transitiveMeasureIds(
|
|
|
505
551
|
result.add(id);
|
|
506
552
|
const derivation = derivations.get(id);
|
|
507
553
|
if (derivation) {
|
|
508
|
-
stack.push(
|
|
554
|
+
stack.push(
|
|
555
|
+
...walkDiagnosticExpression(derivation.expression, "claim", "$", [])
|
|
556
|
+
.dependencies,
|
|
557
|
+
);
|
|
509
558
|
}
|
|
510
559
|
}
|
|
511
560
|
return [...result].sort();
|
|
@@ -524,11 +573,15 @@ interface ProjectedClaimSemantics {
|
|
|
524
573
|
readonly amountBasis?: ProjectedAmountBasis;
|
|
525
574
|
}
|
|
526
575
|
|
|
527
|
-
function projectedAmountBasis(
|
|
576
|
+
function projectedAmountBasis(
|
|
577
|
+
basis: AmountBasisDefinition,
|
|
578
|
+
): ProjectedAmountBasis {
|
|
528
579
|
return {
|
|
529
580
|
currency: basis.currency,
|
|
530
581
|
perspective: basis.perspective,
|
|
531
|
-
components: [...basis.components].sort((left, right) =>
|
|
582
|
+
components: [...basis.components].sort((left, right) =>
|
|
583
|
+
left.id < right.id ? -1 : left.id > right.id ? 1 : 0,
|
|
584
|
+
),
|
|
532
585
|
};
|
|
533
586
|
}
|
|
534
587
|
|
|
@@ -547,23 +600,49 @@ function projectClaimExpression(
|
|
|
547
600
|
const measure = measures.get(expression.measureId);
|
|
548
601
|
if (!measure) return null;
|
|
549
602
|
if (measure.kind !== "amount") {
|
|
550
|
-
return {
|
|
603
|
+
return {
|
|
604
|
+
kind: measure.kind,
|
|
605
|
+
unit: measure.unit,
|
|
606
|
+
reference: semanticReference(measure),
|
|
607
|
+
};
|
|
551
608
|
}
|
|
552
|
-
const basis = measure.basisId
|
|
609
|
+
const basis = measure.basisId
|
|
610
|
+
? amountBases.get(measure.basisId)
|
|
611
|
+
: undefined;
|
|
553
612
|
return basis
|
|
554
|
-
? {
|
|
613
|
+
? {
|
|
614
|
+
kind: "amount",
|
|
615
|
+
unit: measure.unit,
|
|
616
|
+
reference: `amount:${measure.basisId}`,
|
|
617
|
+
amountBasis: projectedAmountBasis(basis),
|
|
618
|
+
}
|
|
555
619
|
: null;
|
|
556
620
|
}
|
|
557
621
|
if (expression.op === "claim-layer") {
|
|
558
622
|
const measure = measures.get(expression.measureId);
|
|
559
623
|
if (!measure || measure.kind !== "amount" || !measure.basisId) {
|
|
560
|
-
pushIssue(
|
|
624
|
+
pushIssue(
|
|
625
|
+
issues,
|
|
626
|
+
"incompatible-semantics",
|
|
627
|
+
path,
|
|
628
|
+
"Claim layers require an amount measure",
|
|
629
|
+
);
|
|
561
630
|
return null;
|
|
562
631
|
}
|
|
563
632
|
const basis = amountBases.get(measure.basisId);
|
|
564
633
|
const component = basis?.components[0];
|
|
565
|
-
if (
|
|
566
|
-
|
|
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
|
+
);
|
|
567
646
|
return null;
|
|
568
647
|
}
|
|
569
648
|
return {
|
|
@@ -573,56 +652,97 @@ function projectClaimExpression(
|
|
|
573
652
|
amountBasis: {
|
|
574
653
|
currency: basis.currency,
|
|
575
654
|
perspective: basis.perspective,
|
|
576
|
-
components: [
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
+
},
|
|
585
666
|
},
|
|
586
|
-
|
|
667
|
+
],
|
|
587
668
|
},
|
|
588
669
|
};
|
|
589
670
|
}
|
|
590
|
-
const children =
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
+
);
|
|
598
686
|
if (projected.some((item) => item === null)) return null;
|
|
599
687
|
const present = projected as ProjectedClaimSemantics[];
|
|
600
688
|
if (expression.op === "subtract") {
|
|
601
689
|
const [left, right] = present;
|
|
602
|
-
if (
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
+
);
|
|
607
704
|
return null;
|
|
608
705
|
}
|
|
609
706
|
return left!;
|
|
610
707
|
}
|
|
611
708
|
const first = present[0]!;
|
|
612
|
-
if (
|
|
613
|
-
|
|
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
|
+
);
|
|
614
718
|
return null;
|
|
615
719
|
}
|
|
616
720
|
if (first.kind !== "amount") {
|
|
617
721
|
if (present.some((item) => item.reference !== first.reference)) {
|
|
618
|
-
pushIssue(
|
|
722
|
+
pushIssue(
|
|
723
|
+
issues,
|
|
724
|
+
"incompatible-semantics",
|
|
725
|
+
path,
|
|
726
|
+
"Claim addition requires one exact semantic basis",
|
|
727
|
+
);
|
|
619
728
|
return null;
|
|
620
729
|
}
|
|
621
730
|
return first;
|
|
622
731
|
}
|
|
623
732
|
const bases = present.map((item) => item.amountBasis!);
|
|
624
|
-
if (
|
|
625
|
-
|
|
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
|
+
);
|
|
626
746
|
return null;
|
|
627
747
|
}
|
|
628
748
|
const componentIds = new Set<string>();
|
|
@@ -630,7 +750,12 @@ function projectClaimExpression(
|
|
|
630
750
|
for (const basis of bases) {
|
|
631
751
|
for (const component of basis.components) {
|
|
632
752
|
if (componentIds.has(component.id)) {
|
|
633
|
-
pushIssue(
|
|
753
|
+
pushIssue(
|
|
754
|
+
issues,
|
|
755
|
+
"incompatible-semantics",
|
|
756
|
+
path,
|
|
757
|
+
`Amount addition overlaps component ${component.id}`,
|
|
758
|
+
);
|
|
634
759
|
return null;
|
|
635
760
|
}
|
|
636
761
|
componentIds.add(component.id);
|
|
@@ -640,9 +765,16 @@ function projectClaimExpression(
|
|
|
640
765
|
const amountBasis = {
|
|
641
766
|
currency: bases[0]!.currency,
|
|
642
767
|
perspective: bases[0]!.perspective,
|
|
643
|
-
components: components.sort((left, right) =>
|
|
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,
|
|
644
777
|
};
|
|
645
|
-
return { kind: "amount", unit: first.unit, reference: "amount:projected", amountBasis };
|
|
646
778
|
}
|
|
647
779
|
|
|
648
780
|
function expressionSemantics(
|
|
@@ -650,21 +782,40 @@ function expressionSemantics(
|
|
|
650
782
|
path: string,
|
|
651
783
|
measures: ReadonlyMap<string, DiagnosticMeasureDefinition>,
|
|
652
784
|
issues: DiagnosticValidationIssue[],
|
|
653
|
-
|
|
654
|
-
|
|
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
|
+
);
|
|
655
797
|
const found = walked.dependencies
|
|
656
798
|
.map((id) => measures.get(id))
|
|
657
|
-
.filter(
|
|
799
|
+
.filter(
|
|
800
|
+
(value): value is DiagnosticMeasureDefinition => value !== undefined,
|
|
801
|
+
);
|
|
658
802
|
for (const id of walked.dependencies) {
|
|
659
803
|
if (!measures.has(id)) {
|
|
660
804
|
pushIssue(issues, "unknown-reference", path, `Unknown measure ${id}`);
|
|
661
805
|
}
|
|
662
806
|
}
|
|
663
|
-
const signatures = new Set(
|
|
664
|
-
|
|
665
|
-
|
|
807
|
+
const signatures = new Set(
|
|
808
|
+
found.map((measure) =>
|
|
809
|
+
canonicalJson([measure.kind, measure.unit, semanticReference(measure)]),
|
|
810
|
+
),
|
|
811
|
+
);
|
|
666
812
|
if (signatures.size > 1) {
|
|
667
|
-
pushIssue(
|
|
813
|
+
pushIssue(
|
|
814
|
+
issues,
|
|
815
|
+
"incompatible-semantics",
|
|
816
|
+
path,
|
|
817
|
+
"Expression combines incompatible measure semantics",
|
|
818
|
+
);
|
|
668
819
|
}
|
|
669
820
|
return {
|
|
670
821
|
measureIds: walked.dependencies,
|
|
@@ -680,60 +831,600 @@ function validateTolerance(
|
|
|
680
831
|
for (const key of ["absolute", "relative"] as const) {
|
|
681
832
|
const component = value?.[key];
|
|
682
833
|
if (component === undefined) continue;
|
|
683
|
-
if (
|
|
834
|
+
if (
|
|
835
|
+
!validateFinite(component, `${path}.${key}`, `${key} tolerance`, issues)
|
|
836
|
+
)
|
|
837
|
+
continue;
|
|
684
838
|
if (component < 0) {
|
|
685
|
-
pushIssue(
|
|
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
|
+
);
|
|
686
928
|
}
|
|
687
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
|
+
}
|
|
688
964
|
}
|
|
689
965
|
|
|
690
|
-
function
|
|
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 {
|
|
691
1232
|
if (!isPlainRecord(axis)) {
|
|
692
|
-
pushIssue(
|
|
1233
|
+
pushIssue(
|
|
1234
|
+
issues,
|
|
1235
|
+
"invalid-type",
|
|
1236
|
+
"$.periodAxis",
|
|
1237
|
+
"Period axis must be an object",
|
|
1238
|
+
);
|
|
693
1239
|
return;
|
|
694
1240
|
}
|
|
695
|
-
if (
|
|
696
|
-
|
|
697
|
-
|
|
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
|
+
);
|
|
698
1256
|
}
|
|
699
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
|
+
);
|
|
700
1272
|
if (!["month", "quarter", "year"].includes(axis.originCadence)) {
|
|
701
|
-
pushIssue(
|
|
1273
|
+
pushIssue(
|
|
1274
|
+
issues,
|
|
1275
|
+
"invalid-period",
|
|
1276
|
+
"$.periodAxis.originCadence",
|
|
1277
|
+
"Unknown origin cadence",
|
|
1278
|
+
);
|
|
702
1279
|
}
|
|
703
1280
|
if (!["month", "quarter", "year"].includes(axis.valuationCadence)) {
|
|
704
|
-
pushIssue(
|
|
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
|
+
);
|
|
705
1303
|
}
|
|
706
1304
|
if (axis.ageUnit !== "month") {
|
|
707
|
-
pushIssue(
|
|
1305
|
+
pushIssue(
|
|
1306
|
+
issues,
|
|
1307
|
+
"invalid-period",
|
|
1308
|
+
"$.periodAxis.ageUnit",
|
|
1309
|
+
"Calendar axes use month age units",
|
|
1310
|
+
);
|
|
708
1311
|
}
|
|
709
1312
|
return;
|
|
710
1313
|
}
|
|
1314
|
+
validateAllowedKeys(
|
|
1315
|
+
axis,
|
|
1316
|
+
["kind", "id", "version", "ageUnit", "ageOffset", "origins", "valuations"],
|
|
1317
|
+
"$.periodAxis",
|
|
1318
|
+
issues,
|
|
1319
|
+
);
|
|
711
1320
|
if (axis.kind !== "ordered") {
|
|
712
|
-
pushIssue(
|
|
1321
|
+
pushIssue(
|
|
1322
|
+
issues,
|
|
1323
|
+
"invalid-period",
|
|
1324
|
+
"$.periodAxis.kind",
|
|
1325
|
+
"Unknown period-axis kind",
|
|
1326
|
+
);
|
|
713
1327
|
return;
|
|
714
1328
|
}
|
|
715
1329
|
validateToken(axis.id, "$.periodAxis.id", "Ordered-axis ID", issues);
|
|
716
|
-
validateToken(
|
|
717
|
-
|
|
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
|
+
);
|
|
718
1342
|
for (const side of ["origins", "valuations"] as const) {
|
|
719
|
-
const coordinates = requireArray(
|
|
1343
|
+
const coordinates = requireArray(
|
|
1344
|
+
axis[side],
|
|
1345
|
+
`$.periodAxis.${side}`,
|
|
1346
|
+
issues,
|
|
1347
|
+
) as readonly DiagnosticPeriodCoordinate[];
|
|
720
1348
|
const names = new Set<string>();
|
|
721
1349
|
const coordinateValues = new Set<number>();
|
|
722
1350
|
coordinates.forEach((coordinate, index) => {
|
|
723
1351
|
const base = `$.periodAxis.${side}[${index}]`;
|
|
724
|
-
if (
|
|
725
|
-
|
|
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
|
+
);
|
|
726
1382
|
names.add(coordinate.label);
|
|
727
1383
|
}
|
|
728
|
-
if (
|
|
729
|
-
|
|
730
|
-
|
|
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
|
+
);
|
|
731
1399
|
} else if (coordinateValues.has(coordinate.coordinate)) {
|
|
732
|
-
pushIssue(
|
|
1400
|
+
pushIssue(
|
|
1401
|
+
issues,
|
|
1402
|
+
"duplicate-id",
|
|
1403
|
+
`${base}.coordinate`,
|
|
1404
|
+
`Duplicate period coordinate ${coordinate.coordinate}`,
|
|
1405
|
+
);
|
|
733
1406
|
} else coordinateValues.add(coordinate.coordinate);
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
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
|
+
);
|
|
737
1428
|
names.add(alias);
|
|
738
1429
|
}
|
|
739
1430
|
});
|
|
@@ -744,37 +1435,121 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
744
1435
|
const issues: DiagnosticValidationIssue[] = [];
|
|
745
1436
|
validateJsonValue(value, issues);
|
|
746
1437
|
if (!isPlainRecord(value)) {
|
|
747
|
-
pushIssue(
|
|
1438
|
+
pushIssue(
|
|
1439
|
+
issues,
|
|
1440
|
+
"invalid-type",
|
|
1441
|
+
"$",
|
|
1442
|
+
"Diagnostic definition must be a plain object",
|
|
1443
|
+
);
|
|
748
1444
|
throw new DiagnosticValidationError(issues);
|
|
749
1445
|
}
|
|
750
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
|
+
);
|
|
751
1467
|
if (definition.diagnosticDefinitionVersion !== "1.0.0") {
|
|
752
|
-
pushIssue(
|
|
1468
|
+
pushIssue(
|
|
1469
|
+
issues,
|
|
1470
|
+
"invalid-type",
|
|
1471
|
+
"$.diagnosticDefinitionVersion",
|
|
1472
|
+
"Diagnostic definition version must be 1.0.0",
|
|
1473
|
+
);
|
|
753
1474
|
}
|
|
754
1475
|
validateToken(definition.id, "$.id", "Definition ID", issues);
|
|
755
1476
|
validateToken(definition.version, "$.version", "Definition version", issues);
|
|
756
|
-
if (
|
|
757
|
-
|
|
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
|
+
);
|
|
758
1487
|
}
|
|
759
1488
|
|
|
760
|
-
const measures = requireArray(
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
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[];
|
|
768
1529
|
|
|
769
1530
|
const measureMap = collectCatalog(measures, "$.measures", issues);
|
|
770
|
-
const populationMap = collectCatalog(
|
|
771
|
-
|
|
1531
|
+
const populationMap = collectCatalog(
|
|
1532
|
+
populations,
|
|
1533
|
+
"$.countPopulations",
|
|
1534
|
+
issues,
|
|
1535
|
+
);
|
|
1536
|
+
const exposureBasisMap = collectCatalog(
|
|
1537
|
+
exposureBases,
|
|
1538
|
+
"$.exposureBases",
|
|
1539
|
+
issues,
|
|
1540
|
+
);
|
|
772
1541
|
const amountBasisMap = collectCatalog(amountBases, "$.amountBases", issues);
|
|
773
1542
|
collectCatalog(derivations, "$.derivedMeasures", issues);
|
|
774
|
-
const derivationsByOutput = new Map<
|
|
1543
|
+
const derivationsByOutput = new Map<
|
|
1544
|
+
string,
|
|
1545
|
+
DiagnosticDerivedMeasureDefinition
|
|
1546
|
+
>();
|
|
775
1547
|
for (const derivation of derivations) {
|
|
776
|
-
if (
|
|
777
|
-
|
|
1548
|
+
if (
|
|
1549
|
+
isPlainRecord(derivation) &&
|
|
1550
|
+
typeof derivation.outputMeasureId === "string" &&
|
|
1551
|
+
!derivationsByOutput.has(derivation.outputMeasureId)
|
|
1552
|
+
) {
|
|
778
1553
|
derivationsByOutput.set(derivation.outputMeasureId, derivation);
|
|
779
1554
|
}
|
|
780
1555
|
}
|
|
@@ -784,135 +1559,600 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
784
1559
|
|
|
785
1560
|
populations.forEach((population, index) => {
|
|
786
1561
|
const base = `$.countPopulations[${index}]`;
|
|
787
|
-
|
|
788
|
-
|
|
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
|
+
);
|
|
789
1589
|
validateToken(population.unit, `${base}.unit`, "Population unit", issues);
|
|
790
|
-
if (
|
|
791
|
-
|
|
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
|
+
);
|
|
792
1606
|
}
|
|
1607
|
+
validateScalarAttributes(
|
|
1608
|
+
population.attributes,
|
|
1609
|
+
`${base}.attributes`,
|
|
1610
|
+
issues,
|
|
1611
|
+
);
|
|
793
1612
|
});
|
|
794
1613
|
|
|
795
1614
|
exposureBases.forEach((basis, index) => {
|
|
796
1615
|
const base = `$.exposureBases[${index}]`;
|
|
797
|
-
|
|
798
|
-
|
|
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
|
+
);
|
|
799
1651
|
validateToken(basis.unit, `${base}.unit`, "Exposure-basis unit", issues);
|
|
800
|
-
if (basis.sourceDescription !== undefined)
|
|
801
|
-
|
|
802
|
-
|
|
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
|
+
);
|
|
803
1670
|
}
|
|
1671
|
+
validateScalarAttributes(basis.attributes, `${base}.attributes`, issues);
|
|
804
1672
|
});
|
|
805
1673
|
|
|
806
1674
|
amountBases.forEach((basis, index) => {
|
|
807
1675
|
const base = `$.amountBases[${index}]`;
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
+
);
|
|
813
1722
|
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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
|
+
);
|
|
819
1751
|
}
|
|
820
1752
|
components.forEach((component, componentIndex) => {
|
|
821
1753
|
const componentBase = `${base}.components[${componentIndex}]`;
|
|
822
1754
|
if (!isPlainRecord(component)) {
|
|
823
|
-
pushIssue(
|
|
1755
|
+
pushIssue(
|
|
1756
|
+
issues,
|
|
1757
|
+
"invalid-type",
|
|
1758
|
+
componentBase,
|
|
1759
|
+
"Amount-basis component must be an object",
|
|
1760
|
+
);
|
|
824
1761
|
return;
|
|
825
1762
|
}
|
|
1763
|
+
validateAllowedKeys(
|
|
1764
|
+
component,
|
|
1765
|
+
["id", "treatment", "limitation"],
|
|
1766
|
+
componentBase,
|
|
1767
|
+
issues,
|
|
1768
|
+
);
|
|
826
1769
|
if (!["included", "excluded", "unknown"].includes(component.treatment)) {
|
|
827
|
-
pushIssue(
|
|
1770
|
+
pushIssue(
|
|
1771
|
+
issues,
|
|
1772
|
+
"invalid-type",
|
|
1773
|
+
`${componentBase}.treatment`,
|
|
1774
|
+
"Unknown component treatment",
|
|
1775
|
+
);
|
|
828
1776
|
}
|
|
829
1777
|
const limitation = component.limitation;
|
|
830
1778
|
if (!isPlainRecord(limitation)) {
|
|
831
|
-
pushIssue(
|
|
1779
|
+
pushIssue(
|
|
1780
|
+
issues,
|
|
1781
|
+
"invalid-type",
|
|
1782
|
+
`${componentBase}.limitation`,
|
|
1783
|
+
"Amount limitation must be an object",
|
|
1784
|
+
);
|
|
832
1785
|
return;
|
|
833
1786
|
}
|
|
834
1787
|
if (limitation.kind === "unknown") {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
+
);
|
|
839
1838
|
}
|
|
840
1839
|
if (limitation.limit !== null) {
|
|
841
|
-
if (
|
|
842
|
-
|
|
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
|
+
);
|
|
843
1855
|
}
|
|
844
|
-
if (
|
|
845
|
-
|
|
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
|
+
);
|
|
846
1867
|
}
|
|
847
1868
|
}
|
|
848
1869
|
if (limitation.derivation?.kind === "sdk") {
|
|
849
|
-
if (limitation.
|
|
850
|
-
|
|
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
|
+
);
|
|
851
1887
|
}
|
|
852
1888
|
} else if (limitation.derivation?.kind === "external") {
|
|
853
|
-
|
|
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
|
+
);
|
|
854
1913
|
} else {
|
|
855
|
-
pushIssue(
|
|
1914
|
+
pushIssue(
|
|
1915
|
+
issues,
|
|
1916
|
+
"invalid-type",
|
|
1917
|
+
`${componentBase}.limitation.derivation`,
|
|
1918
|
+
"Limited basis requires a derivation record",
|
|
1919
|
+
);
|
|
856
1920
|
}
|
|
857
1921
|
} else if (limitation.kind !== "unlimited") {
|
|
858
|
-
pushIssue(
|
|
859
|
-
|
|
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
|
+
);
|
|
860
1935
|
});
|
|
861
1936
|
});
|
|
862
1937
|
|
|
863
1938
|
measures.forEach((measure, index) => {
|
|
864
1939
|
const base = `$.measures[${index}]`;
|
|
865
|
-
|
|
866
|
-
|
|
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
|
+
);
|
|
867
1976
|
validateToken(measure.unit, `${base}.unit`, "Measure unit", issues);
|
|
868
|
-
if (measure.aggregation !== "sum")
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
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
|
+
);
|
|
872
2002
|
}
|
|
873
2003
|
if (measure.kind === "amount") {
|
|
874
|
-
const basis = measure.basisId
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (
|
|
878
|
-
pushIssue(
|
|
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
|
+
);
|
|
879
2032
|
}
|
|
880
2033
|
} else if (measure.kind === "count") {
|
|
881
|
-
const population = measure.countPopulationId
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
if (
|
|
885
|
-
pushIssue(
|
|
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
|
+
);
|
|
886
2062
|
}
|
|
887
2063
|
} else if (measure.kind === "exposure") {
|
|
888
|
-
const basis = measure.exposureBasisId
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
if (
|
|
892
|
-
|
|
893
|
-
|
|
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
|
+
);
|
|
894
2098
|
}
|
|
895
|
-
if (measure.missing !== "unknown")
|
|
896
|
-
|
|
897
|
-
|
|
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
|
+
);
|
|
898
2116
|
}
|
|
899
2117
|
} else {
|
|
900
2118
|
pushIssue(issues, "invalid-type", `${base}.kind`, "Unknown measure kind");
|
|
901
2119
|
}
|
|
902
|
-
if (
|
|
903
|
-
|
|
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
|
+
);
|
|
904
2131
|
}
|
|
905
2132
|
if (measure.kind !== "exposure" && measure.source === "exposure") {
|
|
906
|
-
pushIssue(
|
|
2133
|
+
pushIssue(
|
|
2134
|
+
issues,
|
|
2135
|
+
"incompatible-semantics",
|
|
2136
|
+
`${base}.source`,
|
|
2137
|
+
"Only exposure measures may use exposure source",
|
|
2138
|
+
);
|
|
907
2139
|
}
|
|
908
2140
|
if (measure.kind === "amount" && measure.basisId) {
|
|
909
2141
|
const basis = amountBasisMap.get(measure.basisId);
|
|
910
|
-
const hasSdkLimitation =
|
|
911
|
-
(
|
|
912
|
-
|
|
913
|
-
|
|
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;
|
|
914
2149
|
if (hasSdkLimitation && measure.source !== "derived") {
|
|
915
|
-
pushIssue(
|
|
2150
|
+
pushIssue(
|
|
2151
|
+
issues,
|
|
2152
|
+
"incompatible-semantics",
|
|
2153
|
+
`${base}.source`,
|
|
2154
|
+
"SDK-limited amount measures must be derived",
|
|
2155
|
+
);
|
|
916
2156
|
}
|
|
917
2157
|
}
|
|
918
2158
|
});
|
|
@@ -921,89 +2161,308 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
921
2161
|
const formulaRoleUsage = new Map<string, readonly string[]>();
|
|
922
2162
|
formulas.forEach((formula, index) => {
|
|
923
2163
|
const base = `$.formulas[${index}]`;
|
|
924
|
-
|
|
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
|
+
);
|
|
925
2187
|
if (formula.denominatorPolicy !== "positive-or-null") {
|
|
926
|
-
pushIssue(
|
|
2188
|
+
pushIssue(
|
|
2189
|
+
issues,
|
|
2190
|
+
"incompatible-semantics",
|
|
2191
|
+
`${base}.denominatorPolicy`,
|
|
2192
|
+
"Unknown denominator policy",
|
|
2193
|
+
);
|
|
927
2194
|
}
|
|
928
2195
|
if (!isPlainRecord(formula.roles)) {
|
|
929
|
-
pushIssue(
|
|
2196
|
+
pushIssue(
|
|
2197
|
+
issues,
|
|
2198
|
+
"invalid-type",
|
|
2199
|
+
`${base}.roles`,
|
|
2200
|
+
"Formula roles must be an object",
|
|
2201
|
+
);
|
|
930
2202
|
return;
|
|
931
2203
|
}
|
|
932
2204
|
for (const [roleName, role] of Object.entries(formula.roles)) {
|
|
933
|
-
validateToken(
|
|
934
|
-
|
|
935
|
-
|
|
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
|
+
}
|
|
936
2242
|
}
|
|
937
|
-
if (role.compatibilityGroup !== undefined)
|
|
2243
|
+
if (role.compatibilityGroup !== undefined)
|
|
2244
|
+
validateToken(
|
|
2245
|
+
role.compatibilityGroup,
|
|
2246
|
+
`${propertyPath(`${base}.roles`, roleName)}.compatibilityGroup`,
|
|
2247
|
+
"Compatibility group",
|
|
2248
|
+
issues,
|
|
2249
|
+
);
|
|
938
2250
|
}
|
|
939
|
-
const numerator = walkDiagnosticExpression(
|
|
940
|
-
|
|
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
|
+
);
|
|
941
2263
|
definitionExpressionNodes += numerator.nodeCount + denominator.nodeCount;
|
|
942
|
-
const used = [
|
|
2264
|
+
const used = [
|
|
2265
|
+
...new Set([...numerator.dependencies, ...denominator.dependencies]),
|
|
2266
|
+
].sort();
|
|
943
2267
|
formulaRoleUsage.set(formula.id, used);
|
|
944
2268
|
for (const role of used) {
|
|
945
|
-
if (!
|
|
946
|
-
pushIssue(
|
|
2269
|
+
if (!hasDiagnosticOwn(formula.roles, role)) {
|
|
2270
|
+
pushIssue(
|
|
2271
|
+
issues,
|
|
2272
|
+
"unknown-reference",
|
|
2273
|
+
base,
|
|
2274
|
+
`Formula ${formula.id} references unknown role ${role}`,
|
|
2275
|
+
);
|
|
947
2276
|
}
|
|
948
2277
|
}
|
|
949
2278
|
for (const role of Object.keys(formula.roles)) {
|
|
950
|
-
if (!used.includes(role))
|
|
2279
|
+
if (!used.includes(role))
|
|
2280
|
+
pushIssue(
|
|
2281
|
+
issues,
|
|
2282
|
+
"incompatible-semantics",
|
|
2283
|
+
propertyPath(`${base}.roles`, role),
|
|
2284
|
+
`Formula role ${role} is unused`,
|
|
2285
|
+
);
|
|
951
2286
|
}
|
|
952
|
-
for (const [side, walked] of [
|
|
953
|
-
|
|
954
|
-
|
|
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
|
+
);
|
|
955
2303
|
}
|
|
956
2304
|
});
|
|
957
2305
|
|
|
958
2306
|
const instanceRuleIds = new Set<string>();
|
|
959
2307
|
instances.forEach((instance, index) => {
|
|
960
2308
|
const base = `$.instances[${index}]`;
|
|
961
|
-
|
|
962
|
-
|
|
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
|
+
);
|
|
963
2336
|
const formula = formulaMap.get(instance.formulaId);
|
|
964
2337
|
if (!formula) {
|
|
965
|
-
pushIssue(
|
|
2338
|
+
pushIssue(
|
|
2339
|
+
issues,
|
|
2340
|
+
"unknown-reference",
|
|
2341
|
+
`${base}.formulaId`,
|
|
2342
|
+
`Unknown formula ${instance.formulaId}`,
|
|
2343
|
+
);
|
|
966
2344
|
return;
|
|
967
2345
|
}
|
|
968
2346
|
if (!isPlainRecord(instance.bindings)) {
|
|
969
|
-
pushIssue(
|
|
2347
|
+
pushIssue(
|
|
2348
|
+
issues,
|
|
2349
|
+
"invalid-type",
|
|
2350
|
+
`${base}.bindings`,
|
|
2351
|
+
"Instance bindings must be an object",
|
|
2352
|
+
);
|
|
970
2353
|
return;
|
|
971
2354
|
}
|
|
972
2355
|
const usedRoles = formulaRoleUsage.get(formula.id) ?? [];
|
|
973
|
-
const bindingSemantics = new Map<
|
|
2356
|
+
const bindingSemantics = new Map<
|
|
2357
|
+
string,
|
|
2358
|
+
ReturnType<typeof expressionSemantics>
|
|
2359
|
+
>();
|
|
974
2360
|
for (const roleName of usedRoles) {
|
|
975
|
-
if (!
|
|
976
|
-
pushIssue(
|
|
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
|
+
);
|
|
977
2368
|
continue;
|
|
978
2369
|
}
|
|
979
2370
|
const expression = instance.bindings[roleName]!;
|
|
980
|
-
const semantics = expressionSemantics(
|
|
2371
|
+
const semantics = expressionSemantics(
|
|
2372
|
+
expression,
|
|
2373
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2374
|
+
measureMap,
|
|
2375
|
+
issues,
|
|
2376
|
+
);
|
|
981
2377
|
bindingSemantics.set(roleName, semantics);
|
|
982
|
-
definitionExpressionNodes += walkDiagnosticExpression(
|
|
2378
|
+
definitionExpressionNodes += walkDiagnosticExpression(
|
|
2379
|
+
expression,
|
|
2380
|
+
"measure",
|
|
2381
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2382
|
+
[],
|
|
2383
|
+
).nodeCount;
|
|
983
2384
|
const role = formula.roles[roleName]!;
|
|
984
|
-
const boundMeasures = semantics.measureIds
|
|
2385
|
+
const boundMeasures = semantics.measureIds
|
|
2386
|
+
.map((id) => measureMap.get(id))
|
|
2387
|
+
.filter(
|
|
2388
|
+
(item): item is DiagnosticMeasureDefinition => item !== undefined,
|
|
2389
|
+
);
|
|
985
2390
|
if (boundMeasures.some((measure) => measure.kind !== role.kind)) {
|
|
986
|
-
pushIssue(
|
|
2391
|
+
pushIssue(
|
|
2392
|
+
issues,
|
|
2393
|
+
"incompatible-semantics",
|
|
2394
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2395
|
+
`Binding kind does not match role ${roleName}`,
|
|
2396
|
+
);
|
|
987
2397
|
}
|
|
988
2398
|
if (role.developmentSemantics !== undefined) {
|
|
989
|
-
const leaves = transitiveMeasureIds(
|
|
2399
|
+
const leaves = transitiveMeasureIds(
|
|
2400
|
+
semantics.measureIds,
|
|
2401
|
+
derivationsByOutput,
|
|
2402
|
+
)
|
|
990
2403
|
.map((id) => measureMap.get(id))
|
|
991
|
-
.filter(
|
|
992
|
-
|
|
993
|
-
|
|
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
|
+
);
|
|
994
2420
|
}
|
|
995
2421
|
}
|
|
996
2422
|
}
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
+
);
|
|
1000
2449
|
if (signatures.size > 1) {
|
|
1001
|
-
pushIssue(
|
|
2450
|
+
pushIssue(
|
|
2451
|
+
issues,
|
|
2452
|
+
"incompatible-semantics",
|
|
2453
|
+
`${base}.bindings`,
|
|
2454
|
+
`Bound ${side} arithmetic combines incompatible semantics`,
|
|
2455
|
+
);
|
|
1002
2456
|
}
|
|
1003
2457
|
}
|
|
1004
2458
|
for (const binding of Object.keys(instance.bindings)) {
|
|
1005
2459
|
if (!usedRoles.includes(binding)) {
|
|
1006
|
-
pushIssue(
|
|
2460
|
+
pushIssue(
|
|
2461
|
+
issues,
|
|
2462
|
+
"unknown-reference",
|
|
2463
|
+
propertyPath(`${base}.bindings`, binding),
|
|
2464
|
+
`Unexpected binding for role ${binding}`,
|
|
2465
|
+
);
|
|
1007
2466
|
}
|
|
1008
2467
|
}
|
|
1009
2468
|
const compatibility = new Map<string, string>();
|
|
@@ -1011,67 +2470,366 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
1011
2470
|
const role = formula.roles[roleName]!;
|
|
1012
2471
|
const binding = instance.bindings[roleName];
|
|
1013
2472
|
if (!role.compatibilityGroup || !binding) continue;
|
|
1014
|
-
const semantics = expressionSemantics(
|
|
2473
|
+
const semantics = expressionSemantics(
|
|
2474
|
+
binding,
|
|
2475
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2476
|
+
measureMap,
|
|
2477
|
+
[],
|
|
2478
|
+
);
|
|
1015
2479
|
if (semantics.signature === null) continue;
|
|
1016
2480
|
const previous = compatibility.get(role.compatibilityGroup);
|
|
1017
2481
|
if (previous !== undefined && previous !== semantics.signature) {
|
|
1018
|
-
pushIssue(
|
|
2482
|
+
pushIssue(
|
|
2483
|
+
issues,
|
|
2484
|
+
"incompatible-semantics",
|
|
2485
|
+
propertyPath(`${base}.bindings`, roleName),
|
|
2486
|
+
`Compatibility group ${role.compatibilityGroup} binds different semantics`,
|
|
2487
|
+
);
|
|
1019
2488
|
} else compatibility.set(role.compatibilityGroup, semantics.signature);
|
|
1020
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
|
+
);
|
|
1021
2511
|
for (const [key, text] of [
|
|
1022
2512
|
["displayName", instance.presentation?.displayName],
|
|
1023
2513
|
["description", instance.presentation?.description],
|
|
1024
2514
|
["displayUnit", instance.presentation?.displayUnit],
|
|
1025
2515
|
["numeratorLabel", instance.presentation?.numeratorLabel],
|
|
1026
2516
|
["denominatorLabel", instance.presentation?.denominatorLabel],
|
|
1027
|
-
] as const)
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
+
);
|
|
1030
2539
|
}
|
|
1031
|
-
requireArray(instance.rules, `${base}.rules`, issues).forEach(
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
for (const [side, operand] of [["left", rule.when?.left], ["right", rule.when?.right]] as const) {
|
|
1043
|
-
const operandPath = `${ruleBase}.when.${side}`;
|
|
1044
|
-
if (operand?.source === "measure") {
|
|
1045
|
-
const semantic = expressionSemantics(operand.expression, `${operandPath}.expression`, measureMap, issues);
|
|
1046
|
-
definitionExpressionNodes += 1 + walkDiagnosticExpression(operand.expression, "measure", `${operandPath}.expression`, []).nodeCount;
|
|
1047
|
-
void semantic;
|
|
1048
|
-
} else {
|
|
1049
|
-
definitionExpressionNodes += 1;
|
|
1050
|
-
if (operand?.source === "constant") validateFinite(operand.value, `${operandPath}.value`, "Rule constant", issues);
|
|
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;
|
|
1051
2551
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
+
);
|
|
1054
2740
|
});
|
|
1055
2741
|
|
|
1056
2742
|
const derivedOutputs = new Map<string, DiagnosticDerivedMeasureDefinition>();
|
|
1057
2743
|
derivations.forEach((derivation, index) => {
|
|
1058
2744
|
const base = `$.derivedMeasures[${index}]`;
|
|
1059
|
-
|
|
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
|
+
);
|
|
1060
2766
|
const output = measureMap.get(derivation.outputMeasureId);
|
|
1061
|
-
if (!output)
|
|
1062
|
-
|
|
1063
|
-
|
|
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
|
+
);
|
|
1064
2788
|
derivedOutputs.set(derivation.outputMeasureId, derivation);
|
|
1065
|
-
const walked = walkDiagnosticExpression(
|
|
2789
|
+
const walked = walkDiagnosticExpression(
|
|
2790
|
+
derivation.expression,
|
|
2791
|
+
"claim",
|
|
2792
|
+
`${base}.expression`,
|
|
2793
|
+
issues,
|
|
2794
|
+
);
|
|
1066
2795
|
definitionExpressionNodes += walked.nodeCount;
|
|
1067
|
-
for (const id of walked.dependencies)
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
+
);
|
|
1072
2825
|
}
|
|
1073
2826
|
if (definition.lossRowGrain !== "claim") {
|
|
1074
|
-
pushIssue(
|
|
2827
|
+
pushIssue(
|
|
2828
|
+
issues,
|
|
2829
|
+
"incompatible-semantics",
|
|
2830
|
+
base,
|
|
2831
|
+
"Derived measures require claim-grain input",
|
|
2832
|
+
);
|
|
1075
2833
|
}
|
|
1076
2834
|
const expressionStack: unknown[] = [derivation.expression];
|
|
1077
2835
|
while (expressionStack.length > 0) {
|
|
@@ -1080,33 +2838,84 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
1080
2838
|
if (current.op === "claim-layer") {
|
|
1081
2839
|
const attachment = current.attachment;
|
|
1082
2840
|
const limit = current.limit;
|
|
1083
|
-
const attachmentOk = validateFinite(
|
|
1084
|
-
|
|
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
|
+
);
|
|
1085
2854
|
if (limit !== null) {
|
|
1086
|
-
const limitOk = validateFinite(
|
|
1087
|
-
|
|
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
|
+
);
|
|
1088
2868
|
if (attachmentOk && limitOk && !Number.isFinite(attachment + limit)) {
|
|
1089
|
-
pushIssue(
|
|
2869
|
+
pushIssue(
|
|
2870
|
+
issues,
|
|
2871
|
+
"invalid-number",
|
|
2872
|
+
`${base}.expression.limit`,
|
|
2873
|
+
"Attachment plus claim-layer width must be finite",
|
|
2874
|
+
);
|
|
1090
2875
|
}
|
|
1091
2876
|
}
|
|
1092
|
-
} else if (current.op === "add" && Array.isArray(current.terms))
|
|
1093
|
-
|
|
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);
|
|
1094
2881
|
}
|
|
1095
|
-
const projection = projectClaimExpression(
|
|
2882
|
+
const projection = projectClaimExpression(
|
|
2883
|
+
derivation.expression,
|
|
2884
|
+
`${base}.expression`,
|
|
2885
|
+
measureMap,
|
|
2886
|
+
amountBasisMap,
|
|
2887
|
+
issues,
|
|
2888
|
+
);
|
|
1096
2889
|
if (output && projection) {
|
|
1097
|
-
const expected =
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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 &&
|
|
1101
2897
|
(output.kind === "amount"
|
|
1102
|
-
? expected !== undefined &&
|
|
2898
|
+
? expected !== undefined &&
|
|
2899
|
+
projectedBasisKey(projectedAmountBasis(expected)) ===
|
|
2900
|
+
projectedBasisKey(projection.amountBasis!)
|
|
1103
2901
|
: semanticReference(output) === projection.reference);
|
|
1104
|
-
if (!matches)
|
|
2902
|
+
if (!matches)
|
|
2903
|
+
pushIssue(
|
|
2904
|
+
issues,
|
|
2905
|
+
"incompatible-semantics",
|
|
2906
|
+
`${base}.expression`,
|
|
2907
|
+
"Derived expression does not project the output measure semantics",
|
|
2908
|
+
);
|
|
1105
2909
|
}
|
|
1106
2910
|
});
|
|
1107
2911
|
for (const [measureId, measure] of measureMap) {
|
|
1108
2912
|
if (measure.source === "derived" && !derivedOutputs.has(measureId)) {
|
|
1109
|
-
pushIssue(
|
|
2913
|
+
pushIssue(
|
|
2914
|
+
issues,
|
|
2915
|
+
"missing-required",
|
|
2916
|
+
"$.derivedMeasures",
|
|
2917
|
+
`Derived measure ${measureId} requires exactly one derivation`,
|
|
2918
|
+
);
|
|
1110
2919
|
}
|
|
1111
2920
|
}
|
|
1112
2921
|
const visiting = new Set<string>();
|
|
@@ -1114,14 +2923,25 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
1114
2923
|
const visit = (id: string): void => {
|
|
1115
2924
|
if (visited.has(id)) return;
|
|
1116
2925
|
if (visiting.has(id)) {
|
|
1117
|
-
pushIssue(
|
|
2926
|
+
pushIssue(
|
|
2927
|
+
issues,
|
|
2928
|
+
"cycle",
|
|
2929
|
+
"$.derivedMeasures",
|
|
2930
|
+
`Derived measure graph contains a cycle at ${id}`,
|
|
2931
|
+
);
|
|
1118
2932
|
return;
|
|
1119
2933
|
}
|
|
1120
2934
|
visiting.add(id);
|
|
1121
2935
|
const derivation = derivedOutputs.get(id);
|
|
1122
2936
|
if (derivation) {
|
|
1123
|
-
const deps = walkDiagnosticExpression(
|
|
1124
|
-
|
|
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);
|
|
1125
2945
|
}
|
|
1126
2946
|
visiting.delete(id);
|
|
1127
2947
|
visited.add(id);
|
|
@@ -1130,29 +2950,360 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
1130
2950
|
|
|
1131
2951
|
reviewRules.forEach((rule, index) => {
|
|
1132
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
|
+
}
|
|
1133
2995
|
validateToken(rule.code, `${base}.code`, "Review rule code", issues);
|
|
1134
|
-
validateToken(
|
|
1135
|
-
|
|
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
|
+
);
|
|
1136
3029
|
validateTolerance(rule.tolerance, `${base}.tolerance`, issues);
|
|
1137
|
-
const roots:
|
|
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
|
+
};
|
|
1138
3042
|
if (rule.kind === "compare") {
|
|
1139
|
-
if (
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
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
|
+
}
|
|
1143
3069
|
} else if (rule.kind === "reconcile") {
|
|
1144
|
-
roots.push(rule.actual);
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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
|
+
}
|
|
1156
3307
|
});
|
|
1157
3308
|
});
|
|
1158
3309
|
|
|
@@ -1169,10 +3320,33 @@ function validateDefinition(value: unknown): DiagnosticDefinition {
|
|
|
1169
3320
|
return definition;
|
|
1170
3321
|
}
|
|
1171
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
|
+
|
|
1172
3337
|
function optionalFromNullable<T>(value: T | null | undefined): T | undefined {
|
|
1173
3338
|
return value === null || value === undefined ? undefined : value;
|
|
1174
3339
|
}
|
|
1175
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
|
+
|
|
1176
3350
|
/**
|
|
1177
3351
|
* The normalized wire projection materializes optional semantic fields as
|
|
1178
3352
|
* null. Convert only those documented defaults back to the authored view so
|
|
@@ -1185,27 +3359,47 @@ function authoredView(
|
|
|
1185
3359
|
const reviewRules = definition.reviewRules.map((rule) => {
|
|
1186
3360
|
const tolerance = { ...rule.tolerance };
|
|
1187
3361
|
if (rule.kind !== "control-total") return { ...rule, tolerance };
|
|
1188
|
-
const filter =
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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;
|
|
1202
3389
|
}) as readonly DiagnosticReviewRule[];
|
|
1203
3390
|
return {
|
|
3391
|
+
...definition,
|
|
1204
3392
|
diagnosticDefinitionVersion: definition.diagnosticDefinitionVersion,
|
|
1205
3393
|
id: definition.id,
|
|
1206
3394
|
version: definition.version,
|
|
1207
3395
|
lossRowGrain: definition.lossRowGrain,
|
|
1208
3396
|
measures: definition.measures.map((measure) => ({
|
|
3397
|
+
...omitProperties(measure, [
|
|
3398
|
+
"basisId",
|
|
3399
|
+
"countPopulationId",
|
|
3400
|
+
"exposureBasisId",
|
|
3401
|
+
"exposureTiming",
|
|
3402
|
+
]),
|
|
1209
3403
|
id: measure.id,
|
|
1210
3404
|
displayName: measure.displayName,
|
|
1211
3405
|
description: measure.description,
|
|
@@ -1215,69 +3409,103 @@ function authoredView(
|
|
|
1215
3409
|
developmentSemantics: measure.developmentSemantics,
|
|
1216
3410
|
aggregation: measure.aggregation,
|
|
1217
3411
|
missing: measure.missing,
|
|
1218
|
-
...(measure.basisId === null || measure.basisId === undefined
|
|
1219
|
-
|
|
3412
|
+
...(measure.basisId === null || measure.basisId === undefined
|
|
3413
|
+
? {}
|
|
3414
|
+
: { basisId: measure.basisId }),
|
|
3415
|
+
...(measure.countPopulationId === null ||
|
|
3416
|
+
measure.countPopulationId === undefined
|
|
1220
3417
|
? {}
|
|
1221
3418
|
: { countPopulationId: measure.countPopulationId }),
|
|
1222
|
-
...(measure.exposureBasisId === null ||
|
|
3419
|
+
...(measure.exposureBasisId === null ||
|
|
3420
|
+
measure.exposureBasisId === undefined
|
|
1223
3421
|
? {}
|
|
1224
3422
|
: { exposureBasisId: measure.exposureBasisId }),
|
|
1225
|
-
...(measure.exposureTiming === null ||
|
|
3423
|
+
...(measure.exposureTiming === null ||
|
|
3424
|
+
measure.exposureTiming === undefined
|
|
1226
3425
|
? {}
|
|
1227
3426
|
: { exposureTiming: measure.exposureTiming }),
|
|
1228
3427
|
})),
|
|
1229
|
-
countPopulations: definition.countPopulations.map((population) => ({
|
|
3428
|
+
countPopulations: definition.countPopulations.map((population) => ({
|
|
3429
|
+
...population,
|
|
3430
|
+
})),
|
|
1230
3431
|
exposureBases: definition.exposureBases.map((basis) => ({
|
|
3432
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
1231
3433
|
id: basis.id,
|
|
1232
3434
|
displayName: basis.displayName,
|
|
1233
3435
|
basis: basis.basis,
|
|
1234
3436
|
unit: basis.unit,
|
|
1235
3437
|
description: basis.description,
|
|
1236
|
-
...(basis.sourceDescription === null ||
|
|
3438
|
+
...(basis.sourceDescription === null ||
|
|
3439
|
+
basis.sourceDescription === undefined
|
|
1237
3440
|
? {}
|
|
1238
3441
|
: { sourceDescription: basis.sourceDescription }),
|
|
1239
|
-
...(basis.attributes === undefined
|
|
3442
|
+
...(basis.attributes === undefined
|
|
3443
|
+
? {}
|
|
3444
|
+
: { attributes: basis.attributes }),
|
|
1240
3445
|
})),
|
|
1241
3446
|
amountBases: definition.amountBases.map((basis) => ({
|
|
3447
|
+
...omitProperties(basis, ["sourceDescription"]),
|
|
1242
3448
|
id: basis.id,
|
|
1243
3449
|
displayName: basis.displayName,
|
|
1244
3450
|
currency: basis.currency,
|
|
1245
3451
|
perspective: basis.perspective,
|
|
1246
3452
|
components: basis.components.map((component) => ({
|
|
3453
|
+
...component,
|
|
1247
3454
|
id: component.id,
|
|
1248
3455
|
treatment: component.treatment,
|
|
1249
|
-
limitation:
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
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,
|
|
1257
3467
|
})),
|
|
1258
|
-
...(basis.sourceDescription === null ||
|
|
3468
|
+
...(basis.sourceDescription === null ||
|
|
3469
|
+
basis.sourceDescription === undefined
|
|
1259
3470
|
? {}
|
|
1260
3471
|
: { sourceDescription: basis.sourceDescription }),
|
|
1261
|
-
...(basis.attributes === undefined
|
|
3472
|
+
...(basis.attributes === undefined
|
|
3473
|
+
? {}
|
|
3474
|
+
: { attributes: basis.attributes }),
|
|
3475
|
+
})),
|
|
3476
|
+
derivedMeasures: definition.derivedMeasures.map((derivation) => ({
|
|
3477
|
+
...derivation,
|
|
1262
3478
|
})),
|
|
1263
|
-
derivedMeasures: definition.derivedMeasures.map((derivation) => ({ ...derivation })),
|
|
1264
3479
|
formulas: definition.formulas.map((formula) => ({
|
|
3480
|
+
...formula,
|
|
1265
3481
|
id: formula.id,
|
|
1266
3482
|
version: formula.version,
|
|
1267
|
-
roles: Object.fromEntries(
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
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
|
+
),
|
|
1276
3503
|
numerator: formula.numerator,
|
|
1277
3504
|
denominator: formula.denominator,
|
|
1278
3505
|
denominatorPolicy: formula.denominatorPolicy,
|
|
1279
3506
|
})),
|
|
1280
3507
|
instances: definition.instances.map((instance) => ({
|
|
3508
|
+
...instance,
|
|
1281
3509
|
id: instance.id,
|
|
1282
3510
|
version: instance.version,
|
|
1283
3511
|
formulaId: instance.formulaId,
|
|
@@ -1299,29 +3527,35 @@ export function compileDiagnosticDefinition(
|
|
|
1299
3527
|
): CompiledDiagnosticDefinition {
|
|
1300
3528
|
const boundaryIssues: DiagnosticValidationIssue[] = [];
|
|
1301
3529
|
validateJsonValue(value, boundaryIssues);
|
|
1302
|
-
if (boundaryIssues.length > 0)
|
|
3530
|
+
if (boundaryIssues.length > 0)
|
|
3531
|
+
throw new DiagnosticValidationError(boundaryIssues);
|
|
1303
3532
|
let authored: DiagnosticDefinition;
|
|
1304
3533
|
try {
|
|
1305
3534
|
authored = authoredView(value);
|
|
1306
3535
|
} catch {
|
|
1307
|
-
throw new DiagnosticValidationError([
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
+
]);
|
|
1313
3545
|
}
|
|
1314
3546
|
let definition: DiagnosticDefinition;
|
|
1315
3547
|
try {
|
|
1316
3548
|
definition = validateDefinition(authored);
|
|
1317
3549
|
} catch (error) {
|
|
1318
3550
|
if (error instanceof DiagnosticValidationError) throw error;
|
|
1319
|
-
throw new DiagnosticValidationError([
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
+
]);
|
|
1325
3559
|
}
|
|
1326
3560
|
const normalized = normalizeDiagnosticDefinition(definition);
|
|
1327
3561
|
const identities = buildDiagnosticIdentities(normalized);
|
|
@@ -1341,13 +3575,22 @@ export function compileDiagnosticDefinition(
|
|
|
1341
3575
|
Object.freeze(compiled);
|
|
1342
3576
|
authenticCompiledDefinitions.add(compiled);
|
|
1343
3577
|
compiledInternals.set(compiled, {
|
|
1344
|
-
formulasById: new Map(
|
|
1345
|
-
|
|
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
|
+
),
|
|
1346
3584
|
calculationScopesByInstanceId: identities.calculationScopesByInstanceId,
|
|
1347
|
-
calculationDependenciesByInstanceId:
|
|
1348
|
-
|
|
3585
|
+
calculationDependenciesByInstanceId:
|
|
3586
|
+
identities.calculationDependenciesByInstanceId,
|
|
3587
|
+
evaluationDependenciesByInstanceId:
|
|
3588
|
+
identities.evaluationDependenciesByInstanceId,
|
|
1349
3589
|
derivationsByOutputMeasureId: new Map(
|
|
1350
|
-
normalized.derivedMeasures.map((derivation) => [
|
|
3590
|
+
normalized.derivedMeasures.map((derivation) => [
|
|
3591
|
+
derivation.outputMeasureId,
|
|
3592
|
+
derivation as DiagnosticDerivedMeasureDefinition,
|
|
3593
|
+
]),
|
|
1351
3594
|
),
|
|
1352
3595
|
});
|
|
1353
3596
|
return compiled;
|
|
@@ -1356,13 +3599,19 @@ export function compileDiagnosticDefinition(
|
|
|
1356
3599
|
export function assertCompiledDiagnosticDefinition(
|
|
1357
3600
|
value: unknown,
|
|
1358
3601
|
): asserts value is CompiledDiagnosticDefinition {
|
|
1359
|
-
if (
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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
|
+
]);
|
|
1366
3615
|
}
|
|
1367
3616
|
}
|
|
1368
3617
|
|