@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.
Files changed (133) hide show
  1. package/README.md +12 -3
  2. package/dist/canonical.d.ts +11 -0
  3. package/dist/canonical.d.ts.map +1 -1
  4. package/dist/canonical.js +136 -26
  5. package/dist/canonical.js.map +1 -1
  6. package/dist/capping.d.ts.map +1 -1
  7. package/dist/capping.js +39 -6
  8. package/dist/capping.js.map +1 -1
  9. package/dist/diagnosticAggregation.d.ts +3 -0
  10. package/dist/diagnosticAggregation.d.ts.map +1 -1
  11. package/dist/diagnosticAggregation.js +30 -7
  12. package/dist/diagnosticAggregation.js.map +1 -1
  13. package/dist/diagnosticDefinitions.d.ts +3 -0
  14. package/dist/diagnosticDefinitions.d.ts.map +1 -1
  15. package/dist/diagnosticDefinitions.js +844 -182
  16. package/dist/diagnosticDefinitions.js.map +1 -1
  17. package/dist/diagnosticDerivations.d.ts +23 -0
  18. package/dist/diagnosticDerivations.d.ts.map +1 -1
  19. package/dist/diagnosticDerivations.js +187 -43
  20. package/dist/diagnosticDerivations.js.map +1 -1
  21. package/dist/diagnosticEvidenceIntern.d.ts +16 -0
  22. package/dist/diagnosticEvidenceIntern.d.ts.map +1 -0
  23. package/dist/diagnosticEvidenceIntern.js +139 -0
  24. package/dist/diagnosticEvidenceIntern.js.map +1 -0
  25. package/dist/diagnosticExposure.d.ts +2 -2
  26. package/dist/diagnosticExposure.d.ts.map +1 -1
  27. package/dist/diagnosticExposure.js +319 -21
  28. package/dist/diagnosticExposure.js.map +1 -1
  29. package/dist/diagnosticExpressions.d.ts +1 -1
  30. package/dist/diagnosticExpressions.d.ts.map +1 -1
  31. package/dist/diagnosticExpressions.js +35 -15
  32. package/dist/diagnosticExpressions.js.map +1 -1
  33. package/dist/diagnosticFormulas.d.ts +3 -0
  34. package/dist/diagnosticFormulas.d.ts.map +1 -1
  35. package/dist/diagnosticFormulas.js +160 -26
  36. package/dist/diagnosticFormulas.js.map +1 -1
  37. package/dist/diagnosticIdentity.d.ts +15 -1
  38. package/dist/diagnosticIdentity.d.ts.map +1 -1
  39. package/dist/diagnosticIdentity.js +321 -51
  40. package/dist/diagnosticIdentity.js.map +1 -1
  41. package/dist/diagnosticIdentityStream.d.ts +56 -0
  42. package/dist/diagnosticIdentityStream.d.ts.map +1 -0
  43. package/dist/diagnosticIdentityStream.js +472 -0
  44. package/dist/diagnosticIdentityStream.js.map +1 -0
  45. package/dist/diagnosticOrdering.d.ts +8 -0
  46. package/dist/diagnosticOrdering.d.ts.map +1 -0
  47. package/dist/diagnosticOrdering.js +73 -0
  48. package/dist/diagnosticOrdering.js.map +1 -0
  49. package/dist/diagnosticPeriodAxis.d.ts +9 -0
  50. package/dist/diagnosticPeriodAxis.d.ts.map +1 -0
  51. package/dist/diagnosticPeriodAxis.js +69 -0
  52. package/dist/diagnosticPeriodAxis.js.map +1 -0
  53. package/dist/diagnosticPeriods.d.ts +2 -3
  54. package/dist/diagnosticPeriods.d.ts.map +1 -1
  55. package/dist/diagnosticPeriods.js +20 -68
  56. package/dist/diagnosticPeriods.js.map +1 -1
  57. package/dist/diagnosticPreparation.d.ts +75 -14
  58. package/dist/diagnosticPreparation.d.ts.map +1 -1
  59. package/dist/diagnosticPreparation.js +1606 -112
  60. package/dist/diagnosticPreparation.js.map +1 -1
  61. package/dist/diagnosticReview.d.ts +40 -15
  62. package/dist/diagnosticReview.d.ts.map +1 -1
  63. package/dist/diagnosticReview.js +600 -42
  64. package/dist/diagnosticReview.js.map +1 -1
  65. package/dist/diagnosticReviewSources.d.ts +18 -0
  66. package/dist/diagnosticReviewSources.d.ts.map +1 -0
  67. package/dist/diagnosticReviewSources.js +76 -0
  68. package/dist/diagnosticReviewSources.js.map +1 -0
  69. package/dist/diagnosticReviewStore.d.ts +88 -0
  70. package/dist/diagnosticReviewStore.d.ts.map +1 -0
  71. package/dist/diagnosticReviewStore.js +455 -0
  72. package/dist/diagnosticReviewStore.js.map +1 -0
  73. package/dist/diagnosticRunner.d.ts +27 -2
  74. package/dist/diagnosticRunner.d.ts.map +1 -1
  75. package/dist/diagnosticRunner.js +764 -70
  76. package/dist/diagnosticRunner.js.map +1 -1
  77. package/dist/diagnosticRuntime.d.ts +26 -0
  78. package/dist/diagnosticRuntime.d.ts.map +1 -0
  79. package/dist/diagnosticRuntime.js +243 -0
  80. package/dist/diagnosticRuntime.js.map +1 -0
  81. package/dist/diagnosticSourceOrdering.d.ts +6 -0
  82. package/dist/diagnosticSourceOrdering.d.ts.map +1 -0
  83. package/dist/diagnosticSourceOrdering.js +38 -0
  84. package/dist/diagnosticSourceOrdering.js.map +1 -0
  85. package/dist/fnvAccumulator.d.ts +6 -0
  86. package/dist/fnvAccumulator.d.ts.map +1 -0
  87. package/dist/fnvAccumulator.js +21 -0
  88. package/dist/fnvAccumulator.js.map +1 -0
  89. package/dist/index.d.ts +15 -8
  90. package/dist/index.d.ts.map +1 -1
  91. package/dist/index.js +10 -4
  92. package/dist/index.js.map +1 -1
  93. package/dist/mack.d.ts +4 -0
  94. package/dist/mack.d.ts.map +1 -1
  95. package/dist/mack.js +32 -4
  96. package/dist/mack.js.map +1 -1
  97. package/dist/triangle.d.ts.map +1 -1
  98. package/dist/triangle.js +59 -9
  99. package/dist/triangle.js.map +1 -1
  100. package/dist/types.d.ts +4 -0
  101. package/dist/types.d.ts.map +1 -1
  102. package/dist/types.js +22 -8
  103. package/dist/types.js.map +1 -1
  104. package/dist/version.d.ts +1 -1
  105. package/dist/version.js +1 -1
  106. package/package.json +3 -2
  107. package/src/canonical.ts +144 -26
  108. package/src/capping.ts +109 -18
  109. package/src/diagnosticAggregation.ts +69 -20
  110. package/src/diagnosticDefinitions.ts +2675 -426
  111. package/src/diagnosticDerivations.ts +281 -49
  112. package/src/diagnosticEvidenceIntern.ts +151 -0
  113. package/src/diagnosticExposure.ts +550 -50
  114. package/src/diagnosticExpressions.ts +99 -17
  115. package/src/diagnosticFormulas.ts +239 -39
  116. package/src/diagnosticIdentity.ts +700 -191
  117. package/src/diagnosticIdentityStream.ts +553 -0
  118. package/src/diagnosticOrdering.ts +106 -0
  119. package/src/diagnosticPeriodAxis.ts +92 -0
  120. package/src/diagnosticPeriods.ts +67 -82
  121. package/src/diagnosticPreparation.ts +2558 -132
  122. package/src/diagnosticReview.ts +1072 -58
  123. package/src/diagnosticReviewSources.ts +85 -0
  124. package/src/diagnosticReviewStore.ts +692 -0
  125. package/src/diagnosticRunner.ts +1273 -72
  126. package/src/diagnosticRuntime.ts +264 -0
  127. package/src/diagnosticSourceOrdering.ts +50 -0
  128. package/src/fnvAccumulator.ts +20 -0
  129. package/src/index.ts +69 -4
  130. package/src/mack.ts +72 -9
  131. package/src/triangle.ts +103 -13
  132. package/src/types.ts +55 -20
  133. 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 { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.js";
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<TRow extends { measures: DiagnosticMeasureValues }> =
12
- Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
21
+ export type DiagnosticRowWithDerivedMeasures<
22
+ TRow extends { measures: DiagnosticMeasureValues },
23
+ > = Omit<TRow, "measures"> & { readonly measures: DiagnosticMeasureValues };
13
24
 
14
- function evaluateClaimExpression(
15
- expression: DiagnosticClaimExpression,
16
- measures: Readonly<Record<string, number | null>>,
17
- ): number | null {
18
- if (expression.op === "measure") {
19
- const value = measures[expression.measureId];
20
- return typeof value === "number" && Number.isFinite(value) ? value : null;
21
- }
22
- if (expression.op === "claim-layer") {
23
- const value = measures[expression.measureId];
24
- if (typeof value !== "number" || !Number.isFinite(value)) return null;
25
- const excess = Math.max(value - expression.attachment, 0);
26
- const result = expression.limit === null ? excess : Math.min(excess, expression.limit);
27
- return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
28
- }
29
- if (expression.op === "subtract") {
30
- const left = evaluateClaimExpression(expression.left, measures);
31
- const right = evaluateClaimExpression(expression.right, measures);
32
- if (left === null || right === null) return null;
33
- const result = left - right;
34
- return Number.isFinite(result) ? (Object.is(result, -0) ? 0 : result) : null;
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 term of expression.terms) {
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
- correction += Math.abs(sum) >= Math.abs(value) ? (sum - next) + value : (value - next) + sum;
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) ? (Object.is(result, -0) ? 0 : result) : null;
87
+ return Number.isFinite(result)
88
+ ? { value: Object.is(result, -0) ? 0 : result }
89
+ : { value: null, nonFiniteKind: nonFiniteKind(result) };
48
90
  }
49
91
 
50
- /** Materializes compiler-approved claim-level measures without mutating caller rows. */
51
- export function deriveDiagnosticClaimMeasures<TRow extends { measures: DiagnosticMeasureValues }>(
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
- ): readonly DiagnosticRowWithDerivedMeasures<TRow>[] {
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({ domain: "input", code: "invalid-input-relationship", path: "$.definition.lossRowGrain", message: "Claim derivation requires a claim-grain definition" });
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({ domain: "input", code: "invalid-type", path: "$.rows", message: "Claim rows must be an array" });
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 (measures === null || typeof measures !== "object" || Array.isArray(measures)) {
67
- issues.push({ domain: "input", code: "invalid-type", path: `$.rows[${rowIndex}].measures`, message: "Row measures must be a plain record" });
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) issues.push({ domain: "input", code: "unknown-reference", path, message: `Unknown measure ${measureId}` });
74
- else if (measure.source !== "loss") issues.push({ domain: "input", code: "invalid-input-relationship", path, message: `Caller rows cannot supply ${measure.source} measure ${measureId}` });
75
- if (value !== null && typeof value !== "number") issues.push({ domain: "input", code: "invalid-type", path, message: "Measure value must be a number or null" });
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
- return Object.freeze(rows.map((row) => {
83
- const measures: Record<string, number | null> = { ...row.measures };
84
- for (const derivation of plan) {
85
- measures[derivation.outputMeasureId] = evaluateClaimExpression(derivation.expression, measures);
86
- }
87
- return Object.freeze({ ...row, measures: Object.freeze(measures) }) as DiagnosticRowWithDerivedMeasures<TRow>;
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
+ }