@actuarial-ts/data 0.7.1 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +20 -4
  2. package/dist/customizationContracts.d.ts +2473 -0
  3. package/dist/customizationContracts.d.ts.map +1 -0
  4. package/dist/customizationContracts.js +845 -0
  5. package/dist/customizationContracts.js.map +1 -0
  6. package/dist/customizationExternalState.d.ts +42 -0
  7. package/dist/customizationExternalState.d.ts.map +1 -0
  8. package/dist/customizationExternalState.js +121 -0
  9. package/dist/customizationExternalState.js.map +1 -0
  10. package/dist/customizationHistoryStream.d.ts +26 -0
  11. package/dist/customizationHistoryStream.d.ts.map +1 -0
  12. package/dist/customizationHistoryStream.js +66 -0
  13. package/dist/customizationHistoryStream.js.map +1 -0
  14. package/dist/diagnosticInput.d.ts +33 -0
  15. package/dist/diagnosticInput.d.ts.map +1 -1
  16. package/dist/diagnosticInput.js +71 -1
  17. package/dist/diagnosticInput.js.map +1 -1
  18. package/dist/diagnosticPreparedReview.d.ts.map +1 -1
  19. package/dist/diagnosticPreparedReview.js +38 -0
  20. package/dist/diagnosticPreparedReview.js.map +1 -1
  21. package/dist/historyMapping.d.ts +898 -0
  22. package/dist/historyMapping.d.ts.map +1 -0
  23. package/dist/historyMapping.js +395 -0
  24. package/dist/historyMapping.js.map +1 -0
  25. package/dist/index.d.ts +4 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +4 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/package.json +3 -3
  32. package/src/customizationContracts.ts +963 -0
  33. package/src/customizationExternalState.ts +208 -0
  34. package/src/customizationHistoryStream.ts +90 -0
  35. package/src/diagnosticInput.ts +101 -0
  36. package/src/diagnosticPreparedReview.ts +48 -0
  37. package/src/historyMapping.ts +514 -0
  38. package/src/index.ts +4 -0
  39. package/src/version.ts +1 -1
@@ -0,0 +1,514 @@
1
+ import {
2
+ DiagnosticValidationError,
3
+ diagnosticJsonPreflight,
4
+ isDiagnosticToken,
5
+ isRealIsoDate,
6
+ snapshotDiagnosticJson,
7
+ type HistoricalObservationRecord,
8
+ type HistoricalScalar,
9
+ } from "@actuarial-ts/core";
10
+ import { z } from "zod";
11
+
12
+ export interface HistoricalColumnSelector {
13
+ readonly columns: readonly string[];
14
+ }
15
+
16
+ export interface HistoricalDateSelector extends HistoricalColumnSelector {
17
+ readonly format: "iso-yyyy-mm-dd" | "mdy-slash" | "dmy-slash";
18
+ }
19
+
20
+ export interface HistoricalNumberSelector extends HistoricalColumnSelector {
21
+ readonly decimalSeparator: "." | ",";
22
+ readonly thousandsSeparator: "," | "." | " " | null;
23
+ readonly negative: "leading-minus" | "parentheses-or-leading-minus";
24
+ readonly blank: "null" | "error";
25
+ readonly scale: number;
26
+ }
27
+
28
+ export interface HistoricalDimensionSelector extends HistoricalColumnSelector {
29
+ readonly type: "string" | "number" | "boolean" | "date";
30
+ readonly dateFormat?: HistoricalDateSelector["format"];
31
+ readonly numberFormat?: Omit<HistoricalNumberSelector, "columns">;
32
+ readonly multipleDelimiter?: string;
33
+ }
34
+
35
+ export interface HistoricalSourceMapping {
36
+ readonly id: string;
37
+ readonly version: string;
38
+ readonly sourceNamespace: string;
39
+ readonly artifactId: string;
40
+ readonly grain: HistoricalObservationRecord["grain"];
41
+ readonly completeness: HistoricalObservationRecord["completeness"];
42
+ readonly fields: {
43
+ readonly recordId: HistoricalColumnSelector;
44
+ readonly claimId: HistoricalColumnSelector;
45
+ readonly componentId?: HistoricalColumnSelector;
46
+ readonly accidentDate?: HistoricalDateSelector;
47
+ readonly reportDate?: HistoricalDateSelector;
48
+ readonly evaluationDate?: HistoricalDateSelector;
49
+ readonly effectiveDate?: HistoricalDateSelector;
50
+ readonly status?: HistoricalColumnSelector;
51
+ };
52
+ readonly measures: Readonly<Record<string, HistoricalNumberSelector>>;
53
+ readonly dimensions?: Readonly<Record<string, HistoricalDimensionSelector>>;
54
+ readonly relationships?: {
55
+ readonly claimantIds?: HistoricalColumnSelector & { readonly delimiter: string };
56
+ readonly occurrenceId?: HistoricalColumnSelector;
57
+ readonly policyIds?: HistoricalColumnSelector & { readonly delimiter: string };
58
+ readonly coverageIds?: HistoricalColumnSelector & { readonly delimiter: string };
59
+ };
60
+ readonly revision: {
61
+ readonly id: HistoricalColumnSelector;
62
+ readonly action?: HistoricalColumnSelector;
63
+ readonly sequence?: HistoricalNumberSelector;
64
+ readonly correctedAt?: HistoricalColumnSelector;
65
+ };
66
+ readonly preserveUnmappedAttributes: boolean;
67
+ }
68
+
69
+ export interface HistoricalSourceRow {
70
+ readonly rowNumber: number;
71
+ readonly values: Readonly<Record<string, HistoricalScalar>>;
72
+ }
73
+
74
+ export interface HistoricalMappedRowRejection {
75
+ readonly rowNumber: number;
76
+ readonly source: { readonly artifactId: string; readonly sourceRow: number };
77
+ readonly issues: readonly { readonly path: string; readonly message: string }[];
78
+ }
79
+
80
+ export interface HistoricalMappingResult {
81
+ readonly mappingId: string;
82
+ readonly mappingVersion: string;
83
+ readonly sourceNamespace: string;
84
+ readonly artifactId: string;
85
+ readonly records: readonly HistoricalObservationRecord[];
86
+ readonly rejected: readonly HistoricalMappedRowRejection[];
87
+ readonly attributeCatalog: readonly {
88
+ readonly id: string;
89
+ readonly sourceColumn: string;
90
+ readonly observedTypes: readonly ("string" | "number" | "boolean" | "null")[];
91
+ }[];
92
+ readonly reconciliation: {
93
+ readonly inputRows: number;
94
+ readonly acceptedRows: number;
95
+ readonly rejectedRows: number;
96
+ };
97
+ }
98
+
99
+ const token = z.string().refine(isDiagnosticToken, "Expected a valid nonempty token");
100
+ const columnSelector = z.object({ columns: z.array(token).min(1) }).strict();
101
+ const dateSelector = columnSelector.extend({
102
+ format: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]),
103
+ }).strict();
104
+ const numberFormat = {
105
+ decimalSeparator: z.enum([".", ","]),
106
+ thousandsSeparator: z.enum([",", ".", " "]).nullable(),
107
+ negative: z.enum(["leading-minus", "parentheses-or-leading-minus"]),
108
+ blank: z.enum(["null", "error"]),
109
+ scale: z.number().finite().positive(),
110
+ };
111
+ const numberSelector = columnSelector.extend(numberFormat).strict().superRefine((value, context) => {
112
+ if (value.thousandsSeparator === value.decimalSeparator)
113
+ context.addIssue({
114
+ code: z.ZodIssueCode.custom,
115
+ path: ["thousandsSeparator"],
116
+ message: "Thousands and decimal separators must differ",
117
+ });
118
+ });
119
+ const dimensionSelector = columnSelector
120
+ .extend({
121
+ type: z.enum(["string", "number", "boolean", "date"]),
122
+ dateFormat: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]).optional(),
123
+ numberFormat: z.object(numberFormat).strict().optional(),
124
+ multipleDelimiter: z.string().min(1).optional(),
125
+ })
126
+ .strict()
127
+ .superRefine((value, context) => {
128
+ if (value.type === "date" && value.dateFormat === undefined)
129
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["dateFormat"], message: "Date dimension requires dateFormat" });
130
+ if (value.type === "number" && value.numberFormat === undefined)
131
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["numberFormat"], message: "Number dimension requires numberFormat" });
132
+ if (value.multipleDelimiter !== undefined && value.type !== "string")
133
+ context.addIssue({
134
+ code: z.ZodIssueCode.custom,
135
+ path: ["multipleDelimiter"],
136
+ message: "Only string dimensions support multipleDelimiter",
137
+ });
138
+ });
139
+ const multiSelector = columnSelector.extend({ delimiter: z.string().min(1) }).strict();
140
+
141
+ export const historicalSourceMappingSchema = z
142
+ .object({
143
+ id: token,
144
+ version: token,
145
+ sourceNamespace: token,
146
+ artifactId: token,
147
+ grain: z.enum(["claim-snapshot", "claim-component-snapshot", "transaction"]),
148
+ completeness: z.enum(["complete-snapshot", "partial-snapshot", "change-only"]),
149
+ fields: z
150
+ .object({
151
+ recordId: columnSelector,
152
+ claimId: columnSelector,
153
+ componentId: columnSelector.optional(),
154
+ accidentDate: dateSelector.optional(),
155
+ reportDate: dateSelector.optional(),
156
+ evaluationDate: dateSelector.optional(),
157
+ effectiveDate: dateSelector.optional(),
158
+ status: columnSelector.optional(),
159
+ })
160
+ .strict(),
161
+ measures: z.record(token, numberSelector),
162
+ dimensions: z.record(token, dimensionSelector).optional(),
163
+ relationships: z
164
+ .object({
165
+ claimantIds: multiSelector.optional(),
166
+ occurrenceId: columnSelector.optional(),
167
+ policyIds: multiSelector.optional(),
168
+ coverageIds: multiSelector.optional(),
169
+ })
170
+ .strict()
171
+ .optional(),
172
+ revision: z
173
+ .object({
174
+ id: columnSelector,
175
+ action: columnSelector.optional(),
176
+ sequence: numberSelector.optional(),
177
+ correctedAt: columnSelector.optional(),
178
+ })
179
+ .strict(),
180
+ preserveUnmappedAttributes: z.boolean(),
181
+ })
182
+ .strict()
183
+ .superRefine((mapping, context) => {
184
+ if (mapping.grain === "claim-component-snapshot" && mapping.fields.componentId === undefined)
185
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["fields", "componentId"], message: "Component grain requires componentId mapping" });
186
+ if (mapping.grain === "transaction" && mapping.fields.effectiveDate === undefined)
187
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["fields", "effectiveDate"], message: "Transaction grain requires effectiveDate mapping" });
188
+ if (mapping.grain !== "transaction" && mapping.fields.evaluationDate === undefined)
189
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["fields", "evaluationDate"], message: "Snapshot grain requires evaluationDate mapping" });
190
+ if (mapping.grain === "transaction" && mapping.completeness !== "change-only")
191
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["completeness"], message: "Transaction grain requires change-only completeness" });
192
+ if (Object.keys(mapping.measures).length === 0)
193
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["measures"], message: "At least one measure mapping is required" });
194
+ });
195
+
196
+ function mappingError(error: z.ZodError): DiagnosticValidationError {
197
+ return new DiagnosticValidationError(
198
+ error.issues.map((issue) => ({
199
+ domain: "configuration" as const,
200
+ code: issue.code === z.ZodIssueCode.unrecognized_keys ? ("unknown-key" as const) : ("invalid-configuration" as const),
201
+ path: `$${issue.path.map((part) => (typeof part === "number" ? `[${part}]` : `.${part}`)).join("")}`,
202
+ message: issue.message,
203
+ })),
204
+ );
205
+ }
206
+
207
+ export function parseHistoricalSourceMapping(value: unknown): HistoricalSourceMapping {
208
+ const parsed = historicalSourceMappingSchema.safeParse(value);
209
+ if (!parsed.success) throw mappingError(parsed.error);
210
+ return snapshotDiagnosticJson(parsed.data) as HistoricalSourceMapping;
211
+ }
212
+
213
+ function scalarText(value: HistoricalScalar): string {
214
+ return value === null ? "" : typeof value === "string" ? value.trim() : String(value);
215
+ }
216
+
217
+ function selectedValue(
218
+ row: HistoricalSourceRow,
219
+ selector: HistoricalColumnSelector,
220
+ path: string,
221
+ issues: { path: string; message: string }[],
222
+ required = true,
223
+ ): HistoricalScalar | undefined {
224
+ const selected = selector.columns
225
+ .filter((column) => Object.hasOwn(row.values, column))
226
+ .map((column) => ({ column, value: row.values[column]! }))
227
+ .filter(({ value }) => scalarText(value) !== "");
228
+ if (selected.length === 0) {
229
+ if (required) issues.push({ path, message: `No value found in columns ${selector.columns.join(", ")}` });
230
+ return undefined;
231
+ }
232
+ if (new Set(selected.map(({ value }) => scalarText(value))).size > 1) {
233
+ issues.push({ path, message: `Alias columns disagree: ${selected.map(({ column }) => column).join(", ")}` });
234
+ return undefined;
235
+ }
236
+ return selected[0]!.value;
237
+ }
238
+
239
+ function parseDateValue(
240
+ value: HistoricalScalar | undefined,
241
+ format: HistoricalDateSelector["format"],
242
+ path: string,
243
+ issues: { path: string; message: string }[],
244
+ ): string | undefined {
245
+ if (value === undefined) return undefined;
246
+ const text = scalarText(value);
247
+ let result = text;
248
+ const match = /^(\d{1,2})\/(\d{1,2})\/(\d{4})$/.exec(text);
249
+ if (format !== "iso-yyyy-mm-dd") {
250
+ if (!match) {
251
+ issues.push({ path, message: `Expected ${format} date` });
252
+ return undefined;
253
+ }
254
+ const month = format === "mdy-slash" ? match[1]! : match[2]!;
255
+ const day = format === "mdy-slash" ? match[2]! : match[1]!;
256
+ result = `${match[3]}-${month.padStart(2, "0")}-${day.padStart(2, "0")}`;
257
+ }
258
+ if (!isRealIsoDate(result)) {
259
+ issues.push({ path, message: `Expected a real date; got ${text}` });
260
+ return undefined;
261
+ }
262
+ return result;
263
+ }
264
+
265
+ function parseNumberValue(
266
+ value: HistoricalScalar | undefined,
267
+ selector: Omit<HistoricalNumberSelector, "columns">,
268
+ path: string,
269
+ issues: { path: string; message: string }[],
270
+ ): number | null | undefined {
271
+ if (value === undefined || scalarText(value) === "") {
272
+ if (selector.blank === "error") issues.push({ path, message: "Blank numeric value is not allowed" });
273
+ return selector.blank === "null" ? null : undefined;
274
+ }
275
+ if (typeof value === "number") {
276
+ if (!Number.isFinite(value)) {
277
+ issues.push({ path, message: "Numeric value is not finite" });
278
+ return undefined;
279
+ }
280
+ const result = value * selector.scale;
281
+ if (!Number.isFinite(result)) {
282
+ issues.push({ path, message: "Scaled numeric value is not finite" });
283
+ return undefined;
284
+ }
285
+ return Object.is(result, -0) ? 0 : result;
286
+ }
287
+ let text = scalarText(value);
288
+ let negative = false;
289
+ if (/^\(.*\)$/.test(text)) {
290
+ if (selector.negative !== "parentheses-or-leading-minus") {
291
+ issues.push({ path, message: "Parenthesized negative is not allowed" });
292
+ return undefined;
293
+ }
294
+ negative = true;
295
+ text = text.slice(1, -1);
296
+ }
297
+ if (selector.thousandsSeparator !== null)
298
+ text = text.split(selector.thousandsSeparator).join("");
299
+ if (selector.decimalSeparator === ",") text = text.replace(",", ".");
300
+ if (!/^-?\d+(?:\.\d+)?$/.test(text)) {
301
+ issues.push({ path, message: `Invalid number ${scalarText(value)} for declared format` });
302
+ return undefined;
303
+ }
304
+ const result = Number(text) * selector.scale * (negative ? -1 : 1);
305
+ if (!Number.isFinite(result)) {
306
+ issues.push({ path, message: "Numeric value is not finite" });
307
+ return undefined;
308
+ }
309
+ return Object.is(result, -0) ? 0 : result;
310
+ }
311
+
312
+ function splitIds(value: HistoricalScalar | undefined, delimiter: string): string[] | undefined {
313
+ if (value === undefined) return undefined;
314
+ const values = scalarText(value).split(delimiter).map((item) => item.trim()).filter(Boolean);
315
+ return values.length === 0 ? undefined : [...new Set(values)];
316
+ }
317
+
318
+ function selectorColumns(mapping: HistoricalSourceMapping): Set<string> {
319
+ const result = new Set<string>();
320
+ const add = (selector: HistoricalColumnSelector | undefined) => selector?.columns.forEach((column) => result.add(column));
321
+ Object.values(mapping.fields).forEach(add);
322
+ Object.values(mapping.measures).forEach(add);
323
+ Object.values(mapping.dimensions ?? {}).forEach(add);
324
+ Object.values(mapping.relationships ?? {}).forEach(add);
325
+ Object.values(mapping.revision).forEach(add);
326
+ return result;
327
+ }
328
+
329
+ export function mapHistoricalSourceRows(
330
+ mappingInput: HistoricalSourceMapping | unknown,
331
+ rowsInput: readonly HistoricalSourceRow[],
332
+ ): HistoricalMappingResult {
333
+ const mapping = parseHistoricalSourceMapping(mappingInput);
334
+ const records: HistoricalObservationRecord[] = [];
335
+ const rejected: HistoricalMappedRowRejection[] = [];
336
+ const usedColumns = selectorColumns(mapping);
337
+ const attributeTypes = new Map<string, Set<"string" | "number" | "boolean" | "null">>();
338
+ for (let rowIndex = 0; rowIndex < rowsInput.length; rowIndex += 1) {
339
+ const raw = rowsInput[rowIndex]!;
340
+ const preflight = diagnosticJsonPreflight(raw, "input");
341
+ if (preflight.length > 0) {
342
+ const candidate = (raw as { readonly rowNumber?: unknown }).rowNumber;
343
+ const rowNumber = typeof candidate === "number" && Number.isSafeInteger(candidate) && candidate > 0 ? candidate : rowIndex + 1;
344
+ rejected.push({
345
+ rowNumber,
346
+ source: { artifactId: mapping.artifactId, sourceRow: rowNumber },
347
+ issues: preflight.map((issue) => ({ path: issue.path, message: issue.message })),
348
+ });
349
+ continue;
350
+ }
351
+ const row = snapshotDiagnosticJson(raw);
352
+ const issues: { path: string; message: string }[] = [];
353
+ const pendingAttributeTypes = new Map<string, Set<"string" | "number" | "boolean" | "null">>();
354
+ if (!Number.isSafeInteger(row.rowNumber) || row.rowNumber < 1)
355
+ issues.push({ path: "$.rowNumber", message: "rowNumber must be a positive safe integer" });
356
+ const field = (selector: HistoricalColumnSelector | undefined, name: string, required = false) =>
357
+ selector === undefined ? undefined : selectedValue(row, selector, `$.${name}`, issues, required);
358
+ const recordId = scalarText(field(mapping.fields.recordId, "recordId", true) ?? "");
359
+ const claimId = scalarText(field(mapping.fields.claimId, "claimId", true) ?? "");
360
+ const componentIdValue = field(mapping.fields.componentId, "componentId", mapping.grain === "claim-component-snapshot");
361
+ const statusValue = field(mapping.fields.status, "status");
362
+ const mappedDate = (selector: HistoricalDateSelector | undefined, name: string, required = false) => {
363
+ if (selector === undefined) return undefined;
364
+ return parseDateValue(field(selector, name, required), selector.format, `$.${name}`, issues);
365
+ };
366
+ const accidentDate = mappedDate(mapping.fields.accidentDate, "accidentDate");
367
+ const reportDate = mappedDate(mapping.fields.reportDate, "reportDate");
368
+ const evaluationDate = mappedDate(
369
+ mapping.fields.evaluationDate,
370
+ "evaluationDate",
371
+ mapping.grain !== "transaction",
372
+ );
373
+ const effectiveDate = mappedDate(
374
+ mapping.fields.effectiveDate,
375
+ "effectiveDate",
376
+ mapping.grain === "transaction",
377
+ );
378
+ const measures: Record<string, number | null> = Object.create(null);
379
+ for (const [id, selector] of Object.entries(mapping.measures)) {
380
+ const value = selectedValue(row, selector, `$.measures.${id}`, issues, selector.blank === "error");
381
+ const amount = parseNumberValue(value, selector, `$.measures.${id}`, issues);
382
+ if (amount !== undefined) measures[id] = amount;
383
+ }
384
+ const dimensions: Record<string, HistoricalScalar | readonly HistoricalScalar[]> = Object.create(null);
385
+ for (const [id, selector] of Object.entries(mapping.dimensions ?? {})) {
386
+ const value = selectedValue(row, selector, `$.dimensions.${id}`, issues, false);
387
+ if (value === undefined) continue;
388
+ if (selector.multipleDelimiter !== undefined) {
389
+ dimensions[id] = splitIds(value, selector.multipleDelimiter) ?? [];
390
+ } else if (selector.type === "date") {
391
+ const parsed = parseDateValue(value, selector.dateFormat!, `$.dimensions.${id}`, issues);
392
+ if (parsed !== undefined) dimensions[id] = parsed;
393
+ } else if (selector.type === "number") {
394
+ const parsed = parseNumberValue(value, selector.numberFormat!, `$.dimensions.${id}`, issues);
395
+ if (parsed !== undefined) dimensions[id] = parsed;
396
+ } else if (selector.type === "boolean") {
397
+ const text = scalarText(value).toLowerCase();
398
+ if (text === "true" || text === "1") dimensions[id] = true;
399
+ else if (text === "false" || text === "0") dimensions[id] = false;
400
+ else issues.push({ path: `$.dimensions.${id}`, message: "Expected boolean true/false/1/0" });
401
+ } else dimensions[id] = scalarText(value);
402
+ }
403
+ if (mapping.preserveUnmappedAttributes)
404
+ for (const [column, value] of Object.entries(row.values)) {
405
+ if (usedColumns.has(column)) continue;
406
+ const id = `source:${mapping.sourceNamespace}:${column}`;
407
+ dimensions[id] = value;
408
+ const type = value === null ? "null" : typeof value;
409
+ if (type === "string" || type === "number" || type === "boolean" || type === "null") {
410
+ const current = pendingAttributeTypes.get(id) ?? new Set();
411
+ current.add(type);
412
+ pendingAttributeTypes.set(id, current);
413
+ }
414
+ }
415
+ const relationship = mapping.relationships;
416
+ const occurrence = scalarText(field(relationship?.occurrenceId, "relationships.occurrenceId") ?? "");
417
+ const claimantIds = relationship?.claimantIds
418
+ ? splitIds(field(relationship.claimantIds, "relationships.claimantIds"), relationship.claimantIds.delimiter)
419
+ : undefined;
420
+ const policyIds = relationship?.policyIds
421
+ ? splitIds(field(relationship.policyIds, "relationships.policyIds"), relationship.policyIds.delimiter)
422
+ : undefined;
423
+ const coverageIds = relationship?.coverageIds
424
+ ? splitIds(field(relationship.coverageIds, "relationships.coverageIds"), relationship.coverageIds.delimiter)
425
+ : undefined;
426
+ const revisionId = scalarText(field(mapping.revision.id, "revision.id", true) ?? "");
427
+ const actionText = scalarText(field(mapping.revision.action, "revision.action") ?? "upsert").toLowerCase();
428
+ if (actionText !== "upsert" && actionText !== "delete")
429
+ issues.push({ path: "$.revision.action", message: "Revision action must be upsert or delete" });
430
+ const sequenceValue = mapping.revision.sequence
431
+ ? parseNumberValue(
432
+ field(mapping.revision.sequence, "revision.sequence", true),
433
+ mapping.revision.sequence,
434
+ "$.revision.sequence",
435
+ issues,
436
+ )
437
+ : undefined;
438
+ if (typeof sequenceValue === "number" && !Number.isSafeInteger(sequenceValue))
439
+ issues.push({ path: "$.revision.sequence", message: "Revision sequence must be a safe integer" });
440
+ const correctedAtValue = field(mapping.revision.correctedAt, "revision.correctedAt");
441
+ const correctedAt = correctedAtValue === undefined ? undefined : scalarText(correctedAtValue);
442
+ if (correctedAt !== undefined && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(correctedAt))
443
+ issues.push({ path: "$.revision.correctedAt", message: "correctedAt must be an ISO timestamp with offset" });
444
+ if (!isDiagnosticToken(recordId)) issues.push({ path: "$.recordId", message: "recordId is invalid" });
445
+ if (!isDiagnosticToken(claimId)) issues.push({ path: "$.claimId", message: "claimId is invalid" });
446
+ if (!isDiagnosticToken(revisionId)) issues.push({ path: "$.revision.id", message: "revision id is invalid" });
447
+ if (issues.length > 0) {
448
+ rejected.push({
449
+ rowNumber: row.rowNumber,
450
+ source: { artifactId: mapping.artifactId, sourceRow: row.rowNumber },
451
+ issues,
452
+ });
453
+ continue;
454
+ }
455
+ for (const [id, types] of pendingAttributeTypes) {
456
+ const current = attributeTypes.get(id) ?? new Set();
457
+ types.forEach((type) => current.add(type));
458
+ attributeTypes.set(id, current);
459
+ }
460
+ const relationships = {
461
+ ...(claimantIds === undefined ? {} : { claimantIds }),
462
+ ...(occurrence === "" ? {} : { occurrenceId: occurrence }),
463
+ ...(policyIds === undefined ? {} : { policyIds }),
464
+ ...(coverageIds === undefined ? {} : { coverageIds }),
465
+ };
466
+ records.push({
467
+ recordId,
468
+ sourceNamespace: mapping.sourceNamespace,
469
+ claimId,
470
+ grain: mapping.grain,
471
+ ...(componentIdValue === undefined ? {} : { componentId: scalarText(componentIdValue) }),
472
+ ...(accidentDate === undefined ? {} : { accidentDate }),
473
+ ...(reportDate === undefined ? {} : { reportDate }),
474
+ ...(evaluationDate === undefined ? {} : { evaluationDate }),
475
+ ...(effectiveDate === undefined ? {} : { effectiveDate }),
476
+ measures: actionText === "delete" ? {} : measures,
477
+ ...(statusValue === undefined ? {} : { status: scalarText(statusValue) }),
478
+ ...(Object.keys(dimensions).length === 0 ? {} : { dimensions }),
479
+ ...(Object.keys(relationships).length === 0 ? {} : { relationships }),
480
+ completeness: mapping.completeness,
481
+ revision: {
482
+ id: revisionId,
483
+ action: actionText as "upsert" | "delete",
484
+ ...(typeof sequenceValue !== "number" ? {} : { sequence: sequenceValue }),
485
+ ...(correctedAt === undefined ? {} : { correctedAt }),
486
+ },
487
+ source: { artifactId: mapping.artifactId, sourceRow: row.rowNumber },
488
+ });
489
+ }
490
+ records.sort((left, right) =>
491
+ left.recordId === right.recordId ? 0 : left.recordId < right.recordId ? -1 : 1,
492
+ );
493
+ rejected.sort((left, right) => left.rowNumber - right.rowNumber);
494
+ return snapshotDiagnosticJson({
495
+ mappingId: mapping.id,
496
+ mappingVersion: mapping.version,
497
+ sourceNamespace: mapping.sourceNamespace,
498
+ artifactId: mapping.artifactId,
499
+ records,
500
+ rejected,
501
+ attributeCatalog: [...attributeTypes.entries()]
502
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
503
+ .map(([id, types]) => ({
504
+ id,
505
+ sourceColumn: id.slice(`source:${mapping.sourceNamespace}:`.length),
506
+ observedTypes: [...types].sort(),
507
+ })),
508
+ reconciliation: {
509
+ inputRows: rowsInput.length,
510
+ acceptedRows: records.length,
511
+ rejectedRows: rejected.length,
512
+ },
513
+ });
514
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  export * from "./csv.js";
2
+ export * from "./customizationContracts.js";
3
+ export * from "./customizationExternalState.js";
4
+ export * from "./customizationHistoryStream.js";
5
+ export * from "./historyMapping.js";
2
6
  export * from "./annualDevelopment.js";
3
7
  export * from "./exposure.js";
4
8
  export * from "./diagnosticInput.js";
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const DATA_PACKAGE_VERSION = "0.7.1";
1
+ export const DATA_PACKAGE_VERSION = "0.8.1";