@actuarial-ts/compliance 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ import {
2
+ canonicalJson,
3
+ fnv1a64,
4
+ isDiagnosticToken,
5
+ isRealIsoDate,
6
+ type AnalysisStatisticDefinition,
7
+ type CustomizationResourceLimits,
8
+ type HistoricalDatasetInput,
9
+ type JsonValue,
10
+ type PreparedHistoricalObservation,
11
+ } from "@actuarial-ts/core";
12
+ import {
13
+ externalSortCustomizationRecords,
14
+ parseCustomizationResourceLimits,
15
+ parseHistoricalDatasetInput,
16
+ streamPreparedHistoricalDataset,
17
+ type CustomizationExternalRecord,
18
+ type CustomizationExternalStateAdapter,
19
+ } from "@actuarial-ts/data";
20
+ import { ComplianceError } from "./errors.js";
21
+ import {
22
+ assertComputedDiagnosticArtifactDigest,
23
+ type ComputedDiagnosticArtifactDigest,
24
+ } from "./diagnosticArtifactStream.js";
25
+ import {
26
+ writeBoundedAnalysisReplay,
27
+ parseBoundedReplayMeasures,
28
+ type BoundedReplayMeasure,
29
+ } from "./customizationBoundedReplay.js";
30
+
31
+ type MergeableHistoryStatistic = Extract<
32
+ AnalysisStatisticDefinition,
33
+ {
34
+ readonly kind:
35
+ | "sum"
36
+ | "ratio"
37
+ | "weighted-mean"
38
+ | "minimum"
39
+ | "maximum";
40
+ }
41
+ >;
42
+
43
+ /** Statistics that can be calculated directly while normalized history is streamed. */
44
+ export interface BoundedHistoryReplayMeasure
45
+ extends Omit<BoundedReplayMeasure, "statistic" | "orderedPopulationSize"> {
46
+ readonly statistic: MergeableHistoryStatistic;
47
+ }
48
+
49
+ type HistoryMeasureRecord = {
50
+ readonly measures: Readonly<Record<string, number | null>>;
51
+ };
52
+
53
+ function selectedRecord(
54
+ observation: PreparedHistoricalObservation,
55
+ ): CustomizationExternalRecord<HistoryMeasureRecord & JsonValue> {
56
+ const claimKey = canonicalJson([
57
+ observation.sourceNamespace,
58
+ observation.claimId,
59
+ ]);
60
+ const identity = canonicalJson([
61
+ observation.sourceNamespace,
62
+ observation.claimId,
63
+ observation.evaluationDate,
64
+ ]);
65
+ return {
66
+ id: `selected-${fnv1a64(identity)}`,
67
+ partitionKey: claimKey,
68
+ orderKey: observation.evaluationDate,
69
+ value: { measures: observation.measures } as HistoryMeasureRecord & JsonValue,
70
+ };
71
+ }
72
+
73
+ function reviewFailure(code: string, message: string): never {
74
+ throw new ComplianceError(
75
+ "BAD_DIAGNOSTIC_RUN",
76
+ `Historical preparation review failed (${code}): ${message}`,
77
+ "$.boundedHistoryReplay",
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Connects raw versioned history to independently verifiable bounded replay.
83
+ *
84
+ * The function performs two host-owned external stages. The first resolves raw
85
+ * revisions one claim at a time. The second retains normalized selected
86
+ * records until preparation has completed without findings. No replay header
87
+ * is emitted before that review gate passes, so failed or cancelled work cannot
88
+ * be mistaken for a completed artifact.
89
+ *
90
+ * This path intentionally supports mergeable statistics only. Exact distinct
91
+ * counts and quantiles require their documented external ordering, while
92
+ * exposure joins and shared financial terms use their capability-specific
93
+ * execution paths.
94
+ */
95
+ export async function* writeBoundedHistoryAnalysisReplay(input: {
96
+ readonly runId: string;
97
+ readonly sourceArtifact: ComputedDiagnosticArtifactDigest;
98
+ readonly header: Omit<HistoricalDatasetInput, "records">;
99
+ readonly records: AsyncIterable<unknown> | Iterable<unknown>;
100
+ readonly evaluationDate: string;
101
+ readonly observationSelection: "exact" | "latest-on-or-before";
102
+ readonly measures: readonly BoundedHistoryReplayMeasure[];
103
+ readonly adapter: CustomizationExternalStateAdapter;
104
+ readonly limits: CustomizationResourceLimits;
105
+ readonly signal?: AbortSignal;
106
+ }): AsyncGenerator<Uint8Array> {
107
+ const runId = input.runId;
108
+ const sourceArtifact = input.sourceArtifact;
109
+ const records = input.records;
110
+ const adapter = input.adapter;
111
+ const signal = input.signal;
112
+ const evaluationDate = input.evaluationDate;
113
+ const observationSelection = input.observationSelection;
114
+ const limits = parseCustomizationResourceLimits(input.limits);
115
+ const parsedHeader = parseHistoricalDatasetInput({ ...input.header, records: [] });
116
+ const { records: _emptyRecords, ...header } = parsedHeader;
117
+ const parsedMeasures = parseBoundedReplayMeasures(input.measures, limits);
118
+ if (parsedMeasures.some((measure) => !["sum", "ratio", "weighted-mean", "minimum", "maximum"].includes(measure.statistic.kind)))
119
+ reviewFailure("unsupported-statistic", "Raw-history replay supports mergeable statistics only");
120
+ const measures = parsedMeasures as readonly BoundedHistoryReplayMeasure[];
121
+ const historyContext: JsonValue = JSON.parse(canonicalJson(header));
122
+ if (!isDiagnosticToken(runId))
123
+ reviewFailure("invalid-run", "runId must be a valid token");
124
+ assertComputedDiagnosticArtifactDigest(sourceArtifact);
125
+ if (!isRealIsoDate(evaluationDate))
126
+ reviewFailure("invalid-period", "evaluationDate must be a real Gregorian date");
127
+ if (observationSelection !== "exact" && observationSelection !== "latest-on-or-before")
128
+ reviewFailure("invalid-selection", "observationSelection is invalid");
129
+ async function* normalized(): AsyncGenerator<
130
+ CustomizationExternalRecord<HistoryMeasureRecord & JsonValue>
131
+ > {
132
+ let claimKey: string | undefined;
133
+ let claim: PreparedHistoricalObservation[] = [];
134
+ const selected = function* (): Generator<
135
+ CustomizationExternalRecord<HistoryMeasureRecord & JsonValue>
136
+ > {
137
+ const candidates = claim.filter((observation) =>
138
+ observationSelection === "exact"
139
+ ? observation.evaluationDate === evaluationDate
140
+ : observation.evaluationDate <= evaluationDate,
141
+ );
142
+ const observation = candidates.reduce<PreparedHistoricalObservation | undefined>(
143
+ (latest, candidate) =>
144
+ latest === undefined || candidate.evaluationDate > latest.evaluationDate
145
+ ? candidate
146
+ : latest,
147
+ undefined,
148
+ );
149
+ if (observation !== undefined) yield selectedRecord(observation);
150
+ claim = [];
151
+ };
152
+ for await (const event of streamPreparedHistoricalDataset({
153
+ runId: `${runId}-raw`,
154
+ header,
155
+ records,
156
+ adapter,
157
+ limits,
158
+ ...(signal === undefined ? {} : { signal }),
159
+ })) {
160
+ if (event.kind === "finding")
161
+ reviewFailure(event.finding.code, event.finding.message);
162
+ if (event.kind === "observation") {
163
+ const nextClaimKey = canonicalJson([
164
+ event.observation.sourceNamespace,
165
+ event.observation.claimId,
166
+ ]);
167
+ if (claimKey !== undefined && nextClaimKey !== claimKey) yield* selected();
168
+ claimKey = nextClaimKey;
169
+ claim.push(event.observation);
170
+ }
171
+ }
172
+ yield* selected();
173
+ }
174
+
175
+ const staged = externalSortCustomizationRecords({
176
+ runId: `${runId}-selected`,
177
+ records: normalized(),
178
+ adapter,
179
+ limits,
180
+ ...(signal === undefined ? {} : { signal }),
181
+ });
182
+ const iterator = staged[Symbol.asyncIterator]();
183
+
184
+ // Pulling the first record completes both preparation and the review gate.
185
+ // It retains at most one selected record in SDK memory.
186
+ const first = await iterator.next();
187
+ async function* reviewedRecords(): AsyncGenerator<
188
+ CustomizationExternalRecord<HistoryMeasureRecord & JsonValue>
189
+ > {
190
+ try {
191
+ if (!first.done) yield first.value;
192
+ if (!first.done) {
193
+ for (;;) {
194
+ const next = await iterator.next();
195
+ if (next.done) break;
196
+ yield next.value;
197
+ }
198
+ }
199
+ } finally {
200
+ await iterator.return?.(undefined);
201
+ }
202
+ }
203
+
204
+ try {
205
+ yield* writeBoundedAnalysisReplay({
206
+ runId,
207
+ sourceArtifact,
208
+ measures,
209
+ calculationContext: {
210
+ history: historyContext,
211
+ evaluationDate,
212
+ observationSelection,
213
+ },
214
+ records: reviewedRecords(),
215
+ limits,
216
+ ...(signal === undefined ? {} : { signal }),
217
+ });
218
+ } finally {
219
+ await iterator.return?.(undefined);
220
+ }
221
+ }
@@ -0,0 +1,113 @@
1
+ import {
2
+ canonicalJson,
3
+ fnv1a64,
4
+ isRealIsoDate,
5
+ runAnalysisRecipe,
6
+ type AnalysisDefinition,
7
+ type AnalysisRecipe,
8
+ type AnalysisResult,
9
+ type CustomizationExposureObservation,
10
+ type ExposureAllocationTarget,
11
+ type ExposureRevisionSelection,
12
+ type HistoricalDatasetInput,
13
+ } from "@actuarial-ts/core";
14
+ import {
15
+ parseAnalysisDefinition,
16
+ parseAnalysisRecipe,
17
+ parseAnalysisResult,
18
+ parseCustomizationExposureObservations,
19
+ parseExposureAllocationTargets,
20
+ parseExposureRevisionSelection,
21
+ parseHistoricalDatasetInput,
22
+ } from "@actuarial-ts/data";
23
+ import { ComplianceError } from "./errors.js";
24
+ import {
25
+ assertComputedDiagnosticArtifactDigest,
26
+ type ComputedDiagnosticArtifactDigest,
27
+ } from "./diagnosticArtifactStream.js";
28
+ import type { DiagnosticArtifactDigest } from "./diagnosticRun.js";
29
+
30
+ declare const verifiedCustomizationRunBrand: unique symbol;
31
+ export interface VerifiedCustomizationAnalysisRun {
32
+ readonly [verifiedCustomizationRunBrand]: true;
33
+ readonly assurance: "sdk-rerun-verified";
34
+ readonly runIdentity: string;
35
+ readonly result: AnalysisResult;
36
+ readonly inputArtifacts: readonly DiagnosticArtifactDigest[];
37
+ }
38
+
39
+ const verifiedRuns = new WeakSet<object>();
40
+
41
+ function invalid(message: string, path = "$.customizationRun"): never {
42
+ throw new ComplianceError("BAD_DIAGNOSTIC_RUN", message, path);
43
+ }
44
+
45
+ /**
46
+ * Revalidates and reruns a complete supported customization recipe, then issues
47
+ * an authentic receipt binding the exact output to the supplied source digests.
48
+ */
49
+ export function verifyCustomizationAnalysisRun(input: {
50
+ readonly history: HistoricalDatasetInput;
51
+ readonly definition: AnalysisDefinition;
52
+ readonly recipe: AnalysisRecipe;
53
+ readonly expectedResult: AnalysisResult;
54
+ readonly inputArtifacts: readonly ComputedDiagnosticArtifactDigest[];
55
+ readonly exposureObservations?: readonly CustomizationExposureObservation[];
56
+ readonly exposureTargets?: readonly ExposureAllocationTarget[];
57
+ readonly exposureRevisionSelection?: ExposureRevisionSelection;
58
+ readonly evaluationDate?: string;
59
+ }): VerifiedCustomizationAnalysisRun {
60
+ if (!Array.isArray(input.inputArtifacts) || input.inputArtifacts.length === 0)
61
+ invalid("Customization verification requires at least one authentic input artifact", "$.inputArtifacts");
62
+ const artifacts = [...input.inputArtifacts];
63
+ artifacts.forEach(assertComputedDiagnosticArtifactDigest);
64
+ if (new Set(artifacts.map((artifact) => artifact.id)).size !== artifacts.length)
65
+ invalid("Customization input artifact IDs must be unique", "$.inputArtifacts");
66
+ if (input.evaluationDate !== undefined && !isRealIsoDate(input.evaluationDate))
67
+ invalid("Customization evaluationDate must be a real Gregorian date", "$.evaluationDate");
68
+
69
+ const history = parseHistoricalDatasetInput(input.history);
70
+ const definition = parseAnalysisDefinition(input.definition);
71
+ const recipe = parseAnalysisRecipe(input.recipe);
72
+ const expected = parseAnalysisResult(input.expectedResult);
73
+ const exposureObservations = parseCustomizationExposureObservations(input.exposureObservations ?? []);
74
+ const exposureTargets = parseExposureAllocationTargets(input.exposureTargets ?? []);
75
+ const exposureRevisionSelection = input.exposureRevisionSelection === undefined
76
+ ? undefined
77
+ : parseExposureRevisionSelection(input.exposureRevisionSelection);
78
+ const result = runAnalysisRecipe({
79
+ history,
80
+ definition,
81
+ recipe,
82
+ exposureObservations,
83
+ exposureTargets,
84
+ ...(exposureRevisionSelection === undefined ? {} : { exposureRevisionSelection }),
85
+ ...(input.evaluationDate === undefined ? {} : { evaluationDate: input.evaluationDate }),
86
+ });
87
+ if (canonicalJson(result) !== canonicalJson(expected))
88
+ throw new ComplianceError(
89
+ "DIAGNOSTIC_MISMATCH",
90
+ "Customization result does not match an exact fresh SDK rerun",
91
+ "$.expectedResult",
92
+ );
93
+ const inputArtifacts = Object.freeze(artifacts) as readonly DiagnosticArtifactDigest[];
94
+ const runIdentity = `fnv1a64-jcs-v1:${fnv1a64(canonicalJson({
95
+ result,
96
+ inputArtifacts,
97
+ }))}`;
98
+ const receipt = Object.freeze({
99
+ assurance: "sdk-rerun-verified" as const,
100
+ runIdentity,
101
+ result,
102
+ inputArtifacts,
103
+ }) as VerifiedCustomizationAnalysisRun;
104
+ verifiedRuns.add(receipt);
105
+ return receipt;
106
+ }
107
+
108
+ export function assertVerifiedCustomizationAnalysisRun(
109
+ value: unknown,
110
+ ): asserts value is VerifiedCustomizationAnalysisRun {
111
+ if (value === null || typeof value !== "object" || !verifiedRuns.has(value))
112
+ invalid("Value is not an authentic SDK-rerun-verified customization run");
113
+ }
package/src/index.ts CHANGED
@@ -15,5 +15,8 @@ export {
15
15
  } from "./diagnosticCompactRun.js";
16
16
  export * from "./diagnosticReplayWriter.js";
17
17
  export * from "./diagnosticReplayReader.js";
18
+ export * from "./customizationBoundedReplay.js";
19
+ export * from "./customizationHistoryReplay.js";
20
+ export * from "./customizationRun.js";
18
21
  export * from "./bundle.js";
19
22
  export * from "./ave.js";
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export const COMPLIANCE_PACKAGE_VERSION = "0.7.0";
1
+ export const COMPLIANCE_PACKAGE_VERSION = "0.8.0";