@actuarial-ts/core 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 +12 -3
- package/dist/canonical.d.ts +11 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +136 -26
- package/dist/canonical.js.map +1 -1
- package/dist/capping.d.ts.map +1 -1
- package/dist/capping.js +39 -6
- package/dist/capping.js.map +1 -1
- package/dist/diagnosticAggregation.d.ts +3 -0
- package/dist/diagnosticAggregation.d.ts.map +1 -1
- package/dist/diagnosticAggregation.js +30 -7
- package/dist/diagnosticAggregation.js.map +1 -1
- package/dist/diagnosticDefinitions.d.ts +3 -0
- package/dist/diagnosticDefinitions.d.ts.map +1 -1
- package/dist/diagnosticDefinitions.js +844 -182
- package/dist/diagnosticDefinitions.js.map +1 -1
- package/dist/diagnosticDerivations.d.ts +23 -0
- package/dist/diagnosticDerivations.d.ts.map +1 -1
- package/dist/diagnosticDerivations.js +187 -43
- package/dist/diagnosticDerivations.js.map +1 -1
- package/dist/diagnosticEvidenceIntern.d.ts +16 -0
- package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
- package/dist/diagnosticEvidenceIntern.js +139 -0
- package/dist/diagnosticEvidenceIntern.js.map +1 -0
- package/dist/diagnosticExposure.d.ts +2 -2
- package/dist/diagnosticExposure.d.ts.map +1 -1
- package/dist/diagnosticExposure.js +319 -21
- package/dist/diagnosticExposure.js.map +1 -1
- package/dist/diagnosticExpressions.d.ts +1 -1
- package/dist/diagnosticExpressions.d.ts.map +1 -1
- package/dist/diagnosticExpressions.js +35 -15
- package/dist/diagnosticExpressions.js.map +1 -1
- package/dist/diagnosticFormulas.d.ts +3 -0
- package/dist/diagnosticFormulas.d.ts.map +1 -1
- package/dist/diagnosticFormulas.js +160 -26
- package/dist/diagnosticFormulas.js.map +1 -1
- package/dist/diagnosticIdentity.d.ts +15 -1
- package/dist/diagnosticIdentity.d.ts.map +1 -1
- package/dist/diagnosticIdentity.js +321 -51
- package/dist/diagnosticIdentity.js.map +1 -1
- package/dist/diagnosticIdentityStream.d.ts +56 -0
- package/dist/diagnosticIdentityStream.d.ts.map +1 -0
- package/dist/diagnosticIdentityStream.js +472 -0
- package/dist/diagnosticIdentityStream.js.map +1 -0
- package/dist/diagnosticOrdering.d.ts +8 -0
- package/dist/diagnosticOrdering.d.ts.map +1 -0
- package/dist/diagnosticOrdering.js +73 -0
- package/dist/diagnosticOrdering.js.map +1 -0
- package/dist/diagnosticPeriodAxis.d.ts +9 -0
- package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
- package/dist/diagnosticPeriodAxis.js +69 -0
- package/dist/diagnosticPeriodAxis.js.map +1 -0
- package/dist/diagnosticPeriods.d.ts +2 -3
- package/dist/diagnosticPeriods.d.ts.map +1 -1
- package/dist/diagnosticPeriods.js +20 -68
- package/dist/diagnosticPeriods.js.map +1 -1
- package/dist/diagnosticPreparation.d.ts +75 -14
- package/dist/diagnosticPreparation.d.ts.map +1 -1
- package/dist/diagnosticPreparation.js +1606 -112
- package/dist/diagnosticPreparation.js.map +1 -1
- package/dist/diagnosticReview.d.ts +40 -15
- package/dist/diagnosticReview.d.ts.map +1 -1
- package/dist/diagnosticReview.js +600 -42
- package/dist/diagnosticReview.js.map +1 -1
- package/dist/diagnosticReviewSources.d.ts +18 -0
- package/dist/diagnosticReviewSources.d.ts.map +1 -0
- package/dist/diagnosticReviewSources.js +76 -0
- package/dist/diagnosticReviewSources.js.map +1 -0
- package/dist/diagnosticReviewStore.d.ts +88 -0
- package/dist/diagnosticReviewStore.d.ts.map +1 -0
- package/dist/diagnosticReviewStore.js +455 -0
- package/dist/diagnosticReviewStore.js.map +1 -0
- package/dist/diagnosticRunner.d.ts +27 -2
- package/dist/diagnosticRunner.d.ts.map +1 -1
- package/dist/diagnosticRunner.js +764 -70
- package/dist/diagnosticRunner.js.map +1 -1
- package/dist/diagnosticRuntime.d.ts +26 -0
- package/dist/diagnosticRuntime.d.ts.map +1 -0
- package/dist/diagnosticRuntime.js +243 -0
- package/dist/diagnosticRuntime.js.map +1 -0
- package/dist/diagnosticSourceOrdering.d.ts +6 -0
- package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
- package/dist/diagnosticSourceOrdering.js +38 -0
- package/dist/diagnosticSourceOrdering.js.map +1 -0
- package/dist/fnvAccumulator.d.ts +6 -0
- package/dist/fnvAccumulator.d.ts.map +1 -0
- package/dist/fnvAccumulator.js +21 -0
- package/dist/fnvAccumulator.js.map +1 -0
- package/dist/index.d.ts +15 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -4
- package/dist/index.js.map +1 -1
- package/dist/mack.d.ts +4 -0
- package/dist/mack.d.ts.map +1 -1
- package/dist/mack.js +32 -4
- package/dist/mack.js.map +1 -1
- package/dist/triangle.d.ts.map +1 -1
- package/dist/triangle.js +59 -9
- package/dist/triangle.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +22 -8
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -2
- package/src/canonical.ts +144 -26
- package/src/capping.ts +109 -18
- package/src/diagnosticAggregation.ts +69 -20
- package/src/diagnosticDefinitions.ts +2675 -426
- package/src/diagnosticDerivations.ts +281 -49
- package/src/diagnosticEvidenceIntern.ts +151 -0
- package/src/diagnosticExposure.ts +550 -50
- package/src/diagnosticExpressions.ts +99 -17
- package/src/diagnosticFormulas.ts +239 -39
- package/src/diagnosticIdentity.ts +700 -191
- package/src/diagnosticIdentityStream.ts +553 -0
- package/src/diagnosticOrdering.ts +106 -0
- package/src/diagnosticPeriodAxis.ts +92 -0
- package/src/diagnosticPeriods.ts +67 -82
- package/src/diagnosticPreparation.ts +2558 -132
- package/src/diagnosticReview.ts +1072 -58
- package/src/diagnosticReviewSources.ts +85 -0
- package/src/diagnosticReviewStore.ts +692 -0
- package/src/diagnosticRunner.ts +1273 -72
- package/src/diagnosticRuntime.ts +264 -0
- package/src/diagnosticSourceOrdering.ts +50 -0
- package/src/fnvAccumulator.ts +20 -0
- package/src/index.ts +69 -4
- package/src/mack.ts +72 -9
- package/src/triangle.ts +103 -13
- package/src/types.ts +55 -20
- package/src/version.ts +1 -1
|
@@ -0,0 +1,692 @@
|
|
|
1
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
import type {
|
|
3
|
+
DiagnosticReviewRule,
|
|
4
|
+
DiagnosticSourceLocation,
|
|
5
|
+
} from "./diagnosticDefinitions.js";
|
|
6
|
+
import type { PreparedDiagnosticDataContent } from "./diagnosticPreparation.js";
|
|
7
|
+
import type {
|
|
8
|
+
DiagnosticReviewCoordinate,
|
|
9
|
+
DiagnosticReviewExpressionOverflow,
|
|
10
|
+
DiagnosticReviewRuleEvaluation,
|
|
11
|
+
DiagnosticReviewEvaluationScope,
|
|
12
|
+
} from "./diagnosticReview.js";
|
|
13
|
+
import type { DiagnosticRuleNotEvaluatedReason } from "./diagnosticRules.js";
|
|
14
|
+
import {
|
|
15
|
+
diagnosticJsonPreflight,
|
|
16
|
+
isDiagnosticPlainRecord,
|
|
17
|
+
isDiagnosticToken,
|
|
18
|
+
} from "./diagnosticRuntime.js";
|
|
19
|
+
import { DiagnosticValidationError } from "./types.js";
|
|
20
|
+
import {
|
|
21
|
+
createDiagnosticIdentityArray,
|
|
22
|
+
createDiagnosticIdentityObject,
|
|
23
|
+
createDiagnosticIdentityValue,
|
|
24
|
+
type DiagnosticIdentityDocument,
|
|
25
|
+
} from "./diagnosticIdentityStream.js";
|
|
26
|
+
|
|
27
|
+
export interface DiagnosticReviewStatusCounts {
|
|
28
|
+
readonly pass: number;
|
|
29
|
+
readonly warning: number;
|
|
30
|
+
readonly fail: number;
|
|
31
|
+
readonly notEvaluated: number;
|
|
32
|
+
}
|
|
33
|
+
export type DiagnosticReviewEffectiveStatus =
|
|
34
|
+
| "pass"
|
|
35
|
+
| "warning"
|
|
36
|
+
| "fail"
|
|
37
|
+
| "not-evaluated";
|
|
38
|
+
export interface CompactDiagnosticReviewRuleSummary {
|
|
39
|
+
readonly ruleId: string;
|
|
40
|
+
readonly start: number;
|
|
41
|
+
readonly count: number;
|
|
42
|
+
readonly summary: DiagnosticReviewStatusCounts;
|
|
43
|
+
}
|
|
44
|
+
declare const compactReviewBrand: unique symbol;
|
|
45
|
+
/** Opaque owned storage. No typed-array buffer or mutable dictionary is exposed. */
|
|
46
|
+
export interface CompactDiagnosticReviewEvaluations {
|
|
47
|
+
readonly [compactReviewBrand]: true;
|
|
48
|
+
readonly count: number;
|
|
49
|
+
readonly summary: DiagnosticReviewStatusCounts;
|
|
50
|
+
readonly rules: readonly CompactDiagnosticReviewRuleSummary[];
|
|
51
|
+
}
|
|
52
|
+
export interface DiagnosticReviewEvaluationQuery {
|
|
53
|
+
readonly ruleId?: string;
|
|
54
|
+
readonly effectiveStatus?: DiagnosticReviewEffectiveStatus;
|
|
55
|
+
/** Coordinate filters match either endpoint of a valuation pair; never control-total metadata. */
|
|
56
|
+
readonly sourceGroup?: string;
|
|
57
|
+
readonly origin?: string;
|
|
58
|
+
readonly valuation?: string;
|
|
59
|
+
readonly offset?: number;
|
|
60
|
+
readonly limit?: number;
|
|
61
|
+
}
|
|
62
|
+
export interface DiagnosticReviewSourceQuery {
|
|
63
|
+
readonly offset?: number;
|
|
64
|
+
readonly limit?: number;
|
|
65
|
+
/** Omit to read scope sources; otherwise read this expression overflow's sources. */
|
|
66
|
+
readonly overflowIndex?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface DiagnosticReviewPage<T> {
|
|
69
|
+
readonly total: number;
|
|
70
|
+
readonly offset: number;
|
|
71
|
+
readonly items: readonly T[];
|
|
72
|
+
readonly nextOffset: number | null;
|
|
73
|
+
}
|
|
74
|
+
type ScopeSummary = DiagnosticReviewEvaluationScope extends infer S
|
|
75
|
+
? S extends DiagnosticReviewEvaluationScope
|
|
76
|
+
? Omit<S, "sources"> & { readonly sourceCount: number }
|
|
77
|
+
: never
|
|
78
|
+
: never;
|
|
79
|
+
export interface DiagnosticReviewEvaluationSummary {
|
|
80
|
+
readonly index: number;
|
|
81
|
+
readonly ruleId: string;
|
|
82
|
+
readonly ruleKind: DiagnosticReviewRule["kind"];
|
|
83
|
+
readonly status: DiagnosticReviewRuleEvaluation["status"];
|
|
84
|
+
readonly effectiveStatus: DiagnosticReviewEffectiveStatus;
|
|
85
|
+
readonly severity: "warning" | "fail";
|
|
86
|
+
readonly left: number | null;
|
|
87
|
+
readonly right: number | null;
|
|
88
|
+
readonly relation: DiagnosticReviewRuleEvaluation["relation"];
|
|
89
|
+
readonly triggerReason: DiagnosticReviewRuleEvaluation["triggerReason"];
|
|
90
|
+
readonly notEvaluatedReasons: readonly DiagnosticRuleNotEvaluatedReason[];
|
|
91
|
+
readonly expressionOverflowCount: number;
|
|
92
|
+
readonly scope: ScopeSummary;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const CHUNK_SIZE = 4096;
|
|
96
|
+
const reasons: readonly DiagnosticRuleNotEvaluatedReason[] = [
|
|
97
|
+
"missing",
|
|
98
|
+
"imputed",
|
|
99
|
+
"non-finite",
|
|
100
|
+
"structural-ambiguity",
|
|
101
|
+
"aggregation-overflow",
|
|
102
|
+
"expression-overflow",
|
|
103
|
+
"tolerance-overflow",
|
|
104
|
+
];
|
|
105
|
+
const statuses = ["pass", "triggered", "not-evaluated"] as const;
|
|
106
|
+
const relations = [null, "less", "equal", "greater"] as const;
|
|
107
|
+
const triggers = [
|
|
108
|
+
null,
|
|
109
|
+
"predicate",
|
|
110
|
+
"missing-input",
|
|
111
|
+
"aggregation-overflow",
|
|
112
|
+
"expression-overflow",
|
|
113
|
+
"tolerance-overflow",
|
|
114
|
+
] as const;
|
|
115
|
+
type Control = Extract<
|
|
116
|
+
DiagnosticReviewEvaluationScope,
|
|
117
|
+
{ kind: "control-total" }
|
|
118
|
+
>;
|
|
119
|
+
interface EncodedOverflow {
|
|
120
|
+
path: string;
|
|
121
|
+
coordinate: number | null;
|
|
122
|
+
sources: number;
|
|
123
|
+
}
|
|
124
|
+
interface Columns {
|
|
125
|
+
left: Float64Array;
|
|
126
|
+
right: Float64Array;
|
|
127
|
+
rule: Uint32Array;
|
|
128
|
+
first: Uint32Array;
|
|
129
|
+
second: Uint32Array;
|
|
130
|
+
sources: Uint32Array;
|
|
131
|
+
flags: Uint16Array;
|
|
132
|
+
reasons: Uint8Array;
|
|
133
|
+
}
|
|
134
|
+
interface State {
|
|
135
|
+
count: number;
|
|
136
|
+
chunks: Columns[];
|
|
137
|
+
rules: PreparedDiagnosticDataContent["definition"]["definition"]["reviewRules"];
|
|
138
|
+
coordinates: readonly DiagnosticReviewCoordinate[];
|
|
139
|
+
sourceLocations: readonly DiagnosticSourceLocation[];
|
|
140
|
+
sourceLists: readonly Uint32Array[];
|
|
141
|
+
overflows: ReadonlyMap<number, readonly EncodedOverflow[]>;
|
|
142
|
+
controls: ReadonlyMap<number, Omit<Control, "sources">>;
|
|
143
|
+
}
|
|
144
|
+
const states = new WeakMap<object, State>();
|
|
145
|
+
/** Private index guard: reject capacity overflow before any Uint32 assignment. */
|
|
146
|
+
export function assertCompactReviewCapacity(count: number, addition = 1): void {
|
|
147
|
+
if (
|
|
148
|
+
!Number.isSafeInteger(count) ||
|
|
149
|
+
!Number.isSafeInteger(addition) ||
|
|
150
|
+
count < 0 ||
|
|
151
|
+
addition < 0 ||
|
|
152
|
+
count + addition > 0xffff_ffff
|
|
153
|
+
) {
|
|
154
|
+
throw new DiagnosticValidationError([
|
|
155
|
+
{
|
|
156
|
+
domain: "input",
|
|
157
|
+
code: "expression-limit",
|
|
158
|
+
path: "$",
|
|
159
|
+
message: "Compact review index capacity exceeded",
|
|
160
|
+
},
|
|
161
|
+
]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const zeroCounts = () => ({ pass: 0, warning: 0, fail: 0, notEvaluated: 0 });
|
|
165
|
+
function invalid(message: string, path = "$"): never {
|
|
166
|
+
throw new DiagnosticValidationError([
|
|
167
|
+
{ domain: "input", code: "invalid-input-relationship", path, message },
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
function stateOf(value: CompactDiagnosticReviewEvaluations): State {
|
|
171
|
+
if (value === null || typeof value !== "object" || !states.has(value))
|
|
172
|
+
invalid("Value is not an authentic compact diagnostic review store");
|
|
173
|
+
return states.get(value)!;
|
|
174
|
+
}
|
|
175
|
+
export function assertCompactDiagnosticReviewEvaluations(
|
|
176
|
+
value: unknown,
|
|
177
|
+
): asserts value is CompactDiagnosticReviewEvaluations {
|
|
178
|
+
stateOf(value as CompactDiagnosticReviewEvaluations);
|
|
179
|
+
}
|
|
180
|
+
function checkedIndex(state: State, index: number): void {
|
|
181
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= state.count)
|
|
182
|
+
invalid("Evaluation index is outside this review", "$.index");
|
|
183
|
+
}
|
|
184
|
+
function emptyChunk(): Columns {
|
|
185
|
+
return {
|
|
186
|
+
left: new Float64Array(CHUNK_SIZE),
|
|
187
|
+
right: new Float64Array(CHUNK_SIZE),
|
|
188
|
+
rule: new Uint32Array(CHUNK_SIZE),
|
|
189
|
+
first: new Uint32Array(CHUNK_SIZE),
|
|
190
|
+
second: new Uint32Array(CHUNK_SIZE),
|
|
191
|
+
sources: new Uint32Array(CHUNK_SIZE),
|
|
192
|
+
flags: new Uint16Array(CHUNK_SIZE),
|
|
193
|
+
reasons: new Uint8Array(CHUNK_SIZE),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function effective(
|
|
197
|
+
status: DiagnosticReviewRuleEvaluation["status"],
|
|
198
|
+
severity: "warning" | "fail",
|
|
199
|
+
overflowCount: number,
|
|
200
|
+
): DiagnosticReviewEffectiveStatus {
|
|
201
|
+
return overflowCount > 0
|
|
202
|
+
? "fail"
|
|
203
|
+
: status === "triggered"
|
|
204
|
+
? severity
|
|
205
|
+
: status;
|
|
206
|
+
}
|
|
207
|
+
function increment(
|
|
208
|
+
counts: ReturnType<typeof zeroCounts>,
|
|
209
|
+
status: DiagnosticReviewEffectiveStatus,
|
|
210
|
+
): void {
|
|
211
|
+
counts[status === "not-evaluated" ? "notEvaluated" : status]++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Private constructor, used only by the authenticated core evaluator. */
|
|
215
|
+
export function createCompactReviewBuilder(
|
|
216
|
+
rules: PreparedDiagnosticDataContent["definition"]["definition"]["reviewRules"],
|
|
217
|
+
sourceOwnership?: {
|
|
218
|
+
sourceKey(value: DiagnosticSourceLocation): string | undefined;
|
|
219
|
+
ownsList(value: readonly DiagnosticSourceLocation[]): boolean;
|
|
220
|
+
},
|
|
221
|
+
) {
|
|
222
|
+
const chunks: Columns[] = [];
|
|
223
|
+
const coordinates: DiagnosticReviewCoordinate[] = [];
|
|
224
|
+
const sourceLocations: DiagnosticSourceLocation[] = [];
|
|
225
|
+
const sourceLists: Uint32Array[] = [];
|
|
226
|
+
const coordinateIds = new Map<string, number>();
|
|
227
|
+
const sourceIds = new Map<string, number>();
|
|
228
|
+
// Only this evaluator invocation's snapshots may skip canonicalization.
|
|
229
|
+
// Merely freezing a caller array/object does not prove deep snapshot ownership.
|
|
230
|
+
const ownedSourceIds = new WeakMap<object, number>();
|
|
231
|
+
const ownedListIds = new WeakMap<readonly DiagnosticSourceLocation[], number>();
|
|
232
|
+
const listIds = new Map<string, number>();
|
|
233
|
+
const overflows = new Map<number, readonly EncodedOverflow[]>();
|
|
234
|
+
const controls = new Map<number, Omit<Control, "sources">>();
|
|
235
|
+
const ruleIds = new Map(rules.map((rule, index) => [rule.id, index]));
|
|
236
|
+
const ruleCounts = rules.map(() => zeroCounts());
|
|
237
|
+
const counts = zeroCounts();
|
|
238
|
+
let count = 0;
|
|
239
|
+
let finished = false;
|
|
240
|
+
const coordinateId = (value: DiagnosticReviewCoordinate): number => {
|
|
241
|
+
const key = JSON.stringify([
|
|
242
|
+
value.sourceGroup,
|
|
243
|
+
value.origin,
|
|
244
|
+
value.valuation,
|
|
245
|
+
Object.is(value.developmentAge, -0) ? "-0" : value.developmentAge,
|
|
246
|
+
value.ageUnit,
|
|
247
|
+
]);
|
|
248
|
+
let id = coordinateIds.get(key);
|
|
249
|
+
if (id === undefined) {
|
|
250
|
+
assertCompactReviewCapacity(coordinates.length);
|
|
251
|
+
id = coordinates.length;
|
|
252
|
+
coordinateIds.set(key, id);
|
|
253
|
+
coordinates.push(Object.freeze({ ...value }));
|
|
254
|
+
}
|
|
255
|
+
return id;
|
|
256
|
+
};
|
|
257
|
+
const sourceListId = (
|
|
258
|
+
values: readonly DiagnosticSourceLocation[],
|
|
259
|
+
): number => {
|
|
260
|
+
assertCompactReviewCapacity(values.length, 0);
|
|
261
|
+
const ownedList = sourceOwnership?.ownsList(values) ?? false;
|
|
262
|
+
const knownList = ownedList ? ownedListIds.get(values) : undefined;
|
|
263
|
+
if (knownList !== undefined) return knownList;
|
|
264
|
+
const ids = values.map((value) => {
|
|
265
|
+
const ownedKey = sourceOwnership?.sourceKey(value);
|
|
266
|
+
const owned = ownedKey !== undefined;
|
|
267
|
+
const known = owned ? ownedSourceIds.get(value) : undefined;
|
|
268
|
+
if (known !== undefined) return known;
|
|
269
|
+
const key = ownedKey ?? canonicalJson(value);
|
|
270
|
+
let id = sourceIds.get(key);
|
|
271
|
+
if (id === undefined) {
|
|
272
|
+
assertCompactReviewCapacity(sourceLocations.length);
|
|
273
|
+
id = sourceLocations.length;
|
|
274
|
+
sourceIds.set(key, id);
|
|
275
|
+
sourceLocations.push(Object.freeze({ ...value }));
|
|
276
|
+
}
|
|
277
|
+
if (owned) ownedSourceIds.set(value, id);
|
|
278
|
+
return id;
|
|
279
|
+
});
|
|
280
|
+
// Do not construct a giant signature for high-fanout control-total evidence.
|
|
281
|
+
const key = ids.length <= 64 ? ids.join(",") : undefined;
|
|
282
|
+
let id = key === undefined ? undefined : listIds.get(key);
|
|
283
|
+
if (id === undefined) {
|
|
284
|
+
assertCompactReviewCapacity(sourceLists.length);
|
|
285
|
+
id = sourceLists.length;
|
|
286
|
+
if (key !== undefined) listIds.set(key, id);
|
|
287
|
+
sourceLists.push(Uint32Array.from(ids));
|
|
288
|
+
}
|
|
289
|
+
if (ownedList) ownedListIds.set(values, id);
|
|
290
|
+
return id;
|
|
291
|
+
};
|
|
292
|
+
return {
|
|
293
|
+
append(value: DiagnosticReviewRuleEvaluation): void {
|
|
294
|
+
if (finished) throw new Error("Compact review construction has finished");
|
|
295
|
+
assertCompactReviewCapacity(count);
|
|
296
|
+
const row = count % CHUNK_SIZE;
|
|
297
|
+
if (row === 0) chunks.push(emptyChunk());
|
|
298
|
+
const chunk = chunks[chunks.length - 1]!;
|
|
299
|
+
const rule = ruleIds.get(value.ruleId)!;
|
|
300
|
+
chunk.rule[row] = rule;
|
|
301
|
+
chunk.left[row] = value.left ?? 0;
|
|
302
|
+
chunk.right[row] = value.right ?? 0;
|
|
303
|
+
chunk.flags[row] =
|
|
304
|
+
statuses.indexOf(value.status) |
|
|
305
|
+
(relations.indexOf(value.relation) << 2) |
|
|
306
|
+
(triggers.indexOf(value.triggerReason) << 4) |
|
|
307
|
+
(value.left === null ? 128 : 0) |
|
|
308
|
+
(value.right === null ? 256 : 0);
|
|
309
|
+
chunk.reasons[row] = value.notEvaluatedReasons.reduce(
|
|
310
|
+
(mask, reason) => mask | (1 << reasons.indexOf(reason)),
|
|
311
|
+
0,
|
|
312
|
+
);
|
|
313
|
+
chunk.sources[row] = sourceListId(value.scope.sources);
|
|
314
|
+
if (value.scope.kind === "cell")
|
|
315
|
+
chunk.first[row] = coordinateId(value.scope.cell);
|
|
316
|
+
else if (value.scope.kind === "valuation-pair") {
|
|
317
|
+
chunk.first[row] = coordinateId(value.scope.previous);
|
|
318
|
+
chunk.second[row] = coordinateId(value.scope.current);
|
|
319
|
+
} else {
|
|
320
|
+
const { sources: _sources, ...control } = value.scope;
|
|
321
|
+
controls.set(count, Object.freeze(control));
|
|
322
|
+
}
|
|
323
|
+
if (value.expressionOverflows.length)
|
|
324
|
+
overflows.set(
|
|
325
|
+
count,
|
|
326
|
+
value.expressionOverflows.map((item) => ({
|
|
327
|
+
path: item.expressionPath,
|
|
328
|
+
coordinate:
|
|
329
|
+
item.coordinate === null ? null : coordinateId(item.coordinate),
|
|
330
|
+
sources: sourceListId(item.sources),
|
|
331
|
+
})),
|
|
332
|
+
);
|
|
333
|
+
const status = effective(
|
|
334
|
+
value.status,
|
|
335
|
+
value.severity,
|
|
336
|
+
value.expressionOverflows.length,
|
|
337
|
+
);
|
|
338
|
+
increment(counts, status);
|
|
339
|
+
increment(ruleCounts[rule]!, status);
|
|
340
|
+
count++;
|
|
341
|
+
},
|
|
342
|
+
finish(): CompactDiagnosticReviewEvaluations {
|
|
343
|
+
if (finished) throw new Error("Compact review construction has finished");
|
|
344
|
+
finished = true;
|
|
345
|
+
let start = 0;
|
|
346
|
+
const summaries = rules.map((rule, index) => {
|
|
347
|
+
const summary = Object.freeze(ruleCounts[index]!);
|
|
348
|
+
const length =
|
|
349
|
+
summary.pass + summary.warning + summary.fail + summary.notEvaluated;
|
|
350
|
+
const result = Object.freeze({
|
|
351
|
+
ruleId: rule.id,
|
|
352
|
+
start,
|
|
353
|
+
count: length,
|
|
354
|
+
summary,
|
|
355
|
+
});
|
|
356
|
+
start += length;
|
|
357
|
+
return result;
|
|
358
|
+
});
|
|
359
|
+
const handle = Object.freeze({
|
|
360
|
+
count,
|
|
361
|
+
summary: Object.freeze(counts),
|
|
362
|
+
rules: Object.freeze(summaries),
|
|
363
|
+
}) as unknown as CompactDiagnosticReviewEvaluations;
|
|
364
|
+
states.set(handle, {
|
|
365
|
+
count,
|
|
366
|
+
chunks,
|
|
367
|
+
rules,
|
|
368
|
+
coordinates,
|
|
369
|
+
sourceLocations,
|
|
370
|
+
sourceLists,
|
|
371
|
+
overflows,
|
|
372
|
+
controls,
|
|
373
|
+
});
|
|
374
|
+
return handle;
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function sourceValues(
|
|
380
|
+
state: State,
|
|
381
|
+
id: number,
|
|
382
|
+
): readonly DiagnosticSourceLocation[] {
|
|
383
|
+
return Object.freeze(
|
|
384
|
+
Array.from(
|
|
385
|
+
state.sourceLists[id]!,
|
|
386
|
+
(index) => state.sourceLocations[index]!,
|
|
387
|
+
),
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
function summaryAt(
|
|
391
|
+
state: State,
|
|
392
|
+
index: number,
|
|
393
|
+
): DiagnosticReviewEvaluationSummary {
|
|
394
|
+
const chunk = state.chunks[Math.floor(index / CHUNK_SIZE)]!;
|
|
395
|
+
const row = index % CHUNK_SIZE;
|
|
396
|
+
const rule = state.rules[chunk.rule[row]!]!;
|
|
397
|
+
const flags = chunk.flags[row]!;
|
|
398
|
+
const status = statuses[flags & 3]!;
|
|
399
|
+
const overflowCount = state.overflows.get(index)?.length ?? 0;
|
|
400
|
+
const sourceCount = state.sourceLists[chunk.sources[row]!]!.length;
|
|
401
|
+
const scope: ScopeSummary =
|
|
402
|
+
rule.kind === "monotonic"
|
|
403
|
+
? {
|
|
404
|
+
kind: "valuation-pair",
|
|
405
|
+
previous: state.coordinates[chunk.first[row]!]!,
|
|
406
|
+
current: state.coordinates[chunk.second[row]!]!,
|
|
407
|
+
sourceCount,
|
|
408
|
+
}
|
|
409
|
+
: rule.kind === "control-total"
|
|
410
|
+
? { ...state.controls.get(index)!, sourceCount }
|
|
411
|
+
: {
|
|
412
|
+
kind: "cell",
|
|
413
|
+
cell: state.coordinates[chunk.first[row]!]!,
|
|
414
|
+
sourceCount,
|
|
415
|
+
};
|
|
416
|
+
return Object.freeze({
|
|
417
|
+
index,
|
|
418
|
+
ruleId: rule.id,
|
|
419
|
+
ruleKind: rule.kind,
|
|
420
|
+
status,
|
|
421
|
+
severity: rule.severity,
|
|
422
|
+
effectiveStatus: effective(status, rule.severity, overflowCount),
|
|
423
|
+
left: flags & 128 ? null : chunk.left[row]!,
|
|
424
|
+
right: flags & 256 ? null : chunk.right[row]!,
|
|
425
|
+
relation: relations[(flags >> 2) & 3]!,
|
|
426
|
+
triggerReason: triggers[(flags >> 4) & 7]!,
|
|
427
|
+
notEvaluatedReasons: Object.freeze(
|
|
428
|
+
reasons.filter((_, bit) => (chunk.reasons[row]! & (1 << bit)) !== 0),
|
|
429
|
+
),
|
|
430
|
+
expressionOverflowCount: overflowCount,
|
|
431
|
+
scope: Object.freeze(scope),
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
export function getDiagnosticReviewEvaluationSummary(
|
|
435
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
436
|
+
index: number,
|
|
437
|
+
): DiagnosticReviewEvaluationSummary {
|
|
438
|
+
const state = stateOf(store);
|
|
439
|
+
checkedIndex(state, index);
|
|
440
|
+
return summaryAt(state, index);
|
|
441
|
+
}
|
|
442
|
+
/** Explicitly materializes one complete legacy-shaped evaluation, including all sources. */
|
|
443
|
+
export function getDiagnosticReviewEvaluation(
|
|
444
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
445
|
+
index: number,
|
|
446
|
+
): DiagnosticReviewRuleEvaluation {
|
|
447
|
+
const state = stateOf(store);
|
|
448
|
+
checkedIndex(state, index);
|
|
449
|
+
const {
|
|
450
|
+
index: _index,
|
|
451
|
+
effectiveStatus: _effective,
|
|
452
|
+
expressionOverflowCount: _count,
|
|
453
|
+
scope,
|
|
454
|
+
...base
|
|
455
|
+
} = summaryAt(state, index);
|
|
456
|
+
const { sourceCount: _sourceCount, ...coordinateScope } = scope;
|
|
457
|
+
const chunk = state.chunks[Math.floor(index / CHUNK_SIZE)]!;
|
|
458
|
+
const row = index % CHUNK_SIZE;
|
|
459
|
+
const rule = state.rules[chunk.rule[row]!]!;
|
|
460
|
+
const expressionOverflows: readonly DiagnosticReviewExpressionOverflow[] =
|
|
461
|
+
Object.freeze(
|
|
462
|
+
(state.overflows.get(index) ?? []).map((item) =>
|
|
463
|
+
Object.freeze({
|
|
464
|
+
expressionPath: item.path,
|
|
465
|
+
coordinate:
|
|
466
|
+
item.coordinate === null
|
|
467
|
+
? null
|
|
468
|
+
: state.coordinates[item.coordinate]!,
|
|
469
|
+
sources: sourceValues(state, item.sources),
|
|
470
|
+
}),
|
|
471
|
+
),
|
|
472
|
+
);
|
|
473
|
+
return Object.freeze({
|
|
474
|
+
...base,
|
|
475
|
+
scope: Object.freeze({
|
|
476
|
+
...coordinateScope,
|
|
477
|
+
sources: sourceValues(state, chunk.sources[row]!),
|
|
478
|
+
}),
|
|
479
|
+
expressionOverflows,
|
|
480
|
+
...(rule.kind === "layer-order"
|
|
481
|
+
? { comparability: rule.comparability }
|
|
482
|
+
: {}),
|
|
483
|
+
}) as DiagnosticReviewRuleEvaluation;
|
|
484
|
+
}
|
|
485
|
+
/** Repeatable, complete, rule-major order. Retains at most the currently yielded DTO. */
|
|
486
|
+
export function iterateDiagnosticReviewEvaluations(
|
|
487
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
488
|
+
): IterableIterator<DiagnosticReviewRuleEvaluation> {
|
|
489
|
+
const state = stateOf(store);
|
|
490
|
+
return (function* () {
|
|
491
|
+
for (let index = 0; index < state.count; index++)
|
|
492
|
+
yield getDiagnosticReviewEvaluation(store, index);
|
|
493
|
+
})();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Owner-backed complete identity; neither evaluations nor source arrays are expanded. */
|
|
497
|
+
export function getCompactDiagnosticReviewEvaluationsIdentityDocument(
|
|
498
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
499
|
+
): DiagnosticIdentityDocument {
|
|
500
|
+
const state = stateOf(store);
|
|
501
|
+
const sources = (id: number) => {
|
|
502
|
+
const values = state.sourceLists[id]!;
|
|
503
|
+
return createDiagnosticIdentityArray(values.length, (index) =>
|
|
504
|
+
createDiagnosticIdentityValue(state.sourceLocations[values[index]!]!),
|
|
505
|
+
);
|
|
506
|
+
};
|
|
507
|
+
return createDiagnosticIdentityArray(state.count, (index) => {
|
|
508
|
+
const {
|
|
509
|
+
index: _index,
|
|
510
|
+
effectiveStatus: _effective,
|
|
511
|
+
expressionOverflowCount: _count,
|
|
512
|
+
scope,
|
|
513
|
+
...base
|
|
514
|
+
} = summaryAt(state, index);
|
|
515
|
+
const { sourceCount: _sources, ...scopeValues } = scope;
|
|
516
|
+
const chunk = state.chunks[Math.floor(index / CHUNK_SIZE)]!;
|
|
517
|
+
const row = index % CHUNK_SIZE;
|
|
518
|
+
const rule = state.rules[chunk.rule[row]!]!;
|
|
519
|
+
const overflow = state.overflows.get(index) ?? [];
|
|
520
|
+
return createDiagnosticIdentityObject({
|
|
521
|
+
...Object.fromEntries(
|
|
522
|
+
Object.entries(base).map(([key, value]) => [
|
|
523
|
+
key,
|
|
524
|
+
createDiagnosticIdentityValue(value),
|
|
525
|
+
]),
|
|
526
|
+
),
|
|
527
|
+
scope: createDiagnosticIdentityObject({
|
|
528
|
+
...Object.fromEntries(
|
|
529
|
+
Object.entries(scopeValues).map(([key, value]) => [
|
|
530
|
+
key,
|
|
531
|
+
createDiagnosticIdentityValue(value),
|
|
532
|
+
]),
|
|
533
|
+
),
|
|
534
|
+
sources: sources(chunk.sources[row]!),
|
|
535
|
+
}),
|
|
536
|
+
expressionOverflows: createDiagnosticIdentityArray(
|
|
537
|
+
overflow.length,
|
|
538
|
+
(overflowIndex) => {
|
|
539
|
+
const item = overflow[overflowIndex]!;
|
|
540
|
+
return createDiagnosticIdentityObject({
|
|
541
|
+
expressionPath: createDiagnosticIdentityValue(item.path),
|
|
542
|
+
coordinate: createDiagnosticIdentityValue(
|
|
543
|
+
item.coordinate === null
|
|
544
|
+
? null
|
|
545
|
+
: state.coordinates[item.coordinate]!,
|
|
546
|
+
),
|
|
547
|
+
sources: sources(item.sources),
|
|
548
|
+
});
|
|
549
|
+
},
|
|
550
|
+
),
|
|
551
|
+
...(rule.kind === "layer-order"
|
|
552
|
+
? { comparability: createDiagnosticIdentityValue(rule.comparability) }
|
|
553
|
+
: {}),
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
function checkedQuery<T extends object>(
|
|
558
|
+
query: T,
|
|
559
|
+
allowed: readonly string[],
|
|
560
|
+
): T & { offset: number; limit: number } {
|
|
561
|
+
const issues = diagnosticJsonPreflight(query, "input");
|
|
562
|
+
if (issues.length) throw new DiagnosticValidationError(issues);
|
|
563
|
+
if (
|
|
564
|
+
!isDiagnosticPlainRecord(query) ||
|
|
565
|
+
Object.keys(query).some((key) => !allowed.includes(key))
|
|
566
|
+
)
|
|
567
|
+
invalid("Invalid review page query");
|
|
568
|
+
const offset = query.offset ?? 0;
|
|
569
|
+
const limit = query.limit ?? 100;
|
|
570
|
+
if (typeof offset !== "number" || !Number.isSafeInteger(offset) || offset < 0)
|
|
571
|
+
invalid("Offset must be a nonnegative safe integer", "$.offset");
|
|
572
|
+
if (
|
|
573
|
+
typeof limit !== "number" ||
|
|
574
|
+
!Number.isSafeInteger(limit) ||
|
|
575
|
+
limit < 1 ||
|
|
576
|
+
limit > 1000
|
|
577
|
+
)
|
|
578
|
+
invalid("Page size must be between 1 and 1000", "$.limit");
|
|
579
|
+
return { ...query, offset, limit };
|
|
580
|
+
}
|
|
581
|
+
function page<T>(
|
|
582
|
+
items: T[],
|
|
583
|
+
total: number,
|
|
584
|
+
offset: number,
|
|
585
|
+
): DiagnosticReviewPage<T> {
|
|
586
|
+
return Object.freeze({
|
|
587
|
+
total,
|
|
588
|
+
offset,
|
|
589
|
+
items: Object.freeze(items),
|
|
590
|
+
nextOffset: offset + items.length < total ? offset + items.length : null,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
export function pageDiagnosticReviewEvaluations(
|
|
594
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
595
|
+
query: DiagnosticReviewEvaluationQuery = {},
|
|
596
|
+
): DiagnosticReviewPage<DiagnosticReviewEvaluationSummary> {
|
|
597
|
+
const state = stateOf(store);
|
|
598
|
+
const checked = checkedQuery(query, [
|
|
599
|
+
"ruleId",
|
|
600
|
+
"effectiveStatus",
|
|
601
|
+
"sourceGroup",
|
|
602
|
+
"origin",
|
|
603
|
+
"valuation",
|
|
604
|
+
"offset",
|
|
605
|
+
"limit",
|
|
606
|
+
]);
|
|
607
|
+
for (const key of ["ruleId", "sourceGroup", "origin", "valuation"] as const)
|
|
608
|
+
if (checked[key] !== undefined && !isDiagnosticToken(checked[key]))
|
|
609
|
+
invalid(`Invalid ${key} filter`, `$.${key}`);
|
|
610
|
+
if (
|
|
611
|
+
checked.effectiveStatus !== undefined &&
|
|
612
|
+
!["pass", "warning", "fail", "not-evaluated"].includes(
|
|
613
|
+
checked.effectiveStatus,
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
invalid("Invalid effective status", "$.effectiveStatus");
|
|
617
|
+
let total = 0;
|
|
618
|
+
const items: DiagnosticReviewEvaluationSummary[] = [];
|
|
619
|
+
const range =
|
|
620
|
+
checked.ruleId === undefined
|
|
621
|
+
? { start: 0, count: state.count }
|
|
622
|
+
: store.rules.find((rule) => rule.ruleId === checked.ruleId);
|
|
623
|
+
if (!range) return page(items, 0, checked.offset);
|
|
624
|
+
const coordinateFilter =
|
|
625
|
+
checked.sourceGroup !== undefined ||
|
|
626
|
+
checked.origin !== undefined ||
|
|
627
|
+
checked.valuation !== undefined;
|
|
628
|
+
const matches = (coordinate: DiagnosticReviewCoordinate) =>
|
|
629
|
+
(checked.sourceGroup === undefined ||
|
|
630
|
+
coordinate.sourceGroup === checked.sourceGroup) &&
|
|
631
|
+
(checked.origin === undefined || coordinate.origin === checked.origin) &&
|
|
632
|
+
(checked.valuation === undefined ||
|
|
633
|
+
coordinate.valuation === checked.valuation);
|
|
634
|
+
for (let index = range.start; index < range.start + range.count; index++) {
|
|
635
|
+
const chunk = state.chunks[Math.floor(index / CHUNK_SIZE)]!;
|
|
636
|
+
const row = index % CHUNK_SIZE;
|
|
637
|
+
const rule = state.rules[chunk.rule[row]!]!;
|
|
638
|
+
if (
|
|
639
|
+
checked.effectiveStatus !== undefined &&
|
|
640
|
+
checked.effectiveStatus !==
|
|
641
|
+
effective(
|
|
642
|
+
statuses[chunk.flags[row]! & 3]!,
|
|
643
|
+
rule.severity,
|
|
644
|
+
state.overflows.get(index)?.length ?? 0,
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
continue;
|
|
648
|
+
if (
|
|
649
|
+
coordinateFilter &&
|
|
650
|
+
(rule.kind === "control-total" ||
|
|
651
|
+
(!matches(state.coordinates[chunk.first[row]!]!) &&
|
|
652
|
+
(rule.kind !== "monotonic" ||
|
|
653
|
+
!matches(state.coordinates[chunk.second[row]!]!))))
|
|
654
|
+
)
|
|
655
|
+
continue;
|
|
656
|
+
if (total >= checked.offset && items.length < checked.limit)
|
|
657
|
+
items.push(summaryAt(state, index));
|
|
658
|
+
total++;
|
|
659
|
+
}
|
|
660
|
+
return page(items, total, checked.offset);
|
|
661
|
+
}
|
|
662
|
+
export function pageDiagnosticReviewEvaluationSources(
|
|
663
|
+
store: CompactDiagnosticReviewEvaluations,
|
|
664
|
+
index: number,
|
|
665
|
+
query: DiagnosticReviewSourceQuery = {},
|
|
666
|
+
): DiagnosticReviewPage<DiagnosticSourceLocation> {
|
|
667
|
+
const state = stateOf(store);
|
|
668
|
+
checkedIndex(state, index);
|
|
669
|
+
const checked = checkedQuery(query, ["offset", "limit", "overflowIndex"]);
|
|
670
|
+
const overflowIndex = checked.overflowIndex;
|
|
671
|
+
if (
|
|
672
|
+
overflowIndex !== undefined &&
|
|
673
|
+
(!Number.isSafeInteger(overflowIndex) ||
|
|
674
|
+
overflowIndex < 0 ||
|
|
675
|
+
overflowIndex >= (state.overflows.get(index)?.length ?? 0))
|
|
676
|
+
)
|
|
677
|
+
invalid("Overflow index is outside this evaluation", "$.overflowIndex");
|
|
678
|
+
const chunk = state.chunks[Math.floor(index / CHUNK_SIZE)]!;
|
|
679
|
+
const sourceList =
|
|
680
|
+
overflowIndex === undefined
|
|
681
|
+
? chunk.sources[index % CHUNK_SIZE]!
|
|
682
|
+
: state.overflows.get(index)![overflowIndex]!.sources;
|
|
683
|
+
const ids = state.sourceLists[sourceList]!;
|
|
684
|
+
return page(
|
|
685
|
+
Array.from(
|
|
686
|
+
ids.subarray(checked.offset, checked.offset + checked.limit),
|
|
687
|
+
(id) => state.sourceLocations[id]!,
|
|
688
|
+
),
|
|
689
|
+
ids.length,
|
|
690
|
+
checked.offset,
|
|
691
|
+
);
|
|
692
|
+
}
|