@actuarial-ts/core 0.6.0 → 0.6.1

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