prefix-safe-json 0.0.1-alpha.4 → 0.1.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 (135) hide show
  1. package/LICENSE +4 -0
  2. package/README.md +2 -2
  3. package/package.json +2 -1
  4. package/dist/coordinator/coordinator.d.ts +0 -53
  5. package/dist/coordinator/coordinator.d.ts.map +0 -1
  6. package/dist/coordinator/coordinator.js +0 -448
  7. package/dist/coordinator/coordinator.js.map +0 -1
  8. package/dist/coordinator/diagnostic-codes.d.ts +0 -11
  9. package/dist/coordinator/diagnostic-codes.d.ts.map +0 -1
  10. package/dist/coordinator/diagnostic-codes.js +0 -22
  11. package/dist/coordinator/diagnostic-codes.js.map +0 -1
  12. package/dist/coordinator/protocol.d.ts +0 -61
  13. package/dist/coordinator/protocol.d.ts.map +0 -1
  14. package/dist/coordinator/protocol.js +0 -2
  15. package/dist/coordinator/protocol.js.map +0 -1
  16. package/dist/coordinator/types.d.ts +0 -104
  17. package/dist/coordinator/types.d.ts.map +0 -1
  18. package/dist/coordinator/types.js +0 -7
  19. package/dist/coordinator/types.js.map +0 -1
  20. package/dist/diagnostics/codes.d.ts +0 -24
  21. package/dist/diagnostics/codes.d.ts.map +0 -1
  22. package/dist/diagnostics/codes.js +0 -28
  23. package/dist/diagnostics/codes.js.map +0 -1
  24. package/dist/diagnostics/factory.d.ts +0 -8
  25. package/dist/diagnostics/factory.d.ts.map +0 -1
  26. package/dist/diagnostics/factory.js +0 -17
  27. package/dist/diagnostics/factory.js.map +0 -1
  28. package/dist/gate/decide.d.ts +0 -27
  29. package/dist/gate/decide.d.ts.map +0 -1
  30. package/dist/gate/decide.js +0 -138
  31. package/dist/gate/decide.js.map +0 -1
  32. package/dist/gate/gate.d.ts +0 -25
  33. package/dist/gate/gate.d.ts.map +0 -1
  34. package/dist/gate/gate.js +0 -96
  35. package/dist/gate/gate.js.map +0 -1
  36. package/dist/gate/types.d.ts +0 -184
  37. package/dist/gate/types.d.ts.map +0 -1
  38. package/dist/gate/types.js +0 -2
  39. package/dist/gate/types.js.map +0 -1
  40. package/dist/grammar/frame.d.ts +0 -58
  41. package/dist/grammar/frame.d.ts.map +0 -1
  42. package/dist/grammar/frame.js +0 -87
  43. package/dist/grammar/frame.js.map +0 -1
  44. package/dist/grammar/pointer.d.ts +0 -10
  45. package/dist/grammar/pointer.d.ts.map +0 -1
  46. package/dist/grammar/pointer.js +0 -20
  47. package/dist/grammar/pointer.js.map +0 -1
  48. package/dist/grammar/stack.d.ts +0 -73
  49. package/dist/grammar/stack.d.ts.map +0 -1
  50. package/dist/grammar/stack.js +0 -168
  51. package/dist/grammar/stack.js.map +0 -1
  52. package/dist/guard/ai-sdk.d.ts +0 -48
  53. package/dist/guard/ai-sdk.d.ts.map +0 -1
  54. package/dist/guard/ai-sdk.js +0 -51
  55. package/dist/guard/ai-sdk.js.map +0 -1
  56. package/dist/guard/provider-guard.d.ts +0 -4
  57. package/dist/guard/provider-guard.d.ts.map +0 -1
  58. package/dist/guard/provider-guard.js +0 -38
  59. package/dist/guard/provider-guard.js.map +0 -1
  60. package/dist/guard/types.d.ts +0 -48
  61. package/dist/guard/types.d.ts.map +0 -1
  62. package/dist/guard/types.js +0 -2
  63. package/dist/guard/types.js.map +0 -1
  64. package/dist/index.d.ts +0 -45
  65. package/dist/index.d.ts.map +0 -1
  66. package/dist/index.js +0 -45
  67. package/dist/index.js.map +0 -1
  68. package/dist/lexer/scanner.d.ts +0 -106
  69. package/dist/lexer/scanner.d.ts.map +0 -1
  70. package/dist/lexer/scanner.js +0 -658
  71. package/dist/lexer/scanner.js.map +0 -1
  72. package/dist/lexer/states.d.ts +0 -31
  73. package/dist/lexer/states.d.ts.map +0 -1
  74. package/dist/lexer/states.js +0 -35
  75. package/dist/lexer/states.js.map +0 -1
  76. package/dist/lexer/tokens.d.ts +0 -22
  77. package/dist/lexer/tokens.d.ts.map +0 -1
  78. package/dist/lexer/tokens.js +0 -22
  79. package/dist/lexer/tokens.js.map +0 -1
  80. package/dist/limits.d.ts +0 -14
  81. package/dist/limits.d.ts.map +0 -1
  82. package/dist/limits.js +0 -23
  83. package/dist/limits.js.map +0 -1
  84. package/dist/parser.d.ts +0 -6
  85. package/dist/parser.d.ts.map +0 -1
  86. package/dist/parser.js +0 -922
  87. package/dist/parser.js.map +0 -1
  88. package/dist/providers/adapter.d.ts +0 -10
  89. package/dist/providers/adapter.d.ts.map +0 -1
  90. package/dist/providers/adapter.js +0 -2
  91. package/dist/providers/adapter.js.map +0 -1
  92. package/dist/providers/ai-sdk.d.ts +0 -58
  93. package/dist/providers/ai-sdk.d.ts.map +0 -1
  94. package/dist/providers/ai-sdk.js +0 -212
  95. package/dist/providers/ai-sdk.js.map +0 -1
  96. package/dist/providers/anthropic.d.ts +0 -45
  97. package/dist/providers/anthropic.d.ts.map +0 -1
  98. package/dist/providers/anthropic.js +0 -112
  99. package/dist/providers/anthropic.js.map +0 -1
  100. package/dist/providers/gemini.d.ts +0 -31
  101. package/dist/providers/gemini.d.ts.map +0 -1
  102. package/dist/providers/gemini.js +0 -94
  103. package/dist/providers/gemini.js.map +0 -1
  104. package/dist/providers/openai-compatible.d.ts +0 -33
  105. package/dist/providers/openai-compatible.d.ts.map +0 -1
  106. package/dist/providers/openai-compatible.js +0 -160
  107. package/dist/providers/openai-compatible.js.map +0 -1
  108. package/dist/providers/openai.d.ts +0 -55
  109. package/dist/providers/openai.d.ts.map +0 -1
  110. package/dist/providers/openai.js +0 -263
  111. package/dist/providers/openai.js.map +0 -1
  112. package/dist/providers/openrouter.d.ts +0 -35
  113. package/dist/providers/openrouter.d.ts.map +0 -1
  114. package/dist/providers/openrouter.js +0 -55
  115. package/dist/providers/openrouter.js.map +0 -1
  116. package/dist/semantic/builder.d.ts +0 -55
  117. package/dist/semantic/builder.d.ts.map +0 -1
  118. package/dist/semantic/builder.js +0 -140
  119. package/dist/semantic/builder.js.map +0 -1
  120. package/dist/semantic/events.d.ts +0 -2
  121. package/dist/semantic/events.d.ts.map +0 -1
  122. package/dist/semantic/events.js +0 -5
  123. package/dist/semantic/events.js.map +0 -1
  124. package/dist/semantic/snapshot.d.ts +0 -56
  125. package/dist/semantic/snapshot.d.ts.map +0 -1
  126. package/dist/semantic/snapshot.js +0 -230
  127. package/dist/semantic/snapshot.js.map +0 -1
  128. package/dist/types.d.ts +0 -156
  129. package/dist/types.d.ts.map +0 -1
  130. package/dist/types.js +0 -5
  131. package/dist/types.js.map +0 -1
  132. package/dist/utf8/decoder.d.ts +0 -53
  133. package/dist/utf8/decoder.d.ts.map +0 -1
  134. package/dist/utf8/decoder.js +0 -323
  135. package/dist/utf8/decoder.js.map +0 -1
package/dist/parser.js DELETED
@@ -1,922 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // IncrementalJsonParser — main parser implementation
3
- // ---------------------------------------------------------------------------
4
- //
5
- // Integrates: UTF-8 decoder → Lexer/Scanner → Grammar Stack → Event Builder
6
- //
7
- // Public API: push(), snapshot(), drainEvents(), finish()
8
- // ---------------------------------------------------------------------------
9
- import { DiagnosticCode } from "./diagnostics/codes.js";
10
- import { DEFAULT_LIMITS, sanitizeLimit } from "./limits.js";
11
- import { Utf8Decoder, stringToUtf8 } from "./utf8/decoder.js";
12
- import { Scanner } from "./lexer/scanner.js";
13
- import { ScannerState } from "./lexer/states.js";
14
- import { TokenType } from "./lexer/tokens.js";
15
- import { GrammarStack } from "./grammar/stack.js";
16
- import { EventBuilder } from "./semantic/builder.js";
17
- import { SnapshotBuilder } from "./semantic/snapshot.js";
18
- import { createDiagnostic } from "./diagnostics/factory.js";
19
- /**
20
- * Create a new IncrementalJsonParser.
21
- */
22
- export function createParser(options) {
23
- return new Parser(options);
24
- }
25
- class Parser {
26
- limits;
27
- utf8;
28
- scanner;
29
- grammar;
30
- events;
31
- snapshot_;
32
- allDiagnostics = [];
33
- allRepairs = [];
34
- // Sticky flags mirroring the *existence* of certain diagnostics/repairs,
35
- // set unconditionally in addDiagnostic()/addRepair() regardless of
36
- // whether the corresponding array is at its maxQueuedEvents cap. Several
37
- // checks (isExecutable, determineOutcome) need an exact, monotonic answer
38
- // to "did X ever happen for this stream" — re-deriving that by scanning
39
- // allDiagnostics/allRepairs is unsound once those arrays can be truncated,
40
- // since a disqualifying entry could be the one silently dropped. This is
41
- // the same reasoning that already made grammar.hasDuplicate a dedicated
42
- // boolean instead of an array scan.
43
- everHadFatalDiagnostic = false;
44
- everHadNonRecoverableError = false;
45
- everHadStructuralOrLossyRepair = false;
46
- everHadCloseContainerRepair = false;
47
- phase = "collecting";
48
- receivedBytes = 0;
49
- consumedBytes = 0;
50
- rootComplete = false;
51
- terminal = false;
52
- hasSeenValue = false;
53
- syntax_ = "empty";
54
- trailingDataSeen = false;
55
- trailingDataBytes = 0;
56
- rootScalarValue = undefined;
57
- repairs;
58
- // Track if we're in a duplicate-key value (to skip it)
59
- skipValueDepth = 0;
60
- isSkippingValue = false;
61
- // For finish
62
- finishMeta;
63
- constructor(options) {
64
- const userLimits = options?.limits ?? {};
65
- this.limits = {
66
- maxInputBytes: sanitizeLimit(userLimits.maxInputBytes, DEFAULT_LIMITS.maxInputBytes),
67
- maxDepth: sanitizeLimit(userLimits.maxDepth, DEFAULT_LIMITS.maxDepth),
68
- maxStringBytes: sanitizeLimit(userLimits.maxStringBytes, DEFAULT_LIMITS.maxStringBytes),
69
- maxQueuedEvents: sanitizeLimit(userLimits.maxQueuedEvents, DEFAULT_LIMITS.maxQueuedEvents),
70
- maxTrailingDataBytes: sanitizeLimit(userLimits.maxTrailingDataBytes, DEFAULT_LIMITS.maxTrailingDataBytes),
71
- };
72
- this.repairs = {
73
- rawControlCharacters: options?.repairs?.rawControlCharacters ?? "reject",
74
- trailingData: options?.repairs?.trailingData ?? "isolate",
75
- closeContainersAtFinish: options?.repairs?.closeContainersAtFinish ?? "disabled",
76
- };
77
- this.utf8 = new Utf8Decoder();
78
- this.scanner = new Scanner(options);
79
- this.grammar = new GrammarStack(this.limits.maxDepth);
80
- this.events = new EventBuilder(this.limits.maxQueuedEvents);
81
- this.snapshot_ = new SnapshotBuilder();
82
- }
83
- push(chunk) {
84
- if (this.phase === "finished") {
85
- throw new Error("E_PUSH_AFTER_FINISH: push() called after finish()");
86
- }
87
- if (this.terminal) {
88
- return {
89
- acceptedBytes: 0,
90
- emittedEvents: 0,
91
- syntax: this.syntax_,
92
- terminal: true,
93
- };
94
- }
95
- // Convert string to bytes
96
- const bytes = typeof chunk === "string" ? stringToUtf8(chunk) : chunk;
97
- // Check input limit
98
- if (this.receivedBytes + bytes.length > this.limits.maxInputBytes) {
99
- const diag = createDiagnostic(DiagnosticCode.E_LIMIT_INPUT_BYTES, "fatal", this.receivedBytes, `Input exceeds maximum of ${this.limits.maxInputBytes} bytes`, false);
100
- this.addDiagnostic(diag);
101
- this.terminal = true;
102
- this.syntax_ = "invalid";
103
- return {
104
- acceptedBytes: 0,
105
- emittedEvents: 0,
106
- syntax: this.syntax_,
107
- terminal: true,
108
- };
109
- }
110
- this.receivedBytes += bytes.length;
111
- this.events.resetPushCount();
112
- // Decode UTF-8
113
- const decoded = this.utf8.decode(bytes);
114
- if (decoded.strippedBom) {
115
- const repair = {
116
- code: "R_STRIP_UTF8_BOM",
117
- byteRange: [0, 3],
118
- impact: "representation_preserving",
119
- description: "Stripped UTF-8 BOM at index 0",
120
- };
121
- this.addRepair(repair);
122
- }
123
- // Handle UTF-8 errors
124
- for (const err of decoded.errors) {
125
- // Every UTF-8 decode failure kind (overlong, out_of_range,
126
- // invalid_start_byte, invalid_continuation) maps to the same
127
- // machine-readable code - there is no per-kind DiagnosticCode - so
128
- // `err.kind` only distinguishes the free-text message below, not the
129
- // code itself. A branch here would be equivalent under every mutation
130
- // (the resulting `code` value can never differ), so there is no
131
- // branch to test.
132
- const diag = createDiagnostic(DiagnosticCode.E_INVALID_UTF8, "error", err.byteOffset, `Invalid UTF-8: ${err.kind} at byte ${err.byteOffset}`, false);
133
- this.addDiagnostic(diag);
134
- this.syntax_ = "invalid";
135
- this.terminal = true;
136
- return {
137
- acceptedBytes: decoded.consumed,
138
- emittedEvents: this.events.emittedDuringPush,
139
- syntax: this.syntax_,
140
- terminal: true,
141
- };
142
- }
143
- // Feed decoded text to scanner character by character
144
- const text = decoded.text;
145
- // Track byte positions via UTF-8 encoding
146
- let bytePos = this.consumedBytes;
147
- for (const ch of text) {
148
- if (this.terminal)
149
- break;
150
- const charBytes = getUtf8ByteLength(ch);
151
- // If root is already complete, handle trailing content at parser level
152
- if (this.rootComplete) {
153
- const isWs = ch === " " || ch === "\t" || ch === "\n" || ch === "\r";
154
- if (!isWs) {
155
- if (!this.trailingDataSeen) {
156
- this.trailingDataSeen = true;
157
- if (this.repairs.trailingData === "reject") {
158
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_TRAILING_DATA, "error", bytePos, `Trailing data rejected`, false));
159
- this.syntax_ = "invalid";
160
- this.terminal = true;
161
- return {
162
- acceptedBytes: decoded.consumed,
163
- emittedEvents: this.events.emittedDuringPush,
164
- syntax: this.syntax_,
165
- terminal: true,
166
- };
167
- }
168
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_TRAILING_DATA, "warning", bytePos, `Unexpected data after root JSON value`, true));
169
- const repair = {
170
- code: "R_ISOLATE_TRAILING_DATA",
171
- byteRange: [bytePos, bytePos + charBytes],
172
- impact: "root_preserving",
173
- description: "Isolated trailing data after complete JSON root",
174
- };
175
- this.addRepair(repair);
176
- }
177
- this.trailingDataBytes += charBytes;
178
- if (this.trailingDataBytes > this.limits.maxTrailingDataBytes) {
179
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_LIMIT_INPUT_BYTES, "error", bytePos, `Maximum trailing data bytes exceeded`, false));
180
- this.syntax_ = "invalid";
181
- this.terminal = true;
182
- }
183
- }
184
- bytePos += charBytes;
185
- continue;
186
- }
187
- // Set up scanner for object key context
188
- const frame = this.grammar.current;
189
- if (frame &&
190
- frame.containerType === "object" &&
191
- (frame.objectExpectation === "first_key_or_end" || frame.objectExpectation === "key_after_comma") &&
192
- this.scanner.currentState === ScannerState.Structural) {
193
- this.scanner.setNextStringIsKey(true);
194
- }
195
- this.scanner.feedChar(ch, bytePos, charBytes);
196
- // Process any emitted tokens
197
- const tokens = this.scanner.takeTokens();
198
- for (const token of tokens) {
199
- if (this.terminal)
200
- break;
201
- this.processToken(token);
202
- }
203
- // Process any scanner diagnostics and repairs
204
- const scanDiags = this.scanner.takeDiagnostics();
205
- const scanRepairs = this.scanner.takeRepairs();
206
- for (const repair of scanRepairs) {
207
- this.addRepair(repair);
208
- }
209
- for (const diag of scanDiags) {
210
- this.addDiagnostic(diag);
211
- if (diag.severity === "fatal" ||
212
- (diag.severity === "error" && !diag.recoverable)) {
213
- this.syntax_ = "invalid";
214
- this.terminal = true;
215
- }
216
- }
217
- // Process grammar diagnostics
218
- const grammarDiags = this.grammar.takeDiagnostics();
219
- for (const diag of grammarDiags) {
220
- this.addDiagnostic(diag);
221
- if (diag.severity === "fatal") {
222
- this.terminal = true;
223
- this.syntax_ = "invalid";
224
- }
225
- }
226
- if (this.events.isTerminal) {
227
- this.terminal = true;
228
- this.syntax_ = "invalid";
229
- break;
230
- }
231
- bytePos += charBytes;
232
- }
233
- this.consumedBytes = bytePos;
234
- return {
235
- acceptedBytes: decoded.consumed,
236
- emittedEvents: this.events.emittedDuringPush,
237
- syntax: this.syntax_,
238
- terminal: this.terminal,
239
- };
240
- }
241
- snapshot() {
242
- const pending = this.buildPendingTokens();
243
- return {
244
- phase: this.phase,
245
- syntax: this.syntax_,
246
- stableValue: this.buildStableValue(),
247
- rootComplete: this.rootComplete,
248
- executable: this.isExecutable(),
249
- pending,
250
- repairs: Object.freeze([...this.allRepairs]),
251
- diagnostics: Object.freeze([...this.allDiagnostics]),
252
- receivedBytes: this.receivedBytes,
253
- consumedBytes: this.consumedBytes,
254
- };
255
- }
256
- drainEvents() {
257
- return this.events.drain();
258
- }
259
- finish(meta) {
260
- if (this.phase === "finished") {
261
- throw new Error("E_PUSH_AFTER_FINISH: finish() called more than once");
262
- }
263
- this.phase = "finished";
264
- this.finishMeta = meta;
265
- const reason = meta?.reason ?? "unknown";
266
- // Try to finalize any pending number or literal at stream end
267
- if (!this.terminal) {
268
- // Check for pending number — at stream end, numbers can be finalized
269
- // only if the stream completed normally
270
- const pendingInfo = this.scanner.getPendingInfo();
271
- if (pendingInfo.type === "number") {
272
- if (reason === "complete") {
273
- // Stream ended normally — the number terminator is implicit EOF
274
- const finalized = this.scanner.finalizeNumber();
275
- if (finalized === "finalized") {
276
- const tokens = this.scanner.takeTokens();
277
- for (const token of tokens) {
278
- this.processToken(token);
279
- }
280
- }
281
- else if (finalized === "invalid") {
282
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", pendingInfo.byteStart, `Invalid number at stream end: ${pendingInfo.buffer}`, false));
283
- // Unlike every other error branch in this method, this one
284
- // never routed through commitScalar()/the scanner's own
285
- // Invalid-state transition, so nothing else here sets
286
- // terminal/syntax_. Without this, a definitively malformed
287
- // number (leading zero, trailing "."/exponent - not fixable by
288
- // more input, unlike a genuine truncation) fell through
289
- // determineOutcome()'s generic `!rootComplete` case and was
290
- // misreported as outcome "truncated" instead of "invalid" for
291
- // a bare top-level root (e.g. plain "01" or "-01" as the whole
292
- // document) - the same malformation wrapped in a container
293
- // (e.g. `{"a":01}`) already reported "invalid" correctly via
294
- // the scanner's ScannerState.Invalid path.
295
- this.terminal = true;
296
- this.syntax_ = "invalid";
297
- }
298
- // "not_ready": scanner wasn't in a finalizable number state at all
299
- // (e.g. buffer is just "-", or ends right at a bare "e"/"E") —
300
- // preserved exactly as before: no diagnostic added here.
301
- }
302
- else {
303
- // Truncated — number is incomplete
304
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_NUMBER, "error", pendingInfo.byteStart, `Incomplete number at stream end: ${pendingInfo.buffer}`, false));
305
- }
306
- }
307
- else if (pendingInfo.type === "literal") {
308
- if (reason === "complete" &&
309
- pendingInfo.buffer.length ===
310
- (pendingInfo.buffer === "tru" || pendingInfo.buffer === "fal"
311
- ? 0
312
- : pendingInfo.buffer.length)) {
313
- const finalized = this.scanner.finalizeLiteral();
314
- if (finalized) {
315
- const tokens = this.scanner.takeTokens();
316
- for (const token of tokens) {
317
- this.processToken(token);
318
- }
319
- }
320
- else {
321
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_LITERAL, "error", pendingInfo.byteStart, `Incomplete literal at stream end: ${pendingInfo.buffer}`, false));
322
- }
323
- }
324
- else {
325
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_LITERAL, "error", pendingInfo.byteStart, `Incomplete literal at stream end: ${pendingInfo.buffer}`, false));
326
- }
327
- }
328
- else if (pendingInfo.type === "string" ||
329
- pendingInfo.type === "object_key") {
330
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNTERMINATED_STRING, "error", pendingInfo.byteStart, "Unterminated string at stream end", false));
331
- }
332
- else if (pendingInfo.type === "unicode_escape") {
333
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_UNICODE_ESCAPE, "error", pendingInfo.byteStart, "Incomplete unicode escape at stream end", false));
334
- }
335
- else if (pendingInfo.type === "surrogate") {
336
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNPAIRED_SURROGATE, "error", pendingInfo.byteStart, "Unpaired surrogate at stream end", false));
337
- }
338
- // Check for incomplete UTF-8
339
- if (this.utf8.hasIncomplete()) {
340
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_INCOMPLETE_UTF8, "error", this.utf8.incompleteByteOffset(), "Incomplete UTF-8 sequence at stream end", false));
341
- }
342
- }
343
- // Check for truncation
344
- if (!this.rootComplete &&
345
- reason !== "complete") {
346
- // Attempt structural salvage
347
- let salvaged = false;
348
- if (this.repairs.closeContainersAtFinish === "safe-only" &&
349
- this.scanner.currentState === ScannerState.Structural &&
350
- this.scanner.getPendingInfo().type === "none" &&
351
- this.grammar.canSafelyCloseAll() &&
352
- !this.hasFatalDiagnostic() &&
353
- (this.syntax_ !== "invalid")) {
354
- salvaged = true;
355
- const frames = this.grammar.getFrames();
356
- // We know exactly what to close, just close them all
357
- for (let i = frames.length - 1; i >= 0; i--) {
358
- const frame = frames[i];
359
- if (frame) {
360
- this.events.emitContainerClosed(frame.path, frame.containerType);
361
- }
362
- }
363
- const repair = {
364
- code: "R_CLOSE_CONTAINER",
365
- byteRange: [this.consumedBytes, this.consumedBytes],
366
- impact: "structural",
367
- description: `Safely closed ${frames.length} containers at stream end`,
368
- };
369
- this.addRepair(repair);
370
- this.rootComplete = true;
371
- }
372
- if (!salvaged) {
373
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_STREAM_TRUNCATED, "error", this.consumedBytes, `Stream truncated: ${reason}`, false));
374
- }
375
- }
376
- else if (!this.rootComplete && reason === "complete") {
377
- // Stream said complete but root isn't closed
378
- if (this.hasSeenValue) {
379
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_STREAM_TRUNCATED, "error", this.consumedBytes, "Stream marked complete but JSON root is not closed", false));
380
- }
381
- }
382
- const outcome = this.determineOutcome();
383
- const executable = this.isExecutable();
384
- // Emit finish events
385
- if (this.rootComplete && !this.grammar.hasDuplicate && !this.hasFatalDiagnostic()) {
386
- this.events.emitDocumentComplete(executable);
387
- }
388
- this.events.emitStreamFinished(outcome);
389
- return {
390
- syntax: this.syntax_,
391
- outcome,
392
- stableValue: this.buildStableValue(),
393
- executable,
394
- repairs: Object.freeze([...this.allRepairs]),
395
- diagnostics: Object.freeze([...this.allDiagnostics]),
396
- receivedBytes: this.receivedBytes,
397
- consumedBytes: this.consumedBytes,
398
- };
399
- }
400
- // -----------------------------------------------------------------------
401
- // Token processing
402
- // -----------------------------------------------------------------------
403
- processToken(token) {
404
- // Handle value skipping (duplicate key value)
405
- if (this.isSkippingValue) {
406
- if (token.type === TokenType.ObjectStart ||
407
- token.type === TokenType.ArrayStart) {
408
- this.skipValueDepth++;
409
- }
410
- else if (token.type === TokenType.ObjectEnd ||
411
- token.type === TokenType.ArrayEnd) {
412
- this.skipValueDepth--;
413
- if (this.skipValueDepth <= 0) {
414
- this.isSkippingValue = false;
415
- this.skipValueDepth = 0;
416
- // Resume normal flow — need to advance the object expectation
417
- const frame = this.grammar.current;
418
- if (frame && frame.containerType === "object") {
419
- frame.objectExpectation = "comma_or_end";
420
- }
421
- }
422
- }
423
- else if (this.skipValueDepth === 0) {
424
- // Scalar value for duplicate key — skip it
425
- if (token.type === TokenType.String ||
426
- token.type === TokenType.Number ||
427
- token.type === TokenType.True ||
428
- token.type === TokenType.False ||
429
- token.type === TokenType.Null) {
430
- this.isSkippingValue = false;
431
- const frame = this.grammar.current;
432
- if (frame && frame.containerType === "object") {
433
- frame.objectExpectation = "comma_or_end";
434
- }
435
- }
436
- }
437
- return;
438
- }
439
- switch (token.type) {
440
- case TokenType.ObjectStart:
441
- this.handleObjectStart(token);
442
- break;
443
- case TokenType.ObjectEnd:
444
- this.handleObjectEnd(token);
445
- break;
446
- case TokenType.ArrayStart:
447
- this.handleArrayStart(token);
448
- break;
449
- case TokenType.ArrayEnd:
450
- this.handleArrayEnd(token);
451
- break;
452
- case TokenType.Colon:
453
- this.handleColon(token);
454
- break;
455
- case TokenType.Comma:
456
- this.handleComma(token);
457
- break;
458
- case TokenType.String:
459
- this.handleString(token);
460
- break;
461
- case TokenType.Number:
462
- this.handleNumber(token);
463
- break;
464
- case TokenType.True:
465
- case TokenType.False:
466
- case TokenType.Null:
467
- this.handleLiteral(token);
468
- break;
469
- }
470
- }
471
- handleObjectStart(token) {
472
- const frame = this.grammar.current;
473
- // Validate context
474
- if (frame) {
475
- if (frame.containerType === "object") {
476
- if (frame.objectExpectation !== "value") {
477
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '{'", false));
478
- this.terminal = true;
479
- this.syntax_ = "invalid";
480
- return;
481
- }
482
- }
483
- else if (frame.containerType === "array") {
484
- if (frame.arrayExpectation !== "first_value_or_end" &&
485
- frame.arrayExpectation !== "value_after_comma") {
486
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '{' in array", false));
487
- this.terminal = true;
488
- this.syntax_ = "invalid";
489
- return;
490
- }
491
- }
492
- }
493
- const newFrame = this.grammar.pushObject(token.byteStart);
494
- if (!newFrame) {
495
- this.terminal = true;
496
- this.syntax_ = "invalid";
497
- return;
498
- }
499
- this.hasSeenValue = true;
500
- if (this.syntax_ === "empty") {
501
- this.syntax_ = "incomplete";
502
- }
503
- // Init container in snapshot builder
504
- if (!this.grammar.isEmpty()) {
505
- if (this.grammar.depth === 1) {
506
- this.snapshot_.initRootObject(newFrame);
507
- }
508
- else {
509
- this.snapshot_.initContainerForFrame(newFrame, "object");
510
- }
511
- }
512
- }
513
- handleObjectEnd(token) {
514
- const frame = this.grammar.current;
515
- if (!frame || frame.containerType !== "object") {
516
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '}' — no matching open object", false));
517
- this.terminal = true;
518
- this.syntax_ = "invalid";
519
- return;
520
- }
521
- if (frame.objectExpectation !== "first_key_or_end" &&
522
- frame.objectExpectation !== "comma_or_end") {
523
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, `Unexpected '}' while expecting ${frame.objectExpectation}`, false));
524
- this.terminal = true;
525
- this.syntax_ = "invalid";
526
- return;
527
- }
528
- this.grammar.pop();
529
- this.events.emitContainerClosed(frame.path, "object");
530
- // If this closes the root, emit the full object as committed
531
- if (this.grammar.isEmpty()) {
532
- this.rootComplete = true;
533
- this.syntax_ = "root_complete";
534
- }
535
- else {
536
- // Value committed for parent
537
- this.commitContainerToParent(frame.path, token);
538
- }
539
- }
540
- handleArrayStart(token) {
541
- const frame = this.grammar.current;
542
- // Validate context
543
- if (frame) {
544
- if (frame.containerType === "object") {
545
- if (frame.objectExpectation !== "value") {
546
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '['", false));
547
- this.terminal = true;
548
- this.syntax_ = "invalid";
549
- return;
550
- }
551
- }
552
- else if (frame.containerType === "array") {
553
- if (frame.arrayExpectation !== "first_value_or_end" &&
554
- frame.arrayExpectation !== "value_after_comma") {
555
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected '[' in array", false));
556
- this.terminal = true;
557
- this.syntax_ = "invalid";
558
- return;
559
- }
560
- }
561
- }
562
- const newFrame = this.grammar.pushArray(token.byteStart);
563
- if (!newFrame) {
564
- this.terminal = true;
565
- this.syntax_ = "invalid";
566
- return;
567
- }
568
- this.hasSeenValue = true;
569
- if (this.syntax_ === "empty") {
570
- this.syntax_ = "incomplete";
571
- }
572
- // Init container in snapshot builder
573
- if (this.grammar.depth === 1) {
574
- this.snapshot_.initRootArray(newFrame);
575
- }
576
- else {
577
- this.snapshot_.initContainerForFrame(newFrame, "array");
578
- }
579
- }
580
- handleArrayEnd(token) {
581
- const frame = this.grammar.current;
582
- if (!frame || frame.containerType !== "array") {
583
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ']' — no matching open array", false));
584
- this.terminal = true;
585
- this.syntax_ = "invalid";
586
- return;
587
- }
588
- if (frame.arrayExpectation !== "first_value_or_end" &&
589
- frame.arrayExpectation !== "comma_or_end") {
590
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ']'", false));
591
- this.terminal = true;
592
- this.syntax_ = "invalid";
593
- return;
594
- }
595
- this.grammar.pop();
596
- this.events.emitContainerClosed(frame.path, "array");
597
- if (this.grammar.isEmpty()) {
598
- this.rootComplete = true;
599
- this.syntax_ = "root_complete";
600
- }
601
- else {
602
- this.commitContainerToParent(frame.path, token);
603
- }
604
- }
605
- handleColon(token) {
606
- const frame = this.grammar.current;
607
- if (!frame || frame.containerType !== "object" || frame.objectExpectation !== "colon") {
608
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ':'", false));
609
- this.terminal = true;
610
- this.syntax_ = "invalid";
611
- return;
612
- }
613
- frame.objectExpectation = "value";
614
- }
615
- handleComma(token) {
616
- const frame = this.grammar.current;
617
- if (!frame) {
618
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ','", false));
619
- this.terminal = true;
620
- this.syntax_ = "invalid";
621
- return;
622
- }
623
- if (frame.containerType === "object") {
624
- if (frame.objectExpectation !== "comma_or_end") {
625
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ',' in object", false));
626
- this.terminal = true;
627
- this.syntax_ = "invalid";
628
- return;
629
- }
630
- frame.objectExpectation = "key_after_comma";
631
- }
632
- else {
633
- if (frame.arrayExpectation !== "comma_or_end") {
634
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected ',' in array", false));
635
- this.terminal = true;
636
- this.syntax_ = "invalid";
637
- return;
638
- }
639
- frame.arrayExpectation = "value_after_comma";
640
- }
641
- }
642
- handleString(token) {
643
- const frame = this.grammar.current;
644
- if (!frame) {
645
- // Root-level string value
646
- this.hasSeenValue = true;
647
- this.rootComplete = true;
648
- this.syntax_ = "root_complete";
649
- this.events.emitValueCommitted("", token.value, [
650
- token.byteStart,
651
- token.byteEnd,
652
- ]);
653
- this.rootScalarValue = token.value;
654
- return;
655
- }
656
- if (frame.containerType === "object") {
657
- if (frame.objectExpectation === "first_key_or_end" ||
658
- frame.objectExpectation === "key_after_comma") {
659
- // This is an object key
660
- const isNew = this.grammar.registerObjectKey(token.value, token.byteStart);
661
- if (!isNew) {
662
- // Duplicate key — skip the upcoming value
663
- // The grammar registered the diagnostic already
664
- const dupDiags = this.grammar.takeDiagnostics();
665
- for (const d of dupDiags) {
666
- this.addDiagnostic(d);
667
- }
668
- frame.objectExpectation = "colon";
669
- this.isSkippingValue = true;
670
- this.skipValueDepth = 0;
671
- return;
672
- }
673
- frame.objectExpectation = "colon";
674
- }
675
- else if (frame.objectExpectation === "value") {
676
- // This is a string value for an object field
677
- const path = this.grammar.currentValuePath();
678
- this.events.emitValueCommitted(path, token.value, [
679
- token.byteStart,
680
- token.byteEnd,
681
- ]);
682
- this.snapshot_.processEvent({ path, value: token.value });
683
- frame.objectExpectation = "comma_or_end";
684
- }
685
- }
686
- else if (frame.containerType === "array") {
687
- if (frame.arrayExpectation !== "first_value_or_end" &&
688
- frame.arrayExpectation !== "value_after_comma") {
689
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected string in array", false));
690
- this.terminal = true;
691
- this.syntax_ = "invalid";
692
- return;
693
- }
694
- // Array element
695
- const path = this.grammar.currentValuePath();
696
- this.events.emitValueCommitted(path, token.value, [
697
- token.byteStart,
698
- token.byteEnd,
699
- ]);
700
- this.snapshot_.processEvent({ path, value: token.value });
701
- this.grammar.advanceArrayIndex();
702
- frame.arrayExpectation = "comma_or_end";
703
- }
704
- }
705
- handleNumber(token) {
706
- const value = Number(token.value);
707
- this.commitScalar(value, token);
708
- }
709
- handleLiteral(token) {
710
- let value;
711
- if (token.type === TokenType.True)
712
- value = true;
713
- else if (token.type === TokenType.False)
714
- value = false;
715
- else
716
- value = null;
717
- this.commitScalar(value, token);
718
- }
719
- commitScalar(value, token) {
720
- const frame = this.grammar.current;
721
- if (!frame) {
722
- // Root-level scalar
723
- this.hasSeenValue = true;
724
- this.rootComplete = true;
725
- this.syntax_ = "root_complete";
726
- this.events.emitValueCommitted("", value, [
727
- token.byteStart,
728
- token.byteEnd,
729
- ]);
730
- // Store root scalar in snapshot builder
731
- // For root scalars, we directly set the root value
732
- this.rootScalarValue = value;
733
- return;
734
- }
735
- if (frame.containerType === "object") {
736
- if (frame.objectExpectation === "value") {
737
- const path = this.grammar.currentValuePath();
738
- this.events.emitValueCommitted(path, value, [
739
- token.byteStart,
740
- token.byteEnd,
741
- ]);
742
- this.snapshot_.processEvent({ path, value });
743
- frame.objectExpectation = "comma_or_end";
744
- }
745
- }
746
- else if (frame.containerType === "array") {
747
- if (frame.arrayExpectation !== "first_value_or_end" &&
748
- frame.arrayExpectation !== "value_after_comma") {
749
- this.addDiagnostic(createDiagnostic(DiagnosticCode.E_UNEXPECTED_TOKEN, "error", token.byteStart, "Unexpected scalar in array", false));
750
- this.terminal = true;
751
- this.syntax_ = "invalid";
752
- return;
753
- }
754
- const path = this.grammar.currentValuePath();
755
- this.events.emitValueCommitted(path, value, [
756
- token.byteStart,
757
- token.byteEnd,
758
- ]);
759
- this.snapshot_.processEvent({ path, value });
760
- this.grammar.advanceArrayIndex();
761
- frame.arrayExpectation = "comma_or_end";
762
- }
763
- }
764
- commitContainerToParent(_containerPath, _token) {
765
- const parentFrame = this.grammar.current;
766
- if (!parentFrame)
767
- return;
768
- if (parentFrame.containerType === "object") {
769
- // The closed container was a value in the parent object
770
- // The value_committed for the object field is implicitly done
771
- // when the container closes
772
- parentFrame.objectExpectation = "comma_or_end";
773
- }
774
- else if (parentFrame.containerType === "array") {
775
- this.grammar.advanceArrayIndex();
776
- parentFrame.arrayExpectation = "comma_or_end";
777
- }
778
- }
779
- // -----------------------------------------------------------------------
780
- // Helpers
781
- // -----------------------------------------------------------------------
782
- addDiagnostic(diag) {
783
- // Freeze before storing: snapshot()/finish() freeze the ARRAY they
784
- // return (Object.freeze([...this.allDiagnostics])), but that's shallow
785
- // - it doesn't protect the Diagnostic objects themselves, which are the
786
- // *same* references stored here. Without this, a consumer mutating a
787
- // field on a diagnostic object obtained from one snapshot()/finish()
788
- // call corrupts this parser's internal history, silently visible in
789
- // every later snapshot()/finish() call.
790
- Object.freeze(diag);
791
- // Cap accumulation independently of the EventBuilder's own queue limit:
792
- // a consumer who drains events after every push() keeps that queue
793
- // short, which would otherwise let this array grow without bound for
794
- // the lifetime of the parser (see limits.maxQueuedEvents).
795
- if (diag.severity === "fatal")
796
- this.everHadFatalDiagnostic = true;
797
- if ((diag.severity === "error" || diag.severity === "fatal") &&
798
- !diag.recoverable) {
799
- this.everHadNonRecoverableError = true;
800
- }
801
- if (this.allDiagnostics.length < this.limits.maxQueuedEvents) {
802
- this.allDiagnostics.push(diag);
803
- }
804
- this.events.emitDiagnostic(diag);
805
- }
806
- addRepair(repair) {
807
- // See addDiagnostic()'s matching comment: freeze before storing so
808
- // external mutation of a repair object obtained from a prior
809
- // snapshot()/finish() call can't corrupt this parser's internal
810
- // history. byteRange is a nested array - Object.freeze on the repair
811
- // itself doesn't reach into it, so it needs its own freeze.
812
- Object.freeze(repair.byteRange);
813
- Object.freeze(repair);
814
- if (repair.impact === "structural" || repair.impact === "lossy") {
815
- this.everHadStructuralOrLossyRepair = true;
816
- }
817
- if (repair.code === "R_CLOSE_CONTAINER") {
818
- this.everHadCloseContainerRepair = true;
819
- }
820
- if (this.allRepairs.length < this.limits.maxQueuedEvents) {
821
- this.allRepairs.push(repair);
822
- }
823
- this.events.emitRepairApplied(repair);
824
- }
825
- isExecutable() {
826
- if (this.phase !== "finished")
827
- return false;
828
- if (!this.rootComplete)
829
- return false;
830
- // this.terminal/syntax_ is the same authoritative "something went
831
- // wrong" signal determineOutcome() checks first (syntax_ === "invalid").
832
- // Some fatal conditions — e.g. EventBuilder's own event-queue-capacity
833
- // cutoff (see semantic/builder.ts's enqueue()) — set these directly
834
- // without going through addDiagnostic(), so they don't set
835
- // everHadFatalDiagnostic below. Checking them explicitly here closes
836
- // that gap instead of only catching diagnostics that happen to have
837
- // been routed through addDiagnostic().
838
- if (this.terminal || this.syntax_ === "invalid")
839
- return false;
840
- // Allowlist, not a denylist of known-bad reasons: StreamEndReason is a
841
- // closed union at the type level, but finish()'s `reason` is never
842
- // validated at runtime, and real callers (a provider adapter
843
- // normalizing a raw finish_reason/stop_reason string, or any caller
844
- // that bypasses the type checker) can pass a string outside that union.
845
- // Only an explicitly confirmed "complete" may unlock executable:true -
846
- // an unrecognized reason must never be silently treated as safe.
847
- const reason = this.finishMeta?.reason ?? "unknown";
848
- if (reason !== "complete") {
849
- return false;
850
- }
851
- if (this.grammar.hasDuplicate)
852
- return false;
853
- if (this.hasFatalDiagnostic())
854
- return false;
855
- if (this.hasNonRecoverableError())
856
- return false;
857
- if (this.everHadStructuralOrLossyRepair)
858
- return false;
859
- const pendingInfo = this.scanner.getPendingInfo();
860
- if (pendingInfo.type !== "none")
861
- return false;
862
- if (this.trailingDataSeen)
863
- return false;
864
- return true;
865
- }
866
- hasFatalDiagnostic() {
867
- return this.everHadFatalDiagnostic;
868
- }
869
- hasNonRecoverableError() {
870
- return this.everHadNonRecoverableError;
871
- }
872
- determineOutcome() {
873
- if (this.syntax_ === "invalid")
874
- return "invalid";
875
- if (this.hasFatalDiagnostic())
876
- return "invalid";
877
- if (this.grammar.hasDuplicate)
878
- return "invalid";
879
- // Check if salvaged
880
- if (this.everHadCloseContainerRepair)
881
- return "salvaged";
882
- if (!this.rootComplete)
883
- return "truncated";
884
- // A root that closed while the stream-end `reason` indicates the stream
885
- // was cut off (e.g. "length") is still syntactically valid JSON - the
886
- // reason only affects executable() (see isExecutable(), which checks it
887
- // directly), never outcome.
888
- return "valid";
889
- }
890
- buildStableValue() {
891
- if (this.rootScalarValue !== undefined)
892
- return this.rootScalarValue;
893
- return this.snapshot_.getStableValue();
894
- }
895
- buildPendingTokens() {
896
- const pending = [];
897
- const info = this.scanner.getPendingInfo();
898
- if (info.type !== "none") {
899
- pending.push({
900
- type: info.type === "object_key" ? "object_key" : info.type,
901
- path: this.grammar.currentValuePath(),
902
- buffered: info.buffer,
903
- byteOffset: info.byteStart,
904
- });
905
- }
906
- return pending;
907
- }
908
- }
909
- /**
910
- * Get the UTF-8 byte length of a character.
911
- */
912
- function getUtf8ByteLength(ch) {
913
- const code = ch.codePointAt(0) ?? 0;
914
- if (code <= 0x7f)
915
- return 1;
916
- if (code <= 0x7ff)
917
- return 2;
918
- if (code <= 0xffff)
919
- return 3;
920
- return 4;
921
- }
922
- //# sourceMappingURL=parser.js.map