@actuarial-ts/interchange 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 +3 -3
- package/dist/convert/diagnosticDefinition.d.ts +6 -5
- package/dist/convert/diagnosticDefinition.d.ts.map +1 -1
- package/dist/convert/diagnosticDefinition.js +43 -8
- package/dist/convert/diagnosticDefinition.js.map +1 -1
- package/dist/envelope.d.ts +4 -4
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +10 -6
- package/dist/envelope.js.map +1 -1
- package/dist/json.d.ts +5 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +13 -0
- package/dist/json.js.map +1 -0
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +7 -2
- package/dist/parse.js.map +1 -1
- package/dist/schemas/crosscheck.d.ts +6 -6
- package/dist/schemas/diagnosticDefinition.d.ts +121 -49
- package/dist/schemas/diagnosticDefinition.d.ts.map +1 -1
- package/dist/schemas/diagnosticDefinition.js +6 -5
- package/dist/schemas/diagnosticDefinition.js.map +1 -1
- package/dist/schemas/record.d.ts +5 -0
- package/dist/schemas/record.d.ts.map +1 -0
- package/dist/schemas/record.js +9 -0
- package/dist/schemas/record.js.map +1 -0
- package/dist/schemas/result.d.ts +12 -12
- package/dist/schemas/selection.d.ts +6 -6
- package/dist/schemas/triangle.d.ts +89 -89
- package/dist/schemas/triangle.d.ts.map +1 -1
- package/dist/schemas/triangle.js +20 -5
- package/dist/schemas/triangle.js.map +1 -1
- package/package.json +3 -2
- package/src/convert/diagnosticDefinition.ts +89 -14
- package/src/envelope.ts +25 -8
- package/src/json.ts +17 -0
- package/src/parse.ts +7 -2
- package/src/schemas/diagnosticDefinition.ts +10 -9
- package/src/schemas/record.ts +16 -0
- package/src/schemas/triangle.ts +23 -5
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"triangle.js","sourceRoot":"","sources":["../../src/schemas/triangle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C;;;;;;;;;;;;GAYG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,MAAM;IACN,UAAU;IACV,aAAa;IACb,eAAe;IACf,WAAW;IACX,aAAa;IACb,oBAAoB;CACZ,CAAC;AAIX,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC;IACnC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,aAAa,EAAE,eAAe,CAAC,CAAC;IAC3C,CAAC,
|
|
1
|
+
{"version":3,"file":"triangle.js","sourceRoot":"","sources":["../../src/schemas/triangle.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuarial-ts/interchange",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Language-neutral actuarial-interchange documents for triangles, selections, results, studies, bundles, crosschecks, and portable diagnostic definitions across TypeScript, Python, and R.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
"test:watch": "vitest",
|
|
51
51
|
"typecheck": "tsc --noEmit",
|
|
52
52
|
"prepack": "tsc -p tsconfig.build.json",
|
|
53
|
+
"prepublishOnly": "node ../../tools/release/verify-release-attestation.mjs @actuarial-ts/interchange",
|
|
53
54
|
"emit-schema": "npm run build && node --experimental-strip-types scripts/emit-schema.ts"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
|
-
"@actuarial-ts/core": "^0.
|
|
57
|
+
"@actuarial-ts/core": "^0.7.0",
|
|
57
58
|
"zod": "^3.25.76"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -3,11 +3,21 @@ import {
|
|
|
3
3
|
assertCompiledDiagnosticDefinition,
|
|
4
4
|
canonicalJson,
|
|
5
5
|
compileDiagnosticDefinition,
|
|
6
|
+
isDiagnosticPlainRecord,
|
|
6
7
|
type CompiledDiagnosticDefinition,
|
|
7
8
|
} from "@actuarial-ts/core";
|
|
8
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_GENERATOR,
|
|
11
|
+
INTERCHANGE_SPEC_VERSION,
|
|
12
|
+
stampIntegrity,
|
|
13
|
+
type GeneratorStamp,
|
|
14
|
+
} from "../envelope.js";
|
|
9
15
|
import { parseDocument, type ParseDocumentOptions } from "../parse.js";
|
|
10
|
-
import
|
|
16
|
+
import { snapshotInterchangeJson } from "../json.js";
|
|
17
|
+
import {
|
|
18
|
+
diagnosticDefinitionDocSchema,
|
|
19
|
+
type DiagnosticDefinitionDoc,
|
|
20
|
+
} from "../schemas/diagnosticDefinition.js";
|
|
11
21
|
|
|
12
22
|
export interface DiagnosticDefinitionToDocOptions {
|
|
13
23
|
createdAt: string;
|
|
@@ -15,17 +25,39 @@ export interface DiagnosticDefinitionToDocOptions {
|
|
|
15
25
|
extensions?: Record<string, unknown>;
|
|
16
26
|
}
|
|
17
27
|
|
|
28
|
+
export interface DocToDiagnosticDefinitionResult {
|
|
29
|
+
readonly definition: CompiledDiagnosticDefinition;
|
|
30
|
+
readonly warnings: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export function diagnosticDefinitionToDoc(
|
|
19
34
|
compiled: CompiledDiagnosticDefinition,
|
|
20
35
|
options: DiagnosticDefinitionToDocOptions,
|
|
21
36
|
): DiagnosticDefinitionDoc {
|
|
22
37
|
assertCompiledDiagnosticDefinition(compiled);
|
|
23
|
-
|
|
38
|
+
if (!isDiagnosticPlainRecord(options))
|
|
39
|
+
throw new ReservingError(
|
|
40
|
+
"BAD_INTERCHANGE",
|
|
41
|
+
"Diagnostic document options must be a plain object at $.options",
|
|
42
|
+
);
|
|
43
|
+
for (const key of Object.keys(options))
|
|
44
|
+
if (!["createdAt", "generator", "extensions"].includes(key))
|
|
45
|
+
throw new ReservingError(
|
|
46
|
+
"BAD_INTERCHANGE",
|
|
47
|
+
`Unknown diagnostic document option at $.options.${key}`,
|
|
48
|
+
);
|
|
49
|
+
const ownedOptions = snapshotInterchangeJson(options);
|
|
50
|
+
const candidate = stampIntegrity<DiagnosticDefinitionDoc>({
|
|
24
51
|
interchangeVersion: INTERCHANGE_SPEC_VERSION,
|
|
25
52
|
kind: "diagnostic-definition",
|
|
26
|
-
generator:
|
|
27
|
-
|
|
28
|
-
|
|
53
|
+
generator:
|
|
54
|
+
ownedOptions.generator === undefined
|
|
55
|
+
? DEFAULT_GENERATOR
|
|
56
|
+
: ownedOptions.generator,
|
|
57
|
+
createdAt: ownedOptions.createdAt,
|
|
58
|
+
...(ownedOptions.extensions === undefined
|
|
59
|
+
? {}
|
|
60
|
+
: { extensions: ownedOptions.extensions }),
|
|
29
61
|
diagnosticDefinition: {
|
|
30
62
|
definition: compiled.definition,
|
|
31
63
|
identities: {
|
|
@@ -36,26 +68,63 @@ export function diagnosticDefinitionToDoc(
|
|
|
36
68
|
},
|
|
37
69
|
},
|
|
38
70
|
});
|
|
71
|
+
const parsed = diagnosticDefinitionDocSchema.safeParse(candidate);
|
|
72
|
+
if (!parsed.success) {
|
|
73
|
+
const issue = parsed.error.issues[0];
|
|
74
|
+
const path = issue?.path.length ? ` at $.${issue.path.join(".")}` : "";
|
|
75
|
+
throw new ReservingError(
|
|
76
|
+
"BAD_INTERCHANGE",
|
|
77
|
+
`Invalid diagnostic-definition document${path}: ${issue?.message ?? "schema validation failed"}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return deepFreeze(snapshotInterchangeJson(candidate));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function deepFreeze<T>(value: T): T {
|
|
84
|
+
if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
|
|
85
|
+
for (const child of Object.values(value as Record<string, unknown>))
|
|
86
|
+
deepFreeze(child);
|
|
87
|
+
Object.freeze(value);
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
39
90
|
}
|
|
40
91
|
|
|
41
|
-
function sameRecord(
|
|
92
|
+
function sameRecord(
|
|
93
|
+
left: Readonly<Record<string, string>>,
|
|
94
|
+
right: Readonly<Record<string, string>>,
|
|
95
|
+
): boolean {
|
|
42
96
|
const leftKeys = Object.keys(left).sort();
|
|
43
97
|
const rightKeys = Object.keys(right).sort();
|
|
44
|
-
return
|
|
45
|
-
|
|
98
|
+
return (
|
|
99
|
+
leftKeys.length === rightKeys.length &&
|
|
100
|
+
leftKeys.every(
|
|
101
|
+
(key, index) => key === rightKeys[index] && left[key] === right[key],
|
|
102
|
+
)
|
|
46
103
|
);
|
|
47
104
|
}
|
|
48
105
|
|
|
49
106
|
export function docToDiagnosticDefinition(
|
|
50
107
|
value: unknown,
|
|
51
108
|
options?: ParseDocumentOptions,
|
|
52
|
-
):
|
|
109
|
+
): DocToDiagnosticDefinitionResult {
|
|
53
110
|
const parsed = parseDocument(value, options);
|
|
54
111
|
if (parsed.doc.kind !== "diagnostic-definition") {
|
|
55
|
-
throw new ReservingError(
|
|
112
|
+
throw new ReservingError(
|
|
113
|
+
"BAD_INTERCHANGE",
|
|
114
|
+
`Expected kind "diagnostic-definition"; got kind "${parsed.doc.kind}"`,
|
|
115
|
+
);
|
|
56
116
|
}
|
|
57
117
|
const body = parsed.doc.diagnosticDefinition;
|
|
58
|
-
|
|
118
|
+
let compiled: CompiledDiagnosticDefinition;
|
|
119
|
+
try {
|
|
120
|
+
compiled = compileDiagnosticDefinition(body.definition);
|
|
121
|
+
} catch (error) {
|
|
122
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
+
throw new ReservingError(
|
|
124
|
+
"BAD_INTERCHANGE",
|
|
125
|
+
`Diagnostic definition contains unsupported executable vocabulary: ${message}`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
59
128
|
if (canonicalJson(body.definition) !== canonicalJson(compiled.definition)) {
|
|
60
129
|
throw new ReservingError(
|
|
61
130
|
"BAD_INTERCHANGE",
|
|
@@ -67,9 +136,15 @@ export function docToDiagnosticDefinition(
|
|
|
67
136
|
identities.algorithm !== "fnv1a64-jcs-v1" ||
|
|
68
137
|
identities.definition !== compiled.definitionIntegrity ||
|
|
69
138
|
!sameRecord(identities.formulaById, compiled.formulaFingerprints) ||
|
|
70
|
-
!sameRecord(
|
|
139
|
+
!sameRecord(
|
|
140
|
+
identities.calculationByInstanceId,
|
|
141
|
+
compiled.calculationFingerprints,
|
|
142
|
+
)
|
|
71
143
|
) {
|
|
72
|
-
throw new ReservingError(
|
|
144
|
+
throw new ReservingError(
|
|
145
|
+
"BAD_INTERCHANGE",
|
|
146
|
+
"Diagnostic definition identities do not match the compiled semantic definition",
|
|
147
|
+
);
|
|
73
148
|
}
|
|
74
149
|
return { definition: compiled, warnings: parsed.warnings };
|
|
75
150
|
}
|
package/src/envelope.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
ReservingError,
|
|
3
|
+
canonicalJson,
|
|
4
|
+
fnv1a64,
|
|
5
|
+
isRealIsoDate,
|
|
6
|
+
} from "@actuarial-ts/core";
|
|
2
7
|
import { z } from "zod";
|
|
8
|
+
import { recordSchema } from "./schemas/record.js";
|
|
3
9
|
|
|
4
10
|
/**
|
|
5
11
|
* The interchange document envelope (spec 3.1 / 3.5).
|
|
@@ -37,7 +43,7 @@ export const INTERCHANGE_SPEC_MAJOR = 1;
|
|
|
37
43
|
* This package's version, stamped into `generator` by default. A sync test
|
|
38
44
|
* asserts it matches package.json so it cannot silently drift.
|
|
39
45
|
*/
|
|
40
|
-
export const INTERCHANGE_PACKAGE_VERSION = "0.
|
|
46
|
+
export const INTERCHANGE_PACKAGE_VERSION = "0.7.0";
|
|
41
47
|
|
|
42
48
|
export interface GeneratorStamp {
|
|
43
49
|
name: string;
|
|
@@ -45,10 +51,10 @@ export interface GeneratorStamp {
|
|
|
45
51
|
}
|
|
46
52
|
|
|
47
53
|
/** Default `generator` stamp for documents authored by this package. */
|
|
48
|
-
export const DEFAULT_GENERATOR: GeneratorStamp = {
|
|
54
|
+
export const DEFAULT_GENERATOR: Readonly<GeneratorStamp> = Object.freeze({
|
|
49
55
|
name: "@actuarial-ts/interchange",
|
|
50
56
|
version: INTERCHANGE_PACKAGE_VERSION,
|
|
51
|
-
};
|
|
57
|
+
});
|
|
52
58
|
|
|
53
59
|
export const DOCUMENT_KINDS = [
|
|
54
60
|
"triangle",
|
|
@@ -97,8 +103,14 @@ export function envelopeShape<K extends DocumentKind>(kind: K) {
|
|
|
97
103
|
interchangeVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
98
104
|
kind: z.literal(kind),
|
|
99
105
|
generator: generatorSchema,
|
|
100
|
-
createdAt: z
|
|
101
|
-
|
|
106
|
+
createdAt: z
|
|
107
|
+
.string()
|
|
108
|
+
.datetime({ offset: true })
|
|
109
|
+
.refine(
|
|
110
|
+
(value) => isRealIsoDate(value.slice(0, 10)),
|
|
111
|
+
"expected a real Gregorian timestamp",
|
|
112
|
+
),
|
|
113
|
+
extensions: recordSchema(z.unknown()).optional(),
|
|
102
114
|
integrity: integritySchema,
|
|
103
115
|
};
|
|
104
116
|
}
|
|
@@ -162,7 +174,8 @@ export interface IntegrityCheck {
|
|
|
162
174
|
/** Recomputes the tag from the semantic body and compares to the stated one. */
|
|
163
175
|
export function verifyIntegrity(doc: DocumentLike): IntegrityCheck {
|
|
164
176
|
const expected = computeIntegrity(doc);
|
|
165
|
-
const actual =
|
|
177
|
+
const actual =
|
|
178
|
+
typeof doc["integrity"] === "string" ? (doc["integrity"] as string) : null;
|
|
166
179
|
return { ok: actual === expected, expected, actual };
|
|
167
180
|
}
|
|
168
181
|
|
|
@@ -184,7 +197,11 @@ export function acceptVersion(version: unknown): ParsedVersion {
|
|
|
184
197
|
`interchangeVersion must be a MAJOR.MINOR.PATCH string; got ${JSON.stringify(version)}`,
|
|
185
198
|
);
|
|
186
199
|
}
|
|
187
|
-
const [major, minor, patch] = version.split(".").map(Number) as [
|
|
200
|
+
const [major, minor, patch] = version.split(".").map(Number) as [
|
|
201
|
+
number,
|
|
202
|
+
number,
|
|
203
|
+
number,
|
|
204
|
+
];
|
|
188
205
|
if (major !== INTERCHANGE_SPEC_MAJOR) {
|
|
189
206
|
throw new ReservingError(
|
|
190
207
|
"UNSUPPORTED_VERSION",
|
package/src/json.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ReservingError, canonicalJson } from "@actuarial-ts/core";
|
|
2
|
+
|
|
3
|
+
/** Own the complete JSON document without dropping forward-compatible keys.
|
|
4
|
+
* Use the generic interchange JSON contract, not the stricter diagnostic
|
|
5
|
+
* identifier profile: opaque JSON strings may contain escaped control codes. */
|
|
6
|
+
export function snapshotInterchangeJson<T>(value: T): T {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.parse(canonicalJson(value)) as T;
|
|
9
|
+
} catch (error) {
|
|
10
|
+
throw new ReservingError(
|
|
11
|
+
"BAD_INTERCHANGE",
|
|
12
|
+
`An interchange document must contain only JSON values: ${
|
|
13
|
+
error instanceof Error ? error.message : String(error)
|
|
14
|
+
}`,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/parse.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type { StudyDoc } from "./schemas/study.js";
|
|
|
14
14
|
import type { BundleDoc } from "./schemas/bundle.js";
|
|
15
15
|
import type { CrosscheckReportDoc } from "./schemas/crosscheck.js";
|
|
16
16
|
import type { DiagnosticDefinitionDoc } from "./schemas/diagnosticDefinition.js";
|
|
17
|
+
import { snapshotInterchangeJson } from "./json.js";
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* `parseDocument` (spec 4.1): version-checked, schema-validated,
|
|
@@ -124,14 +125,18 @@ export function parseDocument(
|
|
|
124
125
|
);
|
|
125
126
|
}
|
|
126
127
|
|
|
127
|
-
const
|
|
128
|
+
const snapshot = snapshotInterchangeJson(raw);
|
|
129
|
+
const parsed = schema.safeParse(snapshot);
|
|
128
130
|
if (!parsed.success) {
|
|
129
131
|
throw new ReservingError(
|
|
130
132
|
"BAD_INTERCHANGE",
|
|
131
133
|
`Document of kind "${String(kind)}" failed schema validation: ${formatZodError(parsed.error)}`,
|
|
132
134
|
);
|
|
133
135
|
}
|
|
134
|
-
|
|
136
|
+
// These wire schemas validate only: no defaults, coercions, or semantic
|
|
137
|
+
// transforms. Keep the owned input snapshot because Zod 3's passthrough
|
|
138
|
+
// object assembler otherwise drops unknown own `__proto__` properties.
|
|
139
|
+
const doc = snapshot as InterchangeDocument;
|
|
135
140
|
|
|
136
141
|
const warnings: string[] = [];
|
|
137
142
|
const integrity = verifyIntegrity(doc);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { NormalizedDiagnosticDefinitionIdentity } from "@actuarial-ts/core";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { envelopeShape, type GeneratorStamp } from "../envelope.js";
|
|
4
|
+
import { recordSchema } from "./record.js";
|
|
4
5
|
|
|
5
6
|
const token = z.string().min(1);
|
|
6
7
|
const finite = z.number().finite();
|
|
7
8
|
const nullableToken = token.nullable();
|
|
8
|
-
const attributes =
|
|
9
|
+
const attributes = recordSchema(z.union([z.string(), finite, z.boolean(), z.null()]));
|
|
9
10
|
const tolerance = z.object({ absolute: finite.nonnegative(), relative: finite.nonnegative() }).passthrough();
|
|
10
11
|
|
|
11
12
|
export const diagnosticMeasureExpressionSchema: z.ZodTypeAny = z.lazy(() =>
|
|
@@ -122,28 +123,28 @@ export const normalizedDiagnosticDefinitionSchema = z.object({
|
|
|
122
123
|
derivedMeasures: z.array(z.object({ id: token, outputMeasureId: token, expression: diagnosticClaimExpressionSchema }).passthrough()),
|
|
123
124
|
formulas: z.array(z.object({
|
|
124
125
|
id: token, version: token,
|
|
125
|
-
roles:
|
|
126
|
+
roles: recordSchema(z.object({ kind: z.enum(["count", "amount", "exposure"]), compatibilityGroup: nullableToken, developmentSemantics: z.enum(["cumulative", "incremental", "point-in-time", "unknown"]).nullable() }).passthrough()),
|
|
126
127
|
numerator: diagnosticRoleExpressionSchema, denominator: diagnosticRoleExpressionSchema,
|
|
127
128
|
denominatorPolicy: z.literal("positive-or-null"),
|
|
128
129
|
}).passthrough()),
|
|
129
130
|
instances: z.array(z.object({
|
|
130
|
-
id: token, version: token, formulaId: token, bindings:
|
|
131
|
+
id: token, version: token, formulaId: token, bindings: recordSchema(diagnosticMeasureExpressionSchema),
|
|
131
132
|
presentation: z.object({ displayName: z.string(), description: z.string(), displayUnit: token, scale: finite.positive(), numeratorLabel: z.string(), denominatorLabel: z.string() }).passthrough(),
|
|
132
133
|
rules: z.array(z.object({ id: token, code: token, message: z.string(), severity: z.enum(["warning", "fail"]), when: z.object({ left: ruleOperand, operator, right: ruleOperand, tolerance }).passthrough() }).passthrough()),
|
|
133
134
|
}).passthrough()),
|
|
134
135
|
reviewRules: z.array(reviewRule), periodAxis,
|
|
135
136
|
}).passthrough();
|
|
136
137
|
|
|
137
|
-
export interface
|
|
138
|
+
export interface DiagnosticDefinitionIdentitySet {
|
|
138
139
|
algorithm: "fnv1a64-jcs-v1";
|
|
139
|
-
formulaById: Record<string, string
|
|
140
|
-
calculationByInstanceId: Record<string, string
|
|
140
|
+
formulaById: Readonly<Record<string, string>>;
|
|
141
|
+
calculationByInstanceId: Readonly<Record<string, string>>;
|
|
141
142
|
definition: string;
|
|
142
143
|
}
|
|
143
144
|
|
|
144
145
|
export interface DiagnosticDefinitionBody {
|
|
145
146
|
definition: NormalizedDiagnosticDefinitionIdentity;
|
|
146
|
-
identities:
|
|
147
|
+
identities: DiagnosticDefinitionIdentitySet;
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
export interface DiagnosticDefinitionDoc {
|
|
@@ -161,8 +162,8 @@ const identityTag = z.string().regex(/^fnv1a64-jcs-v1:[0-9a-f]{16}$/);
|
|
|
161
162
|
export const diagnosticDefinitionBodySchema: z.ZodType<DiagnosticDefinitionBody> = z.object({
|
|
162
163
|
definition: normalizedDiagnosticDefinitionSchema as z.ZodType<NormalizedDiagnosticDefinitionIdentity>,
|
|
163
164
|
identities: z.object({
|
|
164
|
-
algorithm: z.literal("fnv1a64-jcs-v1"), formulaById:
|
|
165
|
-
calculationByInstanceId:
|
|
165
|
+
algorithm: z.literal("fnv1a64-jcs-v1"), formulaById: recordSchema(identityTag),
|
|
166
|
+
calculationByInstanceId: recordSchema(identityTag), definition: identityTag,
|
|
166
167
|
}).passthrough(),
|
|
167
168
|
}).passthrough();
|
|
168
169
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/** Preserve ordinary own `__proto__` keys that Zod 3 otherwise drops.
|
|
4
|
+
* Every key is reversibly encoded while its value is schema-validated. */
|
|
5
|
+
export function recordSchema<T extends z.ZodTypeAny>(valueSchema: T) {
|
|
6
|
+
return z
|
|
7
|
+
.record(
|
|
8
|
+
z.string().transform((key) => `:${key}`),
|
|
9
|
+
valueSchema,
|
|
10
|
+
)
|
|
11
|
+
.transform((record): Record<string, z.output<T>> =>
|
|
12
|
+
Object.fromEntries(
|
|
13
|
+
Object.entries(record).map(([key, value]) => [key.slice(1), value]),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
}
|
package/src/schemas/triangle.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRealIsoDate } from "@actuarial-ts/core";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { envelopeShape } from "../envelope.js";
|
|
3
4
|
|
|
@@ -29,7 +30,12 @@ export type CoreMeasure = (typeof CORE_MEASURES)[number];
|
|
|
29
30
|
|
|
30
31
|
export const measureSchema = z.union([
|
|
31
32
|
z.enum([...CORE_MEASURES, "earnedPremium"]),
|
|
32
|
-
z
|
|
33
|
+
z
|
|
34
|
+
.string()
|
|
35
|
+
.regex(
|
|
36
|
+
/^custom:.+$/,
|
|
37
|
+
'custom measures must be namespaced as "custom:<label>"',
|
|
38
|
+
),
|
|
33
39
|
]);
|
|
34
40
|
|
|
35
41
|
export type Measure = z.infer<typeof measureSchema>;
|
|
@@ -43,7 +49,9 @@ export const originLengthMonthsSchema = z.union([
|
|
|
43
49
|
|
|
44
50
|
export type OriginLengthMonths = z.infer<typeof originLengthMonthsSchema>;
|
|
45
51
|
|
|
46
|
-
const isoDateSchema = z
|
|
52
|
+
const isoDateSchema = z
|
|
53
|
+
.string()
|
|
54
|
+
.refine(isRealIsoDate, "expected a real ISO yyyy-mm-dd date");
|
|
47
55
|
|
|
48
56
|
export const triangleOriginSchema = z
|
|
49
57
|
.object({ label: z.string().min(1), start: isoDateSchema })
|
|
@@ -51,7 +59,11 @@ export const triangleOriginSchema = z
|
|
|
51
59
|
|
|
52
60
|
/** Bulk-lane reference (spec 3.3). */
|
|
53
61
|
export const valuesRefSchema = z
|
|
54
|
-
.object({
|
|
62
|
+
.object({
|
|
63
|
+
format: z.literal("arrow"),
|
|
64
|
+
path: z.string().min(1),
|
|
65
|
+
sha256: z.string().min(1),
|
|
66
|
+
})
|
|
55
67
|
.passthrough();
|
|
56
68
|
|
|
57
69
|
export const triangleBodySchema = z
|
|
@@ -73,10 +85,16 @@ export const triangleBodySchema = z
|
|
|
73
85
|
.passthrough()
|
|
74
86
|
.optional(),
|
|
75
87
|
units: z
|
|
76
|
-
.object({
|
|
88
|
+
.object({
|
|
89
|
+
currency: z.string().optional(),
|
|
90
|
+
scale: z.number().positive().optional(),
|
|
91
|
+
})
|
|
92
|
+
.passthrough()
|
|
93
|
+
.optional(),
|
|
94
|
+
segment: z
|
|
95
|
+
.object({ labels: z.record(z.string()) })
|
|
77
96
|
.passthrough()
|
|
78
97
|
.optional(),
|
|
79
|
-
segment: z.object({ labels: z.record(z.string()) }).passthrough().optional(),
|
|
80
98
|
})
|
|
81
99
|
.passthrough()
|
|
82
100
|
.superRefine((body, ctx) => {
|