@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
|
@@ -4,86 +4,318 @@ import {
|
|
|
4
4
|
type CompiledDiagnosticDefinition,
|
|
5
5
|
} from "./diagnosticDefinitions.js";
|
|
6
6
|
import type { DiagnosticClaimExpression } from "./diagnosticExpressions.js";
|
|
7
|
-
import {
|
|
7
|
+
import type { DiagnosticExpressionOverflow } from "./diagnosticRules.js";
|
|
8
|
+
import {
|
|
9
|
+
DiagnosticValidationError,
|
|
10
|
+
type DiagnosticValidationIssue,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
import {
|
|
13
|
+
diagnosticRecord,
|
|
14
|
+
hasDiagnosticOwn,
|
|
15
|
+
isDiagnosticPlainRecord,
|
|
16
|
+
normalizeDiagnosticNumber,
|
|
17
|
+
} from "./diagnosticRuntime.js";
|
|
8
18
|
|
|
9
19
|
export type DiagnosticMeasureValues = Readonly<Record<string, number | null>>;
|
|
10
20
|
|
|
11
|
-
export type DiagnosticRowWithDerivedMeasures<
|
|
12
|
-
|
|
21
|
+
export type DiagnosticRowWithDerivedMeasures<
|
|
22
|
+
TRow extends { measures: DiagnosticMeasureValues },
|
|
23
|
+
> = Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
25
|
+
export type DiagnosticDerivedValueAudit =
|
|
26
|
+
| { readonly status: "observed"; readonly value: number }
|
|
27
|
+
| { readonly status: "missing"; readonly value: null }
|
|
28
|
+
| {
|
|
29
|
+
readonly status: "non-finite";
|
|
30
|
+
readonly value: null;
|
|
31
|
+
readonly nonFiniteKind: "nan" | "positive-infinity" | "negative-infinity";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export interface DiagnosticDerivedRowAudit<
|
|
35
|
+
TRow extends { measures: DiagnosticMeasureValues },
|
|
36
|
+
> {
|
|
37
|
+
readonly row: DiagnosticRowWithDerivedMeasures<TRow>;
|
|
38
|
+
readonly derived: Readonly<Record<string, DiagnosticDerivedValueAudit>>;
|
|
39
|
+
readonly expressionOverflows: Readonly<
|
|
40
|
+
Record<string, readonly DiagnosticExpressionOverflow[]>
|
|
41
|
+
>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ClaimExpressionAudit {
|
|
45
|
+
readonly state: DiagnosticDerivedValueAudit;
|
|
46
|
+
readonly overflows: readonly DiagnosticExpressionOverflow[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nonFiniteKind(
|
|
50
|
+
value: number,
|
|
51
|
+
): "nan" | "positive-infinity" | "negative-infinity" {
|
|
52
|
+
return Number.isNaN(value)
|
|
53
|
+
? "nan"
|
|
54
|
+
: value > 0
|
|
55
|
+
? "positive-infinity"
|
|
56
|
+
: "negative-infinity";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeNonFiniteKinds(
|
|
60
|
+
values: readonly ("nan" | "positive-infinity" | "negative-infinity")[],
|
|
61
|
+
): "nan" | "positive-infinity" | "negative-infinity" {
|
|
62
|
+
const unique = new Set(values);
|
|
63
|
+
return unique.size === 1 ? values[0]! : "nan";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function neumaier(values: readonly number[]): {
|
|
67
|
+
readonly value: number | null;
|
|
68
|
+
readonly nonFiniteKind?: "nan" | "positive-infinity" | "negative-infinity";
|
|
69
|
+
} {
|
|
36
70
|
let sum = 0;
|
|
37
71
|
let correction = 0;
|
|
38
|
-
for (const
|
|
39
|
-
const value = evaluateClaimExpression(term, measures);
|
|
40
|
-
if (value === null) return null;
|
|
72
|
+
for (const value of values) {
|
|
41
73
|
const next = sum + value;
|
|
42
|
-
|
|
74
|
+
if (!Number.isFinite(next))
|
|
75
|
+
return { value: null, nonFiniteKind: nonFiniteKind(next) };
|
|
76
|
+
const nextCorrection =
|
|
77
|
+
correction +
|
|
78
|
+
(Math.abs(sum) >= Math.abs(value)
|
|
79
|
+
? sum - next + value
|
|
80
|
+
: value - next + sum);
|
|
81
|
+
if (!Number.isFinite(nextCorrection))
|
|
82
|
+
return { value: null, nonFiniteKind: nonFiniteKind(nextCorrection) };
|
|
83
|
+
correction = nextCorrection;
|
|
43
84
|
sum = next;
|
|
44
|
-
if (!Number.isFinite(sum) || !Number.isFinite(correction)) return null;
|
|
45
85
|
}
|
|
46
86
|
const result = sum + correction;
|
|
47
|
-
return Number.isFinite(result)
|
|
87
|
+
return Number.isFinite(result)
|
|
88
|
+
? { value: Object.is(result, -0) ? 0 : result }
|
|
89
|
+
: { value: null, nonFiniteKind: nonFiniteKind(result) };
|
|
48
90
|
}
|
|
49
91
|
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
function evaluateClaimExpressionAudit(
|
|
93
|
+
expression: DiagnosticClaimExpression,
|
|
94
|
+
states: Readonly<Record<string, DiagnosticDerivedValueAudit>>,
|
|
95
|
+
path: string,
|
|
96
|
+
): ClaimExpressionAudit {
|
|
97
|
+
if (expression.op === "measure")
|
|
98
|
+
return {
|
|
99
|
+
state: hasDiagnosticOwn(states, expression.measureId)
|
|
100
|
+
? states[expression.measureId]!
|
|
101
|
+
: { status: "missing", value: null },
|
|
102
|
+
overflows: [],
|
|
103
|
+
};
|
|
104
|
+
if (expression.op === "claim-layer") {
|
|
105
|
+
const source = hasDiagnosticOwn(states, expression.measureId)
|
|
106
|
+
? states[expression.measureId]!
|
|
107
|
+
: { status: "missing" as const, value: null };
|
|
108
|
+
if (source.status !== "observed") return { state: source, overflows: [] };
|
|
109
|
+
const value =
|
|
110
|
+
source.value <= expression.attachment
|
|
111
|
+
? 0
|
|
112
|
+
: expression.limit === null
|
|
113
|
+
? source.value - expression.attachment
|
|
114
|
+
: Math.min(source.value - expression.attachment, expression.limit);
|
|
115
|
+
if (Number.isFinite(value))
|
|
116
|
+
return {
|
|
117
|
+
state: { status: "observed", value: normalizeDiagnosticNumber(value) },
|
|
118
|
+
overflows: [],
|
|
119
|
+
};
|
|
120
|
+
return {
|
|
121
|
+
state: {
|
|
122
|
+
status: "non-finite",
|
|
123
|
+
value: null,
|
|
124
|
+
nonFiniteKind: nonFiniteKind(value),
|
|
125
|
+
},
|
|
126
|
+
overflows: [{ expressionPath: path, sources: [] }],
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const children =
|
|
130
|
+
expression.op === "add"
|
|
131
|
+
? expression.terms.map((term, index) =>
|
|
132
|
+
evaluateClaimExpressionAudit(term, states, `${path}/terms/${index}`),
|
|
133
|
+
)
|
|
134
|
+
: [
|
|
135
|
+
evaluateClaimExpressionAudit(expression.left, states, `${path}/left`),
|
|
136
|
+
evaluateClaimExpressionAudit(
|
|
137
|
+
expression.right,
|
|
138
|
+
states,
|
|
139
|
+
`${path}/right`,
|
|
140
|
+
),
|
|
141
|
+
];
|
|
142
|
+
const overflows = children.flatMap((child) => child.overflows);
|
|
143
|
+
const nonFinite = children.flatMap((child) =>
|
|
144
|
+
child.state.status === "non-finite" ? [child.state.nonFiniteKind] : [],
|
|
145
|
+
);
|
|
146
|
+
if (nonFinite.length > 0)
|
|
147
|
+
return {
|
|
148
|
+
state: {
|
|
149
|
+
status: "non-finite",
|
|
150
|
+
value: null,
|
|
151
|
+
nonFiniteKind: mergeNonFiniteKinds(nonFinite),
|
|
152
|
+
},
|
|
153
|
+
overflows,
|
|
154
|
+
};
|
|
155
|
+
if (children.some((child) => child.state.status === "missing"))
|
|
156
|
+
return { state: { status: "missing", value: null }, overflows };
|
|
157
|
+
const values = children.map((child) => child.state.value!);
|
|
158
|
+
const calculation =
|
|
159
|
+
expression.op === "add"
|
|
160
|
+
? neumaier(values)
|
|
161
|
+
: { value: values[0]! - values[1]! };
|
|
162
|
+
if (calculation.value !== null && Number.isFinite(calculation.value)) {
|
|
163
|
+
return {
|
|
164
|
+
state: {
|
|
165
|
+
status: "observed",
|
|
166
|
+
value: normalizeDiagnosticNumber(calculation.value),
|
|
167
|
+
},
|
|
168
|
+
overflows,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
const failedKind =
|
|
172
|
+
calculation.nonFiniteKind ?? nonFiniteKind(calculation.value ?? Number.NaN);
|
|
173
|
+
return {
|
|
174
|
+
state: { status: "non-finite", value: null, nonFiniteKind: failedKind },
|
|
175
|
+
overflows: [...overflows, { expressionPath: path, sources: [] }],
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateRows<TRow extends { measures: DiagnosticMeasureValues }>(
|
|
52
180
|
rows: readonly TRow[],
|
|
53
181
|
definition: CompiledDiagnosticDefinition,
|
|
54
|
-
):
|
|
55
|
-
assertCompiledDiagnosticDefinition(definition);
|
|
182
|
+
): void {
|
|
56
183
|
const internals = getCompiledDiagnosticDefinitionInternals(definition);
|
|
57
184
|
const issues: DiagnosticValidationIssue[] = [];
|
|
58
185
|
if (definition.definition.lossRowGrain !== "claim") {
|
|
59
|
-
issues.push({
|
|
186
|
+
issues.push({
|
|
187
|
+
domain: "input",
|
|
188
|
+
code: "invalid-input-relationship",
|
|
189
|
+
path: "$.definition.lossRowGrain",
|
|
190
|
+
message: "Claim derivation requires a claim-grain definition",
|
|
191
|
+
});
|
|
60
192
|
}
|
|
61
193
|
if (!Array.isArray(rows)) {
|
|
62
|
-
issues.push({
|
|
194
|
+
issues.push({
|
|
195
|
+
domain: "input",
|
|
196
|
+
code: "invalid-type",
|
|
197
|
+
path: "$.rows",
|
|
198
|
+
message: "Claim rows must be an array",
|
|
199
|
+
});
|
|
63
200
|
} else {
|
|
64
201
|
rows.forEach((row, rowIndex) => {
|
|
65
202
|
const measures = row?.measures;
|
|
66
|
-
if (
|
|
67
|
-
issues.push({
|
|
203
|
+
if (!isDiagnosticPlainRecord(measures)) {
|
|
204
|
+
issues.push({
|
|
205
|
+
domain: "input",
|
|
206
|
+
code: "invalid-type",
|
|
207
|
+
path: `$.rows[${rowIndex}].measures`,
|
|
208
|
+
message: "Row measures must be a plain record",
|
|
209
|
+
});
|
|
68
210
|
return;
|
|
69
211
|
}
|
|
70
212
|
for (const [measureId, value] of Object.entries(measures)) {
|
|
71
213
|
const measure = internals.measuresById.get(measureId);
|
|
72
214
|
const path = `$.rows[${rowIndex}].measures[${JSON.stringify(measureId)}]`;
|
|
73
|
-
if (!measure)
|
|
74
|
-
|
|
75
|
-
|
|
215
|
+
if (!measure)
|
|
216
|
+
issues.push({
|
|
217
|
+
domain: "input",
|
|
218
|
+
code: "unknown-reference",
|
|
219
|
+
path,
|
|
220
|
+
message: `Unknown measure ${measureId}`,
|
|
221
|
+
});
|
|
222
|
+
else if (measure.source !== "loss")
|
|
223
|
+
issues.push({
|
|
224
|
+
domain: "input",
|
|
225
|
+
code: "invalid-input-relationship",
|
|
226
|
+
path,
|
|
227
|
+
message: `Caller rows cannot supply ${measure.source} measure ${measureId}`,
|
|
228
|
+
});
|
|
229
|
+
if (value !== null && typeof value !== "number")
|
|
230
|
+
issues.push({
|
|
231
|
+
domain: "input",
|
|
232
|
+
code: "invalid-type",
|
|
233
|
+
path,
|
|
234
|
+
message: "Measure value must be a number or null",
|
|
235
|
+
});
|
|
76
236
|
}
|
|
77
237
|
});
|
|
78
238
|
}
|
|
79
239
|
if (issues.length > 0) throw new DiagnosticValidationError(issues);
|
|
240
|
+
}
|
|
80
241
|
|
|
242
|
+
/** @internal Shared with diagnostic preparation to preserve derivation quality. */
|
|
243
|
+
export function deriveDiagnosticClaimMeasuresWithAudit<
|
|
244
|
+
TRow extends { measures: DiagnosticMeasureValues },
|
|
245
|
+
>(
|
|
246
|
+
rows: readonly TRow[],
|
|
247
|
+
definition: CompiledDiagnosticDefinition,
|
|
248
|
+
): readonly DiagnosticDerivedRowAudit<TRow>[] {
|
|
249
|
+
assertCompiledDiagnosticDefinition(definition);
|
|
250
|
+
validateRows(rows, definition);
|
|
251
|
+
const internals = getCompiledDiagnosticDefinitionInternals(definition);
|
|
81
252
|
const plan = [...internals.derivationsByOutputMeasureId.values()];
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
253
|
+
const normalizedIndex = new Map(
|
|
254
|
+
definition.definition.derivedMeasures.map((derivation, index) => [
|
|
255
|
+
derivation.outputMeasureId,
|
|
256
|
+
index,
|
|
257
|
+
]),
|
|
258
|
+
);
|
|
259
|
+
return Object.freeze(
|
|
260
|
+
rows.map((row) => {
|
|
261
|
+
const measures = diagnosticRecord<number | null>();
|
|
262
|
+
const states = diagnosticRecord<DiagnosticDerivedValueAudit>();
|
|
263
|
+
for (const [id, value] of Object.entries(row.measures)) {
|
|
264
|
+
measures[id] = value;
|
|
265
|
+
states[id] =
|
|
266
|
+
value === null || value === undefined
|
|
267
|
+
? { status: "missing" as const, value: null }
|
|
268
|
+
: Number.isFinite(value)
|
|
269
|
+
? {
|
|
270
|
+
status: "observed" as const,
|
|
271
|
+
value: normalizeDiagnosticNumber(value),
|
|
272
|
+
}
|
|
273
|
+
: {
|
|
274
|
+
status: "non-finite" as const,
|
|
275
|
+
value: null,
|
|
276
|
+
nonFiniteKind: nonFiniteKind(value),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
const derived = diagnosticRecord<DiagnosticDerivedValueAudit>();
|
|
280
|
+
const expressionOverflows =
|
|
281
|
+
diagnosticRecord<readonly DiagnosticExpressionOverflow[]>();
|
|
282
|
+
for (const derivation of plan) {
|
|
283
|
+
const index = normalizedIndex.get(derivation.outputMeasureId)!;
|
|
284
|
+
const evaluated = evaluateClaimExpressionAudit(
|
|
285
|
+
derivation.expression,
|
|
286
|
+
states,
|
|
287
|
+
`/derivedMeasures/${index}/expression`,
|
|
288
|
+
);
|
|
289
|
+
states[derivation.outputMeasureId] = evaluated.state;
|
|
290
|
+
derived[derivation.outputMeasureId] = evaluated.state;
|
|
291
|
+
expressionOverflows[derivation.outputMeasureId] = Object.freeze([
|
|
292
|
+
...evaluated.overflows,
|
|
293
|
+
]);
|
|
294
|
+
measures[derivation.outputMeasureId] = evaluated.state.value;
|
|
295
|
+
}
|
|
296
|
+
return Object.freeze({
|
|
297
|
+
row: Object.freeze({
|
|
298
|
+
...row,
|
|
299
|
+
measures: Object.freeze(measures),
|
|
300
|
+
}) as DiagnosticRowWithDerivedMeasures<TRow>,
|
|
301
|
+
derived: Object.freeze(derived),
|
|
302
|
+
expressionOverflows: Object.freeze(expressionOverflows),
|
|
303
|
+
});
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Materializes compiler-approved claim-level measures without mutating caller rows. */
|
|
309
|
+
export function deriveDiagnosticClaimMeasures<
|
|
310
|
+
TRow extends { measures: DiagnosticMeasureValues },
|
|
311
|
+
>(
|
|
312
|
+
rows: readonly TRow[],
|
|
313
|
+
definition: CompiledDiagnosticDefinition,
|
|
314
|
+
): readonly DiagnosticRowWithDerivedMeasures<TRow>[] {
|
|
315
|
+
assertCompiledDiagnosticDefinition(definition);
|
|
316
|
+
return Object.freeze(
|
|
317
|
+
deriveDiagnosticClaimMeasuresWithAudit(rows, definition).map(
|
|
318
|
+
(item) => item.row,
|
|
319
|
+
),
|
|
320
|
+
);
|
|
89
321
|
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Private, invocation-scoped sharing of small, newly owned evidence nodes.
|
|
3
|
+
* This is not a validation boundary: callers must finish normalizing/cloning
|
|
4
|
+
* every child first, and must never submit caller-owned objects or SDK brands.
|
|
5
|
+
*/
|
|
6
|
+
export interface DiagnosticEvidenceInternOptions {
|
|
7
|
+
readonly maxEntries?: number;
|
|
8
|
+
readonly maxSignatureCharacters?: number;
|
|
9
|
+
readonly maxCandidateProperties?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type EvidenceMode = "plain" | "source" | "free";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_MAX_ENTRIES = 100_000;
|
|
15
|
+
const DEFAULT_MAX_SIGNATURE_CHARACTERS = 16 * 1024 * 1024;
|
|
16
|
+
// Wide result envelopes are usually unique; focus the budget on their small
|
|
17
|
+
// repeated coordinates, sources, scopes, states, and short child lists.
|
|
18
|
+
const DEFAULT_MAX_CANDIDATE_PROPERTIES = 10;
|
|
19
|
+
const MAX_CANDIDATE_STRING_CHARACTERS = 4096;
|
|
20
|
+
|
|
21
|
+
function sameOwnedNode(left: object, right: object): boolean {
|
|
22
|
+
if (Object.getPrototypeOf(left) !== Object.getPrototypeOf(right))
|
|
23
|
+
return false;
|
|
24
|
+
const leftKeys = Reflect.ownKeys(left);
|
|
25
|
+
const rightKeys = Reflect.ownKeys(right);
|
|
26
|
+
if (leftKeys.length !== rightKeys.length) return false;
|
|
27
|
+
return leftKeys.every((key, index) => {
|
|
28
|
+
if (key !== rightKeys[index]) return false;
|
|
29
|
+
const a = Object.getOwnPropertyDescriptor(left, key)!;
|
|
30
|
+
const b = Object.getOwnPropertyDescriptor(right, key)!;
|
|
31
|
+
return (
|
|
32
|
+
"value" in a &&
|
|
33
|
+
"value" in b &&
|
|
34
|
+
a.enumerable === b.enumerable &&
|
|
35
|
+
a.configurable === b.configurable &&
|
|
36
|
+
a.writable === b.writable &&
|
|
37
|
+
Object.is(a.value, b.value)
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createDiagnosticEvidenceInterner(
|
|
43
|
+
options: DiagnosticEvidenceInternOptions = {},
|
|
44
|
+
): {
|
|
45
|
+
internOwned<T extends object>(candidate: T, mode: EvidenceMode): T;
|
|
46
|
+
} {
|
|
47
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
48
|
+
const maxSignatureCharacters =
|
|
49
|
+
options.maxSignatureCharacters ?? DEFAULT_MAX_SIGNATURE_CHARACTERS;
|
|
50
|
+
const maxCandidateProperties =
|
|
51
|
+
options.maxCandidateProperties ?? DEFAULT_MAX_CANDIDATE_PROPERTIES;
|
|
52
|
+
const pool = new Map<string, object>();
|
|
53
|
+
const childIds = new WeakMap<object, number>();
|
|
54
|
+
let nextChildId = 1;
|
|
55
|
+
let signatureCharacters = 0;
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
internOwned<T extends object>(candidate: T, mode: EvidenceMode): T {
|
|
59
|
+
if (!Object.isFrozen(candidate)) return candidate;
|
|
60
|
+
const prototype = Object.getPrototypeOf(candidate);
|
|
61
|
+
const array = Array.isArray(candidate);
|
|
62
|
+
if (
|
|
63
|
+
array
|
|
64
|
+
? prototype !== Array.prototype
|
|
65
|
+
: prototype !== null && prototype !== Object.prototype
|
|
66
|
+
)
|
|
67
|
+
return candidate;
|
|
68
|
+
if (array && candidate.length >= maxCandidateProperties) return candidate;
|
|
69
|
+
const keys = Reflect.ownKeys(candidate);
|
|
70
|
+
if (keys.length > maxCandidateProperties) return candidate;
|
|
71
|
+
const signatureParts: unknown[] = [
|
|
72
|
+
mode,
|
|
73
|
+
array ? "array" : prototype === null ? "null" : "object",
|
|
74
|
+
];
|
|
75
|
+
// A declined candidate must not grow persistent bookkeeping. Stage at
|
|
76
|
+
// most this small candidate's children, including repeated references,
|
|
77
|
+
// and commit their IDs only if the candidate enters the bounded pool.
|
|
78
|
+
let pendingChildIds: Map<object, number> | undefined;
|
|
79
|
+
let stringCharacters = 0;
|
|
80
|
+
for (const key of keys) {
|
|
81
|
+
// Diagnostic JSON does not contain symbols. Declining unexpected
|
|
82
|
+
// shapes is harmless and never invokes an accessor or caller method.
|
|
83
|
+
if (typeof key !== "string") return candidate;
|
|
84
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key)!;
|
|
85
|
+
if (!("value" in descriptor)) return candidate;
|
|
86
|
+
const value: unknown = descriptor.value;
|
|
87
|
+
let tokenType: string;
|
|
88
|
+
let tokenValue: unknown;
|
|
89
|
+
if (value === null) {
|
|
90
|
+
tokenType = "null";
|
|
91
|
+
tokenValue = null;
|
|
92
|
+
} else if (typeof value === "object") {
|
|
93
|
+
if (!Object.isFrozen(value)) return candidate;
|
|
94
|
+
tokenType = "object";
|
|
95
|
+
const existingId = childIds.get(value);
|
|
96
|
+
if (existingId !== undefined) tokenValue = existingId;
|
|
97
|
+
else {
|
|
98
|
+
pendingChildIds ??= new Map();
|
|
99
|
+
let pendingId = pendingChildIds.get(value);
|
|
100
|
+
if (pendingId === undefined) {
|
|
101
|
+
pendingId = nextChildId + pendingChildIds.size;
|
|
102
|
+
pendingChildIds.set(value, pendingId);
|
|
103
|
+
}
|
|
104
|
+
tokenValue = pendingId;
|
|
105
|
+
}
|
|
106
|
+
} else if (typeof value === "number") {
|
|
107
|
+
if (!Number.isFinite(value)) return candidate;
|
|
108
|
+
tokenType = "number";
|
|
109
|
+
tokenValue = Object.is(value, -0) ? "-0" : value;
|
|
110
|
+
} else if (
|
|
111
|
+
typeof value === "string" ||
|
|
112
|
+
typeof value === "boolean" ||
|
|
113
|
+
value === undefined
|
|
114
|
+
) {
|
|
115
|
+
tokenType = typeof value;
|
|
116
|
+
tokenValue = value;
|
|
117
|
+
if (typeof value === "string") stringCharacters += value.length;
|
|
118
|
+
} else return candidate;
|
|
119
|
+
stringCharacters += key.length;
|
|
120
|
+
if (stringCharacters > MAX_CANDIDATE_STRING_CHARACTERS)
|
|
121
|
+
return candidate;
|
|
122
|
+
// Frozen data properties are necessarily non-configurable and
|
|
123
|
+
// non-writable. Keep the varying enumerability bit (including array
|
|
124
|
+
// length) and a fixed four-slot typed tuple, without temporary nested
|
|
125
|
+
// arrays. The independent equality check still verifies every flag.
|
|
126
|
+
signatureParts.push(key, descriptor.enumerable, tokenType, tokenValue);
|
|
127
|
+
}
|
|
128
|
+
const signature = JSON.stringify(signatureParts);
|
|
129
|
+
// An unseen child cannot occur in an existing entry: every admitted
|
|
130
|
+
// entry committed all its child IDs. No equality hit is skipped here.
|
|
131
|
+
const existing = pendingChildIds ? undefined : pool.get(signature);
|
|
132
|
+
// Signatures encode exact children and descriptor/order information;
|
|
133
|
+
// independently check equality before sharing rather than treating a
|
|
134
|
+
// fingerprint as proof of equality.
|
|
135
|
+
if (existing !== undefined && sameOwnedNode(existing, candidate))
|
|
136
|
+
return existing as T;
|
|
137
|
+
if (
|
|
138
|
+
pool.size < maxEntries &&
|
|
139
|
+
signatureCharacters + signature.length <= maxSignatureCharacters
|
|
140
|
+
) {
|
|
141
|
+
if (pendingChildIds) {
|
|
142
|
+
for (const [child, id] of pendingChildIds) childIds.set(child, id);
|
|
143
|
+
nextChildId += pendingChildIds.size;
|
|
144
|
+
}
|
|
145
|
+
pool.set(signature, candidate);
|
|
146
|
+
signatureCharacters += signature.length;
|
|
147
|
+
}
|
|
148
|
+
return candidate;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|