@tabnas/transduce 0.1.0 → 0.1.1

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.
@@ -0,0 +1,661 @@
1
+ /* Copyright (c) 2026 tabnas, MIT License */
2
+
3
+ // The rule-event adapter: `JsonEvents/1` from a live tabnas parse.
4
+ //
5
+ // The engine tells a `ruleDone` subscriber about every rule pass, with the
6
+ // rule's node: the value the rule accumulates. Rules pushed or replaced
7
+ // from a rule whose node is a container share that container (the same
8
+ // object), which is the identity the adapter follows; it watches those
9
+ // containers and nothing grammar-specific, which is what lets one adapter
10
+ // serve every grammar. The algorithm is the Rust runtime's, step for step
11
+ // (rs/src/source/rule_events.rs has the full commentary):
12
+ //
13
+ // - A rule whose node is a container at the end of its OPEN pass, a
14
+ // container not already on the frame stack, STARTS a container.
15
+ // - At the end of a CLOSE pass whose node is the top frame's container, a
16
+ // longer container means new entries: the tail of an array, the last
17
+ // members of a map (insertion ordered). A new entry that is the
18
+ // container which completed last was already streamed; any other is
19
+ // walked whole, late.
20
+ // - A key is announced early when the rule stashed it in `u.key` at its
21
+ // OPEN pass; a key the adapter only learns at insertion is announced
22
+ // then.
23
+ // - A repeated member name does not grow the map. When the announcing
24
+ // rule's CLOSE pass leaves the map at its old length the adapter HOLDS
25
+ // what the map has under the name until its next event, which a pass
26
+ // that failed the parse never sends: a held scalar is streamed, the
27
+ // container just streamed is complete already, any other container is a
28
+ // merge of both values and fails the run with `DUPLICATE_MEMBER`.
29
+ // - A map whose names changed after they were streamed (a YAML merge key)
30
+ // fails when its frame ends, with `STREAMABILITY_UNKNOWN`.
31
+ // - The top frame ENDS when a rule at the frame's depth whose node is the
32
+ // frame's container closes, or the rule that started it closes, unless
33
+ // that close REPLACES the rule (`alt.r`).
34
+ // - A root scalar is emitted at the depth-0 rule's close, unless that close
35
+ // replaces the rule. A frame that starts at the root after a root value
36
+ // completed is refused, and so is a parse root that closes for good over
37
+ // a value that is not the one streamed: `STREAMABILITY_UNKNOWN`, before
38
+ // `end`. `end` is not the adapter's to emit: the source sends it after
39
+ // the engine has returned, so a document is complete only when it
40
+ // validated.
41
+ //
42
+ // Number lexemes are best effort: at the close of a rule whose node is a
43
+ // number, the first open token's source text is kept when it is a JSON
44
+ // number that reads as the same value, and attached to that number when it
45
+ // is inserted next.
46
+ //
47
+ // Pruning drops the elements of an array frame from the engine's array
48
+ // after they were emitted, so a document's rows do not pile up in the
49
+ // engine's tree while the transducer streams them. It changes the value
50
+ // the engine returns, which is why only the incremental source, which
51
+ // discards that value, ever asks for it.
52
+
53
+ import { Fail } from './error'
54
+ import { Ev, JsonEvent } from './event'
55
+ import { Guarded } from './guard'
56
+ import { isJsonNumber } from './json'
57
+ import { AbortFlag, Limits, Metrics } from './limits'
58
+ import { Matcher } from './matcher'
59
+ import { Selector } from './selector'
60
+ import { Flow, Sink } from './sink'
61
+ import { Prune, walkValue } from './source'
62
+ import { engineKeys, engineScalar, isEngineContainer, isEngineMap } from './value'
63
+
64
+ // One open container.
65
+ type Frame = {
66
+ cell: object
67
+ array: boolean
68
+ len: number
69
+ ruleI: number
70
+ depth: number
71
+ // The last member name streamed; the one announced early for the next
72
+ // member, when `hasKey`.
73
+ key: string
74
+ hasKey: boolean
75
+ prune: boolean
76
+ // The distinct names streamed into this map, to compare with the map's
77
+ // names when the frame ends.
78
+ names: Set<string>
79
+ }
80
+
81
+ // A member announced early whose rule closed without growing the map, held
82
+ // until the adapter's next event shows that the close stood. `found` is
83
+ // false when the grammar announced a member it never stored.
84
+ type Held = { key: string; found: boolean; value: unknown }
85
+
86
+ // How the run stands, as seen from inside the callbacks.
87
+ export type Status =
88
+ | { type: 'running' }
89
+ // The sink answered `stop`.
90
+ | { type: 'stopped' }
91
+ // The sink or a limit failed; the parse was told to cancel.
92
+ | { type: 'failed'; error: unknown }
93
+
94
+ type PruneState = { type: 'never' } | { type: 'all' } | { type: 'under'; matcher: Matcher }
95
+
96
+ // The adapter's state, behind the engine's subscriber.
97
+ export class Adapter<S extends Sink> {
98
+ private frames: Frame[] = []
99
+ // The container that completed last: the next entry inserted into its
100
+ // parent is this one, already streamed.
101
+ private lastCompleted: object | null = null
102
+ private held: Held | null = null
103
+ private lexeme = ''
104
+ private lexemeValue = 0
105
+ private lexemeReady = false
106
+ readonly sink: Guarded<S>
107
+ status: Status = { type: 'running' }
108
+ private stop: AbortFlag
109
+ private prune: PruneState
110
+ private pruneHit = false
111
+ // A whole root value (a scalar, or the outermost frame) has been emitted.
112
+ private rootDone = false
113
+ // Anything at all has been emitted for the current document.
114
+ private emitted = false
115
+
116
+ constructor(
117
+ sink: S,
118
+ limits: Limits,
119
+ abort: AbortFlag,
120
+ metrics: Metrics,
121
+ prune: Prune,
122
+ stop: AbortFlag,
123
+ ) {
124
+ this.sink = new Guarded(sink, limits, abort, metrics)
125
+ this.stop = stop
126
+ switch (prune.type) {
127
+ case 'never':
128
+ this.prune = { type: 'never' }
129
+ break
130
+ case 'all_arrays':
131
+ this.prune = { type: 'all' }
132
+ break
133
+ case 'under': {
134
+ // A selector that names the elements names the array one step up;
135
+ // one that names the array is taken as is.
136
+ const steps = prune.selector.steps
137
+ const last = steps[steps.length - 1]
138
+ const array =
139
+ undefined !== last && 'each_index' === last.type
140
+ ? new Selector(steps.slice(0, -1))
141
+ : prune.selector
142
+ this.prune = { type: 'under', matcher: new Matcher([array]) }
143
+ break
144
+ }
145
+ }
146
+ }
147
+
148
+ // Ready for another document into the same sink: the JSON Lines source
149
+ // parses one value per line with one parser and one subscriber.
150
+ reset(): void {
151
+ this.frames.length = 0
152
+ this.lastCompleted = null
153
+ this.held = null
154
+ this.lexemeReady = false
155
+ this.pruneHit = false
156
+ this.rootDone = false
157
+ this.emitted = false
158
+ }
159
+
160
+ // Whether one whole root value was emitted and every frame closed.
161
+ complete(): boolean {
162
+ return this.rootDone && 0 === this.frames.length
163
+ }
164
+
165
+ // Whether nothing at all was emitted for the current document, so the
166
+ // value the engine returned can be walked in its place.
167
+ idle(): boolean {
168
+ return !this.emitted
169
+ }
170
+
171
+ // Emit a whole value through the sink, outside the parse.
172
+ walkWhole(value: unknown): Flow {
173
+ this.emitted = true
174
+ return walkValue(value, this.sink)
175
+ }
176
+
177
+ // Send one event straight to the sink, outside the parse.
178
+ send(ev: JsonEvent): Flow {
179
+ return this.sink.event(ev)
180
+ }
181
+
182
+ private fail(error: unknown): void {
183
+ this.status = { type: 'failed', error }
184
+ this.stop.abort()
185
+ }
186
+
187
+ // Emit one event. `false` means stop: the sink stopped or failed, and the
188
+ // parse has been told to cancel.
189
+ private emit(ev: JsonEvent): boolean {
190
+ this.emitted = true
191
+ if ('under' === this.prune.type) {
192
+ try {
193
+ const hit = this.prune.matcher.event(ev)
194
+ this.pruneHit = 'start' === hit.kind && 0 < hit.begins
195
+ } catch (err) {
196
+ this.fail(err)
197
+ return false
198
+ }
199
+ }
200
+ let flow: Flow
201
+ try {
202
+ flow = this.sink.event(ev)
203
+ } catch (err) {
204
+ this.fail(err)
205
+ return false
206
+ }
207
+ if ('stop' === flow) {
208
+ this.status = { type: 'stopped' }
209
+ this.stop.abort()
210
+ return false
211
+ }
212
+ return true
213
+ }
214
+
215
+ // Emit a scalar value, with the remembered lexeme when it is this
216
+ // number's.
217
+ private scalar(value: unknown): boolean {
218
+ const s = engineScalar(value)
219
+ if (undefined === s) return true
220
+ switch (s.type) {
221
+ case 'null':
222
+ return this.emit(Ev.null)
223
+ case 'bool':
224
+ return this.emit(Ev.bool(s.value))
225
+ case 'string':
226
+ return this.emit(Ev.string(s.value))
227
+ case 'number':
228
+ if (this.lexemeReady && this.lexemeValue === s.value) {
229
+ this.lexemeReady = false
230
+ return this.emit(Ev.number(s.value, this.lexeme))
231
+ }
232
+ // A remembered lexeme that is not this number's may be a later
233
+ // entry's of the same batch; the batch's end drops it.
234
+ return this.emit(Ev.number(s.value))
235
+ }
236
+ }
237
+
238
+ // Emit a whole value the adapter did not see being built (late).
239
+ private walk(value: unknown): boolean {
240
+ this.lexemeReady = false
241
+ if (Array.isArray(value)) {
242
+ if (!this.emit(Ev.arrayStart)) return false
243
+ for (const item of value) {
244
+ if (!this.walk(item)) return false
245
+ }
246
+ return this.emit(Ev.arrayEnd)
247
+ }
248
+ if (isEngineMap(value)) {
249
+ if (!this.emit(Ev.objectStart)) return false
250
+ for (const k of engineKeys(value)) {
251
+ if (!this.emit(Ev.key(k)) || !this.walk(value[k])) return false
252
+ }
253
+ return this.emit(Ev.objectEnd)
254
+ }
255
+ return this.scalar(value)
256
+ }
257
+
258
+ // Open a frame. Its `len` starts at zero whatever the container holds:
259
+ // entries present when the adapter first sees a container were never
260
+ // streamed (jsonic's promoted first value), and the next close pass
261
+ // emits them before the new ones.
262
+ private pushFrame(cell: object, array: boolean, rule: any, prune: boolean): void {
263
+ this.frames.push({
264
+ cell,
265
+ array,
266
+ len: 0,
267
+ ruleI: rule.i,
268
+ depth: rule.d,
269
+ key: '',
270
+ hasKey: false,
271
+ prune,
272
+ names: new Set(),
273
+ })
274
+ }
275
+
276
+ // The subscriber's body.
277
+ onDone(rule: any, done: any): void {
278
+ if ('running' !== this.status.type) return
279
+ try {
280
+ this.step(rule, done)
281
+ } catch (err) {
282
+ // Not a failure the adapter reports itself (those set the status and
283
+ // return): a defect, kept as it is rather than read as the input's.
284
+ this.fail(err)
285
+ }
286
+ }
287
+
288
+ private step(rule: any, done: any): void {
289
+ // Any further event means the pass that held a member stood.
290
+ if (!this.flushHeld()) return
291
+ if (null != done.alt?.err) return
292
+ if ('o' === done.state) {
293
+ this.opened(rule, rule.node)
294
+ } else {
295
+ const replaces = null != done.alt && '' !== (done.alt.r ?? '')
296
+ this.closed(rule, rule.node, replaces)
297
+ }
298
+ }
299
+
300
+ private opened(rule: any, cell: unknown): void {
301
+ if (!isEngineContainer(cell)) return
302
+ const node = cell as object
303
+ const array = Array.isArray(node)
304
+ if (!this.frames.some((f) => f.cell === node)) {
305
+ if (0 === this.frames.length && this.rootDone) {
306
+ this.fail(wrappedRoot())
307
+ return
308
+ }
309
+ if (null !== this.lastCompleted) {
310
+ // The container that completed last was never stored in the one
311
+ // around it, and the grammar is opening another: what left cannot
312
+ // be taken back.
313
+ this.fail(unstoredContainer())
314
+ return
315
+ }
316
+ const top = this.frames[this.frames.length - 1]
317
+ if (undefined !== top && !top.array && !top.hasKey) {
318
+ // A container opening in a map whose next member has no key yet:
319
+ // its events would leave before the key, which no tree's events do.
320
+ this.fail(valueBeforeKey())
321
+ return
322
+ }
323
+ this.lexemeReady = false
324
+ if (!this.emit(array ? Ev.arrayStart : Ev.objectStart)) return
325
+ let prune = false
326
+ switch (this.prune.type) {
327
+ case 'all':
328
+ prune = array
329
+ break
330
+ case 'under':
331
+ prune = array && this.pruneHit
332
+ break
333
+ }
334
+ this.pushFrame(node, array, rule, prune)
335
+ } else if (!array) {
336
+ const top = this.frames[this.frames.length - 1]
337
+ if (top.cell === node && !top.hasKey && top.len === containerLen(node)) {
338
+ const key = rule.u?.key
339
+ if ('string' === typeof key) {
340
+ if (!this.emit(Ev.key(key))) return
341
+ top.key = key
342
+ top.hasKey = true
343
+ }
344
+ }
345
+ }
346
+ }
347
+
348
+ private closed(rule: any, cell: unknown, replaces: boolean): void {
349
+ // A root scalar's lexeme has to be known before it is emitted below.
350
+ this.rememberLexeme(rule)
351
+ // Whether a whole root value had left before this pass: the pass that
352
+ // completes the root is exempt from the check at the end.
353
+ const wasDone = this.rootDone
354
+
355
+ let pruneFrom = -1
356
+ const topI = this.frames.length - 1
357
+ if (0 <= topI && this.frames[topI].cell === cell && isEngineContainer(cell)) {
358
+ const node = cell as any
359
+ const array = Array.isArray(node)
360
+ const keys = array ? null : engineKeys(node)
361
+ const len = array ? node.length : (keys as string[]).length
362
+ const frame = this.frames[topI]
363
+ const old = frame.len
364
+ if (!array && frame.hasKey && len === old) {
365
+ // The announced member did not grow the map, and the rule that
366
+ // announced it is closing: it replaced or merged an earlier member
367
+ // of the same name, or the pass failed the parse before storing it.
368
+ // Hold what the map has under the name; the next event decides.
369
+ if (rule.u?.key === frame.key) this.holdPending(node, topI)
370
+ }
371
+ if (len > old) {
372
+ for (let i = old; i < len; i++) {
373
+ let value: unknown
374
+ if (array) {
375
+ value = node[i]
376
+ } else {
377
+ const key = (keys as string[])[i]
378
+ value = node[key]
379
+ const top = this.frames[topI]
380
+ if (top.hasKey && top.key !== key) {
381
+ // A member landed ahead of the one announced; that one keeps
382
+ // its place in the stream.
383
+ if (!this.settlePending(node, topI)) return
384
+ }
385
+ const announced = top.hasKey
386
+ top.hasKey = false
387
+ top.names.add(key)
388
+ if (!announced) {
389
+ top.key = key
390
+ if (!this.emit(Ev.key(key))) return
391
+ }
392
+ }
393
+ if (!this.entry(value)) return
394
+ }
395
+ this.frames[topI].len = len
396
+ this.lexemeReady = false
397
+ if (array && this.frames[topI].prune) pruneFrom = old
398
+ }
399
+ }
400
+ if (0 <= pruneFrom) {
401
+ ;(cell as unknown[]).length = pruneFrom
402
+ this.frames[this.frames.length - 1].len = pruneFrom
403
+ }
404
+
405
+ if (replaces) return
406
+
407
+ const top = this.frames[this.frames.length - 1]
408
+ if (undefined !== top && (top.ruleI === rule.i || (top.cell === cell && top.depth === rule.d))) {
409
+ const array = top.array
410
+ // A member held by this very pass goes before the frame ends.
411
+ if (!this.flushHeld()) return
412
+ if (top.hasKey) {
413
+ // The announcing rule never closed on this container: the member is
414
+ // whatever the map holds under the name now.
415
+ if (!this.settlePending(cell, this.frames.length - 1)) return
416
+ }
417
+ if (null !== this.lastCompleted) {
418
+ // A container built inside this one was streamed and never stored
419
+ // (jsonic drops a pair's value inside a list when `list.pair` is
420
+ // off): the frame cannot end as the walk's would.
421
+ this.fail(unstoredContainer())
422
+ return
423
+ }
424
+ if (!array) {
425
+ const names = isEngineMap(cell) ? engineKeys(cell) : []
426
+ const same = names.length === top.names.size && names.every((k) => top.names.has(k))
427
+ if (!same) {
428
+ this.fail(rewrittenMap())
429
+ return
430
+ }
431
+ }
432
+ this.frames.pop()
433
+ this.lexemeReady = false
434
+ if (!this.emit(array ? Ev.arrayEnd : Ev.objectEnd)) return
435
+ this.lastCompleted = isEngineContainer(cell) ? (cell as object) : null
436
+ if (0 === this.frames.length) this.rootDone = true
437
+ }
438
+
439
+ if (0 === rule.d && 0 === this.frames.length && !this.rootDone) {
440
+ if (!isEngineContainer(cell)) {
441
+ if (!this.scalar(cell)) return
442
+ this.rootDone = true
443
+ }
444
+ }
445
+
446
+ // The parse root closing for good after the root value left: the node
447
+ // must still hold what was streamed.
448
+ if (wasDone && 0 === rule.d && 0 === this.frames.length) {
449
+ const container = isEngineContainer(cell)
450
+ const streamed =
451
+ null !== this.lastCompleted ? container && cell === this.lastCompleted : !container
452
+ if (!streamed) this.fail(rewrittenRoot())
453
+ }
454
+ }
455
+
456
+ // Emit one entry that just landed in the top frame's container: a scalar
457
+ // as itself, the container that completed last as nothing (it has been
458
+ // streamed), any other container whole, late.
459
+ private entry(value: unknown): boolean {
460
+ if (null !== this.lastCompleted) {
461
+ if (isEngineContainer(value) && value === this.lastCompleted) {
462
+ this.lastCompleted = null
463
+ return true
464
+ }
465
+ // Something else landed: the container streamed last was never
466
+ // stored, and its events cannot be taken back.
467
+ this.fail(unstoredContainer())
468
+ return false
469
+ }
470
+ if (!isEngineContainer(value)) return this.scalar(value)
471
+ return this.walk(value)
472
+ }
473
+
474
+ // The member announced on the frame `topI` did not grow its map: stream
475
+ // what the map holds under that name now.
476
+ private settlePending(node: unknown, topI: number): boolean {
477
+ const top = this.frames[topI]
478
+ top.hasKey = false
479
+ const m = member(node, top.key)
480
+ return this.settle(top.key, m.found, m.value)
481
+ }
482
+
483
+ // Like `settlePending`, but the member is held rather than streamed: the
484
+ // announcing rule's close pass may be one the engine reports although an
485
+ // action failed the parse in it.
486
+ private holdPending(node: unknown, topI: number): void {
487
+ const top = this.frames[topI]
488
+ top.hasKey = false
489
+ const m = member(node, top.key)
490
+ this.held = { key: top.key, found: m.found, value: m.value }
491
+ }
492
+
493
+ // Stream the member held at the last pass, if any. `false` means stop.
494
+ private flushHeld(): boolean {
495
+ const held = this.held
496
+ if (null === held) return true
497
+ this.held = null
498
+ return this.settle(held.key, held.found, held.value)
499
+ }
500
+
501
+ // Stream what a map holds under a repeated member's name: a scalar as
502
+ // itself; the container that completed last as nothing; any other
503
+ // container fails the run as a merge; no value at all fails it as a
504
+ // member the grammar announced and never stored.
505
+ private settle(key: string, found: boolean, value: unknown): boolean {
506
+ if (!found) {
507
+ this.fail(
508
+ new Fail(
509
+ 'STREAMABILITY_UNKNOWN',
510
+ `the grammar announced member ${JSON.stringify(key)} and never stored it, which the ` +
511
+ `incremental source cannot follow; run it with SourceMode.materialize`,
512
+ ),
513
+ )
514
+ return false
515
+ }
516
+ if (!isEngineContainer(value)) return this.scalar(value)
517
+ if (null !== this.lastCompleted && value === this.lastCompleted) {
518
+ this.lastCompleted = null
519
+ return true
520
+ }
521
+ this.fail(mergedMember(key))
522
+ return false
523
+ }
524
+
525
+ // Keep the source text of a number rule's first token when it is a JSON
526
+ // number spelling the node's value.
527
+ private rememberLexeme(rule: any): void {
528
+ const node = rule.node
529
+ if ('number' !== typeof node) return
530
+ const token = rule.o?.[0]
531
+ if (null == token) return
532
+ const src = token.src
533
+ if ('string' === typeof src && isJsonNumber(src) && Number(src) === node) {
534
+ this.lexeme = src
535
+ this.lexemeValue = node
536
+ this.lexemeReady = true
537
+ } else {
538
+ this.lexemeReady = false
539
+ }
540
+ }
541
+ }
542
+
543
+ function containerLen(node: object): number {
544
+ return Array.isArray(node) ? node.length : engineKeys(node).length
545
+ }
546
+
547
+ // The member of a map under `key`.
548
+ function member(node: unknown, key: string): { found: boolean; value: unknown } {
549
+ if (isEngineMap(node) && Object.prototype.hasOwnProperty.call(node, key)) {
550
+ return { found: true, value: node[key] }
551
+ }
552
+ return { found: false, value: undefined }
553
+ }
554
+
555
+ // The subscriber and the parse budget on `parser`: the subscriber hands
556
+ // every rule pass to `onDone`, and the budget cancels the parse as soon as
557
+ // `proceed` answers false, while still consulting any budget check the
558
+ // grammar installed at its own cadence. Member order is recorded on the
559
+ // engine's maps (`map.ordered`), which leaves the values' shape alone.
560
+ export function prepare(
561
+ parser: any,
562
+ proceed: () => boolean,
563
+ onDone?: (rule: any, done: any) => void,
564
+ ): void {
565
+ const budget = parser.options?.parse?.budget ?? {}
566
+ const prev = 'function' === typeof budget.onCheck ? budget.onCheck : null
567
+ const prevN = prev && 0 < budget.checkEveryN ? budget.checkEveryN : 0
568
+ parser.options({
569
+ map: { ordered: true },
570
+ parse: {
571
+ budget: {
572
+ checkEveryN: 1,
573
+ onCheck: (ctx: any) => {
574
+ if (!proceed()) return false
575
+ if (null !== prev && 0 < prevN && 0 === ctx.kI % prevN) return prev(ctx)
576
+ return true
577
+ },
578
+ },
579
+ },
580
+ })
581
+ if (onDone) parser.sub({ ruleDone: (rule: any, _ctx: any, done: any) => onDone(rule, done) })
582
+ }
583
+
584
+ // The failure for an incremental run whose events did not amount to one
585
+ // whole document.
586
+ export function notStreamable(): Fail {
587
+ return new Fail(
588
+ 'STREAMABILITY_UNKNOWN',
589
+ 'the grammar did not build its value through rule events the incremental source can ' +
590
+ 'follow (the events did not amount to one whole document); run it with ' +
591
+ 'SourceMode.materialize',
592
+ )
593
+ }
594
+
595
+ // The failure for a container that starts at the root after a root value
596
+ // has completed.
597
+ export function wrappedRoot(): Fail {
598
+ return new Fail(
599
+ 'STREAMABILITY_UNKNOWN',
600
+ "the grammar wrapped a value already streamed as the document's root in a list (a YAML " +
601
+ 'stream of several documents, a jsonic top-level implicit list); the incremental source ' +
602
+ 'cannot take the root back, so run it with SourceMode.materialize',
603
+ )
604
+ }
605
+
606
+ // The failure for a parse root whose node no longer holds the value
607
+ // streamed as the document when the root rule closes for good.
608
+ export function rewrittenRoot(): Fail {
609
+ return new Fail(
610
+ 'STREAMABILITY_UNKNOWN',
611
+ "the grammar replaced the document's root after the incremental source streamed it (a " +
612
+ 'YAML stream of several documents is wrapped in a list when the source ends); the ' +
613
+ 'incremental source cannot take the root back, so run it with SourceMode.materialize',
614
+ )
615
+ }
616
+
617
+ // The failure for a container the adapter streamed as the grammar built it
618
+ // and the grammar then never stored in the container around it.
619
+ export function unstoredContainer(): Fail {
620
+ return new Fail(
621
+ 'STREAMABILITY_UNKNOWN',
622
+ 'the grammar built a container the incremental source streamed and then never stored it ' +
623
+ 'in the container around it (jsonic drops a pair inside a list when list.pair is off), ' +
624
+ "so the stream would not be the document's; the incremental source cannot follow a " +
625
+ 'grammar that builds a container so: run it with SourceMode.materialize',
626
+ )
627
+ }
628
+
629
+ // The failure for a container the grammar opens inside a map before the
630
+ // member it belongs to has a key.
631
+ export function valueBeforeKey(): Fail {
632
+ return new Fail(
633
+ 'STREAMABILITY_UNKNOWN',
634
+ "the grammar opened a container inside a map before announcing the member's key (it " +
635
+ 'builds the value, or a key that is itself a container, in a rule of its own and ' +
636
+ 'names the member only when the pair closes); its events would leave before the key, ' +
637
+ "so the stream would not be the document's; the incremental source cannot follow a " +
638
+ 'grammar that builds a member so: run it with SourceMode.materialize',
639
+ )
640
+ }
641
+
642
+ // The failure for a map whose members the grammar rewrote after the adapter
643
+ // streamed them.
644
+ export function rewrittenMap(): Fail {
645
+ return new Fail(
646
+ 'STREAMABILITY_UNKNOWN',
647
+ 'the grammar rewrote the members of a map after the incremental source streamed them ' +
648
+ "(a YAML merge key does), so the stream would not be the document's; run it with " +
649
+ 'SourceMode.materialize',
650
+ )
651
+ }
652
+
653
+ // The failure for a repeated member whose values the grammar merged.
654
+ export function mergedMember(key: string): Fail {
655
+ return new Fail(
656
+ 'DUPLICATE_MEMBER',
657
+ `member ${JSON.stringify(key)} appears twice and the grammar merged the two values; the ` +
658
+ 'first was already streamed, so the incremental source cannot emit the merged member: ' +
659
+ 'run it with SourceMode.materialize',
660
+ )
661
+ }