@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,4 +1,131 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
DiagnosticPeriodAxis,
|
|
3
|
+
DiagnosticSourceLocation,
|
|
4
|
+
} from "./diagnosticDefinitions.js";
|
|
5
|
+
import { canonicalJson } from "./canonical.js";
|
|
6
|
+
import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
|
|
7
|
+
import { compareDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
|
|
8
|
+
import { compareDiagnosticIdentityValues } from "./diagnosticOrdering.js";
|
|
9
|
+
import { normalizeDiagnosticPeriodWithAxis } from "./diagnosticPeriodAxis.js";
|
|
10
|
+
import { validateDiagnosticPeriodAxisInput } from "./diagnosticDefinitions.js";
|
|
11
|
+
import {
|
|
12
|
+
DiagnosticValidationError,
|
|
13
|
+
type DiagnosticValidationIssue,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
import {
|
|
16
|
+
diagnosticJsonPreflight,
|
|
17
|
+
diagnosticRecord,
|
|
18
|
+
hasDiagnosticOwn,
|
|
19
|
+
isDiagnosticPlainRecord,
|
|
20
|
+
isDiagnosticToken,
|
|
21
|
+
snapshotDiagnosticJson,
|
|
22
|
+
MAX_DIAGNOSTIC_JSON_DEPTH,
|
|
23
|
+
} from "./diagnosticRuntime.js";
|
|
24
|
+
|
|
25
|
+
// Bound the collection separately from each untrusted record. The generic
|
|
26
|
+
// million-node JSON cap otherwise rejects legitimate files at ~71k–100k rows,
|
|
27
|
+
// depending only on how much source provenance each observation carries.
|
|
28
|
+
const MAX_EXPOSURE_OBSERVATIONS = 250_000;
|
|
29
|
+
|
|
30
|
+
// JSON enumeration skips hidden properties, but the strict exposure schema
|
|
31
|
+
// reads recognized own fields. Refuse hidden accessors before those reads.
|
|
32
|
+
function hiddenExposureAccessorIssues(
|
|
33
|
+
value: unknown,
|
|
34
|
+
path: string,
|
|
35
|
+
): DiagnosticValidationIssue[] {
|
|
36
|
+
if (!isDiagnosticPlainRecord(value)) return [];
|
|
37
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
38
|
+
for (const key of Object.getOwnPropertyNames(value)) {
|
|
39
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
|
|
40
|
+
if (!descriptor.enumerable && !("value" in descriptor))
|
|
41
|
+
issues.push({
|
|
42
|
+
domain: "input",
|
|
43
|
+
code: "invalid-json-value",
|
|
44
|
+
path: propertyPath(path, key),
|
|
45
|
+
message: "JSON objects may contain only data properties",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function exposureCollectionPreflight(
|
|
52
|
+
value: unknown,
|
|
53
|
+
): readonly DiagnosticValidationIssue[] {
|
|
54
|
+
if (!Array.isArray(value)) return diagnosticJsonPreflight(value, "input");
|
|
55
|
+
if (Object.getPrototypeOf(value) !== Array.prototype)
|
|
56
|
+
return [
|
|
57
|
+
{
|
|
58
|
+
domain: "input",
|
|
59
|
+
code: "invalid-json-value",
|
|
60
|
+
path: "$",
|
|
61
|
+
message: "Value must use a plain object or array prototype",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
if (value.length > MAX_EXPOSURE_OBSERVATIONS)
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
domain: "input",
|
|
68
|
+
code: "expression-limit",
|
|
69
|
+
path: "$",
|
|
70
|
+
message: `Exposure observation count exceeds ${MAX_EXPOSURE_OBSERVATIONS}`,
|
|
71
|
+
},
|
|
72
|
+
];
|
|
73
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
74
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
75
|
+
if (key === "length") continue;
|
|
76
|
+
const index = typeof key === "string" ? Number(key) : Number.NaN;
|
|
77
|
+
if (
|
|
78
|
+
Number.isInteger(index) &&
|
|
79
|
+
index >= 0 &&
|
|
80
|
+
index < value.length &&
|
|
81
|
+
String(index) === key
|
|
82
|
+
)
|
|
83
|
+
continue;
|
|
84
|
+
issues.push({
|
|
85
|
+
domain: "input",
|
|
86
|
+
code: "invalid-json-value",
|
|
87
|
+
path: typeof key === "symbol" ? "$" : propertyPath("$", key),
|
|
88
|
+
message: "JSON arrays may contain only indexed data properties",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
for (let index = 0; index < value.length; index++) {
|
|
92
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
93
|
+
if (!descriptor || !("value" in descriptor)) {
|
|
94
|
+
issues.push({
|
|
95
|
+
domain: "input",
|
|
96
|
+
code: "invalid-json-value",
|
|
97
|
+
path: `$[${index}]`,
|
|
98
|
+
message: "JSON arrays may contain only indexed data properties",
|
|
99
|
+
});
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
issues.push(
|
|
103
|
+
...hiddenExposureAccessorIssues(descriptor.value, `$[${index}]`),
|
|
104
|
+
);
|
|
105
|
+
if (isDiagnosticPlainRecord(descriptor.value)) {
|
|
106
|
+
const source = Object.getOwnPropertyDescriptor(
|
|
107
|
+
descriptor.value,
|
|
108
|
+
"source",
|
|
109
|
+
);
|
|
110
|
+
if (source && "value" in source)
|
|
111
|
+
issues.push(
|
|
112
|
+
...hiddenExposureAccessorIssues(source.value, `$[${index}].source`),
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
// The outer collection used to occupy depth 1: keep the exact depth
|
|
116
|
+
// contract, and retain the default million-node guard for each record.
|
|
117
|
+
for (const issue of diagnosticJsonPreflight(descriptor.value, "input", {
|
|
118
|
+
maxDepth: MAX_DIAGNOSTIC_JSON_DEPTH - 1,
|
|
119
|
+
}))
|
|
120
|
+
issues.push({ ...issue, path: `$[${index}]${issue.path.slice(1)}` });
|
|
121
|
+
}
|
|
122
|
+
return issues;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Only for new SDK-owned objects whose leaves have already been validated. */
|
|
126
|
+
function freezeExposureRecord<T extends object>(value: T): T {
|
|
127
|
+
return Object.freeze(Object.assign(diagnosticRecord<unknown>(), value)) as T;
|
|
128
|
+
}
|
|
2
129
|
|
|
3
130
|
export interface DiagnosticExposureObservation {
|
|
4
131
|
readonly key: string;
|
|
@@ -14,7 +141,11 @@ export interface DiagnosticExposureObservation {
|
|
|
14
141
|
export type DiagnosticAuditedNumericValue =
|
|
15
142
|
| { readonly status: "observed"; readonly value: number }
|
|
16
143
|
| { readonly status: "missing"; readonly value: null }
|
|
17
|
-
| {
|
|
144
|
+
| {
|
|
145
|
+
readonly status: "non-finite";
|
|
146
|
+
readonly value: null;
|
|
147
|
+
readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity";
|
|
148
|
+
};
|
|
18
149
|
|
|
19
150
|
export interface DiagnosticExposureAuditObservation {
|
|
20
151
|
readonly sourceGroup: string;
|
|
@@ -26,66 +157,435 @@ export interface DiagnosticExposureAuditObservation {
|
|
|
26
157
|
}
|
|
27
158
|
|
|
28
159
|
export type ReconciledDiagnosticExposure =
|
|
29
|
-
| {
|
|
30
|
-
|
|
160
|
+
| {
|
|
161
|
+
readonly measureId: string;
|
|
162
|
+
readonly key: string;
|
|
163
|
+
readonly status: "valid";
|
|
164
|
+
readonly sourceGroup: string;
|
|
165
|
+
readonly origin: string;
|
|
166
|
+
readonly valuation?: string;
|
|
167
|
+
readonly value: number;
|
|
168
|
+
readonly deduplicated: number;
|
|
169
|
+
readonly sources: readonly DiagnosticSourceLocation[];
|
|
170
|
+
}
|
|
171
|
+
| {
|
|
172
|
+
readonly measureId: string;
|
|
173
|
+
readonly key: string;
|
|
174
|
+
readonly status: "invalid";
|
|
175
|
+
readonly issues: readonly (
|
|
176
|
+
"missing" | "incomplete" | "non-finite" | "duplicate" | "conflict"
|
|
177
|
+
)[];
|
|
178
|
+
readonly value: null;
|
|
179
|
+
readonly observations: readonly DiagnosticExposureAuditObservation[];
|
|
180
|
+
};
|
|
31
181
|
|
|
32
|
-
export function auditDiagnosticNumber(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
182
|
+
export function auditDiagnosticNumber(
|
|
183
|
+
value: number | null,
|
|
184
|
+
): DiagnosticAuditedNumericValue {
|
|
185
|
+
if (value !== null && typeof value !== "number")
|
|
186
|
+
throw new DiagnosticValidationError([
|
|
187
|
+
{
|
|
188
|
+
domain: "input",
|
|
189
|
+
code: "invalid-type",
|
|
190
|
+
path: "$",
|
|
191
|
+
message: "Numeric input must be a number or null",
|
|
192
|
+
},
|
|
193
|
+
]);
|
|
194
|
+
if (value === null) return Object.freeze({ status: "missing", value: null });
|
|
195
|
+
if (!Number.isFinite(value))
|
|
196
|
+
return Object.freeze({
|
|
197
|
+
status: "non-finite",
|
|
198
|
+
value: null,
|
|
199
|
+
nonFiniteKind: Number.isNaN(value)
|
|
200
|
+
? "nan"
|
|
201
|
+
: value > 0
|
|
202
|
+
? "positive-infinity"
|
|
203
|
+
: "negative-infinity",
|
|
204
|
+
});
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
status: "observed",
|
|
207
|
+
value: Object.is(value, -0) ? 0 : value,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function propertyPath(path: string, key: string): string {
|
|
212
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
213
|
+
? `${path}.${key}`
|
|
214
|
+
: `${path}[${JSON.stringify(key)}]`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function validateExposureArguments(
|
|
218
|
+
observations: unknown,
|
|
219
|
+
timingByMeasure: unknown,
|
|
220
|
+
periodAxis: DiagnosticPeriodAxis | undefined,
|
|
221
|
+
): void {
|
|
222
|
+
// Nonfinite exposure amounts are supported audited inputs. They are the
|
|
223
|
+
// only non-JSON numbers permitted here; metadata still must be finite.
|
|
224
|
+
const issues: DiagnosticValidationIssue[] = [
|
|
225
|
+
...exposureCollectionPreflight(observations)
|
|
226
|
+
.filter(
|
|
227
|
+
(issue) =>
|
|
228
|
+
!(
|
|
229
|
+
issue.code === "invalid-json-value" &&
|
|
230
|
+
issue.message === "JSON numeric value must be finite" &&
|
|
231
|
+
/^\$\[\d+\]\.value$/.test(issue.path)
|
|
232
|
+
),
|
|
233
|
+
)
|
|
234
|
+
.map((issue) => ({
|
|
235
|
+
...issue,
|
|
236
|
+
path: `$.observations${issue.path.slice(1)}`,
|
|
237
|
+
})),
|
|
238
|
+
...diagnosticJsonPreflight(timingByMeasure, "configuration").map(
|
|
239
|
+
(issue) => ({
|
|
240
|
+
...issue,
|
|
241
|
+
path: `$.timingByMeasure${issue.path.slice(1)}`,
|
|
242
|
+
}),
|
|
243
|
+
),
|
|
244
|
+
...(periodAxis === undefined
|
|
245
|
+
? []
|
|
246
|
+
: validateDiagnosticPeriodAxisInput(periodAxis)),
|
|
247
|
+
];
|
|
248
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
249
|
+
const issue = (
|
|
250
|
+
domain: DiagnosticValidationIssue["domain"],
|
|
251
|
+
code: DiagnosticValidationIssue["code"],
|
|
252
|
+
path: string,
|
|
253
|
+
message: string,
|
|
254
|
+
) => issues.push({ domain, code, path, message });
|
|
255
|
+
const token = (
|
|
256
|
+
value: unknown,
|
|
257
|
+
path: string,
|
|
258
|
+
domain: DiagnosticValidationIssue["domain"] = "input",
|
|
259
|
+
) => {
|
|
260
|
+
if (!isDiagnosticToken(value))
|
|
261
|
+
issue(
|
|
262
|
+
domain,
|
|
263
|
+
typeof value === "string" ? "invalid-string" : "invalid-type",
|
|
264
|
+
path,
|
|
265
|
+
"Expected a nonempty token with valid Unicode and no U+0000",
|
|
266
|
+
);
|
|
267
|
+
};
|
|
268
|
+
const exactKeys = (
|
|
269
|
+
value: Record<string, unknown>,
|
|
270
|
+
allowed: readonly string[],
|
|
271
|
+
path: string,
|
|
272
|
+
) => {
|
|
273
|
+
for (const key of Object.keys(value))
|
|
274
|
+
if (!allowed.includes(key))
|
|
275
|
+
issue(
|
|
276
|
+
"input",
|
|
277
|
+
"unknown-key",
|
|
278
|
+
propertyPath(path, key),
|
|
279
|
+
`Unknown key ${key}`,
|
|
280
|
+
);
|
|
281
|
+
};
|
|
282
|
+
if (!Array.isArray(observations))
|
|
283
|
+
issue(
|
|
284
|
+
"input",
|
|
285
|
+
"invalid-type",
|
|
286
|
+
"$.observations",
|
|
287
|
+
"Exposure observations must be an array",
|
|
288
|
+
);
|
|
289
|
+
if (!isDiagnosticPlainRecord(timingByMeasure))
|
|
290
|
+
issue(
|
|
291
|
+
"configuration",
|
|
292
|
+
"invalid-type",
|
|
293
|
+
"$.timingByMeasure",
|
|
294
|
+
"Exposure timings must be a plain record",
|
|
295
|
+
);
|
|
296
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
297
|
+
const timings = timingByMeasure as Record<string, unknown>;
|
|
298
|
+
for (const [measureId, timing] of Object.entries(timings)) {
|
|
299
|
+
const path = propertyPath("$.timingByMeasure", measureId);
|
|
300
|
+
token(measureId, path, "configuration");
|
|
301
|
+
if (timing !== "origin-static" && timing !== "valuation-specific")
|
|
302
|
+
issue(
|
|
303
|
+
"configuration",
|
|
304
|
+
"invalid-type",
|
|
305
|
+
path,
|
|
306
|
+
"Exposure timing must be origin-static or valuation-specific",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
for (const [index, observation] of (observations as unknown[]).entries()) {
|
|
310
|
+
const path = `$.observations[${index}]`;
|
|
311
|
+
if (!isDiagnosticPlainRecord(observation)) {
|
|
312
|
+
issue(
|
|
313
|
+
"input",
|
|
314
|
+
"invalid-type",
|
|
315
|
+
path,
|
|
316
|
+
"Exposure observation must be an object",
|
|
317
|
+
);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
exactKeys(
|
|
321
|
+
observation,
|
|
322
|
+
[
|
|
323
|
+
"key",
|
|
324
|
+
"sourceGroup",
|
|
325
|
+
"origin",
|
|
326
|
+
"valuation",
|
|
327
|
+
"measureId",
|
|
328
|
+
"value",
|
|
329
|
+
"complete",
|
|
330
|
+
"source",
|
|
331
|
+
],
|
|
332
|
+
path,
|
|
333
|
+
);
|
|
334
|
+
for (const key of ["key", "sourceGroup", "origin", "measureId"])
|
|
335
|
+
token(observation[key], `${path}.${key}`);
|
|
336
|
+
if (hasDiagnosticOwn(observation, "valuation"))
|
|
337
|
+
token(observation.valuation, `${path}.valuation`);
|
|
338
|
+
if (typeof observation.complete !== "boolean")
|
|
339
|
+
issue(
|
|
340
|
+
"input",
|
|
341
|
+
"invalid-type",
|
|
342
|
+
`${path}.complete`,
|
|
343
|
+
"Exposure completeness must be boolean",
|
|
344
|
+
);
|
|
345
|
+
if (observation.value !== null && typeof observation.value !== "number")
|
|
346
|
+
issue(
|
|
347
|
+
"input",
|
|
348
|
+
"invalid-type",
|
|
349
|
+
`${path}.value`,
|
|
350
|
+
"Exposure value must be a number or null",
|
|
351
|
+
);
|
|
352
|
+
if (isDiagnosticToken(observation.measureId)) {
|
|
353
|
+
if (!hasDiagnosticOwn(timings, observation.measureId))
|
|
354
|
+
issue(
|
|
355
|
+
"configuration",
|
|
356
|
+
"unknown-reference",
|
|
357
|
+
`${path}.measureId`,
|
|
358
|
+
"Exposure measure has no declared timing",
|
|
359
|
+
);
|
|
360
|
+
else if (
|
|
361
|
+
timings[observation.measureId] === "valuation-specific" &&
|
|
362
|
+
!hasDiagnosticOwn(observation, "valuation")
|
|
363
|
+
)
|
|
364
|
+
issue(
|
|
365
|
+
"input",
|
|
366
|
+
"missing-required",
|
|
367
|
+
`${path}.valuation`,
|
|
368
|
+
"Valuation-specific exposure requires a valuation",
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
if (hasDiagnosticOwn(observation, "source")) {
|
|
372
|
+
const source = observation.source;
|
|
373
|
+
if (!isDiagnosticPlainRecord(source))
|
|
374
|
+
issue(
|
|
375
|
+
"input",
|
|
376
|
+
"invalid-type",
|
|
377
|
+
`${path}.source`,
|
|
378
|
+
"Source location must be an object",
|
|
379
|
+
);
|
|
380
|
+
else {
|
|
381
|
+
exactKeys(
|
|
382
|
+
source,
|
|
383
|
+
[
|
|
384
|
+
"artifactId",
|
|
385
|
+
"sourceFile",
|
|
386
|
+
"sourceSheet",
|
|
387
|
+
"sourceRow",
|
|
388
|
+
"sourceCell",
|
|
389
|
+
],
|
|
390
|
+
`${path}.source`,
|
|
391
|
+
);
|
|
392
|
+
token(source.artifactId, `${path}.source.artifactId`);
|
|
393
|
+
for (const key of ["sourceFile", "sourceSheet", "sourceCell"])
|
|
394
|
+
if (hasDiagnosticOwn(source, key))
|
|
395
|
+
token(source[key], `${path}.source.${key}`);
|
|
396
|
+
if (
|
|
397
|
+
hasDiagnosticOwn(source, "sourceRow") &&
|
|
398
|
+
(typeof source.sourceRow !== "number" ||
|
|
399
|
+
!Number.isSafeInteger(source.sourceRow) ||
|
|
400
|
+
source.sourceRow < 0)
|
|
401
|
+
)
|
|
402
|
+
issue(
|
|
403
|
+
"input",
|
|
404
|
+
"invalid-number",
|
|
405
|
+
`${path}.source.sourceRow`,
|
|
406
|
+
"Source row must be a nonnegative safe integer",
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (periodAxis !== undefined)
|
|
411
|
+
for (const side of ["origin", "valuation"] as const) {
|
|
412
|
+
if (
|
|
413
|
+
isDiagnosticToken(observation[side]) &&
|
|
414
|
+
normalizeDiagnosticPeriodWithAxis(
|
|
415
|
+
periodAxis,
|
|
416
|
+
side,
|
|
417
|
+
observation[side],
|
|
418
|
+
) === null
|
|
419
|
+
)
|
|
420
|
+
issue(
|
|
421
|
+
"input",
|
|
422
|
+
"invalid-period",
|
|
423
|
+
`${path}.${side}`,
|
|
424
|
+
`Unknown ${side} period ${JSON.stringify(observation[side])}`,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
36
429
|
}
|
|
37
430
|
|
|
38
|
-
function equalAudit(
|
|
39
|
-
|
|
40
|
-
|
|
431
|
+
function equalAudit(
|
|
432
|
+
left: DiagnosticExposureAuditObservation,
|
|
433
|
+
right: DiagnosticExposureAuditObservation,
|
|
434
|
+
): boolean {
|
|
435
|
+
return (
|
|
436
|
+
left.sourceGroup === right.sourceGroup &&
|
|
437
|
+
left.origin === right.origin &&
|
|
438
|
+
left.complete === right.complete &&
|
|
439
|
+
JSON.stringify(left.value) === JSON.stringify(right.value)
|
|
440
|
+
);
|
|
41
441
|
}
|
|
42
442
|
|
|
43
443
|
export function reconcileDiagnosticExposures(
|
|
44
444
|
observations: readonly DiagnosticExposureObservation[],
|
|
45
|
-
timingByMeasure: Readonly<
|
|
445
|
+
timingByMeasure: Readonly<
|
|
446
|
+
Record<string, "origin-static" | "valuation-specific">
|
|
447
|
+
>,
|
|
448
|
+
periodAxis?: DiagnosticPeriodAxis,
|
|
46
449
|
): readonly ReconciledDiagnosticExposure[] {
|
|
450
|
+
validateExposureArguments(observations, timingByMeasure, periodAxis);
|
|
451
|
+
const periodValue = (
|
|
452
|
+
role: "origin" | "valuation",
|
|
453
|
+
label: string | undefined,
|
|
454
|
+
) =>
|
|
455
|
+
label === undefined || periodAxis === undefined
|
|
456
|
+
? label
|
|
457
|
+
: (normalizeDiagnosticPeriodWithAxis(periodAxis, role, label)
|
|
458
|
+
?.coordinate ?? label);
|
|
459
|
+
const observationOrder = (
|
|
460
|
+
left: DiagnosticExposureAuditObservation,
|
|
461
|
+
right: DiagnosticExposureAuditObservation,
|
|
462
|
+
) =>
|
|
463
|
+
compareDiagnosticIdentityValues(
|
|
464
|
+
[
|
|
465
|
+
left.sourceGroup,
|
|
466
|
+
periodValue("origin", left.origin),
|
|
467
|
+
periodValue("valuation", left.valuation),
|
|
468
|
+
left.value.status,
|
|
469
|
+
left.value.status === "non-finite"
|
|
470
|
+
? left.value.nonFiniteKind
|
|
471
|
+
: left.value.value,
|
|
472
|
+
left.complete,
|
|
473
|
+
],
|
|
474
|
+
[
|
|
475
|
+
right.sourceGroup,
|
|
476
|
+
periodValue("origin", right.origin),
|
|
477
|
+
periodValue("valuation", right.valuation),
|
|
478
|
+
right.value.status,
|
|
479
|
+
right.value.status === "non-finite"
|
|
480
|
+
? right.value.nonFiniteKind
|
|
481
|
+
: right.value.value,
|
|
482
|
+
right.complete,
|
|
483
|
+
],
|
|
484
|
+
) ||
|
|
485
|
+
(left.source === undefined || right.source === undefined
|
|
486
|
+
? compareDiagnosticIdentityValues(left.source, right.source)
|
|
487
|
+
: compareDiagnosticSourceLocations(left.source, right.source));
|
|
47
488
|
const cohorts = new Map<string, DiagnosticExposureObservation[]>();
|
|
48
489
|
for (const observation of observations) {
|
|
49
|
-
const timing = timingByMeasure
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
490
|
+
const timing = hasDiagnosticOwn(timingByMeasure, observation.measureId)
|
|
491
|
+
? timingByMeasure[observation.measureId]
|
|
492
|
+
: undefined;
|
|
493
|
+
const identity = canonicalJson(
|
|
494
|
+
timing === "valuation-specific"
|
|
495
|
+
? [
|
|
496
|
+
observation.measureId,
|
|
497
|
+
observation.key,
|
|
498
|
+
observation.valuation ?? null,
|
|
499
|
+
]
|
|
500
|
+
: [observation.measureId, observation.key],
|
|
501
|
+
);
|
|
53
502
|
const cohort = cohorts.get(identity) ?? [];
|
|
54
503
|
cohort.push(observation);
|
|
55
504
|
cohorts.set(identity, cohort);
|
|
56
505
|
}
|
|
57
|
-
return Object.freeze(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
506
|
+
return Object.freeze(
|
|
507
|
+
[...cohorts.values()]
|
|
508
|
+
.map((cohort): ReconciledDiagnosticExposure => {
|
|
509
|
+
const first = cohort[0]!;
|
|
510
|
+
const timing = hasDiagnosticOwn(timingByMeasure, first.measureId)
|
|
511
|
+
? timingByMeasure[first.measureId]
|
|
512
|
+
: undefined;
|
|
513
|
+
const audited = cohort
|
|
514
|
+
.map((item): DiagnosticExposureAuditObservation =>
|
|
515
|
+
snapshotDiagnosticJson({
|
|
516
|
+
sourceGroup: item.sourceGroup,
|
|
517
|
+
origin: item.origin,
|
|
518
|
+
...(item.valuation === undefined
|
|
519
|
+
? {}
|
|
520
|
+
: { valuation: item.valuation }),
|
|
521
|
+
value: auditDiagnosticNumber(item.value),
|
|
522
|
+
complete: item.complete,
|
|
523
|
+
...(item.source === undefined
|
|
524
|
+
? {}
|
|
525
|
+
: { source: Object.freeze({ ...item.source }) }),
|
|
526
|
+
}),
|
|
527
|
+
)
|
|
528
|
+
.sort(observationOrder);
|
|
529
|
+
const issues: (
|
|
530
|
+
"missing" | "incomplete" | "non-finite" | "duplicate" | "conflict"
|
|
531
|
+
)[] = [];
|
|
532
|
+
if (audited.some((item) => item.value.status === "missing"))
|
|
533
|
+
issues.push("missing");
|
|
534
|
+
if (audited.some((item) => !item.complete)) issues.push("incomplete");
|
|
535
|
+
if (audited.some((item) => item.value.status === "non-finite"))
|
|
536
|
+
issues.push("non-finite");
|
|
537
|
+
if (timing === "valuation-specific" && audited.length > 1)
|
|
538
|
+
issues.push("duplicate");
|
|
539
|
+
if (audited.slice(1).some((item) => !equalAudit(audited[0]!, item)))
|
|
540
|
+
issues.push("conflict");
|
|
541
|
+
const validStaticCopies =
|
|
542
|
+
timing === "origin-static" && issues.length === 0;
|
|
543
|
+
if (issues.length > 0)
|
|
544
|
+
return freezeExposureRecord({
|
|
545
|
+
measureId: first.measureId,
|
|
546
|
+
key: first.key,
|
|
547
|
+
status: "invalid",
|
|
548
|
+
issues: Object.freeze(issues),
|
|
549
|
+
value: null,
|
|
550
|
+
observations: Object.freeze(audited),
|
|
551
|
+
});
|
|
552
|
+
const value = audited[0]!.value;
|
|
553
|
+
if (value.status !== "observed")
|
|
554
|
+
throw new Error("unreachable invalid exposure state");
|
|
555
|
+
const sources = normalizeDiagnosticSourceLocations(
|
|
556
|
+
audited.map((item) => item.source),
|
|
557
|
+
).map((source) => snapshotDiagnosticJson(source));
|
|
558
|
+
return freezeExposureRecord({
|
|
559
|
+
measureId: first.measureId,
|
|
560
|
+
key: first.key,
|
|
561
|
+
status: "valid",
|
|
562
|
+
sourceGroup: first.sourceGroup,
|
|
563
|
+
origin: first.origin,
|
|
564
|
+
...(timing === "valuation-specific"
|
|
565
|
+
? { valuation: first.valuation! }
|
|
566
|
+
: {}),
|
|
567
|
+
value: value.value,
|
|
568
|
+
deduplicated: validStaticCopies ? audited.length - 1 : 0,
|
|
569
|
+
sources: Object.freeze(sources),
|
|
570
|
+
});
|
|
571
|
+
})
|
|
572
|
+
.sort((left, right) => {
|
|
573
|
+
return (
|
|
574
|
+
compareDiagnosticIdentityValues(
|
|
575
|
+
[
|
|
576
|
+
left.measureId,
|
|
577
|
+
left.key,
|
|
578
|
+
left.status,
|
|
579
|
+
left.status === "invalid" ? left.issues : undefined,
|
|
580
|
+
],
|
|
581
|
+
[
|
|
582
|
+
right.measureId,
|
|
583
|
+
right.key,
|
|
584
|
+
right.status,
|
|
585
|
+
right.status === "invalid" ? right.issues : undefined,
|
|
586
|
+
],
|
|
587
|
+
) || compareDiagnosticIdentityValues(left, right)
|
|
588
|
+
);
|
|
589
|
+
}),
|
|
590
|
+
);
|
|
91
591
|
}
|