@actuarial-ts/core 0.7.1 → 0.8.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.
Files changed (115) hide show
  1. package/README.md +15 -4
  2. package/dist/analysisRecipe.d.ts +55 -0
  3. package/dist/analysisRecipe.d.ts.map +1 -0
  4. package/dist/analysisRecipe.js +627 -0
  5. package/dist/analysisRecipe.js.map +1 -0
  6. package/dist/classifications.d.ts +30 -0
  7. package/dist/classifications.d.ts.map +1 -0
  8. package/dist/classifications.js +207 -0
  9. package/dist/classifications.js.map +1 -0
  10. package/dist/customizationContracts.d.ts +749 -0
  11. package/dist/customizationContracts.d.ts.map +1 -0
  12. package/dist/customizationContracts.js +19 -0
  13. package/dist/customizationContracts.js.map +1 -0
  14. package/dist/descriptiveStatistics.d.ts +8 -0
  15. package/dist/descriptiveStatistics.d.ts.map +1 -0
  16. package/dist/descriptiveStatistics.js +283 -0
  17. package/dist/descriptiveStatistics.js.map +1 -0
  18. package/dist/diagnosticAggregation.d.ts.map +1 -1
  19. package/dist/diagnosticAggregation.js +16 -0
  20. package/dist/diagnosticAggregation.js.map +1 -1
  21. package/dist/diagnosticFreeze.d.ts +4 -0
  22. package/dist/diagnosticFreeze.d.ts.map +1 -0
  23. package/dist/diagnosticFreeze.js +37 -0
  24. package/dist/diagnosticFreeze.js.map +1 -0
  25. package/dist/diagnosticIdentityStream.d.ts +4 -0
  26. package/dist/diagnosticIdentityStream.d.ts.map +1 -1
  27. package/dist/diagnosticIdentityStream.js +32 -2
  28. package/dist/diagnosticIdentityStream.js.map +1 -1
  29. package/dist/diagnosticPreparation.d.ts +21 -0
  30. package/dist/diagnosticPreparation.d.ts.map +1 -1
  31. package/dist/diagnosticPreparation.js +231 -19
  32. package/dist/diagnosticPreparation.js.map +1 -1
  33. package/dist/diagnosticReview.d.ts.map +1 -1
  34. package/dist/diagnosticReview.js +43 -25
  35. package/dist/diagnosticReview.js.map +1 -1
  36. package/dist/diagnosticReviewSources.d.ts.map +1 -1
  37. package/dist/diagnosticReviewSources.js +25 -4
  38. package/dist/diagnosticReviewSources.js.map +1 -1
  39. package/dist/diagnosticReviewStore.d.ts +2 -0
  40. package/dist/diagnosticReviewStore.d.ts.map +1 -1
  41. package/dist/diagnosticReviewStore.js +110 -36
  42. package/dist/diagnosticReviewStore.js.map +1 -1
  43. package/dist/diagnosticRunner.d.ts.map +1 -1
  44. package/dist/diagnosticRunner.js +1 -8
  45. package/dist/diagnosticRunner.js.map +1 -1
  46. package/dist/exposureEarning.d.ts +12 -0
  47. package/dist/exposureEarning.d.ts.map +1 -0
  48. package/dist/exposureEarning.js +276 -0
  49. package/dist/exposureEarning.js.map +1 -0
  50. package/dist/exposureMovements.d.ts +8 -0
  51. package/dist/exposureMovements.d.ts.map +1 -0
  52. package/dist/exposureMovements.js +70 -0
  53. package/dist/exposureMovements.js.map +1 -0
  54. package/dist/exposureRevisions.d.ts +13 -0
  55. package/dist/exposureRevisions.d.ts.map +1 -0
  56. package/dist/exposureRevisions.js +161 -0
  57. package/dist/exposureRevisions.js.map +1 -0
  58. package/dist/financialPipeline.d.ts +14 -0
  59. package/dist/financialPipeline.d.ts.map +1 -0
  60. package/dist/financialPipeline.js +75 -0
  61. package/dist/financialPipeline.js.map +1 -0
  62. package/dist/financialTerms.d.ts +20 -0
  63. package/dist/financialTerms.d.ts.map +1 -0
  64. package/dist/financialTerms.js +205 -0
  65. package/dist/financialTerms.js.map +1 -0
  66. package/dist/fnvAccumulator.d.ts.map +1 -1
  67. package/dist/fnvAccumulator.js +11 -5
  68. package/dist/fnvAccumulator.js.map +1 -1
  69. package/dist/historyPreparation.d.ts +8 -0
  70. package/dist/historyPreparation.d.ts.map +1 -0
  71. package/dist/historyPreparation.js +557 -0
  72. package/dist/historyPreparation.js.map +1 -0
  73. package/dist/index.d.ts +15 -2
  74. package/dist/index.d.ts.map +1 -1
  75. package/dist/index.js +15 -2
  76. package/dist/index.js.map +1 -1
  77. package/dist/policyPeriods.d.ts +29 -0
  78. package/dist/policyPeriods.d.ts.map +1 -0
  79. package/dist/policyPeriods.js +322 -0
  80. package/dist/policyPeriods.js.map +1 -0
  81. package/dist/populations.d.ts +11 -0
  82. package/dist/populations.d.ts.map +1 -0
  83. package/dist/populations.js +264 -0
  84. package/dist/populations.js.map +1 -0
  85. package/dist/reservingAdapter.d.ts +16 -0
  86. package/dist/reservingAdapter.d.ts.map +1 -0
  87. package/dist/reservingAdapter.js +74 -0
  88. package/dist/reservingAdapter.js.map +1 -0
  89. package/dist/version.d.ts +1 -1
  90. package/dist/version.js +1 -1
  91. package/package.json +2 -2
  92. package/src/analysisRecipe.ts +696 -0
  93. package/src/classifications.ts +231 -0
  94. package/src/customizationContracts.ts +763 -0
  95. package/src/descriptiveStatistics.ts +320 -0
  96. package/src/diagnosticAggregation.ts +16 -0
  97. package/src/diagnosticFreeze.ts +37 -0
  98. package/src/diagnosticIdentityStream.ts +37 -2
  99. package/src/diagnosticPreparation.ts +259 -26
  100. package/src/diagnosticReview.ts +49 -27
  101. package/src/diagnosticReviewSources.ts +22 -3
  102. package/src/diagnosticReviewStore.ts +139 -54
  103. package/src/diagnosticRunner.ts +1 -11
  104. package/src/exposureEarning.ts +314 -0
  105. package/src/exposureMovements.ts +78 -0
  106. package/src/exposureRevisions.ts +186 -0
  107. package/src/financialPipeline.ts +93 -0
  108. package/src/financialTerms.ts +263 -0
  109. package/src/fnvAccumulator.ts +11 -5
  110. package/src/historyPreparation.ts +645 -0
  111. package/src/index.ts +28 -1
  112. package/src/policyPeriods.ts +375 -0
  113. package/src/populations.ts +307 -0
  114. package/src/reservingAdapter.ts +98 -0
  115. package/src/version.ts +1 -1
@@ -0,0 +1,645 @@
1
+ import { auditedDiagnosticContribution, finalizeDiagnosticContributions } from "./diagnosticAggregation.js";
2
+ import { canonicalJson, fnv1a64 } from "./canonical.js";
3
+ import {
4
+ HISTORICAL_DATASET_CONTRACT_VERSION,
5
+ type CustomizationCapabilityFinding,
6
+ type HistoricalDatasetInput,
7
+ type HistoricalObservationRecord,
8
+ type HistoricalRecordDisposition,
9
+ type HistoricalScalar,
10
+ type PreparedHistoricalDataset,
11
+ type PreparedHistoricalObservation,
12
+ } from "./customizationContracts.js";
13
+ import { diagnosticJsonPreflight, isDiagnosticToken, isRealIsoDate, snapshotDiagnosticJson } from "./diagnosticRuntime.js";
14
+ import { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.js";
15
+
16
+ interface IndexedRecord {
17
+ readonly index: number;
18
+ readonly record: HistoricalObservationRecord;
19
+ }
20
+
21
+ function compareText(left: string, right: string): number {
22
+ return left === right ? 0 : left < right ? -1 : 1;
23
+ }
24
+
25
+ function recordKey(record: HistoricalObservationRecord): string {
26
+ return canonicalJson([record.sourceNamespace, record.recordId]);
27
+ }
28
+
29
+ function claimEvaluationKey(record: HistoricalObservationRecord): string {
30
+ return canonicalJson([record.sourceNamespace, record.claimId, record.evaluationDate]);
31
+ }
32
+
33
+ function claimKey(sourceNamespace: string, claimId: string): string {
34
+ return canonicalJson([sourceNamespace, claimId]);
35
+ }
36
+
37
+ function addToMap<K, V>(map: Map<K, V[]>, key: K, value: V): void {
38
+ const current = map.get(key);
39
+ if (current === undefined) map.set(key, [value]);
40
+ else current.push(value);
41
+ }
42
+
43
+ function inputIssues(input: HistoricalDatasetInput): DiagnosticValidationIssue[] {
44
+ const issues = [...diagnosticJsonPreflight(input, "input")];
45
+ if (input.historyContractVersion !== HISTORICAL_DATASET_CONTRACT_VERSION)
46
+ issues.push({
47
+ domain: "input",
48
+ code: "invalid-configuration",
49
+ path: "$.historyContractVersion",
50
+ message: `Expected ${HISTORICAL_DATASET_CONTRACT_VERSION}`,
51
+ });
52
+ if (!isDiagnosticToken(input.datasetId))
53
+ issues.push({
54
+ domain: "input",
55
+ code: "invalid-string",
56
+ path: "$.datasetId",
57
+ message: "datasetId must be a valid token",
58
+ });
59
+ const namespaces = new Map(input.sources.map((source) => [source.namespace, source]));
60
+ if (namespaces.size !== input.sources.length)
61
+ issues.push({
62
+ domain: "input",
63
+ code: "duplicate-id",
64
+ path: "$.sources",
65
+ message: "Source namespaces must be unique",
66
+ });
67
+ if (
68
+ input.evaluationDates !== undefined &&
69
+ input.evaluationDates.some(
70
+ (date, index) =>
71
+ !isRealIsoDate(date) ||
72
+ (index > 0 && input.evaluationDates![index - 1]! >= date),
73
+ )
74
+ )
75
+ issues.push({
76
+ domain: "input",
77
+ code: "invalid-period",
78
+ path: "$.evaluationDates",
79
+ message: "evaluationDates must be unique, ascending real Gregorian dates",
80
+ });
81
+ input.records.forEach((record, index) => {
82
+ const source = namespaces.get(record.sourceNamespace);
83
+ if (source === undefined)
84
+ issues.push({
85
+ domain: "input",
86
+ code: "unknown-reference",
87
+ path: `$.records[${index}].sourceNamespace`,
88
+ message: "Record references an unknown source namespace",
89
+ });
90
+ else if (source.artifactId !== record.source.artifactId)
91
+ issues.push({
92
+ domain: "input",
93
+ code: "invalid-input-relationship",
94
+ path: `$.records[${index}].source.artifactId`,
95
+ message: "Record artifact does not match its source namespace",
96
+ });
97
+ if (!isRealIsoDate(record.evaluationDate) && record.grain !== "transaction")
98
+ issues.push({
99
+ domain: "input",
100
+ code: "invalid-period",
101
+ path: `$.records[${index}].evaluationDate`,
102
+ message: "Snapshot evaluationDate must be a real Gregorian date",
103
+ });
104
+ if (!isRealIsoDate(record.effectiveDate) && record.grain === "transaction")
105
+ issues.push({
106
+ domain: "input",
107
+ code: "invalid-period",
108
+ path: `$.records[${index}].effectiveDate`,
109
+ message: "Transaction effectiveDate must be a real Gregorian date",
110
+ });
111
+ if (input.revisionPrecedence === "sequence" && !Number.isSafeInteger(record.revision.sequence))
112
+ issues.push({
113
+ domain: "input",
114
+ code: "missing-required",
115
+ path: `$.records[${index}].revision.sequence`,
116
+ message: "Sequence precedence requires a safe integer sequence",
117
+ });
118
+ if (input.revisionPrecedence === "corrected-at" && record.revision.correctedAt === undefined)
119
+ issues.push({
120
+ domain: "input",
121
+ code: "missing-required",
122
+ path: `$.records[${index}].revision.correctedAt`,
123
+ message: "Corrected-at precedence requires correctedAt",
124
+ });
125
+ if (input.revisionView.kind === "as-known" && record.revision.correctedAt === undefined)
126
+ issues.push({
127
+ domain: "input",
128
+ code: "missing-required",
129
+ path: `$.records[${index}].revision.correctedAt`,
130
+ message: "As-known revision views require correctedAt",
131
+ });
132
+ });
133
+ if (input.historyMode !== "full-snapshots" && input.evaluationDates === undefined)
134
+ issues.push({
135
+ domain: "input",
136
+ code: "missing-required",
137
+ path: "$.evaluationDates",
138
+ message: "Change history requires explicit evaluationDates",
139
+ });
140
+ return issues;
141
+ }
142
+
143
+ function precedence(record: HistoricalObservationRecord, input: HistoricalDatasetInput): number | string {
144
+ return input.revisionPrecedence === "sequence"
145
+ ? record.revision.sequence!
146
+ : record.revision.correctedAt!;
147
+ }
148
+
149
+ function comparePrecedence(
150
+ left: HistoricalObservationRecord,
151
+ right: HistoricalObservationRecord,
152
+ input: HistoricalDatasetInput,
153
+ ): number {
154
+ const a = precedence(left, input);
155
+ const b = precedence(right, input);
156
+ return a === b ? 0 : a < b ? -1 : 1;
157
+ }
158
+
159
+ function eligible(record: HistoricalObservationRecord, input: HistoricalDatasetInput): boolean {
160
+ return (
161
+ input.revisionView.kind === "restated" ||
162
+ record.revision.correctedAt! <= input.revisionView.knowledgeCutoff
163
+ );
164
+ }
165
+
166
+ function sumMeasures(
167
+ records: readonly HistoricalObservationRecord[],
168
+ ): Readonly<Record<string, number | null>> {
169
+ const ids = [...new Set(records.flatMap((record) => Object.keys(record.measures)))].sort();
170
+ return Object.freeze(
171
+ Object.fromEntries(
172
+ ids.map((id) => {
173
+ const contributions = records
174
+ .filter((record) => Object.hasOwn(record.measures, id))
175
+ .map((record) =>
176
+ auditedDiagnosticContribution(
177
+ `${record.sourceNamespace}:${record.recordId}`,
178
+ record.measures[id],
179
+ "unknown",
180
+ [record.source],
181
+ ),
182
+ );
183
+ return [id, finalizeDiagnosticContributions(contributions, "unknown").value];
184
+ }),
185
+ ),
186
+ );
187
+ }
188
+
189
+ function mergeDimensions(
190
+ records: readonly HistoricalObservationRecord[],
191
+ findings: CustomizationCapabilityFinding[],
192
+ ): Readonly<Record<string, HistoricalScalar | readonly HistoricalScalar[]>> {
193
+ const result: Record<string, HistoricalScalar | readonly HistoricalScalar[]> = Object.create(null);
194
+ for (const id of [...new Set(records.flatMap((record) => Object.keys(record.dimensions ?? {})))].sort()) {
195
+ const values = records
196
+ .filter((record) => Object.hasOwn(record.dimensions ?? {}, id))
197
+ .map((record) => record.dimensions![id]!);
198
+ const unique = new Map(values.map((value) => [canonicalJson(value), value]));
199
+ if (unique.size === 1) result[id] = unique.values().next().value!;
200
+ else
201
+ findings.push({
202
+ code: "ambiguous-attribution",
203
+ capability: "history-snapshots",
204
+ path: `$.records.${records[0]!.sourceNamespace}.${records[0]!.claimId}.dimensions.${id}`,
205
+ message: `Component snapshots disagree on dimension ${id}`,
206
+ });
207
+ }
208
+ return Object.freeze(result);
209
+ }
210
+
211
+ function mergeSnapshotCohort(
212
+ records: readonly HistoricalObservationRecord[],
213
+ findings: CustomizationCapabilityFinding[],
214
+ ): PreparedHistoricalObservation | null {
215
+ const claimRows = records.filter((record) => record.grain === "claim-snapshot");
216
+ const componentRows = records.filter((record) => record.grain === "claim-component-snapshot");
217
+ if (claimRows.length > 1 || (claimRows.length > 0 && componentRows.length > 0)) {
218
+ findings.push({
219
+ code: "ambiguous-attribution",
220
+ capability: "history-snapshots",
221
+ path: `$.records.${records[0]!.sourceNamespace}.${records[0]!.claimId}.${records[0]!.evaluationDate}`,
222
+ message: "Claim/evaluation has incompatible claim and component snapshot grains",
223
+ });
224
+ return null;
225
+ }
226
+ if (componentRows.length > 0) {
227
+ const components = componentRows.map((record) => record.componentId!);
228
+ if (new Set(components).size !== components.length) {
229
+ findings.push({
230
+ code: "ambiguous-attribution",
231
+ capability: "history-snapshots",
232
+ path: `$.records.${records[0]!.sourceNamespace}.${records[0]!.claimId}.${records[0]!.evaluationDate}`,
233
+ message: "Claim/evaluation repeats a component identity",
234
+ });
235
+ return null;
236
+ }
237
+ }
238
+ const dates = (field: "accidentDate" | "reportDate") => [
239
+ ...new Set(records.map((record) => record[field]).filter((value): value is string => value !== undefined)),
240
+ ];
241
+ const accidentDates = dates("accidentDate");
242
+ const reportDates = dates("reportDate");
243
+ const statuses = [...new Set(records.map((record) => record.status).filter((value): value is string => value !== undefined))];
244
+ const relationships = [
245
+ ...new Map(
246
+ records
247
+ .filter((record) => record.relationships !== undefined)
248
+ .map((record) => [canonicalJson(record.relationships), record.relationships!]),
249
+ ).values(),
250
+ ];
251
+ if (accidentDates.length > 1 || reportDates.length > 1 || statuses.length > 1 || relationships.length > 1) {
252
+ findings.push({
253
+ code: "ambiguous-attribution",
254
+ capability: "history-snapshots",
255
+ path: `$.records.${records[0]!.sourceNamespace}.${records[0]!.claimId}.${records[0]!.evaluationDate}`,
256
+ message: "Component snapshots disagree on claim dates, status, or relationships",
257
+ });
258
+ return null;
259
+ }
260
+ return {
261
+ sourceNamespace: records[0]!.sourceNamespace,
262
+ claimId: records[0]!.claimId,
263
+ evaluationDate: records[0]!.evaluationDate!,
264
+ observationKind: "observed-snapshot",
265
+ accidentDate: accidentDates[0] ?? null,
266
+ reportDate: reportDates[0] ?? null,
267
+ measures: claimRows.length === 1 ? claimRows[0]!.measures : sumMeasures(componentRows),
268
+ status: statuses[0] ?? null,
269
+ dimensions: mergeDimensions(records, findings),
270
+ relationships: relationships[0] ?? {},
271
+ sources: records.map((record) => record.source),
272
+ };
273
+ }
274
+
275
+ function transactionObservations(
276
+ input: HistoricalDatasetInput,
277
+ records: readonly HistoricalObservationRecord[],
278
+ findings: CustomizationCapabilityFinding[],
279
+ ): PreparedHistoricalObservation[] {
280
+ const output: PreparedHistoricalObservation[] = [];
281
+ const byClaim = new Map<string, HistoricalObservationRecord[]>();
282
+ for (const record of records) addToMap(byClaim, claimKey(record.sourceNamespace, record.claimId), record);
283
+ const balances = new Map(
284
+ (input.openingBalances ?? []).map((balance) => [
285
+ claimKey(balance.sourceNamespace, balance.claimId),
286
+ balance,
287
+ ]),
288
+ );
289
+ for (const [key, claimRecords] of byClaim) {
290
+ const opening = balances.get(key);
291
+ if (opening === undefined) {
292
+ findings.push({
293
+ code: "missing-prerequisite",
294
+ capability: "history-changes",
295
+ path: `$.openingBalances.${claimRecords[0]!.sourceNamespace}.${claimRecords[0]!.claimId}`,
296
+ message: "Cumulative history from changes requires an opening balance",
297
+ requiredFields: ["openingBalances"],
298
+ });
299
+ continue;
300
+ }
301
+ const values: Record<string, number | null> = { ...opening.measures };
302
+ const ordered = [...claimRecords].sort(
303
+ (left, right) =>
304
+ compareText(left.effectiveDate!, right.effectiveDate!) ||
305
+ compareText(left.recordId, right.recordId),
306
+ );
307
+ let next = 0;
308
+ const appliedSources = [opening.source];
309
+ for (const evaluationDate of input.evaluationDates ?? []) {
310
+ while (next < ordered.length && ordered[next]!.effectiveDate! <= evaluationDate) {
311
+ const transaction = ordered[next++]!;
312
+ if (transaction.effectiveDate! > opening.asOfDate) {
313
+ for (const [measureId, amount] of Object.entries(transaction.measures)) {
314
+ if (amount === null || values[measureId] === null) values[measureId] = null;
315
+ else values[measureId] = (values[measureId] ?? 0) + amount;
316
+ }
317
+ appliedSources.push(transaction.source);
318
+ }
319
+ }
320
+ const latest = ordered[Math.max(0, next - 1)];
321
+ output.push({
322
+ sourceNamespace: opening.sourceNamespace,
323
+ claimId: opening.claimId,
324
+ evaluationDate,
325
+ observationKind: "reconstructed-from-changes",
326
+ accidentDate: latest?.accidentDate ?? null,
327
+ reportDate: latest?.reportDate ?? null,
328
+ measures: { ...values },
329
+ status: latest?.status ?? null,
330
+ dimensions: latest?.dimensions ?? {},
331
+ relationships: latest?.relationships ?? {},
332
+ sources: [...appliedSources],
333
+ });
334
+ }
335
+ }
336
+ return output;
337
+ }
338
+
339
+ function mixedObservations(
340
+ input: HistoricalDatasetInput,
341
+ snapshots: readonly PreparedHistoricalObservation[],
342
+ transactions: readonly HistoricalObservationRecord[],
343
+ findings: CustomizationCapabilityFinding[],
344
+ ): PreparedHistoricalObservation[] {
345
+ const output: PreparedHistoricalObservation[] = [];
346
+ const snapshotByClaim = new Map<string, PreparedHistoricalObservation[]>();
347
+ const transactionsByClaim = new Map<string, HistoricalObservationRecord[]>();
348
+ for (const observation of snapshots)
349
+ addToMap(snapshotByClaim, claimKey(observation.sourceNamespace, observation.claimId), observation);
350
+ for (const transaction of transactions)
351
+ addToMap(transactionsByClaim, claimKey(transaction.sourceNamespace, transaction.claimId), transaction);
352
+ const openings = new Map((input.openingBalances ?? []).map((opening) => [claimKey(opening.sourceNamespace, opening.claimId), opening]));
353
+ const claimKeys = [...new Set([...snapshotByClaim.keys(), ...transactionsByClaim.keys()])].sort(compareText);
354
+ for (const key of claimKeys) {
355
+ const claimSnapshots = [...(snapshotByClaim.get(key) ?? [])].sort((a, b) => compareText(a.evaluationDate, b.evaluationDate));
356
+ const claimTransactions = [...(transactionsByClaim.get(key) ?? [])].sort((a, b) => compareText(a.effectiveDate!, b.effectiveDate!) || compareText(a.recordId, b.recordId));
357
+ const opening = openings.get(key);
358
+ let baseline: PreparedHistoricalObservation | undefined;
359
+ let baselineDate = opening?.asOfDate;
360
+ let values: Record<string, number | null> | undefined = opening === undefined ? undefined : { ...opening.measures };
361
+ let sources = opening === undefined ? [] : [opening.source];
362
+ let applied = new Set<string>();
363
+ for (const evaluationDate of input.evaluationDates ?? []) {
364
+ const exact = claimSnapshots.find((observation) => observation.evaluationDate === evaluationDate);
365
+ if (exact !== undefined) {
366
+ baseline = exact;
367
+ baselineDate = evaluationDate;
368
+ values = { ...exact.measures };
369
+ sources = [...exact.sources];
370
+ applied = new Set(claimTransactions.filter((transaction) => transaction.effectiveDate! <= evaluationDate).map(recordKey));
371
+ output.push(exact);
372
+ continue;
373
+ }
374
+ const priorSnapshot = [...claimSnapshots].reverse().find((observation) => observation.evaluationDate < evaluationDate && (baselineDate === undefined || observation.evaluationDate > baselineDate));
375
+ if (priorSnapshot !== undefined) {
376
+ baseline = priorSnapshot;
377
+ baselineDate = priorSnapshot.evaluationDate;
378
+ values = { ...priorSnapshot.measures };
379
+ sources = [...priorSnapshot.sources];
380
+ applied = new Set(claimTransactions.filter((transaction) => transaction.effectiveDate! <= priorSnapshot.evaluationDate).map(recordKey));
381
+ }
382
+ if (values === undefined) {
383
+ if (claimTransactions.some((transaction) => transaction.effectiveDate! <= evaluationDate))
384
+ findings.push({
385
+ code: "missing-prerequisite",
386
+ capability: "history-changes",
387
+ path: `$.openingBalances.${claimTransactions[0]!.sourceNamespace}.${claimTransactions[0]!.claimId}`,
388
+ message: "Mixed history requires a prior snapshot or opening balance before applying changes",
389
+ requiredFields: ["prior snapshot", "openingBalances"],
390
+ });
391
+ continue;
392
+ }
393
+ let latestTransaction: HistoricalObservationRecord | undefined;
394
+ for (const transaction of claimTransactions) {
395
+ if (transaction.effectiveDate! > evaluationDate || applied.has(recordKey(transaction))) continue;
396
+ for (const [measureId, amount] of Object.entries(transaction.measures)) {
397
+ if (amount === null || values[measureId] === null) values[measureId] = null;
398
+ else values[measureId] = (values[measureId] ?? 0) + amount;
399
+ }
400
+ sources.push(transaction.source);
401
+ applied.add(recordKey(transaction));
402
+ latestTransaction = transaction;
403
+ }
404
+ output.push({
405
+ sourceNamespace: baseline?.sourceNamespace ?? opening!.sourceNamespace,
406
+ claimId: baseline?.claimId ?? opening!.claimId,
407
+ evaluationDate,
408
+ observationKind: "reconstructed-from-changes",
409
+ accidentDate: latestTransaction?.accidentDate ?? baseline?.accidentDate ?? null,
410
+ reportDate: latestTransaction?.reportDate ?? baseline?.reportDate ?? null,
411
+ measures: { ...values },
412
+ status: latestTransaction?.status ?? baseline?.status ?? null,
413
+ dimensions: latestTransaction?.dimensions ?? baseline?.dimensions ?? {},
414
+ relationships: latestTransaction?.relationships ?? baseline?.relationships ?? {},
415
+ sources: [...sources],
416
+ });
417
+ }
418
+ }
419
+ return output;
420
+ }
421
+
422
+ function addMissingSnapshotObservations(
423
+ input: HistoricalDatasetInput,
424
+ observations: PreparedHistoricalObservation[],
425
+ findings: CustomizationCapabilityFinding[],
426
+ ): void {
427
+ if (input.evaluationDates === undefined) return;
428
+ const byClaim = new Map<string, PreparedHistoricalObservation[]>();
429
+ for (const observation of observations.filter((item) => item.observationKind !== "reconstructed-from-changes"))
430
+ addToMap(byClaim, claimKey(observation.sourceNamespace, observation.claimId), observation);
431
+ for (const claimObservations of byClaim.values()) {
432
+ claimObservations.sort((left, right) => compareText(left.evaluationDate, right.evaluationDate));
433
+ const exact = new Set(claimObservations.map((observation) => observation.evaluationDate));
434
+ for (const evaluationDate of input.evaluationDates) {
435
+ if (exact.has(evaluationDate)) continue;
436
+ const prior = [...claimObservations]
437
+ .reverse()
438
+ .find((observation) => observation.evaluationDate < evaluationDate);
439
+ if (prior === undefined) continue;
440
+ if (input.missingSnapshotRecord === "carried-forward") {
441
+ const carried = snapshotDiagnosticJson({
442
+ ...prior,
443
+ evaluationDate,
444
+ observationKind: "carried-forward" as const,
445
+ });
446
+ observations.push(carried);
447
+ claimObservations.push(carried);
448
+ claimObservations.sort((left, right) => compareText(left.evaluationDate, right.evaluationDate));
449
+ exact.add(evaluationDate);
450
+ } else {
451
+ findings.push({
452
+ code: "missing-prerequisite",
453
+ capability: "history-snapshots",
454
+ path: `$.records.${prior.sourceNamespace}.${prior.claimId}.${evaluationDate}`,
455
+ message: "Missing snapshot remains unknown under the declared history rule",
456
+ });
457
+ }
458
+ }
459
+ }
460
+ }
461
+
462
+ function totalsByDate(
463
+ rows: readonly {
464
+ readonly evaluationDate?: string;
465
+ readonly effectiveDate?: string;
466
+ readonly measures: Readonly<Record<string, number | null>>;
467
+ }[],
468
+ ): Readonly<Record<string, Readonly<Record<string, number>>>> {
469
+ const result: Record<string, Record<string, number>> = Object.create(null);
470
+ for (const row of rows) {
471
+ const date = row.evaluationDate ?? row.effectiveDate;
472
+ if (date === undefined) continue;
473
+ const dateTotals = (result[date] ??= Object.create(null) as Record<string, number>);
474
+ for (const [id, value] of Object.entries(row.measures))
475
+ if (value !== null) dateTotals[id] = (dateTotals[id] ?? 0) + value;
476
+ }
477
+ return Object.freeze(
478
+ Object.fromEntries(
479
+ Object.keys(result)
480
+ .sort()
481
+ .map((date) => [date, Object.freeze(result[date]!)]),
482
+ ),
483
+ );
484
+ }
485
+
486
+ /**
487
+ * Selects immutable source revisions and assembles claim observations without
488
+ * using arrival order. Unknown input should first pass the data-package Zod
489
+ * boundary; this pure core boundary independently protects its typed contract.
490
+ */
491
+ export function prepareHistoricalDataset(input: HistoricalDatasetInput): PreparedHistoricalDataset {
492
+ const issues = inputIssues(input);
493
+ if (issues.length > 0) throw new DiagnosticValidationError(issues);
494
+ const owned = snapshotDiagnosticJson(input);
495
+ const dispositions: HistoricalRecordDisposition[] = [];
496
+ const selected: HistoricalObservationRecord[] = [];
497
+ const byRecord = new Map<string, IndexedRecord[]>();
498
+ owned.records.forEach((record, index) => addToMap(byRecord, recordKey(record), { index, record }));
499
+ for (const cohort of byRecord.values()) {
500
+ const candidates = cohort.filter(({ record }) => eligible(record, owned));
501
+ for (const { record } of cohort.filter(({ record }) => !eligible(record, owned)))
502
+ dispositions.push({
503
+ sourceNamespace: record.sourceNamespace,
504
+ recordId: record.recordId,
505
+ revisionId: record.revision.id,
506
+ disposition: "after-knowledge-cutoff",
507
+ });
508
+ if (candidates.length === 0) continue;
509
+ const maximum = candidates.reduce((best, item) =>
510
+ comparePrecedence(best.record, item.record, owned) < 0 ? item : best,
511
+ );
512
+ const winners = candidates.filter(
513
+ (item) => comparePrecedence(item.record, maximum.record, owned) === 0,
514
+ );
515
+ const uniqueWinners = new Map(winners.map((item) => [canonicalJson(item.record), item]));
516
+ if (uniqueWinners.size > 1) {
517
+ for (const { record } of winners)
518
+ dispositions.push({
519
+ sourceNamespace: record.sourceNamespace,
520
+ recordId: record.recordId,
521
+ revisionId: record.revision.id,
522
+ disposition: "conflict",
523
+ });
524
+ } else {
525
+ const winner = winners[0]!.record;
526
+ if (winner.revision.action === "delete")
527
+ dispositions.push({
528
+ sourceNamespace: winner.sourceNamespace,
529
+ recordId: winner.recordId,
530
+ revisionId: winner.revision.id,
531
+ disposition: "deleted",
532
+ });
533
+ else {
534
+ selected.push(winner);
535
+ dispositions.push({
536
+ sourceNamespace: winner.sourceNamespace,
537
+ recordId: winner.recordId,
538
+ revisionId: winner.revision.id,
539
+ disposition: "selected",
540
+ });
541
+ }
542
+ for (const { record } of winners.slice(1))
543
+ dispositions.push({
544
+ sourceNamespace: record.sourceNamespace,
545
+ recordId: record.recordId,
546
+ revisionId: record.revision.id,
547
+ disposition: "duplicate",
548
+ });
549
+ }
550
+ for (const { record } of candidates.filter(
551
+ (item) => comparePrecedence(item.record, maximum.record, owned) < 0,
552
+ ))
553
+ dispositions.push({
554
+ sourceNamespace: record.sourceNamespace,
555
+ recordId: record.recordId,
556
+ revisionId: record.revision.id,
557
+ disposition: "superseded",
558
+ });
559
+ }
560
+
561
+ const findings: CustomizationCapabilityFinding[] = [];
562
+ for (const disposition of dispositions.filter((item) => item.disposition === "conflict"))
563
+ findings.push({
564
+ code: "ambiguous-attribution",
565
+ capability: "history-snapshots",
566
+ path: `$.records.${disposition.sourceNamespace}.${disposition.recordId}`,
567
+ message: "Equal-precedence revisions conflict; no revision was selected",
568
+ });
569
+
570
+ selected.sort(
571
+ (left, right) =>
572
+ compareText(recordKey(left), recordKey(right)) ||
573
+ comparePrecedence(left, right, owned) ||
574
+ compareText(left.revision.id, right.revision.id),
575
+ );
576
+ const snapshotRecords = selected.filter((record) => record.grain !== "transaction");
577
+ const transactionRecords = selected.filter((record) => record.grain === "transaction");
578
+ let observations: PreparedHistoricalObservation[] = [];
579
+ const byClaimEvaluation = new Map<string, HistoricalObservationRecord[]>();
580
+ for (const record of snapshotRecords)
581
+ addToMap(byClaimEvaluation, claimEvaluationKey(record), record);
582
+ for (const cohort of byClaimEvaluation.values()) {
583
+ const observation = mergeSnapshotCohort(cohort, findings);
584
+ if (observation !== null) observations.push(observation);
585
+ }
586
+ if (owned.historyMode === "mixed")
587
+ observations = mixedObservations(owned, observations, transactionRecords, findings);
588
+ else if (owned.historyMode === "changes")
589
+ observations.push(...transactionObservations(owned, transactionRecords, findings));
590
+ else
591
+ addMissingSnapshotObservations(owned, observations, findings);
592
+ observations.sort(
593
+ (left, right) =>
594
+ compareText(left.sourceNamespace, right.sourceNamespace) ||
595
+ compareText(left.claimId, right.claimId) ||
596
+ compareText(left.evaluationDate, right.evaluationDate),
597
+ );
598
+ dispositions.sort(
599
+ (left, right) =>
600
+ compareText(left.sourceNamespace, right.sourceNamespace) ||
601
+ compareText(left.recordId, right.recordId) ||
602
+ compareText(left.revisionId, right.revisionId) ||
603
+ compareText(left.disposition, right.disposition),
604
+ );
605
+ const economicObservations = observations.map(({ sources: _sources, ...observation }) => observation);
606
+ const calculationIdentityValue = {
607
+ historyContractVersion: owned.historyContractVersion,
608
+ revisionView: owned.revisionView,
609
+ revisionPrecedence: owned.revisionPrecedence,
610
+ historyMode: owned.historyMode,
611
+ missingSnapshotRecord: owned.missingSnapshotRecord,
612
+ observations: economicObservations,
613
+ };
614
+ const calculationRevisionId = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson(calculationIdentityValue))}`;
615
+ const lineageIdentityValue = {
616
+ historyContractVersion: owned.historyContractVersion,
617
+ datasetId: owned.datasetId,
618
+ sources: owned.sources,
619
+ selected,
620
+ calculationRevisionId,
621
+ };
622
+ const lineageRevisionId = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson(lineageIdentityValue))}`;
623
+ const result: PreparedHistoricalDataset = {
624
+ historyContractVersion: HISTORICAL_DATASET_CONTRACT_VERSION,
625
+ datasetId: owned.datasetId,
626
+ datasetRevisionId: lineageRevisionId,
627
+ calculationRevisionId,
628
+ lineageRevisionId,
629
+ revisionView: owned.revisionView,
630
+ observations,
631
+ dispositions,
632
+ reconciliation: {
633
+ inputRecords: owned.records.length,
634
+ selectedRevisions: dispositions.filter((item) => item.disposition === "selected").length,
635
+ duplicateRecords: dispositions.filter((item) => item.disposition === "duplicate").length,
636
+ supersededRecords: dispositions.filter((item) => item.disposition === "superseded").length,
637
+ deletedRecords: dispositions.filter((item) => item.disposition === "deleted").length,
638
+ outputObservations: observations.length,
639
+ inputMeasureTotalsByDate: totalsByDate(selected),
640
+ outputMeasureTotalsByEvaluation: totalsByDate(observations),
641
+ },
642
+ findings,
643
+ };
644
+ return snapshotDiagnosticJson(result);
645
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,30 @@
1
1
  export * from "./types.js";
2
+ export * from "./customizationContracts.js";
3
+ export * from "./historyPreparation.js";
4
+ export * from "./policyPeriods.js";
5
+ export * from "./exposureEarning.js";
6
+ export * from "./exposureRevisions.js";
7
+ export * from "./exposureMovements.js";
8
+ export * from "./populations.js";
9
+ export * from "./classifications.js";
10
+ export * from "./descriptiveStatistics.js";
11
+ export * from "./reservingAdapter.js";
12
+ export * from "./financialTerms.js";
13
+ export * from "./financialPipeline.js";
14
+ export * from "./analysisRecipe.js";
2
15
  export * from "./util.js";
3
16
  export * from "./canonical.js";
4
- export * from "./diagnosticIdentityStream.js";
17
+ export {
18
+ createDiagnosticIdentityValue,
19
+ createDiagnosticIdentityObject,
20
+ createDiagnosticIdentityArray,
21
+ iterateDiagnosticIdentityJson,
22
+ fingerprintDiagnosticIdentity,
23
+ compareDiagnosticIdentityDocuments,
24
+ compareDiagnosticIdentityJsonChunks,
25
+ type DiagnosticIdentityDocument,
26
+ type DiagnosticIdentityComparison,
27
+ } from "./diagnosticIdentityStream.js";
5
28
  export * from "./version.js";
6
29
  export {
7
30
  MAX_DIAGNOSTIC_JSON_DEPTH,
@@ -131,6 +154,10 @@ export {
131
154
  materializePreparedDiagnosticData,
132
155
  prepareDiagnosticData,
133
156
  prepareDiagnosticDataCompact,
157
+ reselectCompactDiagnosticMetrics,
158
+ selectCompactDiagnosticSourceGroups,
159
+ selectCompactDiagnosticOrigins,
160
+ selectCompactDiagnosticCoordinates,
134
161
  verifyPreparedDiagnosticDataIntegrity,
135
162
  } from "./diagnosticPreparation.js";
136
163
  export type {