@actuarial-ts/data 0.9.0 → 0.11.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.
@@ -250,7 +250,10 @@ export const historicalDatasetInputSchema = z
250
250
  sources: z
251
251
  .array(
252
252
  z
253
- .object({ namespace: token, artifactId: token, mappingVersion: token })
253
+ .object({
254
+ namespace: token, artifactId: token, mappingVersion: token,
255
+ additionalArtifacts: z.array(z.object({ artifactId: token, mappingVersion: token }).strict()).min(1).optional(),
256
+ })
254
257
  .strict(),
255
258
  )
256
259
  .min(1),
@@ -273,7 +276,7 @@ export const historicalDatasetInputSchema = z
273
276
  .strict()
274
277
  .superRefine((input, context) => {
275
278
  const namespaces = new Set<string>();
276
- const artifactByNamespace = new Map<string, string>();
279
+ const artifactByNamespace = new Map<string, Set<string>>();
277
280
  input.sources.forEach((source, index) => {
278
281
  if (namespaces.has(source.namespace))
279
282
  context.addIssue({
@@ -282,7 +285,13 @@ export const historicalDatasetInputSchema = z
282
285
  message: "Source namespace is duplicated",
283
286
  });
284
287
  namespaces.add(source.namespace);
285
- artifactByNamespace.set(source.namespace, source.artifactId);
288
+ const artifacts = new Set([source.artifactId]);
289
+ source.additionalArtifacts?.forEach((artifact, position) => {
290
+ if (artifacts.has(artifact.artifactId))
291
+ context.addIssue({ code: z.ZodIssueCode.custom, path: ["sources", index, "additionalArtifacts", position, "artifactId"], message: "Artifact ID is duplicated within the source namespace" });
292
+ artifacts.add(artifact.artifactId);
293
+ });
294
+ artifactByNamespace.set(source.namespace, artifacts);
286
295
  });
287
296
  input.records.forEach((record, index) => {
288
297
  if (!namespaces.has(record.sourceNamespace))
@@ -293,7 +302,7 @@ export const historicalDatasetInputSchema = z
293
302
  });
294
303
  if (
295
304
  artifactByNamespace.has(record.sourceNamespace) &&
296
- artifactByNamespace.get(record.sourceNamespace) !== record.source.artifactId
305
+ !artifactByNamespace.get(record.sourceNamespace)!.has(record.source.artifactId)
297
306
  )
298
307
  context.addIssue({
299
308
  code: z.ZodIssueCode.custom,
@@ -391,7 +400,7 @@ export const historicalDatasetInputSchema = z
391
400
  });
392
401
  if (
393
402
  artifactByNamespace.has(balance.sourceNamespace) &&
394
- artifactByNamespace.get(balance.sourceNamespace) !== balance.source.artifactId
403
+ !artifactByNamespace.get(balance.sourceNamespace)!.has(balance.source.artifactId)
395
404
  )
396
405
  context.addIssue({
397
406
  code: z.ZodIssueCode.custom,
@@ -422,7 +431,7 @@ const statistic = z.discriminatedUnion("kind", [
422
431
  numeratorMeasureId: token,
423
432
  denominatorMeasureId: token,
424
433
  scale: finite,
425
- denominatorRule: z.enum(["positive", "nonzero"]),
434
+ denominatorRule: z.literal("positive"),
426
435
  })
427
436
  .strict(),
428
437
  z
@@ -350,12 +350,33 @@ const compactInputByCompletedRun = new WeakMap<
350
350
  CompletedCompactMetricDiagnosticsRun,
351
351
  CompactValidatedDiagnosticRunInput
352
352
  >();
353
- function freeze<T>(value: T, seen = new WeakSet<object>()): DiagnosticDeepReadonly<T> {
354
- if (value === null || typeof value !== "object" || seen.has(value))
355
- return value as DiagnosticDeepReadonly<T>;
356
- seen.add(value);
357
- for (const child of Object.values(value as Record<string, unknown>)) freeze(child, seen);
358
- return Object.freeze(value) as DiagnosticDeepReadonly<T>;
353
+ // Validated inputs are JSON-shaped trees (the schema copies every row), so a
354
+ // shared node is a small leaf visited once per reference rather than tracked in
355
+ // a table of every node. Ancestors are recorded only past this depth, which a
356
+ // cycle in a caller-supplied graph must exceed before it is cut.
357
+ const UNGUARDED_FREEZE_DEPTH = 64;
358
+ function freeze<T>(value: T, skip?: WeakSet<object>): DiagnosticDeepReadonly<T> {
359
+ let ancestors: Set<object> | undefined;
360
+ const visit = (node: unknown, depth: number): void => {
361
+ if (node === null || typeof node !== "object" || skip?.has(node)) return;
362
+ const guarded = depth > UNGUARDED_FREEZE_DEPTH;
363
+ if (guarded) {
364
+ ancestors ??= new Set();
365
+ if (ancestors.has(node)) return;
366
+ ancestors.add(node);
367
+ }
368
+ if (Array.isArray(node)) {
369
+ for (let index = 0; index < node.length; index++) visit(node[index], depth + 1);
370
+ } else {
371
+ const record = node as Record<string, unknown>;
372
+ const keys = Object.keys(record);
373
+ for (let index = 0; index < keys.length; index++) visit(record[keys[index]!], depth + 1);
374
+ }
375
+ if (guarded) ancestors!.delete(node);
376
+ Object.freeze(node);
377
+ };
378
+ visit(value, 0);
379
+ return value as DiagnosticDeepReadonly<T>;
359
380
  }
360
381
  function issues(error: z.ZodError): DiagnosticValidationError {
361
382
  return new DiagnosticValidationError(
@@ -382,22 +403,39 @@ function sortedRecord<T>(value: Readonly<Record<string, T>>): Readonly<Record<st
382
403
  return result;
383
404
  }
384
405
 
406
+ interface UndefinedScanFrame {
407
+ readonly value: unknown;
408
+ readonly parent: UndefinedScanFrame | null;
409
+ readonly key: string | null;
410
+ }
411
+ /** Renders the JSON path of a frame only when an issue must cite it. */
412
+ function scanFramePath(frame: UndefinedScanFrame): string {
413
+ if (frame.parent === null || frame.key === null) return "$";
414
+ const parentPath = scanFramePath(frame.parent);
415
+ return Array.isArray(frame.parent.value)
416
+ ? `${parentPath}[${frame.key}]`
417
+ : /^[A-Za-z_$][\w$]*$/.test(frame.key)
418
+ ? `${parentPath}.${frame.key}`
419
+ : `${parentPath}[${JSON.stringify(frame.key)}]`;
420
+ }
385
421
  function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
386
422
  const found: DiagnosticValidationIssue[] = [];
387
- const stack: { readonly value: unknown; readonly path: string }[] = [{ value, path: "$" }];
423
+ const stack: UndefinedScanFrame[] = [{ value, parent: null, key: null }];
388
424
  const seen = new WeakSet<object>();
389
425
  while (stack.length > 0) {
390
426
  const current = stack.pop()!;
391
427
  if (current.value === null || typeof current.value !== "object" || seen.has(current.value))
392
428
  continue;
393
429
  seen.add(current.value);
394
- for (const [key, child] of Object.entries(current.value)) {
395
- const path = Array.isArray(current.value)
396
- ? `${current.path}[${key}]`
397
- : /^[A-Za-z_$][\w$]*$/.test(key)
398
- ? `${current.path}.${key}`
399
- : `${current.path}[${JSON.stringify(key)}]`;
400
- if (child === undefined)
430
+ const record = current.value as Record<string, unknown>;
431
+ // Large inputs are ordinary: build no path text or entry pairs on the
432
+ // common path, only the frame links needed to render a path on demand.
433
+ const keys = Object.keys(record);
434
+ for (let index = 0; index < keys.length; index++) {
435
+ const key = keys[index]!;
436
+ const child = record[key];
437
+ if (child === undefined) {
438
+ const path = scanFramePath({ value: child, parent: current, key });
401
439
  found.push({
402
440
  domain: path.startsWith("$.definition")
403
441
  ? "definition"
@@ -410,21 +448,188 @@ function explicitUndefinedIssues(value: unknown): DiagnosticValidationIssue[] {
410
448
  path,
411
449
  message: "Explicit undefined is not allowed",
412
450
  });
413
- else stack.push({ value: child, path });
451
+ } else if (child !== null && typeof child === "object")
452
+ stack.push({ value: child, parent: current, key });
414
453
  }
415
454
  }
416
455
  return found;
417
456
  }
418
457
 
458
+ // ---------------------------------------------------------------------------
459
+ // Large-input fast path for the two row arrays. Zod validates and copies every
460
+ // row through generic machinery, which on 100,000-row inputs allocates more
461
+ // than the preparation itself. Rows are accepted here only under exactly the
462
+ // schema's rules (plain records, strict keys, token strings, boolean flags,
463
+ // safe non-negative integer source rows, raw number-or-null measures) and are
464
+ // copied in the schema's key order, so the owned rows are indistinguishable
465
+ // from the schema's output. Anything the check does not accept, including any
466
+ // non-plain prototype, is handed to the schema unchanged, so every rejection
467
+ // and every error message still comes from zod.
468
+ type FastLossRow = z.output<typeof lossSchema>;
469
+ type FastExposureRow = z.output<typeof exposureSchema>;
470
+ const isPlainRecord = (value: unknown): value is Record<string, unknown> => {
471
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
472
+ const prototype: unknown = Object.getPrototypeOf(value);
473
+ return prototype === Object.prototype || prototype === null;
474
+ };
475
+ const hasOwn = (value: object, key: string): boolean => Object.prototype.hasOwnProperty.call(value, key);
476
+ function fastSource(value: unknown): z.output<typeof sourceSchema> | undefined {
477
+ if (!isPlainRecord(value)) return undefined;
478
+ const artifactId = value.artifactId;
479
+ if (!isDiagnosticToken(artifactId)) return undefined;
480
+ const out: Record<string, unknown> = { artifactId };
481
+ let matched = 1;
482
+ for (const field of ["sourceFile", "sourceSheet"] as const)
483
+ if (hasOwn(value, field)) {
484
+ const item = value[field];
485
+ if (!isDiagnosticToken(item)) return undefined;
486
+ out[field] = item;
487
+ matched++;
488
+ }
489
+ if (hasOwn(value, "sourceRow")) {
490
+ const item = value.sourceRow;
491
+ if (typeof item !== "number" || !Number.isSafeInteger(item) || item < 0) return undefined;
492
+ out.sourceRow = item;
493
+ matched++;
494
+ }
495
+ if (hasOwn(value, "sourceCell")) {
496
+ const item = value.sourceCell;
497
+ if (!isDiagnosticToken(item)) return undefined;
498
+ out.sourceCell = item;
499
+ matched++;
500
+ }
501
+ if (Object.keys(value).length !== matched) return undefined;
502
+ return out as z.output<typeof sourceSchema>;
503
+ }
504
+ function fastMeasures(value: unknown): Record<string, number | null> | undefined {
505
+ if (!isPlainRecord(value)) return undefined;
506
+ const out: Record<string, number | null> = {};
507
+ const keys = Object.keys(value);
508
+ for (let index = 0; index < keys.length; index++) {
509
+ const key = keys[index]!;
510
+ const item = value[key];
511
+ if (item !== null && typeof item !== "number") return undefined;
512
+ // The schema's key round trip keeps a literal "__proto__" as own data.
513
+ if (key === "__proto__")
514
+ Object.defineProperty(out, key, { value: item, enumerable: true, writable: true, configurable: true });
515
+ else out[key] = item;
516
+ }
517
+ return out;
518
+ }
519
+ function fastLossRow(value: unknown): FastLossRow | undefined {
520
+ if (!isPlainRecord(value)) return undefined;
521
+ const { recordId, sourceGroup, origin, valuation, complete, rowType } = value;
522
+ if (
523
+ !isDiagnosticToken(recordId) ||
524
+ !isDiagnosticToken(sourceGroup) ||
525
+ !isDiagnosticToken(origin) ||
526
+ !isDiagnosticToken(valuation) ||
527
+ typeof complete !== "boolean"
528
+ )
529
+ return undefined;
530
+ const out: Record<string, unknown> = { recordId, sourceGroup, origin, valuation, complete };
531
+ let expected = 7;
532
+ if (hasOwn(value, "source")) {
533
+ const source = fastSource(value.source);
534
+ if (source === undefined) return undefined;
535
+ out.source = source;
536
+ expected++;
537
+ }
538
+ const measures = fastMeasures(value.measures);
539
+ if (measures === undefined) return undefined;
540
+ out.measures = measures;
541
+ if (rowType === "claim") {
542
+ const claimId = value.claimId;
543
+ if (!hasOwn(value, "claimId") || !isDiagnosticToken(claimId)) return undefined;
544
+ out.rowType = "claim";
545
+ out.claimId = claimId;
546
+ expected++;
547
+ } else if (rowType === "aggregate") out.rowType = "aggregate";
548
+ else return undefined;
549
+ if (Object.keys(value).length !== expected) return undefined;
550
+ return out as FastLossRow;
551
+ }
552
+ function fastExposureRow(value: unknown): FastExposureRow | undefined {
553
+ if (!isPlainRecord(value)) return undefined;
554
+ const { key, sourceGroup, origin, measureId, complete } = value;
555
+ const measure = value.value;
556
+ if (
557
+ !isDiagnosticToken(key) ||
558
+ !isDiagnosticToken(sourceGroup) ||
559
+ !isDiagnosticToken(origin) ||
560
+ !isDiagnosticToken(measureId) ||
561
+ (measure !== null && typeof measure !== "number") ||
562
+ typeof complete !== "boolean"
563
+ )
564
+ return undefined;
565
+ const out: Record<string, unknown> = { key, sourceGroup, origin };
566
+ let expected = 6;
567
+ if (hasOwn(value, "valuation")) {
568
+ const valuation = value.valuation;
569
+ if (!isDiagnosticToken(valuation)) return undefined;
570
+ out.valuation = valuation;
571
+ expected++;
572
+ }
573
+ out.measureId = measureId;
574
+ out.value = measure;
575
+ out.complete = complete;
576
+ if (hasOwn(value, "source")) {
577
+ const source = fastSource(value.source);
578
+ if (source === undefined) return undefined;
579
+ out.source = source;
580
+ expected++;
581
+ }
582
+ if (Object.keys(value).length !== expected) return undefined;
583
+ return out as FastExposureRow;
584
+ }
585
+ /**
586
+ * Validates and copies the row arrays directly when every row is schema-shaped.
587
+ * Returns the owned rows plus a shell of the input with empty row arrays for
588
+ * the schema to validate everything else; undefined defers the whole input.
589
+ */
590
+ function fastSourceRows(
591
+ value: unknown,
592
+ ): { shell: Record<string, unknown>; losses: FastLossRow[]; exposures: FastExposureRow[] | undefined } | undefined {
593
+ if (!isPlainRecord(value) || !Array.isArray(value.losses)) return undefined;
594
+ const losses: FastLossRow[] = [];
595
+ for (let index = 0; index < value.losses.length; index++) {
596
+ const row = fastLossRow(value.losses[index]);
597
+ if (row === undefined) return undefined;
598
+ losses.push(row);
599
+ }
600
+ let exposures: FastExposureRow[] | undefined;
601
+ if (hasOwn(value, "exposures")) {
602
+ if (!Array.isArray(value.exposures)) return undefined;
603
+ exposures = [];
604
+ for (let index = 0; index < value.exposures.length; index++) {
605
+ const row = fastExposureRow(value.exposures[index]);
606
+ if (row === undefined) return undefined;
607
+ exposures.push(row);
608
+ }
609
+ }
610
+ return {
611
+ shell: { ...value, losses: [], ...(exposures === undefined ? {} : { exposures: [] }) },
612
+ losses,
613
+ exposures,
614
+ };
615
+ }
616
+
419
617
  // Both public gateways share the same full validation/ownership boundary.
420
618
  // Selecting compact storage never invokes the eager preparation first.
421
619
  function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
422
- const undefinedIssues = explicitUndefinedIssues(value);
620
+ // A row the fast check accepts holds no explicit undefined anywhere: every
621
+ // field it reads must be a token, boolean, or number-or-null, and any other
622
+ // value (including undefined) makes it decline the whole input. So when it
623
+ // accepts, scanning its row-free shell reports exactly the same issues.
624
+ const fast = fastSourceRows(value);
625
+ const undefinedIssues = explicitUndefinedIssues(fast === undefined ? value : fast.shell);
423
626
  if (undefinedIssues.length > 0) throw new DiagnosticValidationError(undefinedIssues);
424
- const parsed = runSchema.safeParse(value);
627
+ const parsed = runSchema.safeParse(fast === undefined ? value : fast.shell);
425
628
  if (!parsed.success) throw issues(parsed.error);
629
+ const losses = fast === undefined ? parsed.data.losses : fast.losses;
630
+ const exposures = (fast === undefined ? parsed.data.exposures : fast.exposures) ?? [];
426
631
  const definition = compileDiagnosticDefinition(parsed.data.definition as DiagnosticDefinition);
427
- const relationIssues: DiagnosticValidationIssue[] = parsed.data.losses.flatMap((row, index) =>
632
+ const relationIssues: DiagnosticValidationIssue[] = losses.flatMap((row, index) =>
428
633
  row.rowType === definition.definition.lossRowGrain
429
634
  ? []
430
635
  : [
@@ -436,7 +641,7 @@ function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
436
641
  },
437
642
  ],
438
643
  );
439
- for (const [index, row] of (parsed.data.exposures ?? []).entries()) {
644
+ for (const [index, row] of exposures.entries()) {
440
645
  const measure = definition.definition.measures.find((item) => item.id === row.measureId);
441
646
  if (measure?.exposureTiming === "valuation-specific" && row.valuation === undefined)
442
647
  relationIssues.push({
@@ -485,8 +690,8 @@ function validateRunInputContent(value: unknown): DiagnosticRunInputContent {
485
690
  ]);
486
691
  const result = freeze({
487
692
  definition,
488
- losses: parsed.data.losses,
489
- exposures: parsed.data.exposures ?? [],
693
+ losses,
694
+ exposures,
490
695
  filter: parsed.data.filter ?? null,
491
696
  completePeriodCutoffs: parsed.data.completePeriodCutoffs ?? [],
492
697
  expectedCells: parsed.data.expectedCells ?? null,
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  DiagnosticValidationError,
3
+ canonicalJson,
3
4
  diagnosticJsonPreflight,
4
5
  isDiagnosticToken,
5
6
  isRealIsoDate,
@@ -13,6 +14,12 @@ export interface HistoricalColumnSelector {
13
14
  readonly columns: readonly string[];
14
15
  }
15
16
 
17
+ /** Identity parts are required, ordered strings; each part may declare column aliases. */
18
+ export type HistoricalIdentitySelector = HistoricalColumnSelector | {
19
+ readonly kind: "composite";
20
+ readonly parts: readonly (HistoricalColumnSelector & { readonly format?: HistoricalDateSelector["format"] })[];
21
+ };
22
+
16
23
  export interface HistoricalDateSelector extends HistoricalColumnSelector {
17
24
  readonly format: "iso-yyyy-mm-dd" | "mdy-slash" | "dmy-slash";
18
25
  }
@@ -40,8 +47,8 @@ export interface HistoricalSourceMapping {
40
47
  readonly grain: HistoricalObservationRecord["grain"];
41
48
  readonly completeness: HistoricalObservationRecord["completeness"];
42
49
  readonly fields: {
43
- readonly recordId: HistoricalColumnSelector;
44
- readonly claimId: HistoricalColumnSelector;
50
+ readonly recordId: HistoricalIdentitySelector;
51
+ readonly claimId: HistoricalIdentitySelector;
45
52
  readonly componentId?: HistoricalColumnSelector;
46
53
  readonly accidentDate?: HistoricalDateSelector;
47
54
  readonly reportDate?: HistoricalDateSelector;
@@ -58,7 +65,7 @@ export interface HistoricalSourceMapping {
58
65
  readonly coverageIds?: HistoricalColumnSelector & { readonly delimiter: string };
59
66
  };
60
67
  readonly revision: {
61
- readonly id: HistoricalColumnSelector;
68
+ readonly id: HistoricalIdentitySelector;
62
69
  readonly action?: HistoricalColumnSelector;
63
70
  readonly sequence?: HistoricalNumberSelector;
64
71
  readonly correctedAt?: HistoricalColumnSelector;
@@ -98,6 +105,15 @@ export interface HistoricalMappingResult {
98
105
 
99
106
  const token = z.string().refine(isDiagnosticToken, "Expected a valid nonempty token");
100
107
  const columnSelector = z.object({ columns: z.array(token).min(1) }).strict();
108
+ const identitySelector = z.union([
109
+ columnSelector,
110
+ z.object({
111
+ kind: z.literal("composite"),
112
+ parts: z.array(columnSelector.extend({
113
+ format: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]).optional(),
114
+ }).strict()).min(2).max(8),
115
+ }).strict(),
116
+ ]);
101
117
  const dateSelector = columnSelector.extend({
102
118
  format: z.enum(["iso-yyyy-mm-dd", "mdy-slash", "dmy-slash"]),
103
119
  }).strict();
@@ -148,8 +164,8 @@ export const historicalSourceMappingSchema = z
148
164
  completeness: z.enum(["complete-snapshot", "partial-snapshot", "change-only"]),
149
165
  fields: z
150
166
  .object({
151
- recordId: columnSelector,
152
- claimId: columnSelector,
167
+ recordId: identitySelector,
168
+ claimId: identitySelector,
153
169
  componentId: columnSelector.optional(),
154
170
  accidentDate: dateSelector.optional(),
155
171
  reportDate: dateSelector.optional(),
@@ -171,7 +187,7 @@ export const historicalSourceMappingSchema = z
171
187
  .optional(),
172
188
  revision: z
173
189
  .object({
174
- id: columnSelector,
190
+ id: identitySelector,
175
191
  action: columnSelector.optional(),
176
192
  sequence: numberSelector.optional(),
177
193
  correctedAt: columnSelector.optional(),
@@ -317,7 +333,11 @@ function splitIds(value: HistoricalScalar | undefined, delimiter: string): strin
317
333
 
318
334
  function selectorColumns(mapping: HistoricalSourceMapping): Set<string> {
319
335
  const result = new Set<string>();
320
- const add = (selector: HistoricalColumnSelector | undefined) => selector?.columns.forEach((column) => result.add(column));
336
+ const add = (selector: HistoricalIdentitySelector | undefined) => {
337
+ if (selector === undefined) return;
338
+ for (const part of "parts" in selector ? selector.parts : [selector])
339
+ part.columns.forEach((column) => result.add(column));
340
+ };
321
341
  Object.values(mapping.fields).forEach(add);
322
342
  Object.values(mapping.measures).forEach(add);
323
343
  Object.values(mapping.dimensions ?? {}).forEach(add);
@@ -355,8 +375,19 @@ export function mapHistoricalSourceRows(
355
375
  issues.push({ path: "$.rowNumber", message: "rowNumber must be a positive safe integer" });
356
376
  const field = (selector: HistoricalColumnSelector | undefined, name: string, required = false) =>
357
377
  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) ?? "");
378
+ const identity = (selector: HistoricalIdentitySelector, name: string) => {
379
+ if (!("parts" in selector)) return scalarText(field(selector, name, true) ?? "");
380
+ const parts = selector.parts.map((part, index) => {
381
+ const partName = `${name}.parts[${index}]`;
382
+ const value = field(part, partName, true);
383
+ return part.format === undefined
384
+ ? scalarText(value ?? "")
385
+ : parseDateValue(value, part.format, `$.${partName}`, issues) ?? "";
386
+ });
387
+ return `tuple/1:${canonicalJson(parts)}`;
388
+ };
389
+ const recordId = identity(mapping.fields.recordId, "recordId");
390
+ const claimId = identity(mapping.fields.claimId, "claimId");
360
391
  const componentIdValue = field(mapping.fields.componentId, "componentId", mapping.grain === "claim-component-snapshot");
361
392
  const statusValue = field(mapping.fields.status, "status");
362
393
  const mappedDate = (selector: HistoricalDateSelector | undefined, name: string, required = false) => {
@@ -423,7 +454,7 @@ export function mapHistoricalSourceRows(
423
454
  const coverageIds = relationship?.coverageIds
424
455
  ? splitIds(field(relationship.coverageIds, "relationships.coverageIds"), relationship.coverageIds.delimiter)
425
456
  : undefined;
426
- const revisionId = scalarText(field(mapping.revision.id, "revision.id", true) ?? "");
457
+ const revisionId = identity(mapping.revision.id, "revision.id");
427
458
  const actionText = scalarText(field(mapping.revision.action, "revision.action") ?? "upsert").toLowerCase();
428
459
  if (actionText !== "upsert" && actionText !== "delete")
429
460
  issues.push({ path: "$.revision.action", message: "Revision action must be upsert or delete" });
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const DATA_PACKAGE_VERSION = "0.9.0";
1
+ export const DATA_PACKAGE_VERSION = "0.11.0";