@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,20 +1,66 @@
|
|
|
1
1
|
import { canonicalJson, fnv1a64 } from "./canonical.js";
|
|
2
|
+
import {
|
|
3
|
+
createDiagnosticIdentityArray,
|
|
4
|
+
createDiagnosticIdentityObject,
|
|
5
|
+
createDiagnosticIdentityValue,
|
|
6
|
+
fingerprintDiagnosticIdentity,
|
|
7
|
+
type DiagnosticIdentityDocument,
|
|
8
|
+
} from "./diagnosticIdentityStream.js";
|
|
9
|
+
import {
|
|
10
|
+
normalizeDiagnosticsFilterIdentity,
|
|
11
|
+
projectDiagnosticIdentity,
|
|
12
|
+
type DiagnosticIdentityProjection,
|
|
13
|
+
type NormalizedDiagnosticsFilterIdentity,
|
|
14
|
+
type NormalizedDiagnosticSourceLocationIdentity,
|
|
15
|
+
} from "./diagnosticIdentity.js";
|
|
16
|
+
import {
|
|
17
|
+
compareDiagnosticBlockers,
|
|
18
|
+
compareDiagnosticContributions,
|
|
19
|
+
compareDiagnosticFindings,
|
|
20
|
+
} from "./diagnosticOrdering.js";
|
|
2
21
|
import {
|
|
3
22
|
assertCompiledDiagnosticDefinition,
|
|
4
23
|
getCompiledDiagnosticDefinitionInternals,
|
|
5
24
|
type CompiledDiagnosticDefinition,
|
|
6
25
|
type DiagnosticDeepReadonly,
|
|
26
|
+
type DiagnosticMeasureStats,
|
|
7
27
|
type DiagnosticSourceLocation,
|
|
8
28
|
type DiagnosticsFilter,
|
|
9
29
|
type JsonValue,
|
|
10
30
|
} from "./diagnosticDefinitions.js";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
31
|
+
import {
|
|
32
|
+
auditedDiagnosticContribution,
|
|
33
|
+
finalizeDiagnosticContributions,
|
|
34
|
+
type DiagnosticMeasureContribution,
|
|
35
|
+
type DiagnosticStructuralBlocker,
|
|
36
|
+
} from "./diagnosticAggregation.js";
|
|
37
|
+
import {
|
|
38
|
+
deriveDiagnosticClaimMeasuresWithAudit,
|
|
39
|
+
type DiagnosticDerivedValueAudit,
|
|
40
|
+
} from "./diagnosticDerivations.js";
|
|
41
|
+
import {
|
|
42
|
+
compareDiagnosticPeriods,
|
|
43
|
+
normalizeDiagnosticPeriod,
|
|
44
|
+
type DiagnosticNormalizedPeriod,
|
|
45
|
+
} from "./diagnosticPeriods.js";
|
|
46
|
+
import {
|
|
47
|
+
auditDiagnosticNumber,
|
|
48
|
+
reconcileDiagnosticExposures,
|
|
49
|
+
type DiagnosticAuditedNumericValue,
|
|
50
|
+
type DiagnosticExposureAuditObservation,
|
|
51
|
+
type DiagnosticExposureObservation,
|
|
52
|
+
type ReconciledDiagnosticExposure,
|
|
53
|
+
} from "./diagnosticExposure.js";
|
|
16
54
|
import type { DiagnosticMetricFinding } from "./diagnosticFormulas.js";
|
|
17
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
DiagnosticValidationError,
|
|
57
|
+
type DiagnosticValidationIssue,
|
|
58
|
+
} from "./types.js";
|
|
59
|
+
import { normalizeDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
|
|
60
|
+
import {
|
|
61
|
+
isDiagnosticPlainRecord,
|
|
62
|
+
isDiagnosticToken,
|
|
63
|
+
} from "./diagnosticRuntime.js";
|
|
18
64
|
|
|
19
65
|
export interface DiagnosticLossRecordBase {
|
|
20
66
|
readonly recordId: string;
|
|
@@ -25,12 +71,32 @@ export interface DiagnosticLossRecordBase {
|
|
|
25
71
|
readonly source?: DiagnosticSourceLocation;
|
|
26
72
|
readonly measures: Readonly<Record<string, number | null>>;
|
|
27
73
|
}
|
|
28
|
-
export interface DiagnosticClaimObservation extends DiagnosticLossRecordBase { readonly rowType: "claim"; readonly claimId: string }
|
|
29
|
-
export interface DiagnosticLossSnapshot extends DiagnosticLossRecordBase { readonly rowType: "aggregate" }
|
|
30
|
-
export type DiagnosticLossInput = DiagnosticClaimObservation | DiagnosticLossSnapshot;
|
|
31
74
|
|
|
32
|
-
export interface
|
|
33
|
-
|
|
75
|
+
export interface DiagnosticClaimObservation extends DiagnosticLossRecordBase {
|
|
76
|
+
readonly rowType: "claim";
|
|
77
|
+
readonly claimId: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DiagnosticLossSnapshot extends DiagnosticLossRecordBase {
|
|
81
|
+
readonly rowType: "aggregate";
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type DiagnosticLossInput =
|
|
85
|
+
DiagnosticClaimObservation | DiagnosticLossSnapshot;
|
|
86
|
+
|
|
87
|
+
export interface DiagnosticCompletePeriodCutoff {
|
|
88
|
+
readonly sourceGroup: string;
|
|
89
|
+
readonly originThrough: string | null;
|
|
90
|
+
readonly valuationThrough: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface DiagnosticExpectedCell {
|
|
94
|
+
readonly sourceGroup: string;
|
|
95
|
+
readonly origin: string;
|
|
96
|
+
readonly valuation: string;
|
|
97
|
+
readonly source?: DiagnosticSourceLocation;
|
|
98
|
+
}
|
|
99
|
+
|
|
34
100
|
export interface PrepareDiagnosticDataInput {
|
|
35
101
|
readonly definition: CompiledDiagnosticDefinition;
|
|
36
102
|
readonly losses: readonly DiagnosticLossInput[];
|
|
@@ -40,11 +106,55 @@ export interface PrepareDiagnosticDataInput {
|
|
|
40
106
|
readonly expectedCells?: readonly DiagnosticExpectedCell[];
|
|
41
107
|
}
|
|
42
108
|
|
|
43
|
-
export type DiagnosticInputDisposition =
|
|
109
|
+
export type DiagnosticInputDisposition =
|
|
110
|
+
"invalid" | "complete-period-cutoff" | "filter" | "retained";
|
|
111
|
+
|
|
112
|
+
export interface DiagnosticLossInputAuditSnapshot {
|
|
113
|
+
readonly recordId: string;
|
|
114
|
+
readonly rowType: "claim" | "aggregate";
|
|
115
|
+
readonly claimId: string | null;
|
|
116
|
+
readonly sourceGroup: string;
|
|
117
|
+
readonly origin: string;
|
|
118
|
+
readonly valuation: string;
|
|
119
|
+
readonly complete: boolean;
|
|
120
|
+
readonly measures: Readonly<Record<string, DiagnosticAuditedNumericValue>>;
|
|
121
|
+
readonly source: DiagnosticSourceLocation | null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface DiagnosticExposureInputAuditSnapshot {
|
|
125
|
+
readonly key: string;
|
|
126
|
+
readonly sourceGroup: string;
|
|
127
|
+
readonly origin: string;
|
|
128
|
+
readonly valuation: string | null;
|
|
129
|
+
readonly measureId: string;
|
|
130
|
+
readonly value: DiagnosticAuditedNumericValue;
|
|
131
|
+
readonly complete: boolean;
|
|
132
|
+
readonly source: DiagnosticSourceLocation | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface DiagnosticExpectedCellAuditSnapshot {
|
|
136
|
+
readonly sourceGroup: string;
|
|
137
|
+
readonly origin: string;
|
|
138
|
+
readonly valuation: string;
|
|
139
|
+
readonly source: DiagnosticSourceLocation | null;
|
|
140
|
+
}
|
|
141
|
+
|
|
44
142
|
export type DiagnosticInputAuditRecord =
|
|
45
|
-
| {
|
|
46
|
-
|
|
47
|
-
|
|
143
|
+
| {
|
|
144
|
+
readonly kind: "loss";
|
|
145
|
+
readonly disposition: DiagnosticInputDisposition;
|
|
146
|
+
readonly record: DiagnosticLossInputAuditSnapshot;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
readonly kind: "exposure";
|
|
150
|
+
readonly disposition: DiagnosticInputDisposition;
|
|
151
|
+
readonly record: DiagnosticExposureInputAuditSnapshot;
|
|
152
|
+
}
|
|
153
|
+
| {
|
|
154
|
+
readonly kind: "expected-cell";
|
|
155
|
+
readonly disposition: Exclude<DiagnosticInputDisposition, "invalid">;
|
|
156
|
+
readonly record: DiagnosticExpectedCellAuditSnapshot;
|
|
157
|
+
};
|
|
48
158
|
|
|
49
159
|
export interface PreparedDiagnosticSourceCell {
|
|
50
160
|
readonly sourceGroup: string;
|
|
@@ -53,9 +163,13 @@ export interface PreparedDiagnosticSourceCell {
|
|
|
53
163
|
readonly developmentAge: number;
|
|
54
164
|
readonly ageUnit: string;
|
|
55
165
|
readonly lossRecordIds: readonly string[];
|
|
56
|
-
readonly contributions: Readonly<
|
|
166
|
+
readonly contributions: Readonly<
|
|
167
|
+
Record<string, readonly DiagnosticMeasureContribution[]>
|
|
168
|
+
>;
|
|
57
169
|
readonly components: Readonly<Record<string, DiagnosticMeasureStats>>;
|
|
58
|
-
readonly structuralBlockers: Readonly<
|
|
170
|
+
readonly structuralBlockers: Readonly<
|
|
171
|
+
Record<string, readonly DiagnosticStructuralBlocker[]>
|
|
172
|
+
>;
|
|
59
173
|
readonly findings: readonly DiagnosticMetricFinding[];
|
|
60
174
|
}
|
|
61
175
|
|
|
@@ -74,160 +188,2472 @@ export interface PreparedDiagnosticData {
|
|
|
74
188
|
readonly findings: readonly DiagnosticMetricFinding[];
|
|
75
189
|
}
|
|
76
190
|
|
|
191
|
+
/** Complete prepared content shared by the eager and compact execution paths. */
|
|
192
|
+
export type PreparedDiagnosticDataContent = Omit<
|
|
193
|
+
PreparedDiagnosticData,
|
|
194
|
+
typeof preparedDiagnosticDataBrand | "preparationFingerprint"
|
|
195
|
+
>;
|
|
196
|
+
|
|
197
|
+
declare const compactPreparedDiagnosticDataBrand: unique symbol;
|
|
198
|
+
/**
|
|
199
|
+
* Owned, frozen preparation without an eagerly materialized identity or hash.
|
|
200
|
+
* This is a distinct authenticated value, not an eager preparation with a
|
|
201
|
+
* missing or placeholder fingerprint.
|
|
202
|
+
*/
|
|
203
|
+
export interface CompactPreparedDiagnosticData extends PreparedDiagnosticDataContent {
|
|
204
|
+
readonly [compactPreparedDiagnosticDataBrand]: true;
|
|
205
|
+
}
|
|
206
|
+
|
|
77
207
|
export interface NormalizedDiagnosticPreparationIdentity {
|
|
78
208
|
readonly definitionIntegrity: string;
|
|
79
|
-
readonly filter:
|
|
80
|
-
readonly completePeriodCutoffs: readonly DiagnosticCompletePeriodCutoff[];
|
|
81
|
-
readonly inputAudit: readonly DiagnosticInputAuditRecord[];
|
|
82
|
-
readonly cells: readonly PreparedDiagnosticSourceCell[];
|
|
83
|
-
readonly exposures: readonly ReconciledDiagnosticExposure[];
|
|
209
|
+
readonly filter: NormalizedDiagnosticsFilterIdentity | null;
|
|
210
|
+
readonly completePeriodCutoffs: readonly DiagnosticIdentityProjection<DiagnosticCompletePeriodCutoff>[];
|
|
211
|
+
readonly inputAudit: readonly DiagnosticIdentityProjection<DiagnosticInputAuditRecord>[];
|
|
212
|
+
readonly cells: readonly DiagnosticIdentityProjection<PreparedDiagnosticSourceCell>[];
|
|
213
|
+
readonly exposures: readonly DiagnosticIdentityProjection<ReconciledDiagnosticExposure>[];
|
|
84
214
|
readonly expectedCellsProvided: boolean;
|
|
85
|
-
readonly expectedCells: readonly
|
|
86
|
-
readonly findings: readonly DiagnosticMetricFinding[];
|
|
215
|
+
readonly expectedCells: readonly NormalizedDiagnosticExpectedCellIdentity[];
|
|
216
|
+
readonly findings: readonly DiagnosticIdentityProjection<DiagnosticMetricFinding>[];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export interface NormalizedDiagnosticExpectedCellIdentity {
|
|
220
|
+
readonly sourceGroup: string;
|
|
221
|
+
readonly origin: string;
|
|
222
|
+
readonly valuation: string;
|
|
223
|
+
readonly source: NormalizedDiagnosticSourceLocationIdentity | null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
interface NormalizedCoordinate {
|
|
227
|
+
readonly origin: DiagnosticNormalizedPeriod;
|
|
228
|
+
readonly valuation: DiagnosticNormalizedPeriod;
|
|
229
|
+
readonly developmentAge: number;
|
|
230
|
+
readonly ageUnit: string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
interface LossCandidate {
|
|
234
|
+
row: DiagnosticLossInput;
|
|
235
|
+
snapshot: DiagnosticLossInputAuditSnapshot;
|
|
236
|
+
disposition: DiagnosticInputDisposition;
|
|
237
|
+
coordinate: NormalizedCoordinate | null;
|
|
238
|
+
invalid: boolean;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
interface ExposureCandidate {
|
|
242
|
+
row: DiagnosticExposureObservation;
|
|
243
|
+
snapshot: DiagnosticExposureInputAuditSnapshot;
|
|
244
|
+
disposition: DiagnosticInputDisposition;
|
|
245
|
+
origin: DiagnosticNormalizedPeriod | null;
|
|
246
|
+
valuation: DiagnosticNormalizedPeriod | null;
|
|
247
|
+
developmentAge: number | null;
|
|
248
|
+
ageUnit: string | null;
|
|
249
|
+
timing: "origin-static" | "valuation-specific" | null;
|
|
250
|
+
invalid: boolean;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface ExpectedCandidate {
|
|
254
|
+
readonly row: DiagnosticExpectedCell;
|
|
255
|
+
readonly snapshot: DiagnosticExpectedCellAuditSnapshot;
|
|
256
|
+
disposition: Exclude<DiagnosticInputDisposition, "invalid">;
|
|
257
|
+
readonly coordinate: NormalizedCoordinate;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
interface NormalizedCutoff {
|
|
261
|
+
readonly value: DiagnosticCompletePeriodCutoff;
|
|
262
|
+
readonly originThrough: DiagnosticNormalizedPeriod | null;
|
|
263
|
+
readonly valuationThrough: DiagnosticNormalizedPeriod | null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface PendingBlocker {
|
|
267
|
+
readonly finding: DiagnosticMetricFinding;
|
|
268
|
+
readonly measureIds: readonly string[];
|
|
269
|
+
readonly cellKeys: readonly string[];
|
|
270
|
+
readonly sourceIds: readonly string[];
|
|
87
271
|
}
|
|
88
272
|
|
|
89
273
|
const authentic = new WeakSet<object>();
|
|
90
|
-
const
|
|
274
|
+
const authenticCompact = new WeakSet<object>();
|
|
275
|
+
const compactIdentityDocuments = new WeakMap<
|
|
276
|
+
object,
|
|
277
|
+
DiagnosticIdentityDocument
|
|
278
|
+
>();
|
|
279
|
+
const compactFingerprints = new WeakMap<object, string>();
|
|
280
|
+
const identities = new WeakMap<
|
|
281
|
+
object,
|
|
282
|
+
DiagnosticDeepReadonly<NormalizedDiagnosticPreparationIdentity>
|
|
283
|
+
>();
|
|
284
|
+
|
|
285
|
+
const STRUCTURAL = {
|
|
286
|
+
"duplicate-loss-record-id": ["Loss record identity is duplicated", "fail"],
|
|
287
|
+
"duplicate-claim-snapshot": ["Claim snapshot identity is duplicated", "fail"],
|
|
288
|
+
"claim-identity-conflict": [
|
|
289
|
+
"Claim identity has conflicting source group or origin",
|
|
290
|
+
"fail",
|
|
291
|
+
],
|
|
292
|
+
"duplicate-aggregate-snapshot": [
|
|
293
|
+
"Aggregate source-cell identity is duplicated",
|
|
294
|
+
"fail",
|
|
295
|
+
],
|
|
296
|
+
"duplicate-exposure-identity": ["Exposure identity is duplicated", "fail"],
|
|
297
|
+
"conflicting-exposure-identity": [
|
|
298
|
+
"Exposure identity has conflicting observations",
|
|
299
|
+
"fail",
|
|
300
|
+
],
|
|
301
|
+
"unknown-origin-period": [
|
|
302
|
+
"Origin period is not declared by the period axis",
|
|
303
|
+
"fail",
|
|
304
|
+
],
|
|
305
|
+
"unknown-valuation-period": [
|
|
306
|
+
"Valuation period is not declared by the period axis",
|
|
307
|
+
"fail",
|
|
308
|
+
],
|
|
309
|
+
"valuation-before-origin": ["Valuation precedes origin", "fail"],
|
|
310
|
+
"unsafe-development-age": [
|
|
311
|
+
"Derived development age is not a nonnegative safe integer",
|
|
312
|
+
"fail",
|
|
313
|
+
],
|
|
314
|
+
"undeclared-loss-measure": [
|
|
315
|
+
"Loss input contains an undeclared measure",
|
|
316
|
+
"fail",
|
|
317
|
+
],
|
|
318
|
+
"wrong-source-loss-measure": [
|
|
319
|
+
"Loss input contains a measure that is not raw loss input",
|
|
320
|
+
"fail",
|
|
321
|
+
],
|
|
322
|
+
"undeclared-exposure-measure": [
|
|
323
|
+
"Exposure input contains an undeclared measure",
|
|
324
|
+
"fail",
|
|
325
|
+
],
|
|
326
|
+
"wrong-source-exposure-measure": [
|
|
327
|
+
"Exposure input names a measure that is not exposure input",
|
|
328
|
+
"fail",
|
|
329
|
+
],
|
|
330
|
+
"incomplete-loss-record": ["Loss record is marked incomplete", "fail"],
|
|
331
|
+
"missing-exposure-value": [
|
|
332
|
+
"Exposure observation has a missing value",
|
|
333
|
+
"fail",
|
|
334
|
+
],
|
|
335
|
+
"incomplete-exposure": ["Exposure observation is marked incomplete", "fail"],
|
|
336
|
+
"non-finite-exposure": ["Exposure observation is non-finite", "fail"],
|
|
337
|
+
"loss-without-exposure": [
|
|
338
|
+
"Loss source cell has no matching required exposure",
|
|
339
|
+
"warning",
|
|
340
|
+
],
|
|
341
|
+
"exposure-without-loss": [
|
|
342
|
+
"Exposure has no matching retained loss source cell",
|
|
343
|
+
"warning",
|
|
344
|
+
],
|
|
345
|
+
"missing-expected-cell": ["Expected source cell is absent", "fail"],
|
|
346
|
+
} as const;
|
|
347
|
+
|
|
348
|
+
type StructuralCode = keyof typeof STRUCTURAL;
|
|
349
|
+
|
|
350
|
+
function codeUnit(left: string, right: string): number {
|
|
351
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function own<T>(
|
|
355
|
+
record: Readonly<Record<string, T>>,
|
|
356
|
+
key: string,
|
|
357
|
+
): T | undefined {
|
|
358
|
+
return Object.prototype.hasOwnProperty.call(record, key)
|
|
359
|
+
? record[key]
|
|
360
|
+
: undefined;
|
|
361
|
+
}
|
|
91
362
|
|
|
92
|
-
function deepFreeze<T>(
|
|
93
|
-
|
|
363
|
+
function deepFreeze<T>(
|
|
364
|
+
value: T,
|
|
365
|
+
seen = new WeakSet<object>(),
|
|
366
|
+
): DiagnosticDeepReadonly<T> {
|
|
367
|
+
if (value === null || typeof value !== "object" || seen.has(value))
|
|
368
|
+
return value as DiagnosticDeepReadonly<T>;
|
|
94
369
|
seen.add(value);
|
|
95
|
-
for (const child of Object.values(value as Record<string, unknown>))
|
|
370
|
+
for (const child of Object.values(value as Record<string, unknown>))
|
|
371
|
+
deepFreeze(child, seen);
|
|
96
372
|
return Object.freeze(value) as DiagnosticDeepReadonly<T>;
|
|
97
373
|
}
|
|
98
374
|
|
|
99
|
-
function
|
|
100
|
-
|
|
375
|
+
function uniqueSorted(values: readonly string[]): string[] {
|
|
376
|
+
return [...new Set(values)].sort(codeUnit);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function normalizeSources(
|
|
380
|
+
values: readonly (DiagnosticSourceLocation | undefined | null)[],
|
|
381
|
+
): DiagnosticSourceLocation[] {
|
|
382
|
+
return normalizeDiagnosticSourceLocations(values);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function structuralFinding(
|
|
386
|
+
code: StructuralCode,
|
|
387
|
+
context: Omit<
|
|
388
|
+
DiagnosticMetricFinding,
|
|
389
|
+
"code" | "message" | "severity" | "category" | "sources"
|
|
390
|
+
> & {
|
|
391
|
+
readonly sources?: readonly DiagnosticSourceLocation[];
|
|
392
|
+
} = {},
|
|
393
|
+
): DiagnosticMetricFinding {
|
|
394
|
+
const [message, severity] = STRUCTURAL[code];
|
|
395
|
+
return {
|
|
396
|
+
code,
|
|
397
|
+
message,
|
|
398
|
+
severity,
|
|
399
|
+
category: "structural",
|
|
400
|
+
...context,
|
|
401
|
+
sources: normalizeSources(context.sources ?? []),
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function aggregationFinding(
|
|
406
|
+
code:
|
|
407
|
+
| "diagnostic-measure-missing"
|
|
408
|
+
| "diagnostic-measure-imputed-zero"
|
|
409
|
+
| "diagnostic-measure-non-finite"
|
|
410
|
+
| "diagnostic-measure-overflow",
|
|
411
|
+
context: Omit<
|
|
412
|
+
DiagnosticMetricFinding,
|
|
413
|
+
"code" | "message" | "severity" | "category" | "sources"
|
|
414
|
+
> & { readonly sources?: readonly DiagnosticSourceLocation[] },
|
|
415
|
+
): DiagnosticMetricFinding {
|
|
416
|
+
const catalog = {
|
|
417
|
+
"diagnostic-measure-missing": ["Measure input is missing", "warning"],
|
|
418
|
+
"diagnostic-measure-imputed-zero": [
|
|
419
|
+
"Missing measure input was imputed as zero",
|
|
420
|
+
"info",
|
|
421
|
+
],
|
|
422
|
+
"diagnostic-measure-non-finite": ["Measure input is non-finite", "fail"],
|
|
423
|
+
"diagnostic-measure-overflow": ["Measure aggregation overflowed", "fail"],
|
|
424
|
+
} as const;
|
|
425
|
+
const [message, severity] = catalog[code];
|
|
426
|
+
return {
|
|
427
|
+
code,
|
|
428
|
+
message,
|
|
429
|
+
severity,
|
|
430
|
+
category: "aggregation",
|
|
431
|
+
...context,
|
|
432
|
+
sources: normalizeSources(context.sources ?? []),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function mergeFindings(
|
|
437
|
+
values: readonly DiagnosticMetricFinding[],
|
|
438
|
+
): DiagnosticMetricFinding[] {
|
|
439
|
+
const merged = new Map<string, DiagnosticMetricFinding>();
|
|
440
|
+
for (const value of values) {
|
|
441
|
+
const key = canonicalJson({ ...value, sources: [] });
|
|
442
|
+
const previous = merged.get(key);
|
|
443
|
+
merged.set(
|
|
444
|
+
key,
|
|
445
|
+
previous === undefined
|
|
446
|
+
? { ...value, sources: normalizeSources(value.sources) }
|
|
447
|
+
: {
|
|
448
|
+
...previous,
|
|
449
|
+
sources: normalizeSources([...previous.sources, ...value.sources]),
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
return [...merged.values()].sort(compareDiagnosticFindings);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function snapshotLoss(
|
|
457
|
+
row: DiagnosticLossInput,
|
|
458
|
+
): DiagnosticLossInputAuditSnapshot {
|
|
459
|
+
const measures = Object.fromEntries(
|
|
460
|
+
Object.keys(row.measures)
|
|
461
|
+
.sort(codeUnit)
|
|
462
|
+
.map((measureId) => [
|
|
463
|
+
measureId,
|
|
464
|
+
auditDiagnosticNumber(row.measures[measureId] ?? null),
|
|
465
|
+
]),
|
|
466
|
+
);
|
|
467
|
+
return {
|
|
468
|
+
recordId: row.recordId,
|
|
469
|
+
rowType: row.rowType,
|
|
470
|
+
claimId: row.rowType === "claim" ? row.claimId : null,
|
|
471
|
+
sourceGroup: row.sourceGroup,
|
|
472
|
+
origin: row.origin,
|
|
473
|
+
valuation: row.valuation,
|
|
474
|
+
complete: row.complete,
|
|
475
|
+
measures,
|
|
476
|
+
source: row.source === undefined ? null : { ...row.source },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function snapshotExposure(
|
|
481
|
+
row: DiagnosticExposureObservation,
|
|
482
|
+
): DiagnosticExposureInputAuditSnapshot {
|
|
483
|
+
return {
|
|
484
|
+
key: row.key,
|
|
485
|
+
sourceGroup: row.sourceGroup,
|
|
486
|
+
origin: row.origin,
|
|
487
|
+
valuation: row.valuation ?? null,
|
|
488
|
+
measureId: row.measureId,
|
|
489
|
+
value: auditDiagnosticNumber(row.value),
|
|
490
|
+
complete: row.complete,
|
|
491
|
+
source: row.source === undefined ? null : { ...row.source },
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function normalizeConfiguredPeriod(
|
|
496
|
+
definition: CompiledDiagnosticDefinition,
|
|
497
|
+
side: "origin" | "valuation",
|
|
498
|
+
label: string,
|
|
499
|
+
path: string,
|
|
500
|
+
issues: DiagnosticValidationIssue[],
|
|
501
|
+
): DiagnosticNormalizedPeriod | null {
|
|
502
|
+
try {
|
|
503
|
+
return normalizeDiagnosticPeriod(definition, side, label);
|
|
504
|
+
} catch {
|
|
505
|
+
issues.push({
|
|
506
|
+
domain: "configuration",
|
|
507
|
+
code: "invalid-period",
|
|
508
|
+
path,
|
|
509
|
+
message: `Unknown ${side} period ${JSON.stringify(label)}`,
|
|
510
|
+
});
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizeFilter(
|
|
516
|
+
definition: CompiledDiagnosticDefinition,
|
|
517
|
+
input: DiagnosticsFilter | undefined,
|
|
518
|
+
knownSourceGroups: ReadonlySet<string>,
|
|
519
|
+
issues: DiagnosticValidationIssue[],
|
|
520
|
+
): {
|
|
521
|
+
readonly value: DiagnosticsFilter | null;
|
|
522
|
+
readonly originCoordinates: ReadonlyMap<string, number>;
|
|
523
|
+
readonly valuationCoordinates: ReadonlyMap<string, number>;
|
|
524
|
+
} {
|
|
525
|
+
if (input === undefined)
|
|
526
|
+
return {
|
|
527
|
+
value: null,
|
|
528
|
+
originCoordinates: new Map(),
|
|
529
|
+
valuationCoordinates: new Map(),
|
|
530
|
+
};
|
|
531
|
+
const checkTokens = (
|
|
532
|
+
values: readonly string[] | undefined,
|
|
533
|
+
path: string,
|
|
534
|
+
): string[] | undefined => {
|
|
535
|
+
if (values === undefined) return undefined;
|
|
536
|
+
values.forEach((value, index) => {
|
|
537
|
+
if (typeof value !== "string" || value.length === 0)
|
|
538
|
+
issues.push({
|
|
539
|
+
domain: "configuration",
|
|
540
|
+
code: "invalid-string",
|
|
541
|
+
path: `${path}[${index}]`,
|
|
542
|
+
message: "Filter values must be nonempty strings",
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
return uniqueSorted(
|
|
546
|
+
values.filter((value) => typeof value === "string" && value.length > 0),
|
|
547
|
+
);
|
|
548
|
+
};
|
|
549
|
+
const normalizeList = (
|
|
550
|
+
values: readonly string[] | undefined,
|
|
551
|
+
side: "origin" | "valuation",
|
|
552
|
+
path: string,
|
|
553
|
+
) => {
|
|
554
|
+
const checked = checkTokens(values, path);
|
|
555
|
+
if (checked === undefined)
|
|
556
|
+
return { labels: undefined, coordinates: new Map<string, number>() };
|
|
557
|
+
const normalized = checked
|
|
558
|
+
.map((label, index) =>
|
|
559
|
+
normalizeConfiguredPeriod(
|
|
560
|
+
definition,
|
|
561
|
+
side,
|
|
562
|
+
label,
|
|
563
|
+
`${path}[${index}]`,
|
|
564
|
+
issues,
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
.filter((value): value is DiagnosticNormalizedPeriod => value !== null);
|
|
568
|
+
return {
|
|
569
|
+
labels: uniqueSorted(normalized.map((value) => value.label)),
|
|
570
|
+
coordinates: new Map(
|
|
571
|
+
normalized.map((value) => [value.label, value.coordinate]),
|
|
572
|
+
),
|
|
573
|
+
};
|
|
574
|
+
};
|
|
575
|
+
const origins = normalizeList(input.origins, "origin", "$.filter.origins");
|
|
576
|
+
const valuations = normalizeList(
|
|
577
|
+
input.valuations,
|
|
578
|
+
"valuation",
|
|
579
|
+
"$.filter.valuations",
|
|
580
|
+
);
|
|
581
|
+
const originFrom =
|
|
582
|
+
input.originFrom === undefined
|
|
583
|
+
? null
|
|
584
|
+
: normalizeConfiguredPeriod(
|
|
585
|
+
definition,
|
|
586
|
+
"origin",
|
|
587
|
+
input.originFrom,
|
|
588
|
+
"$.filter.originFrom",
|
|
589
|
+
issues,
|
|
590
|
+
);
|
|
591
|
+
const originThrough =
|
|
592
|
+
input.originThrough === undefined
|
|
593
|
+
? null
|
|
594
|
+
: normalizeConfiguredPeriod(
|
|
595
|
+
definition,
|
|
596
|
+
"origin",
|
|
597
|
+
input.originThrough,
|
|
598
|
+
"$.filter.originThrough",
|
|
599
|
+
issues,
|
|
600
|
+
);
|
|
601
|
+
const valuationFrom =
|
|
602
|
+
input.valuationFrom === undefined
|
|
603
|
+
? null
|
|
604
|
+
: normalizeConfiguredPeriod(
|
|
605
|
+
definition,
|
|
606
|
+
"valuation",
|
|
607
|
+
input.valuationFrom,
|
|
608
|
+
"$.filter.valuationFrom",
|
|
609
|
+
issues,
|
|
610
|
+
);
|
|
611
|
+
const valuationThrough =
|
|
612
|
+
input.valuationThrough === undefined
|
|
613
|
+
? null
|
|
614
|
+
: normalizeConfiguredPeriod(
|
|
615
|
+
definition,
|
|
616
|
+
"valuation",
|
|
617
|
+
input.valuationThrough,
|
|
618
|
+
"$.filter.valuationThrough",
|
|
619
|
+
issues,
|
|
620
|
+
);
|
|
621
|
+
if (
|
|
622
|
+
originFrom &&
|
|
623
|
+
originThrough &&
|
|
624
|
+
compareDiagnosticPeriods(originFrom, originThrough) > 0
|
|
625
|
+
)
|
|
626
|
+
issues.push({
|
|
627
|
+
domain: "configuration",
|
|
628
|
+
code: "invalid-configuration",
|
|
629
|
+
path: "$.filter",
|
|
630
|
+
message: "Origin range start exceeds end",
|
|
631
|
+
});
|
|
632
|
+
if (
|
|
633
|
+
valuationFrom &&
|
|
634
|
+
valuationThrough &&
|
|
635
|
+
compareDiagnosticPeriods(valuationFrom, valuationThrough) > 0
|
|
636
|
+
)
|
|
637
|
+
issues.push({
|
|
638
|
+
domain: "configuration",
|
|
639
|
+
code: "invalid-configuration",
|
|
640
|
+
path: "$.filter",
|
|
641
|
+
message: "Valuation range start exceeds end",
|
|
642
|
+
});
|
|
643
|
+
for (const [name, bound] of [
|
|
644
|
+
["minDevelopmentAge", input.minDevelopmentAge],
|
|
645
|
+
["maxDevelopmentAge", input.maxDevelopmentAge],
|
|
646
|
+
] as const) {
|
|
647
|
+
if (bound !== undefined && (!Number.isSafeInteger(bound) || bound < 0))
|
|
648
|
+
issues.push({
|
|
649
|
+
domain: "configuration",
|
|
650
|
+
code: "invalid-configuration",
|
|
651
|
+
path: `$.filter.${name}`,
|
|
652
|
+
message: "Development-age bounds must be nonnegative safe integers",
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
if (
|
|
656
|
+
input.minDevelopmentAge !== undefined &&
|
|
657
|
+
input.maxDevelopmentAge !== undefined &&
|
|
658
|
+
input.minDevelopmentAge > input.maxDevelopmentAge
|
|
659
|
+
)
|
|
660
|
+
issues.push({
|
|
661
|
+
domain: "configuration",
|
|
662
|
+
code: "invalid-configuration",
|
|
663
|
+
path: "$.filter",
|
|
664
|
+
message: "Minimum development age exceeds maximum",
|
|
665
|
+
});
|
|
666
|
+
const sourceGroups = checkTokens(input.sourceGroups, "$.filter.sourceGroups");
|
|
667
|
+
const outputGroups = checkTokens(input.outputGroups, "$.filter.outputGroups");
|
|
668
|
+
const instanceIds = checkTokens(input.instanceIds, "$.filter.instanceIds");
|
|
669
|
+
const validInstances = new Set(
|
|
670
|
+
definition.definition.instances.map((instance) => instance.id),
|
|
671
|
+
);
|
|
672
|
+
for (const [index, sourceGroup] of (sourceGroups ?? []).entries()) {
|
|
673
|
+
if (!knownSourceGroups.has(sourceGroup))
|
|
674
|
+
issues.push({
|
|
675
|
+
domain: "configuration",
|
|
676
|
+
code: "unknown-reference",
|
|
677
|
+
path: `$.filter.sourceGroups[${index}]`,
|
|
678
|
+
message: `Unknown source group ${sourceGroup}`,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
for (const [index, instanceId] of (instanceIds ?? []).entries())
|
|
682
|
+
if (!validInstances.has(instanceId))
|
|
683
|
+
issues.push({
|
|
684
|
+
domain: "configuration",
|
|
685
|
+
code: "unknown-reference",
|
|
686
|
+
path: `$.filter.instanceIds[${index}]`,
|
|
687
|
+
message: `Unknown metric instance ${instanceId}`,
|
|
688
|
+
});
|
|
689
|
+
const value: DiagnosticsFilter = {
|
|
690
|
+
...(sourceGroups === undefined ? {} : { sourceGroups }),
|
|
691
|
+
...(outputGroups === undefined ? {} : { outputGroups }),
|
|
692
|
+
...(origins.labels === undefined ? {} : { origins: origins.labels }),
|
|
693
|
+
...(originFrom === null ? {} : { originFrom: originFrom.label }),
|
|
694
|
+
...(originThrough === null ? {} : { originThrough: originThrough.label }),
|
|
695
|
+
...(valuations.labels === undefined
|
|
696
|
+
? {}
|
|
697
|
+
: { valuations: valuations.labels }),
|
|
698
|
+
...(valuationFrom === null ? {} : { valuationFrom: valuationFrom.label }),
|
|
699
|
+
...(valuationThrough === null
|
|
700
|
+
? {}
|
|
701
|
+
: { valuationThrough: valuationThrough.label }),
|
|
702
|
+
...(input.minDevelopmentAge === undefined
|
|
703
|
+
? {}
|
|
704
|
+
: { minDevelopmentAge: input.minDevelopmentAge }),
|
|
705
|
+
...(input.maxDevelopmentAge === undefined
|
|
706
|
+
? {}
|
|
707
|
+
: { maxDevelopmentAge: input.maxDevelopmentAge }),
|
|
708
|
+
...(instanceIds === undefined ? {} : { instanceIds }),
|
|
709
|
+
};
|
|
710
|
+
return {
|
|
711
|
+
value,
|
|
712
|
+
originCoordinates: new Map([
|
|
713
|
+
...origins.coordinates,
|
|
714
|
+
...(originFrom
|
|
715
|
+
? [[originFrom.label, originFrom.coordinate] as const]
|
|
716
|
+
: []),
|
|
717
|
+
...(originThrough
|
|
718
|
+
? [[originThrough.label, originThrough.coordinate] as const]
|
|
719
|
+
: []),
|
|
720
|
+
]),
|
|
721
|
+
valuationCoordinates: new Map([
|
|
722
|
+
...valuations.coordinates,
|
|
723
|
+
...(valuationFrom
|
|
724
|
+
? [[valuationFrom.label, valuationFrom.coordinate] as const]
|
|
725
|
+
: []),
|
|
726
|
+
...(valuationThrough
|
|
727
|
+
? [[valuationThrough.label, valuationThrough.coordinate] as const]
|
|
728
|
+
: []),
|
|
729
|
+
]),
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function normalizeCutoffs(
|
|
734
|
+
definition: CompiledDiagnosticDefinition,
|
|
735
|
+
input: readonly DiagnosticCompletePeriodCutoff[],
|
|
736
|
+
knownSourceGroups: ReadonlySet<string>,
|
|
737
|
+
issues: DiagnosticValidationIssue[],
|
|
738
|
+
): readonly NormalizedCutoff[] {
|
|
739
|
+
const groups = new Set<string>();
|
|
740
|
+
const normalized: NormalizedCutoff[] = [];
|
|
741
|
+
input.forEach((cutoff, index) => {
|
|
742
|
+
if (!knownSourceGroups.has(cutoff.sourceGroup))
|
|
743
|
+
issues.push({
|
|
744
|
+
domain: "configuration",
|
|
745
|
+
code: "unknown-reference",
|
|
746
|
+
path: `$.completePeriodCutoffs[${index}].sourceGroup`,
|
|
747
|
+
message: `Unknown source group ${cutoff.sourceGroup}`,
|
|
748
|
+
});
|
|
749
|
+
if (groups.has(cutoff.sourceGroup))
|
|
750
|
+
issues.push({
|
|
751
|
+
domain: "configuration",
|
|
752
|
+
code: "duplicate-id",
|
|
753
|
+
path: `$.completePeriodCutoffs[${index}].sourceGroup`,
|
|
754
|
+
message: "Complete-period cutoff source group is duplicated",
|
|
755
|
+
});
|
|
756
|
+
groups.add(cutoff.sourceGroup);
|
|
757
|
+
const origin =
|
|
758
|
+
cutoff.originThrough === null
|
|
759
|
+
? null
|
|
760
|
+
: normalizeConfiguredPeriod(
|
|
761
|
+
definition,
|
|
762
|
+
"origin",
|
|
763
|
+
cutoff.originThrough,
|
|
764
|
+
`$.completePeriodCutoffs[${index}].originThrough`,
|
|
765
|
+
issues,
|
|
766
|
+
);
|
|
767
|
+
const valuation =
|
|
768
|
+
cutoff.valuationThrough === null
|
|
769
|
+
? null
|
|
770
|
+
: normalizeConfiguredPeriod(
|
|
771
|
+
definition,
|
|
772
|
+
"valuation",
|
|
773
|
+
cutoff.valuationThrough,
|
|
774
|
+
`$.completePeriodCutoffs[${index}].valuationThrough`,
|
|
775
|
+
issues,
|
|
776
|
+
);
|
|
777
|
+
normalized.push({
|
|
778
|
+
value: {
|
|
779
|
+
sourceGroup: cutoff.sourceGroup,
|
|
780
|
+
originThrough: origin?.label ?? null,
|
|
781
|
+
valuationThrough: valuation?.label ?? null,
|
|
782
|
+
},
|
|
783
|
+
originThrough: origin,
|
|
784
|
+
valuationThrough: valuation,
|
|
785
|
+
});
|
|
786
|
+
});
|
|
787
|
+
return normalized.sort((left, right) =>
|
|
788
|
+
codeUnit(left.value.sourceGroup, right.value.sourceGroup),
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function coordinateFor(
|
|
793
|
+
definition: CompiledDiagnosticDefinition,
|
|
794
|
+
originLabel: string,
|
|
795
|
+
valuationLabel: string,
|
|
796
|
+
): {
|
|
797
|
+
readonly coordinate: NormalizedCoordinate | null;
|
|
798
|
+
readonly issue:
|
|
799
|
+
| "unknown-origin-period"
|
|
800
|
+
| "unknown-valuation-period"
|
|
801
|
+
| "valuation-before-origin"
|
|
802
|
+
| "unsafe-development-age"
|
|
803
|
+
| null;
|
|
804
|
+
} {
|
|
805
|
+
let origin: DiagnosticNormalizedPeriod;
|
|
806
|
+
let valuation: DiagnosticNormalizedPeriod;
|
|
807
|
+
try {
|
|
808
|
+
origin = normalizeDiagnosticPeriod(definition, "origin", originLabel);
|
|
809
|
+
} catch {
|
|
810
|
+
return { coordinate: null, issue: "unknown-origin-period" };
|
|
811
|
+
}
|
|
812
|
+
try {
|
|
813
|
+
valuation = normalizeDiagnosticPeriod(
|
|
814
|
+
definition,
|
|
815
|
+
"valuation",
|
|
816
|
+
valuationLabel,
|
|
817
|
+
);
|
|
818
|
+
} catch {
|
|
819
|
+
return { coordinate: null, issue: "unknown-valuation-period" };
|
|
820
|
+
}
|
|
821
|
+
const developmentAge =
|
|
822
|
+
valuation.coordinate -
|
|
823
|
+
origin.coordinate +
|
|
824
|
+
definition.definition.periodAxis.ageOffset;
|
|
825
|
+
if (!Number.isSafeInteger(developmentAge))
|
|
826
|
+
return { coordinate: null, issue: "unsafe-development-age" };
|
|
827
|
+
if (developmentAge < 0)
|
|
828
|
+
return { coordinate: null, issue: "valuation-before-origin" };
|
|
829
|
+
return {
|
|
830
|
+
coordinate: {
|
|
831
|
+
origin,
|
|
832
|
+
valuation,
|
|
833
|
+
developmentAge,
|
|
834
|
+
ageUnit: definition.definition.periodAxis.ageUnit,
|
|
835
|
+
},
|
|
836
|
+
issue: null,
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
function cellKey(
|
|
841
|
+
sourceGroup: string,
|
|
842
|
+
origin: string,
|
|
843
|
+
valuation: string,
|
|
844
|
+
): string {
|
|
845
|
+
return canonicalJson([sourceGroup, origin, valuation]);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function parseCellKey(key: string): readonly [string, string, string] {
|
|
849
|
+
return JSON.parse(key) as [string, string, string];
|
|
850
|
+
}
|
|
101
851
|
|
|
102
|
-
function
|
|
103
|
-
|
|
104
|
-
|
|
852
|
+
function coordinateContext(candidate: {
|
|
853
|
+
readonly row: { readonly sourceGroup: string };
|
|
854
|
+
readonly coordinate: NormalizedCoordinate | null;
|
|
855
|
+
}): Partial<DiagnosticMetricFinding> {
|
|
856
|
+
if (candidate.coordinate === null) return {};
|
|
105
857
|
return {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
858
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
859
|
+
origin: candidate.coordinate.origin.label,
|
|
860
|
+
valuation: candidate.coordinate.valuation.label,
|
|
861
|
+
developmentAge: candidate.coordinate.developmentAge,
|
|
862
|
+
ageUnit: candidate.coordinate.ageUnit,
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function selectedBySource(
|
|
867
|
+
filter: DiagnosticsFilter | null,
|
|
868
|
+
sourceGroup: string,
|
|
869
|
+
): boolean {
|
|
870
|
+
return (
|
|
871
|
+
filter?.sourceGroups === undefined ||
|
|
872
|
+
filter.sourceGroups.includes(sourceGroup)
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function selectedByOrigin(
|
|
877
|
+
filter: DiagnosticsFilter | null,
|
|
878
|
+
origin: DiagnosticNormalizedPeriod,
|
|
879
|
+
coordinates: ReadonlyMap<string, number>,
|
|
880
|
+
): boolean {
|
|
881
|
+
const from =
|
|
882
|
+
filter?.originFrom === undefined
|
|
883
|
+
? null
|
|
884
|
+
: coordinates.get(filter.originFrom)!;
|
|
885
|
+
const through =
|
|
886
|
+
filter?.originThrough === undefined
|
|
887
|
+
? null
|
|
888
|
+
: coordinates.get(filter.originThrough)!;
|
|
889
|
+
return (
|
|
890
|
+
(filter?.origins === undefined || filter.origins.includes(origin.label)) &&
|
|
891
|
+
(from === null || origin.coordinate >= from) &&
|
|
892
|
+
(through === null || origin.coordinate <= through)
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
function selectedByValuation(
|
|
897
|
+
filter: DiagnosticsFilter | null,
|
|
898
|
+
valuation: DiagnosticNormalizedPeriod,
|
|
899
|
+
coordinates: ReadonlyMap<string, number>,
|
|
900
|
+
): boolean {
|
|
901
|
+
const from =
|
|
902
|
+
filter?.valuationFrom === undefined
|
|
903
|
+
? null
|
|
904
|
+
: coordinates.get(filter.valuationFrom)!;
|
|
905
|
+
const through =
|
|
906
|
+
filter?.valuationThrough === undefined
|
|
907
|
+
? null
|
|
908
|
+
: coordinates.get(filter.valuationThrough)!;
|
|
909
|
+
return (
|
|
910
|
+
(filter?.valuations === undefined ||
|
|
911
|
+
filter.valuations.includes(valuation.label)) &&
|
|
912
|
+
(from === null || valuation.coordinate >= from) &&
|
|
913
|
+
(through === null || valuation.coordinate <= through)
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function selectedByAge(filter: DiagnosticsFilter | null, age: number): boolean {
|
|
918
|
+
return (
|
|
919
|
+
(filter?.minDevelopmentAge === undefined ||
|
|
920
|
+
age >= filter.minDevelopmentAge) &&
|
|
921
|
+
(filter?.maxDevelopmentAge === undefined || age <= filter.maxDevelopmentAge)
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function beyondCutoff(
|
|
926
|
+
candidate: {
|
|
927
|
+
readonly row: { readonly sourceGroup: string };
|
|
928
|
+
readonly coordinate: NormalizedCoordinate;
|
|
929
|
+
},
|
|
930
|
+
cutoffByGroup: ReadonlyMap<string, NormalizedCutoff>,
|
|
931
|
+
): boolean {
|
|
932
|
+
const cutoff = cutoffByGroup.get(candidate.row.sourceGroup);
|
|
933
|
+
return (
|
|
934
|
+
cutoff !== undefined &&
|
|
935
|
+
((cutoff.originThrough !== null &&
|
|
936
|
+
compareDiagnosticPeriods(
|
|
937
|
+
candidate.coordinate.origin,
|
|
938
|
+
cutoff.originThrough,
|
|
939
|
+
) > 0) ||
|
|
940
|
+
(cutoff.valuationThrough !== null &&
|
|
941
|
+
compareDiagnosticPeriods(
|
|
942
|
+
candidate.coordinate.valuation,
|
|
943
|
+
cutoff.valuationThrough,
|
|
944
|
+
) > 0))
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function addToMap<K, V>(map: Map<K, V[]>, key: K, value: V): void {
|
|
949
|
+
const values = map.get(key) ?? [];
|
|
950
|
+
values.push(value);
|
|
951
|
+
map.set(key, values);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function commonCoordinate(
|
|
955
|
+
candidates: readonly LossCandidate[],
|
|
956
|
+
): Partial<DiagnosticMetricFinding> {
|
|
957
|
+
const keys = uniqueSorted(
|
|
958
|
+
candidates.flatMap((candidate) =>
|
|
959
|
+
candidate.coordinate === null
|
|
960
|
+
? []
|
|
961
|
+
: [
|
|
962
|
+
cellKey(
|
|
963
|
+
candidate.row.sourceGroup,
|
|
964
|
+
candidate.coordinate.origin.label,
|
|
965
|
+
candidate.coordinate.valuation.label,
|
|
966
|
+
),
|
|
967
|
+
],
|
|
968
|
+
),
|
|
969
|
+
);
|
|
970
|
+
return keys.length === 1 ? coordinateContext(candidates[0]!) : {};
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function exposureIdentity(candidate: ExposureCandidate): string {
|
|
974
|
+
return canonicalJson(
|
|
975
|
+
candidate.timing === "valuation-specific"
|
|
976
|
+
? [
|
|
977
|
+
candidate.row.measureId,
|
|
978
|
+
candidate.row.key,
|
|
979
|
+
candidate.row.valuation ?? null,
|
|
980
|
+
]
|
|
981
|
+
: [candidate.row.measureId, candidate.row.key],
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
function exposureCoordinateContext(
|
|
986
|
+
candidates: readonly ExposureCandidate[],
|
|
987
|
+
): Partial<DiagnosticMetricFinding> {
|
|
988
|
+
const coherentSource = uniqueSorted(
|
|
989
|
+
candidates.map((candidate) => candidate.row.sourceGroup),
|
|
990
|
+
);
|
|
991
|
+
const coherentOrigin = uniqueSorted(
|
|
992
|
+
candidates.flatMap((candidate) =>
|
|
993
|
+
candidate.origin ? [candidate.origin.label] : [],
|
|
994
|
+
),
|
|
995
|
+
);
|
|
996
|
+
const coherentValuation = uniqueSorted(
|
|
997
|
+
candidates.flatMap((candidate) =>
|
|
998
|
+
candidate.valuation ? [candidate.valuation.label] : [],
|
|
999
|
+
),
|
|
1000
|
+
);
|
|
1001
|
+
if (coherentSource.length !== 1 || coherentOrigin.length !== 1) return {};
|
|
1002
|
+
const first = candidates[0]!;
|
|
1003
|
+
if (
|
|
1004
|
+
first.timing === "valuation-specific" &&
|
|
1005
|
+
coherentValuation.length === 1 &&
|
|
1006
|
+
first.developmentAge !== null &&
|
|
1007
|
+
first.ageUnit !== null
|
|
1008
|
+
) {
|
|
1009
|
+
return {
|
|
1010
|
+
sourceGroup: coherentSource[0],
|
|
1011
|
+
origin: coherentOrigin[0],
|
|
1012
|
+
valuation: coherentValuation[0],
|
|
1013
|
+
developmentAge: first.developmentAge,
|
|
1014
|
+
ageUnit: first.ageUnit,
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
return { sourceGroup: coherentSource[0], origin: coherentOrigin[0] };
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
function createAuditComparator(): (
|
|
1021
|
+
left: DiagnosticInputAuditRecord,
|
|
1022
|
+
right: DiagnosticInputAuditRecord,
|
|
1023
|
+
) => number {
|
|
1024
|
+
const rank = { loss: 0, exposure: 1, "expected-cell": 2 } as const;
|
|
1025
|
+
const disposition = {
|
|
1026
|
+
invalid: 0,
|
|
1027
|
+
"complete-period-cutoff": 1,
|
|
1028
|
+
filter: 2,
|
|
1029
|
+
retained: 3,
|
|
1030
|
+
} as const;
|
|
1031
|
+
// Audit snapshots are already owned and complete before sorting. Preserve the
|
|
1032
|
+
// exact canonical-text ordering, but serialize each compared record once.
|
|
1033
|
+
// Keep this memo local to the sort; it must not retain keys with the result.
|
|
1034
|
+
const keys = new WeakMap<DiagnosticInputAuditRecord["record"], string>();
|
|
1035
|
+
const key = (record: DiagnosticInputAuditRecord["record"]): string => {
|
|
1036
|
+
const previous = keys.get(record);
|
|
1037
|
+
if (previous !== undefined) return previous;
|
|
1038
|
+
const text = canonicalJson(record);
|
|
1039
|
+
keys.set(record, text);
|
|
1040
|
+
return text;
|
|
1041
|
+
};
|
|
1042
|
+
return (left, right) =>
|
|
1043
|
+
rank[left.kind] - rank[right.kind] ||
|
|
1044
|
+
codeUnit(key(left.record), key(right.record)) ||
|
|
1045
|
+
disposition[left.disposition] - disposition[right.disposition];
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
1049
|
+
return isDiagnosticPlainRecord(value);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function preparationBoundaryIssues(
|
|
1053
|
+
value: unknown,
|
|
1054
|
+
): DiagnosticValidationIssue[] {
|
|
1055
|
+
const issues: DiagnosticValidationIssue[] = [];
|
|
1056
|
+
if (!isPlainRecord(value))
|
|
1057
|
+
return [
|
|
1058
|
+
{
|
|
1059
|
+
domain: "input",
|
|
1060
|
+
code: "invalid-type",
|
|
1061
|
+
path: "$",
|
|
1062
|
+
message: "Preparation input must be an object",
|
|
1063
|
+
},
|
|
1064
|
+
];
|
|
1065
|
+
const issue = (
|
|
1066
|
+
domain: "input" | "configuration",
|
|
1067
|
+
code: DiagnosticValidationIssue["code"],
|
|
1068
|
+
path: string,
|
|
1069
|
+
message: string,
|
|
1070
|
+
) => issues.push({ domain, code, path, message });
|
|
1071
|
+
const token = (
|
|
1072
|
+
item: unknown,
|
|
1073
|
+
path: string,
|
|
1074
|
+
domain: "input" | "configuration" = "input",
|
|
1075
|
+
) => {
|
|
1076
|
+
if (typeof item !== "string")
|
|
1077
|
+
issue(domain, "invalid-type", path, "Expected a string");
|
|
1078
|
+
else if (!isDiagnosticToken(item))
|
|
1079
|
+
issue(
|
|
1080
|
+
domain,
|
|
1081
|
+
"invalid-string",
|
|
1082
|
+
path,
|
|
1083
|
+
"Expected a nonempty token with valid Unicode and no U+0000",
|
|
1084
|
+
);
|
|
1085
|
+
};
|
|
1086
|
+
const exactKeys = (
|
|
1087
|
+
item: Record<string, unknown>,
|
|
1088
|
+
allowed: readonly string[],
|
|
1089
|
+
path: string,
|
|
1090
|
+
domain: "input" | "configuration",
|
|
1091
|
+
) => {
|
|
1092
|
+
for (const key of Object.keys(item)) {
|
|
1093
|
+
const keyPath = `${path}.${key}`;
|
|
1094
|
+
if (!allowed.includes(key))
|
|
1095
|
+
issue(domain, "unknown-key", keyPath, `Unknown key ${key}`);
|
|
1096
|
+
else if (item[key] === undefined)
|
|
1097
|
+
issue(
|
|
1098
|
+
domain,
|
|
1099
|
+
"invalid-type",
|
|
1100
|
+
keyPath,
|
|
1101
|
+
"Explicit undefined is not allowed",
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
const source = (item: unknown, path: string) => {
|
|
1106
|
+
if (!isPlainRecord(item)) {
|
|
1107
|
+
issue("input", "invalid-type", path, "Source location must be an object");
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
exactKeys(
|
|
1111
|
+
item,
|
|
1112
|
+
["artifactId", "sourceFile", "sourceSheet", "sourceRow", "sourceCell"],
|
|
1113
|
+
path,
|
|
1114
|
+
"input",
|
|
1115
|
+
);
|
|
1116
|
+
token(item.artifactId, `${path}.artifactId`);
|
|
1117
|
+
for (const key of ["sourceFile", "sourceSheet", "sourceCell"] as const)
|
|
1118
|
+
if (item[key] !== undefined) token(item[key], `${path}.${key}`);
|
|
1119
|
+
if (
|
|
1120
|
+
item.sourceRow !== undefined &&
|
|
1121
|
+
(typeof item.sourceRow !== "number" ||
|
|
1122
|
+
!Number.isSafeInteger(item.sourceRow) ||
|
|
1123
|
+
item.sourceRow < 0)
|
|
1124
|
+
)
|
|
1125
|
+
issue(
|
|
1126
|
+
"input",
|
|
1127
|
+
"invalid-number",
|
|
1128
|
+
`${path}.sourceRow`,
|
|
1129
|
+
"Source row must be a nonnegative safe integer",
|
|
1130
|
+
);
|
|
1131
|
+
};
|
|
1132
|
+
const array = (
|
|
1133
|
+
item: unknown,
|
|
1134
|
+
path: string,
|
|
1135
|
+
domain: "input" | "configuration",
|
|
1136
|
+
): readonly unknown[] => {
|
|
1137
|
+
if (!Array.isArray(item)) {
|
|
1138
|
+
issue(domain, "invalid-type", path, "Expected an array");
|
|
1139
|
+
return [];
|
|
1140
|
+
}
|
|
1141
|
+
return item;
|
|
1142
|
+
};
|
|
1143
|
+
exactKeys(
|
|
1144
|
+
value,
|
|
1145
|
+
[
|
|
1146
|
+
"definition",
|
|
1147
|
+
"losses",
|
|
1148
|
+
"exposures",
|
|
1149
|
+
"filter",
|
|
1150
|
+
"completePeriodCutoffs",
|
|
1151
|
+
"expectedCells",
|
|
1152
|
+
],
|
|
1153
|
+
"$",
|
|
1154
|
+
"configuration",
|
|
1155
|
+
);
|
|
1156
|
+
for (const [index, raw] of array(
|
|
1157
|
+
value.losses,
|
|
1158
|
+
"$.losses",
|
|
1159
|
+
"input",
|
|
1160
|
+
).entries()) {
|
|
1161
|
+
const path = `$.losses[${index}]`;
|
|
1162
|
+
if (!isPlainRecord(raw)) {
|
|
1163
|
+
issue("input", "invalid-type", path, "Loss record must be an object");
|
|
1164
|
+
continue;
|
|
1165
|
+
}
|
|
1166
|
+
exactKeys(
|
|
1167
|
+
raw,
|
|
1168
|
+
[
|
|
1169
|
+
"rowType",
|
|
1170
|
+
"recordId",
|
|
1171
|
+
"claimId",
|
|
1172
|
+
"sourceGroup",
|
|
1173
|
+
"origin",
|
|
1174
|
+
"valuation",
|
|
1175
|
+
"complete",
|
|
1176
|
+
"source",
|
|
1177
|
+
"measures",
|
|
1178
|
+
],
|
|
1179
|
+
path,
|
|
1180
|
+
"input",
|
|
1181
|
+
);
|
|
1182
|
+
if (raw.rowType !== "claim" && raw.rowType !== "aggregate")
|
|
1183
|
+
issue(
|
|
1184
|
+
"input",
|
|
1185
|
+
"invalid-type",
|
|
1186
|
+
`${path}.rowType`,
|
|
1187
|
+
"Loss row type must be claim or aggregate",
|
|
1188
|
+
);
|
|
1189
|
+
token(raw.recordId, `${path}.recordId`);
|
|
1190
|
+
token(raw.sourceGroup, `${path}.sourceGroup`);
|
|
1191
|
+
token(raw.origin, `${path}.origin`);
|
|
1192
|
+
token(raw.valuation, `${path}.valuation`);
|
|
1193
|
+
if (raw.rowType === "claim") token(raw.claimId, `${path}.claimId`);
|
|
1194
|
+
else if (raw.claimId !== undefined)
|
|
1195
|
+
issue(
|
|
1196
|
+
"input",
|
|
1197
|
+
"unknown-key",
|
|
1198
|
+
`${path}.claimId`,
|
|
1199
|
+
"Aggregate loss rows cannot contain claimId",
|
|
1200
|
+
);
|
|
1201
|
+
if (typeof raw.complete !== "boolean")
|
|
1202
|
+
issue(
|
|
1203
|
+
"input",
|
|
1204
|
+
"invalid-type",
|
|
1205
|
+
`${path}.complete`,
|
|
1206
|
+
"Loss completeness must be boolean",
|
|
1207
|
+
);
|
|
1208
|
+
if (raw.source !== undefined) source(raw.source, `${path}.source`);
|
|
1209
|
+
if (!isPlainRecord(raw.measures))
|
|
1210
|
+
issue(
|
|
1211
|
+
"input",
|
|
1212
|
+
"invalid-type",
|
|
1213
|
+
`${path}.measures`,
|
|
1214
|
+
"Measures must be an object",
|
|
1215
|
+
);
|
|
1216
|
+
else
|
|
1217
|
+
for (const [measureId, amount] of Object.entries(raw.measures)) {
|
|
1218
|
+
token(measureId, `${path}.measures`);
|
|
1219
|
+
if (amount !== null && typeof amount !== "number")
|
|
1220
|
+
issue(
|
|
1221
|
+
"input",
|
|
1222
|
+
"invalid-type",
|
|
1223
|
+
`${path}.measures.${measureId}`,
|
|
1224
|
+
"Measure value must be a number or null",
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
for (const [index, raw] of array(
|
|
1229
|
+
value.exposures,
|
|
1230
|
+
"$.exposures",
|
|
1231
|
+
"input",
|
|
1232
|
+
).entries()) {
|
|
1233
|
+
const path = `$.exposures[${index}]`;
|
|
1234
|
+
if (!isPlainRecord(raw)) {
|
|
1235
|
+
issue(
|
|
1236
|
+
"input",
|
|
1237
|
+
"invalid-type",
|
|
1238
|
+
path,
|
|
1239
|
+
"Exposure observation must be an object",
|
|
1240
|
+
);
|
|
1241
|
+
continue;
|
|
1242
|
+
}
|
|
1243
|
+
exactKeys(
|
|
1244
|
+
raw,
|
|
1245
|
+
[
|
|
1246
|
+
"key",
|
|
1247
|
+
"sourceGroup",
|
|
1248
|
+
"origin",
|
|
1249
|
+
"valuation",
|
|
1250
|
+
"measureId",
|
|
1251
|
+
"value",
|
|
1252
|
+
"complete",
|
|
1253
|
+
"source",
|
|
1254
|
+
],
|
|
1255
|
+
path,
|
|
1256
|
+
"input",
|
|
1257
|
+
);
|
|
1258
|
+
token(raw.key, `${path}.key`);
|
|
1259
|
+
token(raw.sourceGroup, `${path}.sourceGroup`);
|
|
1260
|
+
token(raw.origin, `${path}.origin`);
|
|
1261
|
+
token(raw.measureId, `${path}.measureId`);
|
|
1262
|
+
if (raw.valuation !== undefined) token(raw.valuation, `${path}.valuation`);
|
|
1263
|
+
if (raw.value !== null && typeof raw.value !== "number")
|
|
1264
|
+
issue(
|
|
1265
|
+
"input",
|
|
1266
|
+
"invalid-type",
|
|
1267
|
+
`${path}.value`,
|
|
1268
|
+
"Exposure value must be a number or null",
|
|
1269
|
+
);
|
|
1270
|
+
if (typeof raw.complete !== "boolean")
|
|
1271
|
+
issue(
|
|
1272
|
+
"input",
|
|
1273
|
+
"invalid-type",
|
|
1274
|
+
`${path}.complete`,
|
|
1275
|
+
"Exposure completeness must be boolean",
|
|
1276
|
+
);
|
|
1277
|
+
if (raw.source !== undefined) source(raw.source, `${path}.source`);
|
|
1278
|
+
}
|
|
1279
|
+
if (value.filter !== undefined && !isPlainRecord(value.filter))
|
|
1280
|
+
issue(
|
|
1281
|
+
"configuration",
|
|
1282
|
+
"invalid-type",
|
|
1283
|
+
"$.filter",
|
|
1284
|
+
"Filter must be an object",
|
|
1285
|
+
);
|
|
1286
|
+
if (isPlainRecord(value.filter)) {
|
|
1287
|
+
const filter = value.filter;
|
|
1288
|
+
const keys = [
|
|
1289
|
+
"sourceGroups",
|
|
1290
|
+
"outputGroups",
|
|
1291
|
+
"origins",
|
|
1292
|
+
"originFrom",
|
|
1293
|
+
"originThrough",
|
|
1294
|
+
"valuations",
|
|
1295
|
+
"valuationFrom",
|
|
1296
|
+
"valuationThrough",
|
|
1297
|
+
"minDevelopmentAge",
|
|
1298
|
+
"maxDevelopmentAge",
|
|
1299
|
+
"instanceIds",
|
|
1300
|
+
] as const;
|
|
1301
|
+
exactKeys(filter, keys, "$.filter", "configuration");
|
|
1302
|
+
for (const key of [
|
|
1303
|
+
"sourceGroups",
|
|
1304
|
+
"outputGroups",
|
|
1305
|
+
"origins",
|
|
1306
|
+
"valuations",
|
|
1307
|
+
"instanceIds",
|
|
1308
|
+
] as const)
|
|
1309
|
+
if (filter[key] !== undefined)
|
|
1310
|
+
array(filter[key], `$.filter.${key}`, "configuration").forEach(
|
|
1311
|
+
(item, index) =>
|
|
1312
|
+
token(item, `$.filter.${key}[${index}]`, "configuration"),
|
|
1313
|
+
);
|
|
1314
|
+
for (const key of [
|
|
1315
|
+
"originFrom",
|
|
1316
|
+
"originThrough",
|
|
1317
|
+
"valuationFrom",
|
|
1318
|
+
"valuationThrough",
|
|
1319
|
+
] as const)
|
|
1320
|
+
if (filter[key] !== undefined)
|
|
1321
|
+
token(filter[key], `$.filter.${key}`, "configuration");
|
|
1322
|
+
}
|
|
1323
|
+
if (value.completePeriodCutoffs !== undefined)
|
|
1324
|
+
for (const [index, raw] of array(
|
|
1325
|
+
value.completePeriodCutoffs,
|
|
1326
|
+
"$.completePeriodCutoffs",
|
|
1327
|
+
"configuration",
|
|
1328
|
+
).entries()) {
|
|
1329
|
+
const path = `$.completePeriodCutoffs[${index}]`;
|
|
1330
|
+
if (!isPlainRecord(raw)) {
|
|
1331
|
+
issue(
|
|
1332
|
+
"configuration",
|
|
1333
|
+
"invalid-type",
|
|
1334
|
+
path,
|
|
1335
|
+
"Cutoff must be an object",
|
|
1336
|
+
);
|
|
1337
|
+
continue;
|
|
1338
|
+
}
|
|
1339
|
+
exactKeys(
|
|
1340
|
+
raw,
|
|
1341
|
+
["sourceGroup", "originThrough", "valuationThrough"],
|
|
1342
|
+
path,
|
|
1343
|
+
"configuration",
|
|
1344
|
+
);
|
|
1345
|
+
token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
|
|
1346
|
+
for (const key of ["originThrough", "valuationThrough"] as const)
|
|
1347
|
+
if (raw[key] !== null)
|
|
1348
|
+
token(raw[key], `${path}.${key}`, "configuration");
|
|
1349
|
+
}
|
|
1350
|
+
if (value.expectedCells !== undefined)
|
|
1351
|
+
for (const [index, raw] of array(
|
|
1352
|
+
value.expectedCells,
|
|
1353
|
+
"$.expectedCells",
|
|
1354
|
+
"configuration",
|
|
1355
|
+
).entries()) {
|
|
1356
|
+
const path = `$.expectedCells[${index}]`;
|
|
1357
|
+
if (!isPlainRecord(raw)) {
|
|
1358
|
+
issue(
|
|
1359
|
+
"configuration",
|
|
1360
|
+
"invalid-type",
|
|
1361
|
+
path,
|
|
1362
|
+
"Expected cell must be an object",
|
|
1363
|
+
);
|
|
1364
|
+
continue;
|
|
1365
|
+
}
|
|
1366
|
+
exactKeys(
|
|
1367
|
+
raw,
|
|
1368
|
+
["sourceGroup", "origin", "valuation", "source"],
|
|
1369
|
+
path,
|
|
1370
|
+
"configuration",
|
|
1371
|
+
);
|
|
1372
|
+
token(raw.sourceGroup, `${path}.sourceGroup`, "configuration");
|
|
1373
|
+
token(raw.origin, `${path}.origin`, "configuration");
|
|
1374
|
+
token(raw.valuation, `${path}.valuation`, "configuration");
|
|
1375
|
+
if (raw.source !== undefined) source(raw.source, `${path}.source`);
|
|
1376
|
+
}
|
|
1377
|
+
return issues;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
export function prepareDiagnosticData(
|
|
1381
|
+
input: PrepareDiagnosticDataInput,
|
|
1382
|
+
): PreparedDiagnosticData {
|
|
1383
|
+
return prepareDiagnosticDataInMode(input, "eager") as PreparedDiagnosticData;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
export function prepareDiagnosticDataCompact(
|
|
1387
|
+
input: PrepareDiagnosticDataInput,
|
|
1388
|
+
): CompactPreparedDiagnosticData {
|
|
1389
|
+
return prepareDiagnosticDataInMode(
|
|
1390
|
+
input,
|
|
1391
|
+
"compact",
|
|
1392
|
+
) as CompactPreparedDiagnosticData;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function prepareDiagnosticDataInMode(
|
|
1396
|
+
input: PrepareDiagnosticDataInput,
|
|
1397
|
+
mode: "eager" | "compact",
|
|
1398
|
+
): PreparedDiagnosticData | CompactPreparedDiagnosticData {
|
|
1399
|
+
assertCompiledDiagnosticDefinition(
|
|
1400
|
+
(input as unknown as { readonly definition?: unknown } | null)?.definition,
|
|
1401
|
+
);
|
|
1402
|
+
const boundaryIssues = preparationBoundaryIssues(input);
|
|
1403
|
+
if (boundaryIssues.length > 0)
|
|
1404
|
+
throw new DiagnosticValidationError(boundaryIssues);
|
|
134
1405
|
const definition = input.definition;
|
|
135
1406
|
const internals = getCompiledDiagnosticDefinitionInternals(definition);
|
|
136
1407
|
const issues: DiagnosticValidationIssue[] = [];
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
1408
|
+
const knownSourceGroups = new Set<string>([
|
|
1409
|
+
...input.losses.map((row) => row.sourceGroup),
|
|
1410
|
+
...input.exposures.map((row) => row.sourceGroup),
|
|
1411
|
+
...(input.expectedCells ?? []).map((row) => row.sourceGroup),
|
|
1412
|
+
]);
|
|
1413
|
+
const normalizedFilter = normalizeFilter(
|
|
1414
|
+
definition,
|
|
1415
|
+
input.filter,
|
|
1416
|
+
knownSourceGroups,
|
|
1417
|
+
issues,
|
|
1418
|
+
);
|
|
1419
|
+
const normalizedCutoffs = normalizeCutoffs(
|
|
1420
|
+
definition,
|
|
1421
|
+
input.completePeriodCutoffs ?? [],
|
|
1422
|
+
knownSourceGroups,
|
|
1423
|
+
issues,
|
|
1424
|
+
);
|
|
1425
|
+
const cutoffByGroup = new Map(
|
|
1426
|
+
normalizedCutoffs.map((cutoff) => [cutoff.value.sourceGroup, cutoff]),
|
|
1427
|
+
);
|
|
1428
|
+
|
|
140
1429
|
input.losses.forEach((row, index) => {
|
|
141
|
-
if (row.rowType !== definition.definition.lossRowGrain)
|
|
1430
|
+
if (row.rowType !== definition.definition.lossRowGrain)
|
|
1431
|
+
issues.push({
|
|
1432
|
+
domain: "input",
|
|
1433
|
+
code: "invalid-input-relationship",
|
|
1434
|
+
path: `$.losses[${index}].rowType`,
|
|
1435
|
+
message: "Loss row type does not match definition grain",
|
|
1436
|
+
});
|
|
142
1437
|
});
|
|
143
1438
|
input.exposures.forEach((row, index) => {
|
|
144
1439
|
const measure = internals.measuresById.get(row.measureId);
|
|
145
|
-
if (
|
|
1440
|
+
if (
|
|
1441
|
+
measure?.exposureTiming === "valuation-specific" &&
|
|
1442
|
+
row.valuation === undefined
|
|
1443
|
+
)
|
|
1444
|
+
issues.push({
|
|
1445
|
+
domain: "input",
|
|
1446
|
+
code: "missing-required",
|
|
1447
|
+
path: `$.exposures[${index}].valuation`,
|
|
1448
|
+
message: "Valuation-specific exposure requires valuation",
|
|
1449
|
+
});
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const expectedCandidates: ExpectedCandidate[] = [];
|
|
1453
|
+
const expectedKeys = new Set<string>();
|
|
1454
|
+
(input.expectedCells ?? []).forEach((row, index) => {
|
|
1455
|
+
const origin = normalizeConfiguredPeriod(
|
|
1456
|
+
definition,
|
|
1457
|
+
"origin",
|
|
1458
|
+
row.origin,
|
|
1459
|
+
`$.expectedCells[${index}].origin`,
|
|
1460
|
+
issues,
|
|
1461
|
+
);
|
|
1462
|
+
const valuation = normalizeConfiguredPeriod(
|
|
1463
|
+
definition,
|
|
1464
|
+
"valuation",
|
|
1465
|
+
row.valuation,
|
|
1466
|
+
`$.expectedCells[${index}].valuation`,
|
|
1467
|
+
issues,
|
|
1468
|
+
);
|
|
1469
|
+
if (!origin || !valuation) return;
|
|
1470
|
+
const developmentAge =
|
|
1471
|
+
valuation.coordinate -
|
|
1472
|
+
origin.coordinate +
|
|
1473
|
+
definition.definition.periodAxis.ageOffset;
|
|
1474
|
+
if (!Number.isSafeInteger(developmentAge) || developmentAge < 0) {
|
|
1475
|
+
issues.push({
|
|
1476
|
+
domain: "configuration",
|
|
1477
|
+
code: "invalid-period",
|
|
1478
|
+
path: `$.expectedCells[${index}].valuation`,
|
|
1479
|
+
message:
|
|
1480
|
+
"Expected-cell valuation precedes origin or produces an unsafe development age",
|
|
1481
|
+
});
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
const normalized = {
|
|
1485
|
+
...row,
|
|
1486
|
+
...(row.source === undefined ? {} : { source: { ...row.source } }),
|
|
1487
|
+
origin: origin.label,
|
|
1488
|
+
valuation: valuation.label,
|
|
1489
|
+
};
|
|
1490
|
+
const key = cellKey(row.sourceGroup, origin.label, valuation.label);
|
|
1491
|
+
if (expectedKeys.has(key))
|
|
1492
|
+
issues.push({
|
|
1493
|
+
domain: "configuration",
|
|
1494
|
+
code: "duplicate-id",
|
|
1495
|
+
path: `$.expectedCells[${index}]`,
|
|
1496
|
+
message:
|
|
1497
|
+
"Expected source cell is duplicated after period normalization",
|
|
1498
|
+
});
|
|
1499
|
+
expectedKeys.add(key);
|
|
1500
|
+
expectedCandidates.push({
|
|
1501
|
+
row: normalized,
|
|
1502
|
+
snapshot: {
|
|
1503
|
+
sourceGroup: row.sourceGroup,
|
|
1504
|
+
origin: origin.label,
|
|
1505
|
+
valuation: valuation.label,
|
|
1506
|
+
source: row.source === undefined ? null : { ...row.source },
|
|
1507
|
+
},
|
|
1508
|
+
disposition: "retained",
|
|
1509
|
+
coordinate: {
|
|
1510
|
+
origin,
|
|
1511
|
+
valuation,
|
|
1512
|
+
developmentAge,
|
|
1513
|
+
ageUnit: definition.definition.periodAxis.ageUnit,
|
|
1514
|
+
},
|
|
1515
|
+
});
|
|
146
1516
|
});
|
|
147
1517
|
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
148
1518
|
|
|
149
|
-
const
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
1519
|
+
const filter = normalizedFilter.value;
|
|
1520
|
+
const findings: DiagnosticMetricFinding[] = [];
|
|
1521
|
+
const pendingBlockers: PendingBlocker[] = [];
|
|
1522
|
+
const lossCandidates: LossCandidate[] = input.losses.map((inputRow) => ({
|
|
1523
|
+
row: {
|
|
1524
|
+
...inputRow,
|
|
1525
|
+
...(inputRow.source === undefined
|
|
1526
|
+
? {}
|
|
1527
|
+
: { source: { ...inputRow.source } }),
|
|
1528
|
+
measures: Object.fromEntries(
|
|
1529
|
+
Object.keys(inputRow.measures)
|
|
1530
|
+
.sort(codeUnit)
|
|
1531
|
+
.map((key) => [key, inputRow.measures[key]!]),
|
|
1532
|
+
),
|
|
1533
|
+
} as DiagnosticLossInput,
|
|
1534
|
+
snapshot: snapshotLoss(inputRow),
|
|
1535
|
+
disposition: selectedBySource(filter, inputRow.sourceGroup)
|
|
1536
|
+
? "retained"
|
|
1537
|
+
: "filter",
|
|
1538
|
+
coordinate: null,
|
|
1539
|
+
invalid: false,
|
|
1540
|
+
}));
|
|
1541
|
+
|
|
1542
|
+
for (const candidate of lossCandidates) {
|
|
1543
|
+
if (candidate.disposition !== "retained") continue;
|
|
1544
|
+
let origin: DiagnosticNormalizedPeriod | null = null;
|
|
1545
|
+
let valuation: DiagnosticNormalizedPeriod | null = null;
|
|
153
1546
|
try {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} catch {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const normalizedExposureInputs: DiagnosticExposureObservation[] = [];
|
|
173
|
-
for (const row of input.exposures) {
|
|
1547
|
+
origin = normalizeDiagnosticPeriod(
|
|
1548
|
+
definition,
|
|
1549
|
+
"origin",
|
|
1550
|
+
candidate.row.origin,
|
|
1551
|
+
);
|
|
1552
|
+
} catch {
|
|
1553
|
+
findings.push(
|
|
1554
|
+
structuralFinding("unknown-origin-period", {
|
|
1555
|
+
recordId: candidate.row.recordId,
|
|
1556
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1557
|
+
origin: candidate.row.origin,
|
|
1558
|
+
valuation: candidate.row.valuation,
|
|
1559
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1560
|
+
}),
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
if (origin !== null)
|
|
1564
|
+
candidate.snapshot = { ...candidate.snapshot, origin: origin.label };
|
|
174
1565
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
1566
|
+
valuation = normalizeDiagnosticPeriod(
|
|
1567
|
+
definition,
|
|
1568
|
+
"valuation",
|
|
1569
|
+
candidate.row.valuation,
|
|
1570
|
+
);
|
|
1571
|
+
} catch {
|
|
1572
|
+
findings.push(
|
|
1573
|
+
structuralFinding("unknown-valuation-period", {
|
|
1574
|
+
recordId: candidate.row.recordId,
|
|
1575
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1576
|
+
origin: origin?.label ?? candidate.row.origin,
|
|
1577
|
+
valuation: candidate.row.valuation,
|
|
1578
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1579
|
+
}),
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
if (valuation !== null)
|
|
1583
|
+
candidate.snapshot = {
|
|
1584
|
+
...candidate.snapshot,
|
|
1585
|
+
valuation: valuation.label,
|
|
1586
|
+
};
|
|
1587
|
+
if (origin === null || valuation === null) {
|
|
1588
|
+
candidate.disposition = "invalid";
|
|
1589
|
+
candidate.invalid = true;
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const developmentAge =
|
|
1593
|
+
valuation.coordinate -
|
|
1594
|
+
origin.coordinate +
|
|
1595
|
+
definition.definition.periodAxis.ageOffset;
|
|
1596
|
+
if (!Number.isSafeInteger(developmentAge) || developmentAge < 0) {
|
|
1597
|
+
const code: StructuralCode = Number.isSafeInteger(developmentAge)
|
|
1598
|
+
? "valuation-before-origin"
|
|
1599
|
+
: "unsafe-development-age";
|
|
1600
|
+
findings.push(
|
|
1601
|
+
structuralFinding(code, {
|
|
1602
|
+
recordId: candidate.row.recordId,
|
|
1603
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1604
|
+
origin: origin.label,
|
|
1605
|
+
valuation: valuation.label,
|
|
1606
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1607
|
+
}),
|
|
1608
|
+
);
|
|
1609
|
+
candidate.disposition = "invalid";
|
|
1610
|
+
candidate.invalid = true;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
candidate.coordinate = {
|
|
1614
|
+
origin,
|
|
1615
|
+
valuation,
|
|
1616
|
+
developmentAge,
|
|
1617
|
+
ageUnit: definition.definition.periodAxis.ageUnit,
|
|
1618
|
+
};
|
|
1619
|
+
candidate.row = {
|
|
1620
|
+
...candidate.row,
|
|
1621
|
+
origin: origin.label,
|
|
1622
|
+
valuation: valuation.label,
|
|
1623
|
+
} as DiagnosticLossInput;
|
|
1624
|
+
candidate.snapshot = {
|
|
1625
|
+
...candidate.snapshot,
|
|
1626
|
+
origin: candidate.row.origin,
|
|
1627
|
+
valuation: candidate.row.valuation,
|
|
1628
|
+
};
|
|
1629
|
+
if (
|
|
1630
|
+
beyondCutoff(
|
|
1631
|
+
candidate as LossCandidate & { coordinate: NormalizedCoordinate },
|
|
1632
|
+
cutoffByGroup,
|
|
1633
|
+
)
|
|
1634
|
+
)
|
|
1635
|
+
candidate.disposition = "complete-period-cutoff";
|
|
1636
|
+
else if (
|
|
1637
|
+
!selectedByOrigin(filter, origin, normalizedFilter.originCoordinates) ||
|
|
1638
|
+
!selectedByValuation(
|
|
1639
|
+
filter,
|
|
1640
|
+
valuation,
|
|
1641
|
+
normalizedFilter.valuationCoordinates,
|
|
1642
|
+
) ||
|
|
1643
|
+
!selectedByAge(filter, developmentAge)
|
|
1644
|
+
)
|
|
1645
|
+
candidate.disposition = "filter";
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
const exposureCandidates: ExposureCandidate[] = input.exposures.map(
|
|
1649
|
+
(inputRow) => ({
|
|
1650
|
+
row: {
|
|
1651
|
+
...inputRow,
|
|
1652
|
+
...(inputRow.source === undefined
|
|
1653
|
+
? {}
|
|
1654
|
+
: { source: { ...inputRow.source } }),
|
|
1655
|
+
},
|
|
1656
|
+
snapshot: snapshotExposure(inputRow),
|
|
1657
|
+
disposition: selectedBySource(filter, inputRow.sourceGroup)
|
|
1658
|
+
? "retained"
|
|
1659
|
+
: "filter",
|
|
1660
|
+
origin: null,
|
|
1661
|
+
valuation: null,
|
|
1662
|
+
developmentAge: null,
|
|
1663
|
+
ageUnit: null,
|
|
1664
|
+
timing:
|
|
1665
|
+
internals.measuresById.get(inputRow.measureId)?.exposureTiming ?? null,
|
|
1666
|
+
invalid: false,
|
|
1667
|
+
}),
|
|
1668
|
+
);
|
|
1669
|
+
for (const candidate of exposureCandidates) {
|
|
1670
|
+
if (candidate.disposition !== "retained") continue;
|
|
1671
|
+
try {
|
|
1672
|
+
candidate.origin = normalizeDiagnosticPeriod(
|
|
1673
|
+
definition,
|
|
1674
|
+
"origin",
|
|
1675
|
+
candidate.row.origin,
|
|
1676
|
+
);
|
|
1677
|
+
} catch {
|
|
1678
|
+
findings.push(
|
|
1679
|
+
structuralFinding("unknown-origin-period", {
|
|
1680
|
+
measureId: candidate.row.measureId,
|
|
1681
|
+
exposureKey: candidate.row.key,
|
|
1682
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1683
|
+
origin: candidate.row.origin,
|
|
1684
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1685
|
+
}),
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
if (candidate.origin !== null)
|
|
1689
|
+
candidate.snapshot = {
|
|
1690
|
+
...candidate.snapshot,
|
|
1691
|
+
origin: candidate.origin.label,
|
|
1692
|
+
};
|
|
1693
|
+
if (candidate.row.valuation !== undefined) {
|
|
1694
|
+
try {
|
|
1695
|
+
candidate.valuation = normalizeDiagnosticPeriod(
|
|
1696
|
+
definition,
|
|
1697
|
+
"valuation",
|
|
1698
|
+
candidate.row.valuation,
|
|
1699
|
+
);
|
|
1700
|
+
} catch {
|
|
1701
|
+
findings.push(
|
|
1702
|
+
structuralFinding("unknown-valuation-period", {
|
|
1703
|
+
measureId: candidate.row.measureId,
|
|
1704
|
+
exposureKey: candidate.row.key,
|
|
1705
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1706
|
+
origin: candidate.origin?.label ?? candidate.row.origin,
|
|
1707
|
+
valuation: candidate.row.valuation,
|
|
1708
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1709
|
+
}),
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
if (candidate.valuation !== null)
|
|
1713
|
+
candidate.snapshot = {
|
|
1714
|
+
...candidate.snapshot,
|
|
1715
|
+
valuation: candidate.valuation.label,
|
|
1716
|
+
};
|
|
1717
|
+
}
|
|
1718
|
+
if (
|
|
1719
|
+
candidate.origin === null ||
|
|
1720
|
+
(candidate.row.valuation !== undefined && candidate.valuation === null)
|
|
1721
|
+
) {
|
|
1722
|
+
candidate.disposition = "invalid";
|
|
1723
|
+
candidate.invalid = true;
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (candidate.valuation !== null) {
|
|
1727
|
+
const age =
|
|
1728
|
+
candidate.valuation.coordinate -
|
|
1729
|
+
candidate.origin.coordinate +
|
|
1730
|
+
definition.definition.periodAxis.ageOffset;
|
|
1731
|
+
if (!Number.isSafeInteger(age) || age < 0) {
|
|
1732
|
+
const code: StructuralCode = Number.isSafeInteger(age)
|
|
1733
|
+
? "valuation-before-origin"
|
|
1734
|
+
: "unsafe-development-age";
|
|
1735
|
+
findings.push(
|
|
1736
|
+
structuralFinding(code, {
|
|
1737
|
+
measureId: candidate.row.measureId,
|
|
1738
|
+
exposureKey: candidate.row.key,
|
|
1739
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
1740
|
+
origin: candidate.origin.label,
|
|
1741
|
+
valuation: candidate.valuation.label,
|
|
1742
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1743
|
+
}),
|
|
1744
|
+
);
|
|
1745
|
+
candidate.disposition = "invalid";
|
|
1746
|
+
candidate.invalid = true;
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
candidate.developmentAge = age;
|
|
1750
|
+
candidate.ageUnit = definition.definition.periodAxis.ageUnit;
|
|
1751
|
+
}
|
|
1752
|
+
candidate.row = {
|
|
1753
|
+
...candidate.row,
|
|
1754
|
+
origin: candidate.origin.label,
|
|
1755
|
+
...(candidate.valuation ? { valuation: candidate.valuation.label } : {}),
|
|
1756
|
+
};
|
|
1757
|
+
candidate.snapshot = {
|
|
1758
|
+
...candidate.snapshot,
|
|
1759
|
+
origin: candidate.origin.label,
|
|
1760
|
+
valuation: candidate.valuation?.label ?? null,
|
|
1761
|
+
};
|
|
1762
|
+
const cutoff = cutoffByGroup.get(candidate.row.sourceGroup);
|
|
1763
|
+
const beyondOrigin =
|
|
1764
|
+
cutoff?.originThrough !== null &&
|
|
1765
|
+
cutoff?.originThrough !== undefined &&
|
|
1766
|
+
compareDiagnosticPeriods(candidate.origin, cutoff.originThrough) > 0;
|
|
1767
|
+
const beyondValuation =
|
|
1768
|
+
candidate.timing === "valuation-specific" &&
|
|
1769
|
+
cutoff?.valuationThrough !== null &&
|
|
1770
|
+
cutoff?.valuationThrough !== undefined &&
|
|
1771
|
+
candidate.valuation !== null &&
|
|
1772
|
+
compareDiagnosticPeriods(candidate.valuation, cutoff.valuationThrough) >
|
|
1773
|
+
0;
|
|
1774
|
+
if (beyondOrigin || beyondValuation)
|
|
1775
|
+
candidate.disposition = "complete-period-cutoff";
|
|
1776
|
+
else {
|
|
1777
|
+
const originSelected = selectedByOrigin(
|
|
1778
|
+
filter,
|
|
1779
|
+
candidate.origin,
|
|
1780
|
+
normalizedFilter.originCoordinates,
|
|
1781
|
+
);
|
|
1782
|
+
const valuationSelected =
|
|
1783
|
+
candidate.timing === "valuation-specific" &&
|
|
1784
|
+
candidate.valuation !== null
|
|
1785
|
+
? selectedByValuation(
|
|
1786
|
+
filter,
|
|
1787
|
+
candidate.valuation,
|
|
1788
|
+
normalizedFilter.valuationCoordinates,
|
|
1789
|
+
) && selectedByAge(filter, candidate.developmentAge!)
|
|
1790
|
+
: true;
|
|
1791
|
+
if (!originSelected || !valuationSelected)
|
|
1792
|
+
candidate.disposition = "filter";
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
for (const candidate of expectedCandidates) {
|
|
1797
|
+
if (!selectedBySource(filter, candidate.row.sourceGroup))
|
|
1798
|
+
candidate.disposition = "filter";
|
|
1799
|
+
else if (beyondCutoff(candidate, cutoffByGroup))
|
|
1800
|
+
candidate.disposition = "complete-period-cutoff";
|
|
1801
|
+
else if (
|
|
1802
|
+
!selectedByOrigin(
|
|
1803
|
+
filter,
|
|
1804
|
+
candidate.coordinate.origin,
|
|
1805
|
+
normalizedFilter.originCoordinates,
|
|
1806
|
+
) ||
|
|
1807
|
+
!selectedByValuation(
|
|
1808
|
+
filter,
|
|
1809
|
+
candidate.coordinate.valuation,
|
|
1810
|
+
normalizedFilter.valuationCoordinates,
|
|
1811
|
+
) ||
|
|
1812
|
+
!selectedByAge(filter, candidate.coordinate.developmentAge)
|
|
1813
|
+
)
|
|
1814
|
+
candidate.disposition = "filter";
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
const provisionalLosses = lossCandidates.filter(
|
|
1818
|
+
(candidate) => candidate.disposition === "retained",
|
|
1819
|
+
);
|
|
1820
|
+
const lossFindings = (
|
|
1821
|
+
code: StructuralCode,
|
|
1822
|
+
candidates: readonly LossCandidate[],
|
|
1823
|
+
extra: Partial<DiagnosticMetricFinding> = {},
|
|
1824
|
+
) => {
|
|
1825
|
+
const finding = structuralFinding(code, {
|
|
1826
|
+
...commonCoordinate(candidates),
|
|
1827
|
+
...extra,
|
|
1828
|
+
sources: normalizeSources(
|
|
1829
|
+
candidates.map((candidate) => candidate.row.source),
|
|
1830
|
+
),
|
|
1831
|
+
});
|
|
1832
|
+
findings.push(finding);
|
|
1833
|
+
for (const candidate of candidates) candidate.invalid = true;
|
|
1834
|
+
pendingBlockers.push({
|
|
1835
|
+
finding,
|
|
1836
|
+
measureIds: definition.definition.measures
|
|
1837
|
+
.filter((measure) => measure.source !== "exposure")
|
|
1838
|
+
.map((measure) => measure.id),
|
|
1839
|
+
cellKeys: uniqueSorted(
|
|
1840
|
+
candidates.flatMap((candidate) =>
|
|
1841
|
+
candidate.coordinate
|
|
1842
|
+
? [
|
|
1843
|
+
cellKey(
|
|
1844
|
+
candidate.row.sourceGroup,
|
|
1845
|
+
candidate.coordinate.origin.label,
|
|
1846
|
+
candidate.coordinate.valuation.label,
|
|
1847
|
+
),
|
|
1848
|
+
]
|
|
1849
|
+
: [],
|
|
1850
|
+
),
|
|
1851
|
+
),
|
|
1852
|
+
sourceIds: uniqueSorted(
|
|
1853
|
+
candidates.map((candidate) => candidate.row.recordId),
|
|
1854
|
+
),
|
|
1855
|
+
});
|
|
1856
|
+
};
|
|
1857
|
+
|
|
1858
|
+
for (const candidate of provisionalLosses) {
|
|
1859
|
+
const localCodes: {
|
|
1860
|
+
code: StructuralCode;
|
|
1861
|
+
measureId?: string;
|
|
1862
|
+
offendingKey?: string;
|
|
1863
|
+
}[] = [];
|
|
1864
|
+
for (const measureId of Object.keys(candidate.row.measures).sort(
|
|
1865
|
+
codeUnit,
|
|
1866
|
+
)) {
|
|
1867
|
+
const measure = internals.measuresById.get(measureId);
|
|
1868
|
+
if (!measure)
|
|
1869
|
+
localCodes.push({
|
|
1870
|
+
code: "undeclared-loss-measure",
|
|
1871
|
+
measureId,
|
|
1872
|
+
offendingKey: measureId,
|
|
1873
|
+
});
|
|
1874
|
+
else if (measure.source !== "loss")
|
|
1875
|
+
localCodes.push({
|
|
1876
|
+
code: "wrong-source-loss-measure",
|
|
1877
|
+
measureId,
|
|
1878
|
+
offendingKey: measureId,
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
if (!candidate.row.complete)
|
|
1882
|
+
localCodes.push({ code: "incomplete-loss-record" });
|
|
1883
|
+
for (const item of localCodes)
|
|
1884
|
+
lossFindings(item.code, [candidate], {
|
|
1885
|
+
recordId: candidate.row.recordId,
|
|
1886
|
+
...(candidate.row.rowType === "claim"
|
|
1887
|
+
? { claimId: candidate.row.claimId }
|
|
1888
|
+
: {}),
|
|
1889
|
+
...(item.measureId ? { measureId: item.measureId } : {}),
|
|
1890
|
+
...(item.offendingKey ? { offendingKey: item.offendingKey } : {}),
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
const byRecordId = new Map<string, LossCandidate[]>();
|
|
1895
|
+
for (const candidate of provisionalLosses)
|
|
1896
|
+
addToMap(byRecordId, candidate.row.recordId, candidate);
|
|
1897
|
+
for (const [recordId, cohort] of byRecordId)
|
|
1898
|
+
if (cohort.length > 1)
|
|
1899
|
+
lossFindings("duplicate-loss-record-id", cohort, { recordId });
|
|
1900
|
+
if (definition.definition.lossRowGrain === "aggregate") {
|
|
1901
|
+
const byCell = new Map<string, LossCandidate[]>();
|
|
1902
|
+
for (const candidate of provisionalLosses)
|
|
1903
|
+
addToMap(
|
|
1904
|
+
byCell,
|
|
1905
|
+
cellKey(
|
|
1906
|
+
candidate.row.sourceGroup,
|
|
1907
|
+
candidate.row.origin,
|
|
1908
|
+
candidate.row.valuation,
|
|
1909
|
+
),
|
|
1910
|
+
candidate,
|
|
1911
|
+
);
|
|
1912
|
+
for (const cohort of byCell.values())
|
|
1913
|
+
if (cohort.length > 1)
|
|
1914
|
+
lossFindings("duplicate-aggregate-snapshot", cohort);
|
|
1915
|
+
} else {
|
|
1916
|
+
const bySnapshot = new Map<string, LossCandidate[]>();
|
|
1917
|
+
const byClaim = new Map<string, LossCandidate[]>();
|
|
1918
|
+
for (const candidate of provisionalLosses) {
|
|
1919
|
+
const claimId = (candidate.row as DiagnosticClaimObservation).claimId;
|
|
1920
|
+
addToMap(
|
|
1921
|
+
bySnapshot,
|
|
1922
|
+
canonicalJson([
|
|
1923
|
+
claimId,
|
|
1924
|
+
candidate.row.sourceGroup,
|
|
1925
|
+
candidate.row.origin,
|
|
1926
|
+
candidate.row.valuation,
|
|
1927
|
+
]),
|
|
1928
|
+
candidate,
|
|
1929
|
+
);
|
|
1930
|
+
addToMap(byClaim, claimId, candidate);
|
|
1931
|
+
}
|
|
1932
|
+
for (const cohort of bySnapshot.values())
|
|
1933
|
+
if (cohort.length > 1)
|
|
1934
|
+
lossFindings("duplicate-claim-snapshot", cohort, {
|
|
1935
|
+
claimId: (cohort[0]!.row as DiagnosticClaimObservation).claimId,
|
|
1936
|
+
});
|
|
1937
|
+
for (const [claimId, cohort] of byClaim)
|
|
1938
|
+
if (
|
|
1939
|
+
uniqueSorted(
|
|
1940
|
+
cohort.map((candidate) =>
|
|
1941
|
+
canonicalJson([candidate.row.sourceGroup, candidate.row.origin]),
|
|
1942
|
+
),
|
|
1943
|
+
).length > 1
|
|
1944
|
+
)
|
|
1945
|
+
lossFindings("claim-identity-conflict", cohort, { claimId });
|
|
1946
|
+
}
|
|
1947
|
+
for (const candidate of provisionalLosses)
|
|
1948
|
+
if (candidate.invalid) candidate.disposition = "invalid";
|
|
1949
|
+
|
|
1950
|
+
const declaredExposureCandidates: ExposureCandidate[] = [];
|
|
1951
|
+
for (const candidate of exposureCandidates.filter(
|
|
1952
|
+
(item) => item.disposition === "retained",
|
|
1953
|
+
)) {
|
|
1954
|
+
const measure = internals.measuresById.get(candidate.row.measureId);
|
|
1955
|
+
if (!measure) {
|
|
1956
|
+
findings.push(
|
|
1957
|
+
structuralFinding("undeclared-exposure-measure", {
|
|
1958
|
+
measureId: candidate.row.measureId,
|
|
1959
|
+
offendingKey: candidate.row.measureId,
|
|
1960
|
+
exposureKey: candidate.row.key,
|
|
1961
|
+
...(candidate.origin ? { origin: candidate.origin.label } : {}),
|
|
1962
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1963
|
+
}),
|
|
1964
|
+
);
|
|
1965
|
+
candidate.invalid = true;
|
|
1966
|
+
candidate.disposition = "invalid";
|
|
1967
|
+
} else if (measure.source !== "exposure") {
|
|
1968
|
+
findings.push(
|
|
1969
|
+
structuralFinding("wrong-source-exposure-measure", {
|
|
1970
|
+
measureId: candidate.row.measureId,
|
|
1971
|
+
offendingKey: candidate.row.measureId,
|
|
1972
|
+
exposureKey: candidate.row.key,
|
|
1973
|
+
...(candidate.origin ? { origin: candidate.origin.label } : {}),
|
|
1974
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
1975
|
+
}),
|
|
1976
|
+
);
|
|
1977
|
+
candidate.invalid = true;
|
|
1978
|
+
candidate.disposition = "invalid";
|
|
1979
|
+
} else declaredExposureCandidates.push(candidate);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const reconciled = reconcileDiagnosticExposures(
|
|
1983
|
+
declaredExposureCandidates.map((candidate) => candidate.row),
|
|
1984
|
+
Object.fromEntries(
|
|
1985
|
+
definition.definition.measures
|
|
1986
|
+
.filter((measure) => measure.source === "exposure")
|
|
1987
|
+
.map((measure) => [measure.id, measure.exposureTiming!]),
|
|
1988
|
+
),
|
|
1989
|
+
definition.definition.periodAxis,
|
|
1990
|
+
) as ReconciledDiagnosticExposure[];
|
|
1991
|
+
const exposureCohorts = new Map<string, ExposureCandidate[]>();
|
|
1992
|
+
for (const candidate of declaredExposureCandidates)
|
|
1993
|
+
addToMap(exposureCohorts, exposureIdentity(candidate), candidate);
|
|
1994
|
+
const invalidExposureBlockers: {
|
|
1995
|
+
finding: DiagnosticMetricFinding;
|
|
1996
|
+
candidates: readonly ExposureCandidate[];
|
|
1997
|
+
}[] = [];
|
|
1998
|
+
for (const exposure of reconciled) {
|
|
1999
|
+
const timing = internals.measuresById.get(
|
|
2000
|
+
exposure.measureId,
|
|
2001
|
+
)?.exposureTiming;
|
|
2002
|
+
const identity = canonicalJson(
|
|
2003
|
+
timing === "valuation-specific"
|
|
2004
|
+
? [
|
|
2005
|
+
exposure.measureId,
|
|
2006
|
+
exposure.key,
|
|
2007
|
+
exposure.status === "valid"
|
|
2008
|
+
? (exposure.valuation ?? null)
|
|
2009
|
+
: (exposure.observations[0]?.valuation ?? null),
|
|
2010
|
+
]
|
|
2011
|
+
: [exposure.measureId, exposure.key],
|
|
2012
|
+
);
|
|
2013
|
+
const cohort = exposureCohorts.get(identity) ?? [];
|
|
2014
|
+
if (exposure.status === "valid") continue;
|
|
2015
|
+
for (const candidate of cohort) {
|
|
2016
|
+
candidate.invalid = true;
|
|
2017
|
+
candidate.disposition = "invalid";
|
|
2018
|
+
}
|
|
2019
|
+
const context = exposureCoordinateContext(cohort);
|
|
2020
|
+
const issueCodes: Record<(typeof exposure.issues)[number], StructuralCode> =
|
|
2021
|
+
{
|
|
2022
|
+
missing: "missing-exposure-value",
|
|
2023
|
+
incomplete: "incomplete-exposure",
|
|
2024
|
+
"non-finite": "non-finite-exposure",
|
|
2025
|
+
duplicate: "duplicate-exposure-identity",
|
|
2026
|
+
conflict: "conflicting-exposure-identity",
|
|
2027
|
+
};
|
|
2028
|
+
for (const issue of exposure.issues) {
|
|
2029
|
+
const finding = structuralFinding(issueCodes[issue], {
|
|
2030
|
+
...context,
|
|
2031
|
+
measureId: exposure.measureId,
|
|
2032
|
+
exposureKey: exposure.key,
|
|
2033
|
+
sources: normalizeSources(
|
|
2034
|
+
cohort.map((candidate) => candidate.row.source),
|
|
2035
|
+
),
|
|
2036
|
+
});
|
|
2037
|
+
findings.push(finding);
|
|
2038
|
+
invalidExposureBlockers.push({ finding, candidates: cohort });
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
const retainedCandidates = lossCandidates.filter(
|
|
2043
|
+
(candidate) => candidate.disposition === "retained",
|
|
2044
|
+
);
|
|
2045
|
+
const retainedRows = retainedCandidates.map((candidate) => candidate.row);
|
|
2046
|
+
const derivedAudits =
|
|
2047
|
+
definition.definition.lossRowGrain === "claim"
|
|
2048
|
+
? deriveDiagnosticClaimMeasuresWithAudit(
|
|
2049
|
+
retainedRows as readonly DiagnosticClaimObservation[],
|
|
2050
|
+
definition,
|
|
2051
|
+
)
|
|
2052
|
+
: [];
|
|
2053
|
+
const derivedRows =
|
|
2054
|
+
definition.definition.lossRowGrain === "claim"
|
|
2055
|
+
? derivedAudits.map((item) => item.row as DiagnosticLossInput)
|
|
2056
|
+
: retainedRows;
|
|
2057
|
+
const derivedByRecord = new Map(
|
|
2058
|
+
derivedRows.map((row) => [row.recordId, row]),
|
|
2059
|
+
);
|
|
2060
|
+
const derivedAuditByRecord = new Map(
|
|
2061
|
+
derivedAudits.map((item) => [
|
|
2062
|
+
(item.row as DiagnosticClaimObservation).recordId,
|
|
2063
|
+
item,
|
|
2064
|
+
]),
|
|
2065
|
+
);
|
|
2066
|
+
const cellRows = new Map<string, DiagnosticLossInput[]>();
|
|
2067
|
+
for (const candidate of retainedCandidates)
|
|
2068
|
+
addToMap(
|
|
2069
|
+
cellRows,
|
|
2070
|
+
cellKey(
|
|
2071
|
+
candidate.row.sourceGroup,
|
|
2072
|
+
candidate.row.origin,
|
|
2073
|
+
candidate.row.valuation,
|
|
2074
|
+
),
|
|
2075
|
+
derivedByRecord.get(candidate.row.recordId)!,
|
|
2076
|
+
);
|
|
2077
|
+
|
|
2078
|
+
// Exposure attachment is a coordinate join. Index the established cell order
|
|
2079
|
+
// once; scanning every cell for each origin-static exposure is quadratic.
|
|
2080
|
+
const cellKeysBySourceOrigin = new Map<string, string[]>();
|
|
2081
|
+
for (const key of cellRows.keys()) {
|
|
2082
|
+
const [sourceGroup, origin] = parseCellKey(key);
|
|
2083
|
+
addToMap(cellKeysBySourceOrigin, canonicalJson([sourceGroup, origin]), key);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const allMeasureIds = definition.definition.measures
|
|
2087
|
+
.map((measure) => measure.id)
|
|
2088
|
+
.sort(codeUnit);
|
|
2089
|
+
const nonExposureMeasures = definition.definition.measures
|
|
2090
|
+
.filter((measure) => measure.source !== "exposure")
|
|
2091
|
+
.sort((left, right) => codeUnit(left.id, right.id));
|
|
2092
|
+
const exposureMeasures = definition.definition.measures
|
|
2093
|
+
.filter((measure) => measure.source === "exposure")
|
|
2094
|
+
.sort((left, right) => codeUnit(left.id, right.id));
|
|
189
2095
|
const preparedCells: PreparedDiagnosticSourceCell[] = [];
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
2096
|
+
const invalidExposureMeasuresByCell = new Map<string, Set<string>>();
|
|
2097
|
+
for (const item of invalidExposureBlockers) {
|
|
2098
|
+
const targets = new Set<string>();
|
|
2099
|
+
for (const candidate of item.candidates) {
|
|
2100
|
+
if (!candidate.origin) continue;
|
|
2101
|
+
if (candidate.timing === "valuation-specific" && candidate.valuation)
|
|
2102
|
+
targets.add(
|
|
2103
|
+
cellKey(
|
|
2104
|
+
candidate.row.sourceGroup,
|
|
2105
|
+
candidate.origin.label,
|
|
2106
|
+
candidate.valuation.label,
|
|
2107
|
+
),
|
|
2108
|
+
);
|
|
2109
|
+
else
|
|
2110
|
+
for (const key of cellKeysBySourceOrigin.get(
|
|
2111
|
+
canonicalJson([candidate.row.sourceGroup, candidate.origin.label]),
|
|
2112
|
+
) ?? [])
|
|
2113
|
+
targets.add(key);
|
|
2114
|
+
}
|
|
2115
|
+
for (const key of targets) {
|
|
2116
|
+
const measures =
|
|
2117
|
+
invalidExposureMeasuresByCell.get(key) ?? new Set<string>();
|
|
2118
|
+
measures.add(item.finding.measureId!);
|
|
2119
|
+
invalidExposureMeasuresByCell.set(key, measures);
|
|
2120
|
+
}
|
|
2121
|
+
pendingBlockers.push({
|
|
2122
|
+
finding: item.finding,
|
|
2123
|
+
measureIds: [item.finding.measureId!],
|
|
2124
|
+
cellKeys: [...targets],
|
|
2125
|
+
sourceIds: item.finding.exposureKey ? [item.finding.exposureKey] : [],
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
const validExposuresByCell = new Map<
|
|
2130
|
+
string,
|
|
2131
|
+
Extract<ReconciledDiagnosticExposure, { status: "valid" }>[]
|
|
2132
|
+
>();
|
|
2133
|
+
for (const exposure of reconciled) {
|
|
2134
|
+
if (exposure.status !== "valid") continue;
|
|
2135
|
+
const timing = internals.measuresById.get(
|
|
2136
|
+
exposure.measureId,
|
|
2137
|
+
)!.exposureTiming;
|
|
2138
|
+
const coordinateKey =
|
|
2139
|
+
exposure.valuation === undefined
|
|
2140
|
+
? null
|
|
2141
|
+
: cellKey(exposure.sourceGroup, exposure.origin, exposure.valuation);
|
|
2142
|
+
const keys =
|
|
2143
|
+
timing === "origin-static"
|
|
2144
|
+
? (cellKeysBySourceOrigin.get(
|
|
2145
|
+
canonicalJson([exposure.sourceGroup, exposure.origin]),
|
|
2146
|
+
) ?? [])
|
|
2147
|
+
: coordinateKey !== null && cellRows.has(coordinateKey)
|
|
2148
|
+
? [coordinateKey]
|
|
2149
|
+
: [];
|
|
2150
|
+
for (const key of keys) addToMap(validExposuresByCell, key, exposure);
|
|
2151
|
+
if (keys.length === 0)
|
|
2152
|
+
findings.push(
|
|
2153
|
+
structuralFinding("exposure-without-loss", {
|
|
2154
|
+
measureId: exposure.measureId,
|
|
2155
|
+
exposureKey: exposure.key,
|
|
2156
|
+
sourceGroup: exposure.sourceGroup,
|
|
2157
|
+
origin: exposure.origin,
|
|
2158
|
+
...(exposure.valuation ? { valuation: exposure.valuation } : {}),
|
|
2159
|
+
sources: exposure.sources,
|
|
2160
|
+
}),
|
|
2161
|
+
);
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
const pendingBlockersByCell = new Map<
|
|
2165
|
+
string,
|
|
2166
|
+
(typeof pendingBlockers)[number][]
|
|
2167
|
+
>();
|
|
2168
|
+
for (const pending of pendingBlockers)
|
|
2169
|
+
for (const key of new Set(pending.cellKeys))
|
|
2170
|
+
addToMap(pendingBlockersByCell, key, pending);
|
|
2171
|
+
|
|
2172
|
+
for (const [key, rows] of cellRows) {
|
|
2173
|
+
const [sourceGroup, origin, valuation] = parseCellKey(key);
|
|
2174
|
+
const coordinate = coordinateFor(definition, origin, valuation).coordinate!;
|
|
2175
|
+
const contributions = Object.fromEntries(
|
|
2176
|
+
allMeasureIds.map((measureId) => [
|
|
2177
|
+
measureId,
|
|
2178
|
+
[] as DiagnosticMeasureContribution[],
|
|
2179
|
+
]),
|
|
2180
|
+
) as Record<string, DiagnosticMeasureContribution[]>;
|
|
2181
|
+
const blockers = Object.fromEntries(
|
|
2182
|
+
allMeasureIds.map((measureId) => [
|
|
2183
|
+
measureId,
|
|
2184
|
+
[] as DiagnosticStructuralBlocker[],
|
|
2185
|
+
]),
|
|
2186
|
+
) as Record<string, DiagnosticStructuralBlocker[]>;
|
|
2187
|
+
const cellFindings: DiagnosticMetricFinding[] = [];
|
|
2188
|
+
const derivedContribution = (
|
|
2189
|
+
row: DiagnosticLossInput,
|
|
2190
|
+
missingPolicy: "unknown" | "zero",
|
|
2191
|
+
state: DiagnosticDerivedValueAudit,
|
|
2192
|
+
): DiagnosticMeasureContribution => {
|
|
2193
|
+
const base = {
|
|
2194
|
+
sourceId: row.recordId,
|
|
2195
|
+
sources: row.source ? [row.source] : [],
|
|
2196
|
+
deduplicated: 0,
|
|
2197
|
+
} as const;
|
|
2198
|
+
if (state.status === "observed")
|
|
2199
|
+
return { ...base, status: "observed", value: state.value };
|
|
2200
|
+
if (state.status === "non-finite")
|
|
2201
|
+
return {
|
|
2202
|
+
...base,
|
|
2203
|
+
status: "non-finite",
|
|
2204
|
+
value: null,
|
|
2205
|
+
nonFiniteKind: state.nonFiniteKind,
|
|
2206
|
+
};
|
|
2207
|
+
return missingPolicy === "zero"
|
|
2208
|
+
? { ...base, status: "imputed-zero", value: 0 }
|
|
2209
|
+
: { ...base, status: "missing", value: null };
|
|
2210
|
+
};
|
|
2211
|
+
for (const row of rows)
|
|
2212
|
+
for (const measure of nonExposureMeasures) {
|
|
2213
|
+
if (measure.source === "derived") {
|
|
2214
|
+
const state = derivedAuditByRecord.get(row.recordId)!.derived[
|
|
2215
|
+
measure.id
|
|
2216
|
+
]!;
|
|
2217
|
+
contributions[measure.id]!.push(
|
|
2218
|
+
derivedContribution(row, measure.missing, state),
|
|
2219
|
+
);
|
|
2220
|
+
for (const overflow of derivedAuditByRecord.get(row.recordId)!
|
|
2221
|
+
.expressionOverflows[measure.id] ?? []) {
|
|
2222
|
+
cellFindings.push({
|
|
2223
|
+
code: "diagnostic-expression-overflow",
|
|
2224
|
+
message: "Measure expression overflowed",
|
|
2225
|
+
severity: "fail",
|
|
2226
|
+
category: "aggregation",
|
|
2227
|
+
measureId: measure.id,
|
|
2228
|
+
expressionPath: overflow.expressionPath,
|
|
2229
|
+
recordId: row.recordId,
|
|
2230
|
+
sourceGroup,
|
|
2231
|
+
origin,
|
|
2232
|
+
valuation,
|
|
2233
|
+
developmentAge: coordinate.developmentAge,
|
|
2234
|
+
ageUnit: coordinate.ageUnit,
|
|
2235
|
+
sources: row.source ? [row.source] : [],
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
2238
|
+
} else
|
|
2239
|
+
contributions[measure.id]!.push(
|
|
2240
|
+
auditedDiagnosticContribution(
|
|
2241
|
+
row.recordId,
|
|
2242
|
+
own(row.measures, measure.id),
|
|
2243
|
+
measure.missing,
|
|
2244
|
+
row.source ? [row.source] : [],
|
|
2245
|
+
),
|
|
2246
|
+
);
|
|
199
2247
|
}
|
|
2248
|
+
const attachedExposures = validExposuresByCell.get(key) ?? [];
|
|
2249
|
+
for (const exposure of attachedExposures)
|
|
2250
|
+
contributions[exposure.measureId]!.push(
|
|
2251
|
+
auditedDiagnosticContribution(
|
|
2252
|
+
exposure.key,
|
|
2253
|
+
exposure.value,
|
|
2254
|
+
"unknown",
|
|
2255
|
+
exposure.sources,
|
|
2256
|
+
exposure.deduplicated,
|
|
2257
|
+
),
|
|
2258
|
+
);
|
|
2259
|
+
for (const pending of pendingBlockersByCell.get(key) ?? []) {
|
|
2260
|
+
cellFindings.push(pending.finding);
|
|
2261
|
+
const blocker = {
|
|
2262
|
+
code: pending.finding.code,
|
|
2263
|
+
message: pending.finding.message,
|
|
2264
|
+
sourceIds: uniqueSorted(pending.sourceIds),
|
|
2265
|
+
sources: normalizeSources(pending.finding.sources),
|
|
2266
|
+
finding: pending.finding,
|
|
2267
|
+
};
|
|
2268
|
+
for (const measureId of pending.measureIds)
|
|
2269
|
+
blockers[measureId]!.push(blocker);
|
|
200
2270
|
}
|
|
201
|
-
for (const
|
|
202
|
-
|
|
203
|
-
|
|
2271
|
+
for (const measure of exposureMeasures) {
|
|
2272
|
+
const attempted =
|
|
2273
|
+
attachedExposures.some(
|
|
2274
|
+
(exposure) => exposure.measureId === measure.id,
|
|
2275
|
+
) || invalidExposureMeasuresByCell.get(key)?.has(measure.id);
|
|
2276
|
+
if (contributions[measure.id]!.length === 0 && !attempted) {
|
|
2277
|
+
const sources = normalizeSources(rows.map((row) => row.source));
|
|
2278
|
+
const finding = structuralFinding("loss-without-exposure", {
|
|
2279
|
+
measureId: measure.id,
|
|
2280
|
+
sourceGroup,
|
|
2281
|
+
origin,
|
|
2282
|
+
valuation,
|
|
2283
|
+
developmentAge: coordinate.developmentAge,
|
|
2284
|
+
ageUnit: coordinate.ageUnit,
|
|
2285
|
+
sources,
|
|
2286
|
+
});
|
|
2287
|
+
findings.push(finding);
|
|
2288
|
+
cellFindings.push(finding);
|
|
2289
|
+
blockers[measure.id]!.push({
|
|
2290
|
+
code: finding.code,
|
|
2291
|
+
message: finding.message,
|
|
2292
|
+
sourceIds: uniqueSorted(rows.map((row) => row.recordId)),
|
|
2293
|
+
sources,
|
|
2294
|
+
finding,
|
|
2295
|
+
});
|
|
2296
|
+
}
|
|
204
2297
|
}
|
|
205
|
-
for (const
|
|
206
|
-
|
|
2298
|
+
for (const measureId of allMeasureIds) {
|
|
2299
|
+
blockers[measureId] = [
|
|
2300
|
+
...new Map(
|
|
2301
|
+
blockers[measureId]!.map((blocker) => [
|
|
2302
|
+
canonicalJson(blocker),
|
|
2303
|
+
blocker,
|
|
2304
|
+
]),
|
|
2305
|
+
).values(),
|
|
2306
|
+
].sort(compareDiagnosticBlockers);
|
|
2307
|
+
contributions[measureId]!.sort(compareDiagnosticContributions);
|
|
207
2308
|
}
|
|
208
|
-
const components = Object.fromEntries(
|
|
209
|
-
|
|
2309
|
+
const components = Object.fromEntries(
|
|
2310
|
+
allMeasureIds.map((measureId) => {
|
|
2311
|
+
const measure = internals.measuresById.get(measureId)!;
|
|
2312
|
+
return [
|
|
2313
|
+
measureId,
|
|
2314
|
+
finalizeDiagnosticContributions(
|
|
2315
|
+
contributions[measureId]!,
|
|
2316
|
+
measure.missing,
|
|
2317
|
+
blockers[measureId],
|
|
2318
|
+
),
|
|
2319
|
+
];
|
|
2320
|
+
}),
|
|
2321
|
+
) as Record<string, DiagnosticMeasureStats>;
|
|
2322
|
+
for (const measureId of allMeasureIds) {
|
|
2323
|
+
const measureContributions = contributions[measureId]!;
|
|
2324
|
+
const context = {
|
|
2325
|
+
measureId,
|
|
2326
|
+
sourceGroup,
|
|
2327
|
+
origin,
|
|
2328
|
+
valuation,
|
|
2329
|
+
developmentAge: coordinate.developmentAge,
|
|
2330
|
+
ageUnit: coordinate.ageUnit,
|
|
2331
|
+
};
|
|
2332
|
+
const missing = measureContributions.filter(
|
|
2333
|
+
(item) => item.status === "missing",
|
|
2334
|
+
);
|
|
2335
|
+
const imputed = measureContributions.filter(
|
|
2336
|
+
(item) => item.status === "imputed-zero",
|
|
2337
|
+
);
|
|
2338
|
+
const nonFinite = measureContributions.filter(
|
|
2339
|
+
(item) => item.status === "non-finite",
|
|
2340
|
+
);
|
|
2341
|
+
if (missing.length > 0)
|
|
2342
|
+
cellFindings.push(
|
|
2343
|
+
aggregationFinding("diagnostic-measure-missing", {
|
|
2344
|
+
...context,
|
|
2345
|
+
sources: missing.flatMap((item) => item.sources),
|
|
2346
|
+
}),
|
|
2347
|
+
);
|
|
2348
|
+
if (imputed.length > 0)
|
|
2349
|
+
cellFindings.push(
|
|
2350
|
+
aggregationFinding("diagnostic-measure-imputed-zero", {
|
|
2351
|
+
...context,
|
|
2352
|
+
sources: imputed.flatMap((item) => item.sources),
|
|
2353
|
+
}),
|
|
2354
|
+
);
|
|
2355
|
+
if (nonFinite.length > 0)
|
|
2356
|
+
cellFindings.push(
|
|
2357
|
+
aggregationFinding("diagnostic-measure-non-finite", {
|
|
2358
|
+
...context,
|
|
2359
|
+
sources: nonFinite.flatMap((item) => item.sources),
|
|
2360
|
+
}),
|
|
2361
|
+
);
|
|
2362
|
+
if (
|
|
2363
|
+
components[measureId]!.sum === null &&
|
|
2364
|
+
components[measureId]!.nonFinite === 0 &&
|
|
2365
|
+
components[measureId]!.structural === 0
|
|
2366
|
+
)
|
|
2367
|
+
cellFindings.push(
|
|
2368
|
+
aggregationFinding("diagnostic-measure-overflow", {
|
|
2369
|
+
...context,
|
|
2370
|
+
sources: measureContributions.flatMap((item) => item.sources),
|
|
2371
|
+
}),
|
|
2372
|
+
);
|
|
2373
|
+
}
|
|
2374
|
+
findings.push(...cellFindings);
|
|
2375
|
+
preparedCells.push({
|
|
2376
|
+
sourceGroup,
|
|
2377
|
+
origin,
|
|
2378
|
+
valuation,
|
|
2379
|
+
developmentAge: coordinate.developmentAge,
|
|
2380
|
+
ageUnit: coordinate.ageUnit,
|
|
2381
|
+
lossRecordIds: uniqueSorted(rows.map((row) => row.recordId)),
|
|
2382
|
+
contributions,
|
|
2383
|
+
components,
|
|
2384
|
+
structuralBlockers: blockers,
|
|
2385
|
+
findings: mergeFindings(cellFindings),
|
|
2386
|
+
});
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
for (const candidate of expectedCandidates.filter(
|
|
2390
|
+
(item) => item.disposition === "retained",
|
|
2391
|
+
)) {
|
|
2392
|
+
const key = cellKey(
|
|
2393
|
+
candidate.row.sourceGroup,
|
|
2394
|
+
candidate.row.origin,
|
|
2395
|
+
candidate.row.valuation,
|
|
2396
|
+
);
|
|
2397
|
+
if (!cellRows.has(key))
|
|
2398
|
+
findings.push(
|
|
2399
|
+
structuralFinding("missing-expected-cell", {
|
|
2400
|
+
sourceGroup: candidate.row.sourceGroup,
|
|
2401
|
+
origin: candidate.row.origin,
|
|
2402
|
+
valuation: candidate.row.valuation,
|
|
2403
|
+
developmentAge: candidate.coordinate.developmentAge,
|
|
2404
|
+
ageUnit: candidate.coordinate.ageUnit,
|
|
2405
|
+
sources: candidate.row.source ? [candidate.row.source] : [],
|
|
2406
|
+
}),
|
|
2407
|
+
);
|
|
2408
|
+
}
|
|
2409
|
+
|
|
2410
|
+
const originCoordinates = new Map<string, number>();
|
|
2411
|
+
const valuationCoordinates = new Map<string, number>();
|
|
2412
|
+
for (const cell of preparedCells) {
|
|
2413
|
+
originCoordinates.set(
|
|
2414
|
+
cell.origin,
|
|
2415
|
+
normalizeDiagnosticPeriod(definition, "origin", cell.origin).coordinate,
|
|
2416
|
+
);
|
|
2417
|
+
valuationCoordinates.set(
|
|
2418
|
+
cell.valuation,
|
|
2419
|
+
normalizeDiagnosticPeriod(definition, "valuation", cell.valuation)
|
|
2420
|
+
.coordinate,
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
preparedCells.sort(
|
|
2424
|
+
(left, right) =>
|
|
2425
|
+
codeUnit(left.sourceGroup, right.sourceGroup) ||
|
|
2426
|
+
originCoordinates.get(left.origin)! -
|
|
2427
|
+
originCoordinates.get(right.origin)! ||
|
|
2428
|
+
valuationCoordinates.get(left.valuation)! -
|
|
2429
|
+
valuationCoordinates.get(right.valuation)! ||
|
|
2430
|
+
codeUnit(left.origin, right.origin) ||
|
|
2431
|
+
codeUnit(left.valuation, right.valuation),
|
|
2432
|
+
);
|
|
2433
|
+
const expectedCells = expectedCandidates
|
|
2434
|
+
.filter((candidate) => candidate.disposition === "retained")
|
|
2435
|
+
.sort(
|
|
2436
|
+
(left, right) =>
|
|
2437
|
+
codeUnit(left.row.sourceGroup, right.row.sourceGroup) ||
|
|
2438
|
+
compareDiagnosticPeriods(
|
|
2439
|
+
left.coordinate.origin,
|
|
2440
|
+
right.coordinate.origin,
|
|
2441
|
+
) ||
|
|
2442
|
+
compareDiagnosticPeriods(
|
|
2443
|
+
left.coordinate.valuation,
|
|
2444
|
+
right.coordinate.valuation,
|
|
2445
|
+
),
|
|
2446
|
+
)
|
|
2447
|
+
.map((candidate) => candidate.row);
|
|
2448
|
+
const audit: DiagnosticInputAuditRecord[] = [
|
|
2449
|
+
...lossCandidates.map((candidate): DiagnosticInputAuditRecord => ({
|
|
2450
|
+
kind: "loss",
|
|
2451
|
+
disposition: candidate.disposition,
|
|
2452
|
+
record: candidate.snapshot,
|
|
2453
|
+
})),
|
|
2454
|
+
...exposureCandidates.map((candidate): DiagnosticInputAuditRecord => ({
|
|
2455
|
+
kind: "exposure",
|
|
2456
|
+
disposition: candidate.disposition,
|
|
2457
|
+
record: candidate.snapshot,
|
|
2458
|
+
})),
|
|
2459
|
+
...expectedCandidates.map((candidate): DiagnosticInputAuditRecord => ({
|
|
2460
|
+
kind: "expected-cell",
|
|
2461
|
+
disposition: candidate.disposition,
|
|
2462
|
+
record: candidate.snapshot,
|
|
2463
|
+
})),
|
|
2464
|
+
].sort(createAuditComparator());
|
|
2465
|
+
const normalizedFindings = mergeFindings(findings);
|
|
2466
|
+
const frozenCells = preparedCells.map((cell) => deepFreeze(cell));
|
|
2467
|
+
const cutoffValues = normalizedCutoffs.map((cutoff) => cutoff.value);
|
|
2468
|
+
const ownedData = deepFreeze({
|
|
2469
|
+
definitionIntegrity: definition.definitionIntegrity,
|
|
2470
|
+
filter,
|
|
2471
|
+
completePeriodCutoffs: cutoffValues,
|
|
2472
|
+
inputAudit: audit,
|
|
2473
|
+
cells: frozenCells,
|
|
2474
|
+
exposures: reconciled,
|
|
2475
|
+
expectedCellsProvided: input.expectedCells !== undefined,
|
|
2476
|
+
expectedCells,
|
|
2477
|
+
findings: normalizedFindings,
|
|
2478
|
+
});
|
|
2479
|
+
if (mode === "compact") {
|
|
2480
|
+
// Every child was frozen above (the compiled definition owns its snapshot).
|
|
2481
|
+
const prepared = Object.freeze({
|
|
2482
|
+
definition,
|
|
2483
|
+
...ownedData,
|
|
2484
|
+
}) as unknown as CompactPreparedDiagnosticData;
|
|
2485
|
+
authenticCompact.add(prepared);
|
|
2486
|
+
return prepared;
|
|
210
2487
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
2488
|
+
return createEagerPreparedDiagnosticData(definition, ownedData);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
function createEagerPreparedDiagnosticData(
|
|
2492
|
+
definition: CompiledDiagnosticDefinition,
|
|
2493
|
+
ownedData: Omit<PreparedDiagnosticDataContent, "definition"> & {
|
|
2494
|
+
readonly definitionIntegrity: string;
|
|
2495
|
+
},
|
|
2496
|
+
): PreparedDiagnosticData {
|
|
2497
|
+
const identity: NormalizedDiagnosticPreparationIdentity =
|
|
2498
|
+
projectDiagnosticIdentity({
|
|
2499
|
+
...ownedData,
|
|
2500
|
+
filter: normalizeDiagnosticsFilterIdentity(ownedData.filter),
|
|
2501
|
+
expectedCells: ownedData.expectedCells.map((cell) => ({
|
|
2502
|
+
...cell,
|
|
2503
|
+
source: cell.source ?? null,
|
|
2504
|
+
})),
|
|
2505
|
+
});
|
|
215
2506
|
const preparationFingerprint = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
|
|
216
|
-
const
|
|
217
|
-
|
|
2507
|
+
const prepared = deepFreeze({
|
|
2508
|
+
definition,
|
|
2509
|
+
preparationFingerprint,
|
|
2510
|
+
...ownedData,
|
|
2511
|
+
}) as unknown as PreparedDiagnosticData;
|
|
218
2512
|
authentic.add(prepared);
|
|
219
2513
|
identities.set(prepared, identity);
|
|
220
2514
|
return prepared;
|
|
221
2515
|
}
|
|
222
2516
|
|
|
223
|
-
export function
|
|
224
|
-
|
|
2517
|
+
export function assertCompactPreparedDiagnosticData(
|
|
2518
|
+
value: unknown,
|
|
2519
|
+
): asserts value is CompactPreparedDiagnosticData {
|
|
2520
|
+
if (
|
|
2521
|
+
value === null ||
|
|
2522
|
+
typeof value !== "object" ||
|
|
2523
|
+
!authenticCompact.has(value)
|
|
2524
|
+
)
|
|
2525
|
+
throw new DiagnosticValidationError([
|
|
2526
|
+
{
|
|
2527
|
+
domain: "input",
|
|
2528
|
+
code: "invalid-input-relationship",
|
|
2529
|
+
path: "$.prepared",
|
|
2530
|
+
message: "Compact prepared diagnostic data is not authentic",
|
|
2531
|
+
},
|
|
2532
|
+
]);
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
/**
|
|
2536
|
+
* Explicit eager bridge for callers that need the legacy complete JSON shape.
|
|
2537
|
+
* This materializes and hashes the full identity; compact execution never calls
|
|
2538
|
+
* it implicitly, and the compact value does not retain the resulting graph.
|
|
2539
|
+
*/
|
|
2540
|
+
export function materializePreparedDiagnosticData(
|
|
2541
|
+
value: CompactPreparedDiagnosticData,
|
|
2542
|
+
): PreparedDiagnosticData {
|
|
2543
|
+
assertCompactPreparedDiagnosticData(value);
|
|
2544
|
+
return createEagerPreparedDiagnosticData(value.definition, {
|
|
2545
|
+
definitionIntegrity: value.definition.definitionIntegrity,
|
|
2546
|
+
filter: value.filter,
|
|
2547
|
+
completePeriodCutoffs: value.completePeriodCutoffs,
|
|
2548
|
+
inputAudit: value.inputAudit,
|
|
2549
|
+
cells: value.cells,
|
|
2550
|
+
exposures: value.exposures,
|
|
2551
|
+
expectedCellsProvided: value.expectedCellsProvided,
|
|
2552
|
+
expectedCells: value.expectedCells,
|
|
2553
|
+
findings: value.findings,
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
|
|
2557
|
+
/**
|
|
2558
|
+
* Core-owned, complete identity projection, emitted without a second retained
|
|
2559
|
+
* preparation graph. A structural document is not itself actuarial authority:
|
|
2560
|
+
* this factory authenticates the frozen preparation before creating one.
|
|
2561
|
+
*/
|
|
2562
|
+
export function getCompactPreparedDiagnosticDataIdentityDocument(
|
|
2563
|
+
value: CompactPreparedDiagnosticData,
|
|
2564
|
+
): DiagnosticIdentityDocument {
|
|
2565
|
+
assertCompactPreparedDiagnosticData(value);
|
|
2566
|
+
const previous = compactIdentityDocuments.get(value);
|
|
2567
|
+
if (previous) return previous;
|
|
2568
|
+
const document = createDiagnosticIdentityObject({
|
|
2569
|
+
definitionIntegrity: createDiagnosticIdentityValue(
|
|
2570
|
+
value.definition.definitionIntegrity,
|
|
2571
|
+
),
|
|
2572
|
+
filter: createDiagnosticIdentityValue(
|
|
2573
|
+
normalizeDiagnosticsFilterIdentity(value.filter),
|
|
2574
|
+
),
|
|
2575
|
+
completePeriodCutoffs: createDiagnosticIdentityValue(
|
|
2576
|
+
value.completePeriodCutoffs,
|
|
2577
|
+
),
|
|
2578
|
+
inputAudit: createDiagnosticIdentityValue(value.inputAudit),
|
|
2579
|
+
cells: createDiagnosticIdentityValue(value.cells),
|
|
2580
|
+
exposures: createDiagnosticIdentityValue(value.exposures),
|
|
2581
|
+
expectedCellsProvided: createDiagnosticIdentityValue(
|
|
2582
|
+
value.expectedCellsProvided,
|
|
2583
|
+
),
|
|
2584
|
+
expectedCells: createDiagnosticIdentityArray(
|
|
2585
|
+
value.expectedCells.length,
|
|
2586
|
+
(index) => {
|
|
2587
|
+
const cell = value.expectedCells[index]!;
|
|
2588
|
+
return createDiagnosticIdentityObject({
|
|
2589
|
+
sourceGroup: createDiagnosticIdentityValue(cell.sourceGroup),
|
|
2590
|
+
origin: createDiagnosticIdentityValue(cell.origin),
|
|
2591
|
+
valuation: createDiagnosticIdentityValue(cell.valuation),
|
|
2592
|
+
source: createDiagnosticIdentityValue(cell.source ?? null),
|
|
2593
|
+
});
|
|
2594
|
+
},
|
|
2595
|
+
),
|
|
2596
|
+
findings: createDiagnosticIdentityValue(value.findings),
|
|
2597
|
+
});
|
|
2598
|
+
compactIdentityDocuments.set(value, document);
|
|
2599
|
+
return document;
|
|
225
2600
|
}
|
|
226
|
-
|
|
227
|
-
|
|
2601
|
+
|
|
2602
|
+
/** Compute the exact legacy preparation tag on explicit evidence access only. */
|
|
2603
|
+
export function getCompactPreparedDiagnosticDataFingerprint(
|
|
2604
|
+
value: CompactPreparedDiagnosticData,
|
|
2605
|
+
): string {
|
|
2606
|
+
assertCompactPreparedDiagnosticData(value);
|
|
2607
|
+
const previous = compactFingerprints.get(value);
|
|
2608
|
+
if (previous) return previous;
|
|
2609
|
+
const tag = fingerprintDiagnosticIdentity(
|
|
2610
|
+
getCompactPreparedDiagnosticDataIdentityDocument(value),
|
|
2611
|
+
{ kind: "diagnostic-preparation", property: "preparation" },
|
|
2612
|
+
);
|
|
2613
|
+
compactFingerprints.set(value, tag);
|
|
2614
|
+
return tag;
|
|
2615
|
+
}
|
|
2616
|
+
|
|
2617
|
+
export function assertPreparedDiagnosticData(
|
|
2618
|
+
value: unknown,
|
|
2619
|
+
): asserts value is PreparedDiagnosticData {
|
|
2620
|
+
if (value === null || typeof value !== "object" || !authentic.has(value))
|
|
2621
|
+
throw new DiagnosticValidationError([
|
|
2622
|
+
{
|
|
2623
|
+
domain: "input",
|
|
2624
|
+
code: "invalid-input-relationship",
|
|
2625
|
+
path: "$.prepared",
|
|
2626
|
+
message: "Prepared diagnostic data is not authentic",
|
|
2627
|
+
},
|
|
2628
|
+
]);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
export function getPreparedDiagnosticDataIdentity(
|
|
2632
|
+
value: PreparedDiagnosticData,
|
|
2633
|
+
): DiagnosticDeepReadonly<NormalizedDiagnosticPreparationIdentity> {
|
|
2634
|
+
assertPreparedDiagnosticData(value);
|
|
2635
|
+
return identities.get(value)!;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
export function verifyPreparedDiagnosticDataIntegrity(
|
|
2639
|
+
value: PreparedDiagnosticData,
|
|
2640
|
+
): void {
|
|
228
2641
|
const identity = getPreparedDiagnosticDataIdentity(value);
|
|
229
2642
|
const tag = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({ identityVersion: 1, kind: "diagnostic-preparation", preparation: identity }))}`;
|
|
230
|
-
if (tag !== value.preparationFingerprint)
|
|
2643
|
+
if (tag !== value.preparationFingerprint)
|
|
2644
|
+
throw new DiagnosticValidationError([
|
|
2645
|
+
{
|
|
2646
|
+
domain: "input",
|
|
2647
|
+
code: "invalid-input-relationship",
|
|
2648
|
+
path: "$.prepared.preparationFingerprint",
|
|
2649
|
+
message:
|
|
2650
|
+
"Prepared diagnostic data integrity does not match its content",
|
|
2651
|
+
},
|
|
2652
|
+
]);
|
|
231
2653
|
}
|
|
232
2654
|
|
|
233
|
-
export type {
|
|
2655
|
+
export type {
|
|
2656
|
+
JsonValue,
|
|
2657
|
+
DiagnosticAuditedNumericValue,
|
|
2658
|
+
DiagnosticExposureAuditObservation,
|
|
2659
|
+
};
|