@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,553 @@
|
|
|
1
|
+
import { projectDiagnosticIdentity } from "./diagnosticIdentity.js";
|
|
2
|
+
import {
|
|
3
|
+
MAX_DIAGNOSTIC_JSON_DEPTH,
|
|
4
|
+
isDiagnosticPlainRecord,
|
|
5
|
+
isDiagnosticToken,
|
|
6
|
+
isWellFormedDiagnosticString,
|
|
7
|
+
} from "./diagnosticRuntime.js";
|
|
8
|
+
import { createFnvAccumulator } from "./fnvAccumulator.js";
|
|
9
|
+
import { DiagnosticValidationError, type DiagnosticValidationIssue } from "./types.js";
|
|
10
|
+
|
|
11
|
+
declare const documentBrand: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* A structural serialization descriptor, NOT proof of diagnostic authenticity.
|
|
14
|
+
* Values and sequence callbacks are live views, not snapshots. Re-iteration
|
|
15
|
+
* observes later mutations. Compliance must obtain documents from genuine
|
|
16
|
+
* prepared/review/completed-run owners, never trust an arbitrary document.
|
|
17
|
+
*/
|
|
18
|
+
export interface DiagnosticIdentityDocument {
|
|
19
|
+
readonly [documentBrand]: true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
type Node =
|
|
23
|
+
| { readonly kind: "value"; readonly value: unknown }
|
|
24
|
+
| { readonly kind: "object"; readonly fields: Fields }
|
|
25
|
+
| {
|
|
26
|
+
readonly kind: "array";
|
|
27
|
+
readonly count: number;
|
|
28
|
+
readonly itemAt: (index: number) => DiagnosticIdentityDocument;
|
|
29
|
+
};
|
|
30
|
+
type Mode = { readonly sourceSlot: boolean; readonly freeJson: boolean };
|
|
31
|
+
const plainMode: Mode = { sourceSlot: false, freeJson: false };
|
|
32
|
+
const sourceMode: Mode = { sourceSlot: true, freeJson: false };
|
|
33
|
+
const freeMode: Mode = { sourceSlot: false, freeJson: true };
|
|
34
|
+
/** Sorted sibling snapshots; raw children do not need public descriptor brands. */
|
|
35
|
+
interface Fields {
|
|
36
|
+
readonly keys: readonly string[];
|
|
37
|
+
readonly values: readonly unknown[];
|
|
38
|
+
readonly documents: boolean;
|
|
39
|
+
}
|
|
40
|
+
const nodes = new WeakMap<DiagnosticIdentityDocument, Node>();
|
|
41
|
+
const CHUNK_UNITS = 16_384;
|
|
42
|
+
const QUOTE_UNITS = 4_096;
|
|
43
|
+
|
|
44
|
+
function fail(
|
|
45
|
+
path: string,
|
|
46
|
+
message: string,
|
|
47
|
+
code: DiagnosticValidationIssue["code"] = "invalid-json-value",
|
|
48
|
+
): never {
|
|
49
|
+
throw new DiagnosticValidationError([{ domain: "input", code, path, message }]);
|
|
50
|
+
}
|
|
51
|
+
function descriptor(node: Node): DiagnosticIdentityDocument {
|
|
52
|
+
const document = Object.freeze({}) as DiagnosticIdentityDocument;
|
|
53
|
+
nodes.set(document, node);
|
|
54
|
+
return document;
|
|
55
|
+
}
|
|
56
|
+
function readDocument(value: DiagnosticIdentityDocument, path: string): Node {
|
|
57
|
+
if (value === null || typeof value !== "object" || !nodes.has(value))
|
|
58
|
+
fail(path, "Value is not a diagnostic identity serialization descriptor", "invalid-type");
|
|
59
|
+
return nodes.get(value)!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A live projected view; construction neither clones nor freezes caller data. */
|
|
63
|
+
export function createDiagnosticIdentityValue(value: unknown): DiagnosticIdentityDocument {
|
|
64
|
+
return descriptor({ kind: "value", value });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Copies the field-to-descriptor map, not the live values behind its children. */
|
|
68
|
+
export function createDiagnosticIdentityObject(
|
|
69
|
+
fields: Readonly<Record<string, DiagnosticIdentityDocument>>,
|
|
70
|
+
): DiagnosticIdentityDocument {
|
|
71
|
+
if (!isDiagnosticPlainRecord(fields))
|
|
72
|
+
fail("$", "Identity fields must be a plain record", "invalid-type");
|
|
73
|
+
const keys = Object.keys(fields).sort();
|
|
74
|
+
const values: DiagnosticIdentityDocument[] = [];
|
|
75
|
+
for (const key of keys) {
|
|
76
|
+
if (!isWellFormedDiagnosticString(key))
|
|
77
|
+
fail(`$.${key}`, "Object key is not well-formed UTF-16 or contains U+0000");
|
|
78
|
+
const field = Object.getOwnPropertyDescriptor(fields, key);
|
|
79
|
+
if (!field || !("value" in field))
|
|
80
|
+
fail(`$.${key}`, "JSON objects may contain only data properties");
|
|
81
|
+
readDocument(field.value, `$.${key}`);
|
|
82
|
+
values.push(field.value);
|
|
83
|
+
}
|
|
84
|
+
return descriptor({ kind: "object", fields: { keys, values, documents: true } });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A structural lazy sequence. The callback grants NO review/run authority and
|
|
89
|
+
* is invoked only as iteration reaches an index. It must remain repeatable for
|
|
90
|
+
* deterministic output; only an immutable SDK owner may cache a resulting tag.
|
|
91
|
+
*/
|
|
92
|
+
export function createDiagnosticIdentityArray(
|
|
93
|
+
count: number,
|
|
94
|
+
itemAt: (index: number) => DiagnosticIdentityDocument,
|
|
95
|
+
): DiagnosticIdentityDocument {
|
|
96
|
+
if (!Number.isSafeInteger(count) || count < 0)
|
|
97
|
+
fail("$.count", "Identity sequence count must be a nonnegative safe integer", "invalid-number");
|
|
98
|
+
if (typeof itemAt !== "function")
|
|
99
|
+
fail("$.itemAt", "Identity sequence reader must be a function", "invalid-type");
|
|
100
|
+
return descriptor({ kind: "array", count, itemAt });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function childMode(mode: Mode, key: string): Mode {
|
|
104
|
+
if (mode.freeJson || key === "groupDimensions" || key === "dimensions") return freeMode;
|
|
105
|
+
return key === "source" || key === "sources" ? sourceMode : plainMode;
|
|
106
|
+
}
|
|
107
|
+
function childPath(path: string, key: string): string {
|
|
108
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key)
|
|
109
|
+
? `${path}.${key}`
|
|
110
|
+
: `${path}[${JSON.stringify(key)}]`;
|
|
111
|
+
}
|
|
112
|
+
function scalar(fields: Fields, index: number): unknown {
|
|
113
|
+
if (!fields.documents) return fields.values[index];
|
|
114
|
+
const node = readDocument(fields.values[index] as DiagnosticIdentityDocument, "$");
|
|
115
|
+
// Non-scalar composition has an object value, and cannot be a source token.
|
|
116
|
+
return node.kind === "value" ? node.value : {};
|
|
117
|
+
}
|
|
118
|
+
function sourceFields(fields: Fields, mode: Mode, path: string): Fields {
|
|
119
|
+
if (mode.freeJson || !mode.sourceSlot) return fields;
|
|
120
|
+
const artifact = fields.keys.indexOf("artifactId");
|
|
121
|
+
if (artifact < 0 || typeof scalar(fields, artifact) !== "string") return fields;
|
|
122
|
+
const source = Object.create(null) as Record<string, unknown>;
|
|
123
|
+
for (let index = 0; index < fields.keys.length; index++)
|
|
124
|
+
source[fields.keys[index]!] = scalar(fields, index);
|
|
125
|
+
try {
|
|
126
|
+
// Reuse the authoritative source-slot normalization/validation, including
|
|
127
|
+
// optional nulls, rather than maintaining a second source schema.
|
|
128
|
+
const normalized = projectDiagnosticIdentity({ source }).source;
|
|
129
|
+
const keys = Object.keys(normalized).sort();
|
|
130
|
+
return {
|
|
131
|
+
keys,
|
|
132
|
+
values: keys.map((key) => normalized[key as keyof typeof normalized]),
|
|
133
|
+
documents: false,
|
|
134
|
+
};
|
|
135
|
+
} catch (error) {
|
|
136
|
+
if (error instanceof DiagnosticValidationError)
|
|
137
|
+
throw new DiagnosticValidationError(
|
|
138
|
+
error.issues.map((issue) => ({
|
|
139
|
+
...issue,
|
|
140
|
+
path: issue.path.replace(/^\$\.source/, path),
|
|
141
|
+
})),
|
|
142
|
+
);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function rawFields(raw: object, path: string): Fields {
|
|
148
|
+
const keys = Object.keys(raw).sort();
|
|
149
|
+
const values: unknown[] = [];
|
|
150
|
+
// Read every sibling descriptor before traversing a child, exactly as in the
|
|
151
|
+
// legacy streaming projection. Accessors fail without ever being invoked.
|
|
152
|
+
for (const key of keys) {
|
|
153
|
+
const location = childPath(path, key);
|
|
154
|
+
if (!isWellFormedDiagnosticString(key))
|
|
155
|
+
fail(location, "Object key is not well-formed UTF-16 or contains U+0000");
|
|
156
|
+
const field = Object.getOwnPropertyDescriptor(raw, key);
|
|
157
|
+
if (!field || !("value" in field))
|
|
158
|
+
fail(location, "JSON objects may contain only data properties");
|
|
159
|
+
values.push(field.value);
|
|
160
|
+
}
|
|
161
|
+
return { keys, values, documents: false };
|
|
162
|
+
}
|
|
163
|
+
function safeEnd(text: string, start: number, count: number): number {
|
|
164
|
+
let end = Math.min(start + count, text.length);
|
|
165
|
+
const before = text.charCodeAt(end - 1),
|
|
166
|
+
after = text.charCodeAt(end);
|
|
167
|
+
if (before >= 0xd800 && before <= 0xdbff && after >= 0xdc00 && after <= 0xdfff) end--;
|
|
168
|
+
return end;
|
|
169
|
+
}
|
|
170
|
+
function* quote(text: string): Generator<string> {
|
|
171
|
+
yield '"';
|
|
172
|
+
for (let start = 0; start < text.length; ) {
|
|
173
|
+
const end = safeEnd(text, start, QUOTE_UNITS);
|
|
174
|
+
yield JSON.stringify(text.slice(start, end)).slice(1, -1);
|
|
175
|
+
start = end;
|
|
176
|
+
}
|
|
177
|
+
yield '"';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface Location {
|
|
181
|
+
readonly mode: Mode;
|
|
182
|
+
readonly path: string;
|
|
183
|
+
readonly depth: number;
|
|
184
|
+
}
|
|
185
|
+
type Frame =
|
|
186
|
+
| (Location & { readonly kind: "document"; readonly document: DiagnosticIdentityDocument })
|
|
187
|
+
| (Location & { readonly kind: "value"; readonly value: unknown })
|
|
188
|
+
| (Location & {
|
|
189
|
+
readonly kind: "object";
|
|
190
|
+
readonly fields: Fields;
|
|
191
|
+
index: number;
|
|
192
|
+
phase: "separator" | "key" | "colon" | "child";
|
|
193
|
+
})
|
|
194
|
+
| (Location & { readonly kind: "array"; readonly values: unknown[]; index: number })
|
|
195
|
+
| (Location & {
|
|
196
|
+
readonly kind: "sequence";
|
|
197
|
+
readonly node: Extract<Node, { kind: "array" }>;
|
|
198
|
+
index: number;
|
|
199
|
+
phase: "separator" | "child";
|
|
200
|
+
})
|
|
201
|
+
| { readonly kind: "quote"; readonly text: string; start: number; opened: boolean }
|
|
202
|
+
| { readonly kind: "leave-document"; readonly document: DiagnosticIdentityDocument }
|
|
203
|
+
| { readonly kind: "leave-value"; readonly value: object };
|
|
204
|
+
|
|
205
|
+
function checkDepth(path: string, depth: number): void {
|
|
206
|
+
if (depth > MAX_DIAGNOSTIC_JSON_DEPTH)
|
|
207
|
+
fail(path, `JSON depth exceeds ${MAX_DIAGNOSTIC_JSON_DEPTH}`, "expression-limit");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Exact legacy diagnostic projection, emitted in bounded canonical chunks.
|
|
212
|
+
* Arrays are visited in order; object key lists and paths still scale with
|
|
213
|
+
* object width/depth. No full identity graph or escaped string atom is made.
|
|
214
|
+
* One explicit traversal stack feeds the bounded sink directly: raw fields
|
|
215
|
+
* never allocate public descriptors, and tokens do not bounce through nested
|
|
216
|
+
* generators. Diagnostic validation still rejects accessors without invoking
|
|
217
|
+
* them, and each iteration reads the live descriptor values afresh.
|
|
218
|
+
*/
|
|
219
|
+
export function* iterateDiagnosticIdentityJson(
|
|
220
|
+
document: DiagnosticIdentityDocument,
|
|
221
|
+
): Generator<string> {
|
|
222
|
+
readDocument(document, "$");
|
|
223
|
+
const activeDocuments = new Set<DiagnosticIdentityDocument>();
|
|
224
|
+
const activeValues = new Set<object>();
|
|
225
|
+
const stack: Frame[] = [{ kind: "document", document, mode: plainMode, path: "$", depth: 1 }];
|
|
226
|
+
let buffered = "";
|
|
227
|
+
let token = "";
|
|
228
|
+
let tokenStart = 0;
|
|
229
|
+
try {
|
|
230
|
+
while (stack.length || tokenStart < token.length) {
|
|
231
|
+
// Drain one bounded token before visiting anything else. Keeping the
|
|
232
|
+
// token/chunk boundaries also preserves when lazy callbacks are read.
|
|
233
|
+
if (tokenStart < token.length) {
|
|
234
|
+
const end = safeEnd(token, tokenStart, CHUNK_UNITS - buffered.length);
|
|
235
|
+
if (end === tokenStart) {
|
|
236
|
+
yield buffered;
|
|
237
|
+
buffered = "";
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
buffered += token.slice(tokenStart, end);
|
|
241
|
+
tokenStart = end;
|
|
242
|
+
if (buffered.length >= CHUNK_UNITS - 1) {
|
|
243
|
+
yield buffered;
|
|
244
|
+
buffered = "";
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
token = "";
|
|
249
|
+
tokenStart = 0;
|
|
250
|
+
const frame = stack[stack.length - 1]!;
|
|
251
|
+
switch (frame.kind) {
|
|
252
|
+
case "leave-document":
|
|
253
|
+
activeDocuments.delete(frame.document);
|
|
254
|
+
stack.pop();
|
|
255
|
+
break;
|
|
256
|
+
case "leave-value":
|
|
257
|
+
activeValues.delete(frame.value);
|
|
258
|
+
stack.pop();
|
|
259
|
+
break;
|
|
260
|
+
case "quote": {
|
|
261
|
+
if (!frame.opened) {
|
|
262
|
+
frame.opened = true;
|
|
263
|
+
token = '"';
|
|
264
|
+
} else if (frame.start < frame.text.length) {
|
|
265
|
+
const end = safeEnd(frame.text, frame.start, QUOTE_UNITS);
|
|
266
|
+
token = JSON.stringify(frame.text.slice(frame.start, end)).slice(1, -1);
|
|
267
|
+
frame.start = end;
|
|
268
|
+
} else {
|
|
269
|
+
token = '"';
|
|
270
|
+
stack.pop();
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "document": {
|
|
275
|
+
const { document: current, mode, path, depth } = frame;
|
|
276
|
+
const node = readDocument(current, path);
|
|
277
|
+
checkDepth(path, depth);
|
|
278
|
+
if (activeDocuments.has(current))
|
|
279
|
+
fail(path, "Identity document contains a cycle", "cycle");
|
|
280
|
+
activeDocuments.add(current);
|
|
281
|
+
stack[stack.length - 1] = { kind: "leave-document", document: current };
|
|
282
|
+
if (node.kind === "value")
|
|
283
|
+
stack.push({ kind: "value", value: node.value, mode, path, depth });
|
|
284
|
+
else if (node.kind === "object") {
|
|
285
|
+
const fields = sourceFields(node.fields, mode, path);
|
|
286
|
+
stack.push({ kind: "object", fields, mode, path, depth, index: 0, phase: "separator" });
|
|
287
|
+
token = "{";
|
|
288
|
+
} else {
|
|
289
|
+
stack.push({ kind: "sequence", node, mode, path, depth, index: 0, phase: "separator" });
|
|
290
|
+
token = "[";
|
|
291
|
+
}
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case "value": {
|
|
295
|
+
const { value: raw, mode, path, depth } = frame;
|
|
296
|
+
checkDepth(path, depth);
|
|
297
|
+
stack.pop();
|
|
298
|
+
if (raw === null) token = "null";
|
|
299
|
+
else if (typeof raw === "boolean") token = String(raw);
|
|
300
|
+
else if (typeof raw === "number") {
|
|
301
|
+
if (!Number.isFinite(raw)) fail(path, "JSON numeric value must be finite");
|
|
302
|
+
token = Object.is(raw, -0) ? "0" : String(raw);
|
|
303
|
+
} else if (typeof raw === "string") {
|
|
304
|
+
if (!isWellFormedDiagnosticString(raw))
|
|
305
|
+
fail(path, "String is not well-formed UTF-16 or contains U+0000");
|
|
306
|
+
stack.push({ kind: "quote", text: raw, start: 0, opened: false });
|
|
307
|
+
} else {
|
|
308
|
+
if (typeof raw !== "object") fail(path, "Value is not plain JSON data");
|
|
309
|
+
const array = Array.isArray(raw);
|
|
310
|
+
if (
|
|
311
|
+
array ? Object.getPrototypeOf(raw) !== Array.prototype : !isDiagnosticPlainRecord(raw)
|
|
312
|
+
)
|
|
313
|
+
fail(path, "Value must use a plain object or array prototype");
|
|
314
|
+
if (activeValues.has(raw)) fail(path, "JSON value contains a cycle", "cycle");
|
|
315
|
+
activeValues.add(raw);
|
|
316
|
+
stack.push({ kind: "leave-value", value: raw });
|
|
317
|
+
if (array) {
|
|
318
|
+
const values = raw as unknown[];
|
|
319
|
+
for (const key of Reflect.ownKeys(values)) {
|
|
320
|
+
if (key === "length") continue;
|
|
321
|
+
const index = typeof key === "string" ? Number(key) : NaN;
|
|
322
|
+
if (
|
|
323
|
+
!Number.isInteger(index) ||
|
|
324
|
+
index < 0 ||
|
|
325
|
+
index >= values.length ||
|
|
326
|
+
String(index) !== key
|
|
327
|
+
)
|
|
328
|
+
fail(
|
|
329
|
+
typeof key === "symbol" ? path : childPath(path, key),
|
|
330
|
+
"JSON arrays may contain only indexed data properties",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
stack.push({ kind: "array", values, mode, path, depth, index: 0 });
|
|
334
|
+
token = "[";
|
|
335
|
+
} else {
|
|
336
|
+
const fields = sourceFields(rawFields(raw, path), mode, path);
|
|
337
|
+
stack.push({
|
|
338
|
+
kind: "object",
|
|
339
|
+
fields,
|
|
340
|
+
mode,
|
|
341
|
+
path,
|
|
342
|
+
depth,
|
|
343
|
+
index: 0,
|
|
344
|
+
phase: "separator",
|
|
345
|
+
});
|
|
346
|
+
token = "{";
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "object": {
|
|
352
|
+
const { fields, index, mode, path, depth } = frame;
|
|
353
|
+
if (index === fields.keys.length) {
|
|
354
|
+
token = "}";
|
|
355
|
+
stack.pop();
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
const key = fields.keys[index]!;
|
|
359
|
+
if (frame.phase === "separator") {
|
|
360
|
+
frame.phase = "key";
|
|
361
|
+
if (index) token = ",";
|
|
362
|
+
} else if (frame.phase === "key") {
|
|
363
|
+
frame.phase = "colon";
|
|
364
|
+
stack.push({ kind: "quote", text: key, start: 0, opened: false });
|
|
365
|
+
} else if (frame.phase === "colon") {
|
|
366
|
+
frame.phase = "child";
|
|
367
|
+
token = ":";
|
|
368
|
+
} else {
|
|
369
|
+
const child = fields.values[index];
|
|
370
|
+
const location = {
|
|
371
|
+
mode: childMode(mode, key),
|
|
372
|
+
path: childPath(path, key),
|
|
373
|
+
depth: depth + 1,
|
|
374
|
+
};
|
|
375
|
+
stack.push(
|
|
376
|
+
fields.documents
|
|
377
|
+
? { kind: "document", document: child as DiagnosticIdentityDocument, ...location }
|
|
378
|
+
: { kind: "value", value: child, ...location },
|
|
379
|
+
);
|
|
380
|
+
frame.index++;
|
|
381
|
+
frame.phase = "separator";
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
case "array": {
|
|
386
|
+
const { values, index, mode, path, depth } = frame;
|
|
387
|
+
if (index >= values.length) {
|
|
388
|
+
token = "]";
|
|
389
|
+
stack.pop();
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
const field = Object.getOwnPropertyDescriptor(values, String(index));
|
|
393
|
+
if (!field || !("value" in field))
|
|
394
|
+
fail(`${path}[${index}]`, "JSON arrays may contain only indexed data properties");
|
|
395
|
+
stack.push({
|
|
396
|
+
kind: "value",
|
|
397
|
+
value: field.value,
|
|
398
|
+
mode,
|
|
399
|
+
path: `${path}[${index}]`,
|
|
400
|
+
depth: depth + 1,
|
|
401
|
+
});
|
|
402
|
+
frame.index++;
|
|
403
|
+
if (index) token = ",";
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
case "sequence": {
|
|
407
|
+
const { node, index, mode, path, depth } = frame;
|
|
408
|
+
if (index === node.count) {
|
|
409
|
+
token = "]";
|
|
410
|
+
stack.pop();
|
|
411
|
+
} else if (frame.phase === "separator") {
|
|
412
|
+
frame.phase = "child";
|
|
413
|
+
if (index) token = ",";
|
|
414
|
+
} else {
|
|
415
|
+
const child = node.itemAt(index);
|
|
416
|
+
stack.push({
|
|
417
|
+
kind: "document",
|
|
418
|
+
document: child,
|
|
419
|
+
mode,
|
|
420
|
+
path: `${path}[${index}]`,
|
|
421
|
+
depth: depth + 1,
|
|
422
|
+
});
|
|
423
|
+
frame.index++;
|
|
424
|
+
frame.phase = "separator";
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (buffered) yield buffered;
|
|
431
|
+
} finally {
|
|
432
|
+
// Cancellation must release live views without traversing later children.
|
|
433
|
+
stack.length = 0;
|
|
434
|
+
activeDocuments.clear();
|
|
435
|
+
activeValues.clear();
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Full legacy FNV tag; structure alone is not proof of a completed SDK run. */
|
|
440
|
+
export function fingerprintDiagnosticIdentity(
|
|
441
|
+
document: DiagnosticIdentityDocument,
|
|
442
|
+
envelope: { readonly kind: string; readonly property: string },
|
|
443
|
+
): string {
|
|
444
|
+
if (
|
|
445
|
+
!isDiagnosticToken(envelope?.kind) ||
|
|
446
|
+
!isDiagnosticToken(envelope?.property) ||
|
|
447
|
+
["kind", "identityVersion"].includes(envelope.property)
|
|
448
|
+
)
|
|
449
|
+
fail(
|
|
450
|
+
"$.envelope",
|
|
451
|
+
"Identity envelope requires distinct nonempty kind/property tokens",
|
|
452
|
+
"invalid-string",
|
|
453
|
+
);
|
|
454
|
+
readDocument(document, "$");
|
|
455
|
+
function* envelopeTokens(): Generator<string> {
|
|
456
|
+
yield "{";
|
|
457
|
+
let index = 0;
|
|
458
|
+
for (const key of ["identityVersion", "kind", envelope.property].sort()) {
|
|
459
|
+
if (index++) yield ",";
|
|
460
|
+
yield* quote(key);
|
|
461
|
+
yield ":";
|
|
462
|
+
if (key === "identityVersion") yield "1";
|
|
463
|
+
else if (key === "kind") yield* quote(envelope.kind);
|
|
464
|
+
// The body is already an identity projection. The envelope must not
|
|
465
|
+
// change its source context or consume an extra diagnostic depth level.
|
|
466
|
+
else yield* iterateDiagnosticIdentityJson(document);
|
|
467
|
+
}
|
|
468
|
+
yield "}";
|
|
469
|
+
}
|
|
470
|
+
const hash = createFnvAccumulator();
|
|
471
|
+
const encoder = new TextEncoder();
|
|
472
|
+
const bytes = new Uint8Array(CHUNK_UNITS * 3);
|
|
473
|
+
for (const text of envelopeTokens()) {
|
|
474
|
+
for (let start = 0; start < text.length; ) {
|
|
475
|
+
const end = safeEnd(text, start, CHUNK_UNITS);
|
|
476
|
+
const { written } = encoder.encodeInto(text.slice(start, end), bytes);
|
|
477
|
+
hash.update(bytes.subarray(0, written));
|
|
478
|
+
start = end;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return `fnv1a64-jcs-v1:${hash.digest()}`;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export type DiagnosticIdentityComparison =
|
|
485
|
+
| { readonly equal: true }
|
|
486
|
+
| { readonly equal: false; readonly codeUnitOffset: number };
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Exact canonical text comparison, independent of chunk boundaries; never hash
|
|
490
|
+
* equality. A mismatch stops iteration and reports its UTF-16 offset, without
|
|
491
|
+
* building a giant candidate just to recover a nested JSON path.
|
|
492
|
+
*/
|
|
493
|
+
export function compareDiagnosticIdentityDocuments(
|
|
494
|
+
left: DiagnosticIdentityDocument,
|
|
495
|
+
right: DiagnosticIdentityDocument,
|
|
496
|
+
): DiagnosticIdentityComparison {
|
|
497
|
+
return compareDiagnosticIdentityJsonChunks(
|
|
498
|
+
iterateDiagnosticIdentityJson(left),
|
|
499
|
+
iterateDiagnosticIdentityJson(right),
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Exact UTF-16 text comparison only: this does NOT validate JSON, normalize
|
|
505
|
+
* input, or authenticate evidence. Candidate decoders must validate their own
|
|
506
|
+
* format, and diagnostic verification must independently authenticate SDK
|
|
507
|
+
* owners. Empty chunks and split surrogate pairs do not affect equality.
|
|
508
|
+
*/
|
|
509
|
+
export function compareDiagnosticIdentityJsonChunks(
|
|
510
|
+
left: Iterable<string>,
|
|
511
|
+
right: Iterable<string>,
|
|
512
|
+
): DiagnosticIdentityComparison {
|
|
513
|
+
const a = left[Symbol.iterator]();
|
|
514
|
+
let b: Iterator<string> | undefined;
|
|
515
|
+
const next = (iterator: Iterator<string>): IteratorResult<string> => {
|
|
516
|
+
for (;;) {
|
|
517
|
+
const item = iterator.next();
|
|
518
|
+
if (item.done) return item;
|
|
519
|
+
if (typeof item.value !== "string")
|
|
520
|
+
fail("$", "Canonical text chunks must be strings", "invalid-type");
|
|
521
|
+
if (item.value.length) return item;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
try {
|
|
525
|
+
b = right[Symbol.iterator]();
|
|
526
|
+
let x = next(a),
|
|
527
|
+
y = next(b),
|
|
528
|
+
xi = 0,
|
|
529
|
+
yi = 0,
|
|
530
|
+
offset = 0;
|
|
531
|
+
while (!x.done && !y.done) {
|
|
532
|
+
const count = Math.min(x.value.length - xi, y.value.length - yi);
|
|
533
|
+
for (let index = 0; index < count; index++)
|
|
534
|
+
if (x.value.charCodeAt(xi + index) !== y.value.charCodeAt(yi + index))
|
|
535
|
+
return { equal: false, codeUnitOffset: offset + index };
|
|
536
|
+
offset += count;
|
|
537
|
+
xi += count;
|
|
538
|
+
yi += count;
|
|
539
|
+
if (xi === x.value.length) {
|
|
540
|
+
x = next(a);
|
|
541
|
+
xi = 0;
|
|
542
|
+
}
|
|
543
|
+
if (yi === y.value.length) {
|
|
544
|
+
y = next(b);
|
|
545
|
+
yi = 0;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return x.done && y.done ? { equal: true } : { equal: false, codeUnitOffset: offset };
|
|
549
|
+
} finally {
|
|
550
|
+
a.return?.();
|
|
551
|
+
b?.return?.();
|
|
552
|
+
}
|
|
553
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { canonicalJson } from "./canonical.js";
|
|
2
|
+
import type {
|
|
3
|
+
DiagnosticMeasureContribution,
|
|
4
|
+
DiagnosticStructuralBlocker,
|
|
5
|
+
} from "./diagnosticAggregation.js";
|
|
6
|
+
import type { DiagnosticMetricFinding } from "./diagnosticFormulas.js";
|
|
7
|
+
import { compareDiagnosticSourceLocations } from "./diagnosticSourceOrdering.js";
|
|
8
|
+
import type { DiagnosticSourceLocation } from "./diagnosticDefinitions.js";
|
|
9
|
+
|
|
10
|
+
/** Section 15 total order, for values already validated by a public boundary. */
|
|
11
|
+
export function compareDiagnosticIdentityValues(
|
|
12
|
+
left: unknown,
|
|
13
|
+
right: unknown,
|
|
14
|
+
): number {
|
|
15
|
+
if (left === right) return 0;
|
|
16
|
+
if (left === undefined) return -1;
|
|
17
|
+
if (right === undefined) return 1;
|
|
18
|
+
if (left === null) return -1;
|
|
19
|
+
if (right === null) return 1;
|
|
20
|
+
if (typeof left === "number" && typeof right === "number")
|
|
21
|
+
return left - right;
|
|
22
|
+
if (typeof left === "boolean" && typeof right === "boolean")
|
|
23
|
+
return Number(left) - Number(right);
|
|
24
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
25
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
26
|
+
const comparison = compareDiagnosticIdentityValues(left[i], right[i]);
|
|
27
|
+
if (comparison !== 0) return comparison;
|
|
28
|
+
}
|
|
29
|
+
return left.length - right.length;
|
|
30
|
+
}
|
|
31
|
+
const a = typeof left === "string" ? left : canonicalJson(left);
|
|
32
|
+
const b = typeof right === "string" ? right : canonicalJson(right);
|
|
33
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function compareSources(
|
|
37
|
+
left: readonly DiagnosticSourceLocation[],
|
|
38
|
+
right: readonly DiagnosticSourceLocation[],
|
|
39
|
+
): number {
|
|
40
|
+
for (let i = 0; i < Math.min(left.length, right.length); i++) {
|
|
41
|
+
const comparison = compareDiagnosticSourceLocations(left[i]!, right[i]!);
|
|
42
|
+
if (comparison !== 0) return comparison;
|
|
43
|
+
}
|
|
44
|
+
return left.length - right.length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function compareDiagnosticFindings(
|
|
48
|
+
left: DiagnosticMetricFinding,
|
|
49
|
+
right: DiagnosticMetricFinding,
|
|
50
|
+
): number {
|
|
51
|
+
const fields = [
|
|
52
|
+
"category",
|
|
53
|
+
"severity",
|
|
54
|
+
"code",
|
|
55
|
+
"ruleId",
|
|
56
|
+
"instanceId",
|
|
57
|
+
"measureId",
|
|
58
|
+
"expressionPath",
|
|
59
|
+
"sourceGroup",
|
|
60
|
+
"group",
|
|
61
|
+
"origin",
|
|
62
|
+
"valuation",
|
|
63
|
+
"developmentAge",
|
|
64
|
+
"ageUnit",
|
|
65
|
+
"recordId",
|
|
66
|
+
"claimId",
|
|
67
|
+
"exposureKey",
|
|
68
|
+
"message",
|
|
69
|
+
] as const;
|
|
70
|
+
return (
|
|
71
|
+
compareDiagnosticIdentityValues(
|
|
72
|
+
fields.map((key) => left[key]),
|
|
73
|
+
fields.map((key) => right[key]),
|
|
74
|
+
) ||
|
|
75
|
+
compareDiagnosticIdentityValues(left.offendingKey, right.offendingKey) ||
|
|
76
|
+
compareSources(left.sources, right.sources)
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function compareDiagnosticContributions(
|
|
81
|
+
left: DiagnosticMeasureContribution,
|
|
82
|
+
right: DiagnosticMeasureContribution,
|
|
83
|
+
): number {
|
|
84
|
+
return (
|
|
85
|
+
compareDiagnosticIdentityValues(
|
|
86
|
+
[left.sourceId, left.status, left.deduplicated],
|
|
87
|
+
[right.sourceId, right.status, right.deduplicated],
|
|
88
|
+
) ||
|
|
89
|
+
compareSources(left.sources, right.sources) ||
|
|
90
|
+
compareDiagnosticIdentityValues(left, right)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function compareDiagnosticBlockers(
|
|
95
|
+
left: DiagnosticStructuralBlocker,
|
|
96
|
+
right: DiagnosticStructuralBlocker,
|
|
97
|
+
): number {
|
|
98
|
+
return (
|
|
99
|
+
compareDiagnosticIdentityValues(
|
|
100
|
+
[left.code, left.message, left.sourceIds],
|
|
101
|
+
[right.code, right.message, right.sourceIds],
|
|
102
|
+
) ||
|
|
103
|
+
compareSources(left.sources, right.sources) ||
|
|
104
|
+
compareDiagnosticIdentityValues(left.finding, right.finding)
|
|
105
|
+
);
|
|
106
|
+
}
|