@actuarial-ts/interchange 0.5.0 → 0.6.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.
- package/README.md +36 -70
- package/dist/convert/diagnosticDefinition.d.ts +16 -0
- package/dist/convert/diagnosticDefinition.d.ts.map +1 -0
- package/dist/convert/diagnosticDefinition.js +82 -0
- package/dist/convert/diagnosticDefinition.js.map +1 -0
- package/dist/convert/result.d.ts.map +1 -1
- package/dist/convert/result.js +2 -2
- package/dist/convert/result.js.map +1 -1
- package/dist/envelope.d.ts +6 -6
- package/dist/envelope.d.ts.map +1 -1
- package/dist/envelope.js +13 -7
- package/dist/envelope.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.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 +2 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +8 -2
- package/dist/parse.js.map +1 -1
- package/dist/schemas/bundle.d.ts +2 -0
- package/dist/schemas/bundle.d.ts.map +1 -1
- package/dist/schemas/bundle.js +2 -0
- package/dist/schemas/bundle.js.map +1 -1
- package/dist/schemas/crosscheck.d.ts +6 -6
- package/dist/schemas/diagnosticDefinition.d.ts +11572 -0
- package/dist/schemas/diagnosticDefinition.d.ts.map +1 -0
- package/dist/schemas/diagnosticDefinition.js +133 -0
- package/dist/schemas/diagnosticDefinition.js.map +1 -0
- package/dist/schemas/manifest.d.ts.map +1 -1
- package/dist/schemas/manifest.js +7 -1
- package/dist/schemas/manifest.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 +4 -3
- package/src/convert/diagnosticDefinition.ts +150 -0
- package/src/convert/result.ts +2 -1
- package/src/envelope.ts +28 -9
- package/src/index.ts +2 -0
- package/src/json.ts +17 -0
- package/src/parse.ts +11 -3
- package/src/schemas/bundle.ts +3 -0
- package/src/schemas/diagnosticDefinition.ts +172 -0
- package/src/schemas/manifest.ts +7 -1
- 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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actuarial-ts/interchange",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
7
7
|
"main": "./dist/index.js",
|
|
@@ -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.6.1",
|
|
57
58
|
"zod": "^3.25.76"
|
|
58
59
|
},
|
|
59
60
|
"devDependencies": {
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ReservingError,
|
|
3
|
+
assertCompiledDiagnosticDefinition,
|
|
4
|
+
canonicalJson,
|
|
5
|
+
compileDiagnosticDefinition,
|
|
6
|
+
isDiagnosticPlainRecord,
|
|
7
|
+
type CompiledDiagnosticDefinition,
|
|
8
|
+
} from "@actuarial-ts/core";
|
|
9
|
+
import {
|
|
10
|
+
DEFAULT_GENERATOR,
|
|
11
|
+
INTERCHANGE_SPEC_VERSION,
|
|
12
|
+
stampIntegrity,
|
|
13
|
+
type GeneratorStamp,
|
|
14
|
+
} from "../envelope.js";
|
|
15
|
+
import { parseDocument, type ParseDocumentOptions } from "../parse.js";
|
|
16
|
+
import { snapshotInterchangeJson } from "../json.js";
|
|
17
|
+
import {
|
|
18
|
+
diagnosticDefinitionDocSchema,
|
|
19
|
+
type DiagnosticDefinitionDoc,
|
|
20
|
+
} from "../schemas/diagnosticDefinition.js";
|
|
21
|
+
|
|
22
|
+
export interface DiagnosticDefinitionToDocOptions {
|
|
23
|
+
createdAt: string;
|
|
24
|
+
generator?: GeneratorStamp;
|
|
25
|
+
extensions?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DocToDiagnosticDefinitionResult {
|
|
29
|
+
readonly definition: CompiledDiagnosticDefinition;
|
|
30
|
+
readonly warnings: readonly string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function diagnosticDefinitionToDoc(
|
|
34
|
+
compiled: CompiledDiagnosticDefinition,
|
|
35
|
+
options: DiagnosticDefinitionToDocOptions,
|
|
36
|
+
): DiagnosticDefinitionDoc {
|
|
37
|
+
assertCompiledDiagnosticDefinition(compiled);
|
|
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>({
|
|
51
|
+
interchangeVersion: INTERCHANGE_SPEC_VERSION,
|
|
52
|
+
kind: "diagnostic-definition",
|
|
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 }),
|
|
61
|
+
diagnosticDefinition: {
|
|
62
|
+
definition: compiled.definition,
|
|
63
|
+
identities: {
|
|
64
|
+
algorithm: "fnv1a64-jcs-v1",
|
|
65
|
+
formulaById: { ...compiled.formulaFingerprints },
|
|
66
|
+
calculationByInstanceId: { ...compiled.calculationFingerprints },
|
|
67
|
+
definition: compiled.definitionIntegrity,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
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;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function sameRecord(
|
|
93
|
+
left: Readonly<Record<string, string>>,
|
|
94
|
+
right: Readonly<Record<string, string>>,
|
|
95
|
+
): boolean {
|
|
96
|
+
const leftKeys = Object.keys(left).sort();
|
|
97
|
+
const rightKeys = Object.keys(right).sort();
|
|
98
|
+
return (
|
|
99
|
+
leftKeys.length === rightKeys.length &&
|
|
100
|
+
leftKeys.every(
|
|
101
|
+
(key, index) => key === rightKeys[index] && left[key] === right[key],
|
|
102
|
+
)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function docToDiagnosticDefinition(
|
|
107
|
+
value: unknown,
|
|
108
|
+
options?: ParseDocumentOptions,
|
|
109
|
+
): DocToDiagnosticDefinitionResult {
|
|
110
|
+
const parsed = parseDocument(value, options);
|
|
111
|
+
if (parsed.doc.kind !== "diagnostic-definition") {
|
|
112
|
+
throw new ReservingError(
|
|
113
|
+
"BAD_INTERCHANGE",
|
|
114
|
+
`Expected kind "diagnostic-definition"; got kind "${parsed.doc.kind}"`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const body = parsed.doc.diagnosticDefinition;
|
|
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
|
+
}
|
|
128
|
+
if (canonicalJson(body.definition) !== canonicalJson(compiled.definition)) {
|
|
129
|
+
throw new ReservingError(
|
|
130
|
+
"BAD_INTERCHANGE",
|
|
131
|
+
"Diagnostic definition contains unsupported executable vocabulary or is not in normalized form",
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
const identities = body.identities;
|
|
135
|
+
if (
|
|
136
|
+
identities.algorithm !== "fnv1a64-jcs-v1" ||
|
|
137
|
+
identities.definition !== compiled.definitionIntegrity ||
|
|
138
|
+
!sameRecord(identities.formulaById, compiled.formulaFingerprints) ||
|
|
139
|
+
!sameRecord(
|
|
140
|
+
identities.calculationByInstanceId,
|
|
141
|
+
compiled.calculationFingerprints,
|
|
142
|
+
)
|
|
143
|
+
) {
|
|
144
|
+
throw new ReservingError(
|
|
145
|
+
"BAD_INTERCHANGE",
|
|
146
|
+
"Diagnostic definition identities do not match the compiled semantic definition",
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return { definition: compiled, warnings: parsed.warnings };
|
|
150
|
+
}
|
package/src/convert/result.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type BornhuetterFergusonResult,
|
|
4
4
|
type ChainLadderResult,
|
|
5
5
|
type MackResult,
|
|
6
|
+
CORE_PACKAGE_VERSION,
|
|
6
7
|
ReservingError,
|
|
7
8
|
} from "@actuarial-ts/core";
|
|
8
9
|
import {
|
|
@@ -46,7 +47,7 @@ export type ConvertibleResult =
|
|
|
46
47
|
* The engine stamp for results computed by @actuarial-ts/core. A sync test
|
|
47
48
|
* pins the version to the installed core package so it cannot drift.
|
|
48
49
|
*/
|
|
49
|
-
export const CORE_ENGINE: EngineStamp = { name: "actuarial-ts", version:
|
|
50
|
+
export const CORE_ENGINE: EngineStamp = { name: "actuarial-ts", version: CORE_PACKAGE_VERSION };
|
|
50
51
|
|
|
51
52
|
export interface ResultToDocOptions {
|
|
52
53
|
/** The triangle document the result was computed from. */
|
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).
|
|
@@ -28,7 +34,7 @@ import { z } from "zod";
|
|
|
28
34
|
*/
|
|
29
35
|
|
|
30
36
|
/** The interchange spec version this package writes. */
|
|
31
|
-
export const INTERCHANGE_SPEC_VERSION = "1.
|
|
37
|
+
export const INTERCHANGE_SPEC_VERSION = "1.1.0";
|
|
32
38
|
|
|
33
39
|
/** The spec major this package accepts (spec 3.5: readers accept same-major). */
|
|
34
40
|
export const INTERCHANGE_SPEC_MAJOR = 1;
|
|
@@ -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.6.1";
|
|
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",
|
|
@@ -58,6 +64,7 @@ export const DOCUMENT_KINDS = [
|
|
|
58
64
|
"study",
|
|
59
65
|
"bundle",
|
|
60
66
|
"crosscheck-report",
|
|
67
|
+
"diagnostic-definition",
|
|
61
68
|
] as const;
|
|
62
69
|
|
|
63
70
|
export type DocumentKind = (typeof DOCUMENT_KINDS)[number];
|
|
@@ -75,6 +82,7 @@ const SEMANTIC_BODY_KEYS: Record<DocumentKind, readonly string[]> = {
|
|
|
75
82
|
study: ["study"],
|
|
76
83
|
bundle: ["bundle", "interchange"],
|
|
77
84
|
"crosscheck-report": ["report"],
|
|
85
|
+
"diagnostic-definition": ["diagnosticDefinition"],
|
|
78
86
|
};
|
|
79
87
|
|
|
80
88
|
/** A 16-hex-char fnv1a64 tag. */
|
|
@@ -95,8 +103,14 @@ export function envelopeShape<K extends DocumentKind>(kind: K) {
|
|
|
95
103
|
interchangeVersion: z.string().regex(/^\d+\.\d+\.\d+$/),
|
|
96
104
|
kind: z.literal(kind),
|
|
97
105
|
generator: generatorSchema,
|
|
98
|
-
createdAt: z
|
|
99
|
-
|
|
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(),
|
|
100
114
|
integrity: integritySchema,
|
|
101
115
|
};
|
|
102
116
|
}
|
|
@@ -160,7 +174,8 @@ export interface IntegrityCheck {
|
|
|
160
174
|
/** Recomputes the tag from the semantic body and compares to the stated one. */
|
|
161
175
|
export function verifyIntegrity(doc: DocumentLike): IntegrityCheck {
|
|
162
176
|
const expected = computeIntegrity(doc);
|
|
163
|
-
const actual =
|
|
177
|
+
const actual =
|
|
178
|
+
typeof doc["integrity"] === "string" ? (doc["integrity"] as string) : null;
|
|
164
179
|
return { ok: actual === expected, expected, actual };
|
|
165
180
|
}
|
|
166
181
|
|
|
@@ -182,7 +197,11 @@ export function acceptVersion(version: unknown): ParsedVersion {
|
|
|
182
197
|
`interchangeVersion must be a MAJOR.MINOR.PATCH string; got ${JSON.stringify(version)}`,
|
|
183
198
|
);
|
|
184
199
|
}
|
|
185
|
-
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
|
+
];
|
|
186
205
|
if (major !== INTERCHANGE_SPEC_MAJOR) {
|
|
187
206
|
throw new ReservingError(
|
|
188
207
|
"UNSUPPORTED_VERSION",
|
package/src/index.ts
CHANGED
|
@@ -6,10 +6,12 @@ export * from "./schemas/result.js";
|
|
|
6
6
|
export * from "./schemas/study.js";
|
|
7
7
|
export * from "./schemas/bundle.js";
|
|
8
8
|
export * from "./schemas/crosscheck.js";
|
|
9
|
+
export * from "./schemas/diagnosticDefinition.js";
|
|
9
10
|
export * from "./schemas/manifest.js";
|
|
10
11
|
export * from "./convert/triangle.js";
|
|
11
12
|
export * from "./convert/selection.js";
|
|
12
13
|
export * from "./convert/result.js";
|
|
14
|
+
export * from "./convert/diagnosticDefinition.js";
|
|
13
15
|
export * from "./referee/profiles.js";
|
|
14
16
|
export * from "./referee/crosscheck.js";
|
|
15
17
|
export * from "./referee/crosscheckStochastic.js";
|
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
|
@@ -13,6 +13,8 @@ import type { MethodResultDoc, StochasticResultDoc } from "./schemas/result.js";
|
|
|
13
13
|
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
|
+
import type { DiagnosticDefinitionDoc } from "./schemas/diagnosticDefinition.js";
|
|
17
|
+
import { snapshotInterchangeJson } from "./json.js";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* `parseDocument` (spec 4.1): version-checked, schema-validated,
|
|
@@ -48,7 +50,8 @@ export type InterchangeDocument =
|
|
|
48
50
|
| StochasticResultDoc
|
|
49
51
|
| StudyDoc
|
|
50
52
|
| BundleDoc
|
|
51
|
-
| CrosscheckReportDoc
|
|
53
|
+
| CrosscheckReportDoc
|
|
54
|
+
| DiagnosticDefinitionDoc;
|
|
52
55
|
|
|
53
56
|
export interface ParseDocumentOptions {
|
|
54
57
|
/** How to treat a failed integrity check. Default "refuse". */
|
|
@@ -93,6 +96,7 @@ function embeddedDocuments(doc: InterchangeDocument): { path: string; value: unk
|
|
|
93
96
|
collect("$.interchange.triangles", mirror["triangles"]);
|
|
94
97
|
collect("$.interchange.selections", mirror["selections"]);
|
|
95
98
|
collect("$.interchange.results", mirror["results"]);
|
|
99
|
+
collect("$.interchange.diagnosticDefinitions", mirror["diagnosticDefinitions"]);
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
return found;
|
|
@@ -121,14 +125,18 @@ export function parseDocument(
|
|
|
121
125
|
);
|
|
122
126
|
}
|
|
123
127
|
|
|
124
|
-
const
|
|
128
|
+
const snapshot = snapshotInterchangeJson(raw);
|
|
129
|
+
const parsed = schema.safeParse(snapshot);
|
|
125
130
|
if (!parsed.success) {
|
|
126
131
|
throw new ReservingError(
|
|
127
132
|
"BAD_INTERCHANGE",
|
|
128
133
|
`Document of kind "${String(kind)}" failed schema validation: ${formatZodError(parsed.error)}`,
|
|
129
134
|
);
|
|
130
135
|
}
|
|
131
|
-
|
|
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;
|
|
132
140
|
|
|
133
141
|
const warnings: string[] = [];
|
|
134
142
|
const integrity = verifyIntegrity(doc);
|
package/src/schemas/bundle.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
methodResultDocSchema,
|
|
9
9
|
stochasticResultDocSchema,
|
|
10
10
|
} from "./result.js";
|
|
11
|
+
import { type DiagnosticDefinitionDoc, diagnosticDefinitionDocSchema } from "./diagnosticDefinition.js";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* BundleDoc (spec 3.2) — the wrapped reproducibility bundle.
|
|
@@ -31,6 +32,7 @@ export interface BundleInterchange {
|
|
|
31
32
|
triangles: TriangleDoc[];
|
|
32
33
|
selections: SelectionDoc[];
|
|
33
34
|
results: (MethodResultDoc | StochasticResultDoc)[];
|
|
35
|
+
diagnosticDefinitions?: DiagnosticDefinitionDoc[];
|
|
34
36
|
[key: string]: unknown;
|
|
35
37
|
}
|
|
36
38
|
|
|
@@ -52,6 +54,7 @@ export const bundleInterchangeSchema: z.ZodType<BundleInterchange> = z
|
|
|
52
54
|
triangles: z.array(triangleDocSchema),
|
|
53
55
|
selections: z.array(selectionDocSchema),
|
|
54
56
|
results: z.array(z.union([methodResultDocSchema, stochasticResultDocSchema])),
|
|
57
|
+
diagnosticDefinitions: z.array(diagnosticDefinitionDocSchema).optional(),
|
|
55
58
|
})
|
|
56
59
|
.passthrough();
|
|
57
60
|
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { NormalizedDiagnosticDefinitionIdentity } from "@actuarial-ts/core";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { envelopeShape, type GeneratorStamp } from "../envelope.js";
|
|
4
|
+
import { recordSchema } from "./record.js";
|
|
5
|
+
|
|
6
|
+
const token = z.string().min(1);
|
|
7
|
+
const finite = z.number().finite();
|
|
8
|
+
const nullableToken = token.nullable();
|
|
9
|
+
const attributes = recordSchema(z.union([z.string(), finite, z.boolean(), z.null()]));
|
|
10
|
+
const tolerance = z.object({ absolute: finite.nonnegative(), relative: finite.nonnegative() }).passthrough();
|
|
11
|
+
|
|
12
|
+
export const diagnosticMeasureExpressionSchema: z.ZodTypeAny = z.lazy(() =>
|
|
13
|
+
z.discriminatedUnion("op", [
|
|
14
|
+
z.object({ op: z.literal("measure"), measureId: token }).passthrough(),
|
|
15
|
+
z.object({ op: z.literal("add"), terms: z.array(diagnosticMeasureExpressionSchema).min(1) }).passthrough(),
|
|
16
|
+
z.object({ op: z.literal("subtract"), left: diagnosticMeasureExpressionSchema, right: diagnosticMeasureExpressionSchema }).passthrough(),
|
|
17
|
+
]),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
export const diagnosticClaimExpressionSchema: z.ZodTypeAny = z.lazy(() =>
|
|
21
|
+
z.discriminatedUnion("op", [
|
|
22
|
+
z.object({ op: z.literal("measure"), measureId: token }).passthrough(),
|
|
23
|
+
z.object({ op: z.literal("add"), terms: z.array(diagnosticClaimExpressionSchema).min(1) }).passthrough(),
|
|
24
|
+
z.object({ op: z.literal("subtract"), left: diagnosticClaimExpressionSchema, right: diagnosticClaimExpressionSchema }).passthrough(),
|
|
25
|
+
z.object({ op: z.literal("claim-layer"), measureId: token, attachment: finite.nonnegative(), limit: finite.positive().nullable() }).passthrough(),
|
|
26
|
+
]),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
export const diagnosticRoleExpressionSchema: z.ZodTypeAny = z.lazy(() =>
|
|
30
|
+
z.discriminatedUnion("op", [
|
|
31
|
+
z.object({ op: z.literal("role"), role: token }).passthrough(),
|
|
32
|
+
z.object({ op: z.literal("add"), terms: z.array(diagnosticRoleExpressionSchema).min(1) }).passthrough(),
|
|
33
|
+
z.object({ op: z.literal("subtract"), left: diagnosticRoleExpressionSchema, right: diagnosticRoleExpressionSchema }).passthrough(),
|
|
34
|
+
]),
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const limitation = z.discriminatedUnion("kind", [
|
|
38
|
+
z.object({ kind: z.literal("unlimited") }).passthrough(),
|
|
39
|
+
z.object({ kind: z.literal("unknown"), description: z.string().nullable() }).passthrough(),
|
|
40
|
+
z.object({
|
|
41
|
+
kind: z.enum(["layer", "pre-limited"]),
|
|
42
|
+
attachment: finite.nonnegative(),
|
|
43
|
+
limit: finite.positive().nullable(),
|
|
44
|
+
application: z.enum(["claim", "occurrence", "policy", "source-defined"]),
|
|
45
|
+
derivation: z.discriminatedUnion("kind", [
|
|
46
|
+
z.object({ kind: z.literal("sdk") }).passthrough(),
|
|
47
|
+
z.object({ kind: z.literal("external"), actor: z.enum(["caller", "source"]), transformationRef: token }).passthrough(),
|
|
48
|
+
]),
|
|
49
|
+
}).passthrough(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const ruleOperand = z.discriminatedUnion("source", [
|
|
53
|
+
z.object({ source: z.literal("measure"), expression: diagnosticMeasureExpressionSchema }).passthrough(),
|
|
54
|
+
z.object({ source: z.literal("calculation"), field: z.enum(["numerator", "denominator"]) }).passthrough(),
|
|
55
|
+
z.object({ source: z.literal("constant"), value: finite }).passthrough(),
|
|
56
|
+
]);
|
|
57
|
+
const operator = z.enum(["lt", "lte", "eq", "neq", "gte", "gt"]);
|
|
58
|
+
const reviewOperand = z.union([
|
|
59
|
+
diagnosticMeasureExpressionSchema,
|
|
60
|
+
z.object({ op: z.literal("constant"), value: finite }).passthrough(),
|
|
61
|
+
]);
|
|
62
|
+
const reviewBase = {
|
|
63
|
+
id: token,
|
|
64
|
+
code: token,
|
|
65
|
+
description: z.string(),
|
|
66
|
+
severity: z.enum(["warning", "fail"]),
|
|
67
|
+
missingInput: z.enum(["not-evaluated", "finding"]),
|
|
68
|
+
tolerance,
|
|
69
|
+
};
|
|
70
|
+
const reviewFilter = z.object({
|
|
71
|
+
sourceGroups: z.array(token).nullable(), origins: z.array(token).nullable(),
|
|
72
|
+
originFrom: nullableToken, originThrough: nullableToken, valuations: z.array(token).nullable(),
|
|
73
|
+
valuationFrom: nullableToken, valuationThrough: nullableToken,
|
|
74
|
+
minDevelopmentAge: finite.nullable(), maxDevelopmentAge: finite.nullable(),
|
|
75
|
+
}).passthrough();
|
|
76
|
+
const reviewRule = z.discriminatedUnion("kind", [
|
|
77
|
+
z.object({ ...reviewBase, kind: z.literal("compare"), when: z.object({ left: reviewOperand, operator, right: reviewOperand }).passthrough() }).passthrough(),
|
|
78
|
+
z.object({ ...reviewBase, kind: z.literal("reconcile"), actual: diagnosticMeasureExpressionSchema, expected: reviewOperand }).passthrough(),
|
|
79
|
+
z.object({ ...reviewBase, kind: z.literal("monotonic"), expression: diagnosticMeasureExpressionSchema, direction: z.enum(["nondecreasing", "nonincreasing"]) }).passthrough(),
|
|
80
|
+
z.object({ ...reviewBase, kind: z.literal("layer-order"), narrower: diagnosticMeasureExpressionSchema, broader: diagnosticMeasureExpressionSchema, comparability: z.discriminatedUnion("kind", [
|
|
81
|
+
z.object({ kind: z.literal("compiler-proven") }).passthrough(),
|
|
82
|
+
z.object({ kind: z.literal("caller-asserted"), rationaleArtifactId: token }).passthrough(),
|
|
83
|
+
]) }).passthrough(),
|
|
84
|
+
z.object({ ...reviewBase, kind: z.literal("control-total"), expression: diagnosticMeasureExpressionSchema, expected: finite, filter: reviewFilter.nullable(), projection: z.discriminatedUnion("kind", [
|
|
85
|
+
z.object({ kind: z.literal("valuation"), valuation: token }).passthrough(),
|
|
86
|
+
z.object({ kind: z.literal("latest-valuation-per-origin") }).passthrough(),
|
|
87
|
+
z.object({ kind: z.literal("all-cells") }).passthrough(),
|
|
88
|
+
]) }).passthrough(),
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const periodAxis = z.discriminatedUnion("kind", [
|
|
92
|
+
z.object({
|
|
93
|
+
kind: z.literal("calendar"), originCadence: z.enum(["month", "quarter", "year"]),
|
|
94
|
+
valuationCadence: z.enum(["month", "quarter", "year"]), originAnchor: z.enum(["start", "end"]),
|
|
95
|
+
valuationAnchor: z.enum(["start", "end"]), ageUnit: z.literal("month"), ageOffset: finite,
|
|
96
|
+
}).passthrough(),
|
|
97
|
+
z.object({
|
|
98
|
+
kind: z.literal("ordered"), id: token, version: token, ageUnit: token, ageOffset: finite,
|
|
99
|
+
origins: z.array(z.object({ label: token, aliases: z.array(token), coordinate: finite }).passthrough()),
|
|
100
|
+
valuations: z.array(z.object({ label: token, aliases: z.array(token), coordinate: finite }).passthrough()),
|
|
101
|
+
}).passthrough(),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generic wire schema for the normalized diagnostic vocabulary. Known fields
|
|
106
|
+
* are validated and same-major additions are preserved recursively; the
|
|
107
|
+
* executable converter applies core's closed semantic compiler afterward.
|
|
108
|
+
*/
|
|
109
|
+
export const normalizedDiagnosticDefinitionSchema = z.object({
|
|
110
|
+
diagnosticDefinitionVersion: z.literal("1.0.0"), id: token, version: token,
|
|
111
|
+
lossRowGrain: z.enum(["claim", "aggregate"]),
|
|
112
|
+
measures: z.array(z.object({
|
|
113
|
+
id: token, displayName: z.string(), description: z.string(), source: z.enum(["loss", "exposure", "derived"]),
|
|
114
|
+
kind: z.enum(["count", "amount", "exposure"]), unit: token,
|
|
115
|
+
developmentSemantics: z.enum(["cumulative", "incremental", "point-in-time", "unknown"]),
|
|
116
|
+
aggregation: z.literal("sum"), missing: z.enum(["unknown", "zero"]), basisId: nullableToken,
|
|
117
|
+
countPopulationId: nullableToken, exposureBasisId: nullableToken,
|
|
118
|
+
exposureTiming: z.enum(["origin-static", "valuation-specific"]).nullable(),
|
|
119
|
+
}).passthrough()),
|
|
120
|
+
countPopulations: z.array(z.object({ id: token, displayName: z.string(), subject: z.enum(["claim", "claimant", "policy", "occurrence", "other", "unknown"]), unit: token, description: z.string(), attributes }).passthrough()),
|
|
121
|
+
exposureBases: z.array(z.object({ id: token, displayName: z.string(), basis: z.enum(["earned", "written", "in-force", "other", "unknown"]), unit: token, description: z.string(), sourceDescription: z.string().nullable(), attributes }).passthrough()),
|
|
122
|
+
amountBases: z.array(z.object({ id: token, displayName: z.string(), currency: token, perspective: z.enum(["gross", "net", "ceded", "other", "unknown"]), components: z.array(z.object({ id: token, treatment: z.enum(["included", "excluded", "unknown"]), limitation }).passthrough()), sourceDescription: z.string().nullable(), attributes }).passthrough()),
|
|
123
|
+
derivedMeasures: z.array(z.object({ id: token, outputMeasureId: token, expression: diagnosticClaimExpressionSchema }).passthrough()),
|
|
124
|
+
formulas: z.array(z.object({
|
|
125
|
+
id: token, version: token,
|
|
126
|
+
roles: recordSchema(z.object({ kind: z.enum(["count", "amount", "exposure"]), compatibilityGroup: nullableToken, developmentSemantics: z.enum(["cumulative", "incremental", "point-in-time", "unknown"]).nullable() }).passthrough()),
|
|
127
|
+
numerator: diagnosticRoleExpressionSchema, denominator: diagnosticRoleExpressionSchema,
|
|
128
|
+
denominatorPolicy: z.literal("positive-or-null"),
|
|
129
|
+
}).passthrough()),
|
|
130
|
+
instances: z.array(z.object({
|
|
131
|
+
id: token, version: token, formulaId: token, bindings: recordSchema(diagnosticMeasureExpressionSchema),
|
|
132
|
+
presentation: z.object({ displayName: z.string(), description: z.string(), displayUnit: token, scale: finite.positive(), numeratorLabel: z.string(), denominatorLabel: z.string() }).passthrough(),
|
|
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()),
|
|
134
|
+
}).passthrough()),
|
|
135
|
+
reviewRules: z.array(reviewRule), periodAxis,
|
|
136
|
+
}).passthrough();
|
|
137
|
+
|
|
138
|
+
export interface DiagnosticDefinitionIdentitySet {
|
|
139
|
+
algorithm: "fnv1a64-jcs-v1";
|
|
140
|
+
formulaById: Readonly<Record<string, string>>;
|
|
141
|
+
calculationByInstanceId: Readonly<Record<string, string>>;
|
|
142
|
+
definition: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface DiagnosticDefinitionBody {
|
|
146
|
+
definition: NormalizedDiagnosticDefinitionIdentity;
|
|
147
|
+
identities: DiagnosticDefinitionIdentitySet;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface DiagnosticDefinitionDoc {
|
|
151
|
+
interchangeVersion: string;
|
|
152
|
+
kind: "diagnostic-definition";
|
|
153
|
+
generator: GeneratorStamp & { [key: string]: unknown };
|
|
154
|
+
createdAt: string;
|
|
155
|
+
extensions?: Record<string, unknown>;
|
|
156
|
+
integrity: string;
|
|
157
|
+
diagnosticDefinition: DiagnosticDefinitionBody;
|
|
158
|
+
[key: string]: unknown;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const identityTag = z.string().regex(/^fnv1a64-jcs-v1:[0-9a-f]{16}$/);
|
|
162
|
+
export const diagnosticDefinitionBodySchema: z.ZodType<DiagnosticDefinitionBody> = z.object({
|
|
163
|
+
definition: normalizedDiagnosticDefinitionSchema as z.ZodType<NormalizedDiagnosticDefinitionIdentity>,
|
|
164
|
+
identities: z.object({
|
|
165
|
+
algorithm: z.literal("fnv1a64-jcs-v1"), formulaById: recordSchema(identityTag),
|
|
166
|
+
calculationByInstanceId: recordSchema(identityTag), definition: identityTag,
|
|
167
|
+
}).passthrough(),
|
|
168
|
+
}).passthrough();
|
|
169
|
+
|
|
170
|
+
export const diagnosticDefinitionDocSchema: z.ZodType<DiagnosticDefinitionDoc> = z.object({
|
|
171
|
+
...envelopeShape("diagnostic-definition"), diagnosticDefinition: diagnosticDefinitionBodySchema,
|
|
172
|
+
}).passthrough();
|
package/src/schemas/manifest.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { methodResultDocSchema, stochasticResultDocSchema } from "./result.js";
|
|
|
6
6
|
import { studyDocSchema } from "./study.js";
|
|
7
7
|
import { bundleDocSchema } from "./bundle.js";
|
|
8
8
|
import { crosscheckReportDocSchema } from "./crosscheck.js";
|
|
9
|
+
import { diagnosticDefinitionDocSchema } from "./diagnosticDefinition.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Schema publication manifest (spec 3.4). The zod schemas here are the
|
|
@@ -46,11 +47,16 @@ export const INTERCHANGE_SCHEMA_MANIFEST: readonly SchemaManifestEntry[] = [
|
|
|
46
47
|
fileName: "crosscheck-report.schema.json",
|
|
47
48
|
schema: crosscheckReportDocSchema,
|
|
48
49
|
},
|
|
50
|
+
{
|
|
51
|
+
kind: "diagnostic-definition",
|
|
52
|
+
fileName: "diagnostic-definition.schema.json",
|
|
53
|
+
schema: diagnosticDefinitionDocSchema,
|
|
54
|
+
},
|
|
49
55
|
];
|
|
50
56
|
|
|
51
57
|
/** Options both the emit script and the drift test must pass to the converter. */
|
|
52
58
|
export function emitOptionsFor(kind: DocumentKind): { name: string; [key: string]: unknown } {
|
|
53
|
-
return { name: kind, target: "jsonSchema7", $refStrategy: "
|
|
59
|
+
return { name: kind, target: "jsonSchema7", $refStrategy: "root" };
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export interface EmittedSchema {
|
|
@@ -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
|
+
}
|