@fitchmultz/pi-subagents 0.50.0 → 0.51.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 (169) hide show
  1. package/CHANGELOG.md +51 -2
  2. package/README.md +36 -5
  3. package/dist/agent/contracts.js +6 -24
  4. package/dist/agent/host.js +3 -16
  5. package/dist/agent/index.js +207 -173
  6. package/dist/agent/journal-tail.js +115 -156
  7. package/dist/agent/launch.js +20 -8
  8. package/dist/agent/legacy.js +65 -23
  9. package/dist/agent/notifications.js +138 -47
  10. package/dist/agent/observation-state.js +246 -0
  11. package/dist/agent/observation-view.js +258 -0
  12. package/dist/agent/observation.js +346 -0
  13. package/dist/agent/observe-card.js +243 -0
  14. package/dist/agent/observe-chrome.js +85 -0
  15. package/dist/agent/observe-client.js +285 -0
  16. package/dist/agent/observe-layout.js +65 -0
  17. package/dist/agent/observe-reading.js +84 -0
  18. package/dist/agent/observe-screen.js +400 -96
  19. package/dist/agent/observe-transcript.js +382 -0
  20. package/dist/agent/observe.js +250 -43
  21. package/dist/agent/process.js +198 -25
  22. package/dist/agent/receipts.js +92 -20
  23. package/dist/agent/runner.js +125 -20
  24. package/dist/agent/shell.js +2 -1
  25. package/dist/agent/storage.js +29 -12
  26. package/dist/agent/tool-schemas.js +20 -0
  27. package/dist/agent/typebox.js +10 -5
  28. package/dist/agent/usage.js +0 -115
  29. package/dist/agent/view.js +46 -16
  30. package/dist/agent/wake.js +99 -0
  31. package/dist/build-info.js +1 -1
  32. package/dist/intercom-client.d.ts +26 -0
  33. package/dist/intercom-client.js +45 -0
  34. package/dist/pi-intercom/agent-dir.d.ts +1 -0
  35. package/dist/pi-intercom/bridge-session.js +2 -1
  36. package/dist/pi-intercom/broker/broker.js +27 -21
  37. package/dist/pi-intercom/broker/client-last.d.ts +13 -0
  38. package/dist/pi-intercom/broker/client-last.js +17 -5
  39. package/dist/pi-intercom/broker/client.d.ts +60 -0
  40. package/dist/pi-intercom/broker/client.js +47 -16
  41. package/dist/pi-intercom/broker/connection.d.ts +29 -0
  42. package/dist/pi-intercom/broker/framing.d.ts +17 -0
  43. package/dist/pi-intercom/broker/framing.js +10 -7
  44. package/dist/pi-intercom/broker/guard.js +23 -0
  45. package/dist/pi-intercom/broker/last.d.ts +26 -0
  46. package/dist/pi-intercom/broker/last.js +57 -32
  47. package/dist/pi-intercom/broker/paths.d.ts +13 -0
  48. package/dist/pi-intercom/broker/paths.js +35 -12
  49. package/dist/pi-intercom/broker/pid.js +5 -1
  50. package/dist/pi-intercom/broker/server-connection.js +19 -0
  51. package/dist/pi-intercom/broker/snapshot-reader.d.ts +9 -0
  52. package/dist/pi-intercom/broker/snapshots.d.ts +29 -0
  53. package/dist/pi-intercom/broker/targets.d.ts +9 -0
  54. package/dist/pi-intercom/connection.js +10 -2
  55. package/dist/pi-intercom/identity.js +6 -5
  56. package/dist/pi-intercom/inbound-delivery.js +16 -60
  57. package/dist/pi-intercom/inbound-journal.js +83 -6
  58. package/dist/pi-intercom/inbound-record.js +18 -1
  59. package/dist/pi-intercom/inbound.js +12 -38
  60. package/dist/pi-intercom/index.js +3 -0
  61. package/dist/pi-intercom/last-message.js +4 -16
  62. package/dist/pi-intercom/lifecycle.js +88 -3
  63. package/dist/pi-intercom/outbound.js +5 -15
  64. package/dist/pi-intercom/reply-tracker.js +7 -6
  65. package/dist/pi-intercom/reply-wait.js +2 -2
  66. package/dist/pi-intercom/session-targets.d.ts +24 -0
  67. package/dist/pi-intercom/tool-registration.js +4 -1
  68. package/dist/pi-intercom/tool-results.js +5 -7
  69. package/dist/pi-intercom/tool-schemas.js +12 -26
  70. package/dist/pi-intercom/types.d.ts +212 -0
  71. package/dist/pi-intercom/types.js +54 -3
  72. package/dist/pi-intercom/validation.d.ts +12 -0
  73. package/dist/pi-intercom/validation.js +3 -0
  74. package/dist/shared/bounded-json.js +151 -0
  75. package/dist/shared/compact-cards.js +66 -0
  76. package/dist/shared/journal-frames.js +3 -6
  77. package/dist/shared/json-projection.js +267 -129
  78. package/dist/shared/native-journal.js +123 -56
  79. package/dist/shared/post-exit-stdio-guard.js +120 -7
  80. package/dist/shared/tui-action.js +29 -0
  81. package/dist/shared/unknown.d.ts +9 -0
  82. package/docs/code-quality.md +2 -0
  83. package/docs/development.md +40 -1
  84. package/docs/intercom-bridge.md +9 -4
  85. package/docs/intercom.md +226 -6
  86. package/docs/performance.md +87 -11
  87. package/docs/reference.md +157 -20
  88. package/docs/retired-history-maintenance.md +252 -0
  89. package/package.json +28 -15
  90. package/scripts/archive-retired-history.mjs +464 -0
  91. package/scripts/build.mjs +48 -28
  92. package/scripts/measure-size.mjs +1 -1
  93. package/scripts/native-package-smoke.mjs +153 -12
  94. package/scripts/package-smoke.mjs +80 -11
  95. package/scripts/prepare.mjs +2 -2
  96. package/scripts/real-pi-live.mjs +377 -0
  97. package/scripts/real-pi-processes.mjs +6 -1
  98. package/scripts/real-pi-smoke.mjs +6 -282
  99. package/scripts/run-tests.mjs +4 -2
  100. package/scripts/typescript.mjs +35 -0
  101. package/skills/pi-intercom/SKILL.md +2 -1
  102. package/skills/pi-subagents/SKILL.md +23 -5
  103. package/src/agent/contracts.ts +8 -34
  104. package/src/agent/host.ts +19 -20
  105. package/src/agent/index.ts +224 -190
  106. package/src/agent/journal-tail.ts +141 -153
  107. package/src/agent/launch.ts +33 -9
  108. package/src/agent/legacy.ts +85 -31
  109. package/src/agent/notifications.ts +192 -67
  110. package/src/agent/observation-state.ts +277 -0
  111. package/src/agent/observation-view.ts +267 -0
  112. package/src/agent/observation.ts +405 -0
  113. package/src/agent/observe-card.ts +304 -0
  114. package/src/agent/observe-chrome.ts +170 -0
  115. package/src/agent/observe-client.ts +318 -0
  116. package/src/agent/observe-layout.ts +79 -0
  117. package/src/agent/observe-reading.ts +97 -0
  118. package/src/agent/observe-screen.ts +443 -129
  119. package/src/agent/observe-transcript.ts +434 -0
  120. package/src/agent/observe.ts +304 -71
  121. package/src/agent/process.ts +240 -34
  122. package/src/agent/receipts.ts +114 -24
  123. package/src/agent/runner.ts +125 -27
  124. package/src/agent/shell.ts +2 -1
  125. package/src/agent/storage.ts +27 -11
  126. package/src/agent/tool-schemas.ts +33 -0
  127. package/src/agent/typebox.ts +12 -7
  128. package/src/agent/usage.ts +0 -130
  129. package/src/agent/view.ts +70 -20
  130. package/src/agent/wake.ts +107 -0
  131. package/src/intercom-client.ts +108 -0
  132. package/src/pi-intercom/bridge-session.ts +2 -2
  133. package/src/pi-intercom/broker/broker.ts +27 -23
  134. package/src/pi-intercom/broker/client-last.ts +25 -12
  135. package/src/pi-intercom/broker/client.ts +55 -33
  136. package/src/pi-intercom/broker/framing.ts +11 -8
  137. package/src/pi-intercom/broker/guard.ts +33 -0
  138. package/src/pi-intercom/broker/last.ts +86 -33
  139. package/src/pi-intercom/broker/paths.ts +39 -12
  140. package/src/pi-intercom/broker/pid.ts +6 -1
  141. package/src/pi-intercom/broker/server-connection.ts +25 -0
  142. package/src/pi-intercom/connection.ts +21 -10
  143. package/src/pi-intercom/identity.ts +8 -5
  144. package/src/pi-intercom/inbound-delivery.ts +20 -66
  145. package/src/pi-intercom/inbound-journal.ts +103 -7
  146. package/src/pi-intercom/inbound-record.ts +21 -4
  147. package/src/pi-intercom/inbound.ts +18 -49
  148. package/src/pi-intercom/index.ts +3 -0
  149. package/src/pi-intercom/last-message.ts +10 -19
  150. package/src/pi-intercom/lifecycle.ts +117 -14
  151. package/src/pi-intercom/outbound.ts +7 -16
  152. package/src/pi-intercom/reply-tracker.ts +8 -6
  153. package/src/pi-intercom/reply-wait.ts +3 -2
  154. package/src/pi-intercom/runtime-types.ts +0 -1
  155. package/src/pi-intercom/tool-registration.ts +4 -1
  156. package/src/pi-intercom/tool-results.ts +10 -9
  157. package/src/pi-intercom/tool-schemas.ts +12 -44
  158. package/src/pi-intercom/types.ts +115 -4
  159. package/src/pi-intercom/validation.ts +4 -0
  160. package/src/shared/bounded-json.ts +162 -0
  161. package/src/shared/compact-cards.ts +118 -0
  162. package/src/shared/journal-frames.ts +3 -19
  163. package/src/shared/json-projection.ts +267 -147
  164. package/src/shared/native-journal.ts +157 -75
  165. package/src/shared/post-exit-stdio-guard.ts +165 -7
  166. package/src/shared/tui-action.ts +39 -0
  167. package/tsconfig.client.json +8 -0
  168. package/tsconfig.json +1 -0
  169. package/src/shared/stream-json.d.ts +0 -10
@@ -1,5 +1,3 @@
1
- import { jsonParser, type Token } from "stream-json/core/parser.js";
2
- import { none } from "stream-chain/defs.js";
3
1
  import { isRecord, type UnknownRecord } from "./unknown.ts";
4
2
 
5
3
  export type Projection = (
@@ -12,86 +10,126 @@ export type StringChunk = (
12
10
  text: string,
13
11
  root?: UnknownRecord,
14
12
  ) => void;
15
- export interface ProjectionLimits {
16
- readonly depth: number;
17
- readonly nodes: number;
18
- readonly arrayLength: number;
19
- readonly keyLength: number;
20
- }
21
13
  interface Container {
22
- path: (string | number)[];
23
- value?: Record<string, unknown> | unknown[];
14
+ readonly path: readonly (string | number)[];
15
+ readonly value?: Record<string, unknown> | unknown[];
16
+ readonly array: boolean;
24
17
  key: string;
25
18
  index: number;
26
19
  }
27
20
 
21
+ // Grammar positions between tokens, then the two scalar states that can span chunks.
22
+ const VALUE = 0;
23
+ const FIRST_VALUE = 1;
24
+ const KEY = 2;
25
+ const FIRST_KEY = 3;
26
+ const COLON = 4;
27
+ const AFTER = 5;
28
+ const DONE = 6;
29
+ const STRING = 7;
30
+ const NUMBER = 8;
31
+ // Unescaped units exclude quote, backslash and C0 controls. Native regexp code skips a bounded
32
+ // run of them and valid escapes, so JavaScript handles only the unit that stopped it.
33
+ const BODY = /[ !#-[\]-\uffff]*(?:\\(?:["\\/bfnrt]|u[\da-fA-F]{4})[ !#-[\]-\uffff]*){0,1024}/y;
34
+ const LITERALS = new Map<number, readonly [string, boolean | null]>([
35
+ [102, ["false", false]],
36
+ [110, ["null", null]],
37
+ [116, ["true", true]],
38
+ ]);
39
+ // Number states: start, sign, zero, integer, point, fraction, e, exponent sign, exponent.
40
+ // Columns: "0", "1"-"9", ".", "e"/"E", "+"/"-". A missing transition ends the number;
41
+ // only "-" reaches the start state's sign column because values dispatch on their first unit.
42
+ const NUMBER_NEXT = [
43
+ [2, 3, -1, -1, 1],
44
+ [2, 3],
45
+ [-1, -1, 4, 6],
46
+ [3, 3, 4, 6],
47
+ [5, 5],
48
+ [5, 5, -1, 6],
49
+ [8, 8, -1, -1, 7],
50
+ [8, 8],
51
+ [8, 8],
52
+ ];
53
+ const NUMBER_END = new Set([2, 3, 5, 8]);
54
+ const NUMBER_COLUMN = new Int8Array(128).fill(-1);
55
+ for (const [column, units] of ["0", "123456789", ".", "eE", "+-"].entries()) {
56
+ for (const unit of units) {
57
+ NUMBER_COLUMN[unit.charCodeAt(0)] = column;
58
+ }
59
+ }
60
+ /**
61
+ * Classify where BODY stopped short of a closing quote. True means the chunk ends at or inside an
62
+ * escape, so the rest carries; false means a bounded run ended at a valid escape. Controls throw,
63
+ * and so does a complete escape BODY made no progress at, because BODY consumes any valid one.
64
+ */
65
+ function chunkEnds(text: string, end: number, progressed: boolean): boolean {
66
+ if (end === text.length) {
67
+ return true;
68
+ }
69
+ if (text.charCodeAt(end) !== 92) {
70
+ throw new SyntaxError("Bad control character in JSON string");
71
+ }
72
+ if (end + (text.charCodeAt(end + 1) === 117 ? 6 : 2) > text.length) {
73
+ return true;
74
+ }
75
+ if (!progressed) {
76
+ throw new SyntaxError("Bad escaped character in JSON string");
77
+ }
78
+ return false;
79
+ }
80
+ function unexpected(): SyntaxError {
81
+ return new SyntaxError("Unexpected character in JSON");
82
+ }
28
83
  function retained(keep: boolean | number): boolean {
29
84
  return keep === true || (typeof keep === "number" && keep !== 0 && !Number.isNaN(keep));
30
85
  }
31
86
 
32
- /** Validate all tokens, assembling selected values only. Skipped strings remain unpacked. */
87
+ /** Validate all JSON text, assembling selected values only. Skipped strings are never decoded. */
33
88
  export class JsonProjection {
34
- private readonly parse = jsonParser({ packValues: false });
35
89
  private readonly stack: Container[] = [];
36
- private scalar?: { path: (string | number)[]; text: string; limit: number; number: boolean };
90
+ private state = VALUE;
91
+ private carry = "";
92
+ private path: readonly (string | number)[] = [];
37
93
  private key = false;
38
- private keyLimit = 4096;
39
- private nodes = 0;
94
+ private textCap = 0;
95
+ private text = "";
96
+ private numberState = 0;
40
97
  private readonly select: Projection;
41
98
  private readonly stringChunk?: StringChunk;
42
99
  private readonly keys: KeyLimit;
43
- private readonly limits?: ProjectionLimits;
44
100
  value: unknown;
45
- constructor(
46
- select: Projection,
47
- stringChunk?: StringChunk,
48
- keys: KeyLimit = () => 4096,
49
- limits?: ProjectionLimits,
50
- ) {
101
+ constructor(select: Projection, stringChunk?: StringChunk, keys: KeyLimit = () => 4096) {
51
102
  this.select = select;
52
103
  this.stringChunk = stringChunk;
53
104
  this.keys = keys;
54
- this.limits = limits;
55
105
  }
56
106
  private root(): UnknownRecord | undefined {
57
107
  return isRecord(this.value) ? this.value : undefined;
58
108
  }
59
- private parent(): Container {
60
- const parent = this.stack.at(-1);
61
- if (!parent) {
62
- throw new SyntaxError("JSON token requires a containing object");
63
- }
64
- return parent;
65
- }
66
- private selected(path: readonly (string | number)[]): boolean | number {
67
- const parent = this.stack.at(-1);
68
- const keep = parent && parent.value === undefined ? false : this.select(path, this.root());
69
- if (retained(keep) && this.limits && ++this.nodes > this.limits.nodes) {
70
- throw new RangeError("JSON projection exceeds its assembled-node budget.");
71
- }
72
- return keep;
109
+ private selected(): boolean | number {
110
+ return this.select(this.path, this.root());
73
111
  }
74
- private nextPath(): (string | number)[] {
112
+ /** Paths inside skipped containers are only built for string observers, never selected. */
113
+ private start(): boolean | number {
75
114
  const parent = this.stack.at(-1);
76
- if (
77
- this.limits &&
78
- (this.stack.length > this.limits.depth || (parent && parent.index >= this.limits.arrayLength))
79
- ) {
80
- throw new RangeError("JSON projection exceeds its depth or array-position budget.");
81
- }
82
115
  if (!parent) {
83
- return [];
116
+ this.path = [];
117
+ return this.selected();
118
+ }
119
+ const step = parent.array ? parent.index++ : parent.key;
120
+ if (parent.value !== undefined) {
121
+ this.path = [...parent.path, step];
122
+ return this.selected();
84
123
  }
85
- return [...parent.path, parent.index >= 0 ? parent.index++ : parent.key];
124
+ this.path = this.stringChunk ? [...parent.path, step] : parent.path;
125
+ return false;
86
126
  }
87
- private put(path: readonly (string | number)[], value: unknown): void {
127
+ private put(value: unknown): void {
88
128
  const parent = this.stack.at(-1);
129
+ const key = this.path.at(-1);
89
130
  if (!parent) {
90
131
  this.value = value;
91
- return;
92
- }
93
- const key = path.at(-1);
94
- if (parent.value !== undefined && key !== undefined) {
132
+ } else if (parent.value !== undefined && key !== undefined) {
95
133
  Object.defineProperty(parent.value, key, {
96
134
  value,
97
135
  enumerable: true,
@@ -100,125 +138,207 @@ export class JsonProjection {
100
138
  });
101
139
  }
102
140
  }
103
- private startContainer(array: boolean): void {
104
- const path = this.nextPath();
105
- const keep = this.selected(path);
141
+ private ended(): void {
142
+ this.state = this.stack.length === 0 ? DONE : AFTER;
143
+ }
144
+ private close(index: number): number {
145
+ this.stack.pop();
146
+ this.ended();
147
+ return index + 1;
148
+ }
149
+ private scalar(state: number, key: boolean, keep: boolean | number): void {
150
+ const cap = keep === true ? Infinity : Number(keep);
151
+ this.state = state;
152
+ this.key = key;
153
+ this.textCap = Number.isNaN(cap) ? 0 : cap;
154
+ this.text = "";
155
+ this.numberState = 0;
156
+ }
157
+ private append(text: string): void {
158
+ if (this.text.length < this.textCap) {
159
+ this.text += text.slice(0, this.textCap - this.text.length);
160
+ }
161
+ }
162
+ private container(array: boolean): void {
106
163
  let value: Container["value"];
107
- if (retained(keep)) {
164
+ if (retained(this.start())) {
108
165
  value = array ? [] : {};
109
- this.put(path, value);
166
+ this.put(value);
110
167
  }
111
- this.stack.push({ path, value, key: "", index: array ? 0 : -1 });
168
+ this.stack.push({ path: this.path, value, array, key: "", index: 0 });
169
+ this.state = array ? FIRST_VALUE : FIRST_KEY;
112
170
  }
113
- private startScalar(number: boolean): void {
114
- const path = this.nextPath();
115
- const keep = this.selected(path);
116
- const selectedLimit = keep === true ? Infinity : Number(keep);
117
- const limit = Number.isNaN(selectedLimit) ? 0 : selectedLimit;
118
- this.scalar = { path, text: "", limit, number };
171
+ private literal(text: string, index: number, code: number): number {
172
+ const literal = LITERALS.get(code);
173
+ if (!literal) {
174
+ throw unexpected();
175
+ }
176
+ const [word, value] = literal;
177
+ if (text.startsWith(word, index)) {
178
+ if (retained(this.start())) {
179
+ this.put(value);
180
+ }
181
+ this.ended();
182
+ return index + word.length;
183
+ }
184
+ if (text.length - index < word.length && word.startsWith(text.slice(index))) {
185
+ this.carry = text.slice(index);
186
+ return text.length;
187
+ }
188
+ throw unexpected();
119
189
  }
120
- private appendKey(text: string): void {
121
- const parent = this.parent();
122
- if (
123
- parent.value !== undefined &&
124
- this.limits &&
125
- parent.key.length + text.length > this.limits.keyLength
126
- ) {
127
- throw new RangeError("JSON projection exceeds its property-key budget.");
190
+ private startValue(text: string, index: number, code: number): number {
191
+ if (code === 34) {
192
+ this.scalar(STRING, false, this.start());
193
+ return index + 1;
194
+ }
195
+ if (code === 45 || (code >= 48 && code <= 57)) {
196
+ this.scalar(NUMBER, false, this.start());
197
+ return index;
128
198
  }
129
- if (parent.key.length < this.keyLimit) {
130
- parent.key += text.slice(0, this.keyLimit - parent.key.length);
199
+ if (code === 123 || code === 91) {
200
+ this.container(code === 91);
201
+ return index + 1;
131
202
  }
203
+ return this.literal(text, index, code);
132
204
  }
133
- private appendScalar(text: string, string: boolean): void {
134
- const scalar = this.scalar;
135
- if (!scalar) {
136
- return;
205
+ private member(index: number, code: number): number {
206
+ const parent = this.stack.at(-1);
207
+ if (code === 125 && this.state === FIRST_KEY) {
208
+ return this.close(index);
137
209
  }
138
- if (string) {
139
- this.stringChunk?.(scalar.path, text, this.root());
210
+ if (code !== 34 || !parent) {
211
+ throw unexpected();
140
212
  }
141
- if (scalar.text.length < scalar.limit) {
142
- scalar.text += text.slice(0, scalar.limit - scalar.text.length);
213
+ this.scalar(STRING, true, parent.value === undefined ? 0 : this.keys(parent.path, this.root()));
214
+ return index + 1;
215
+ }
216
+ private after(index: number, code: number): number {
217
+ const parent = this.stack.at(-1);
218
+ if (parent && code === 44) {
219
+ this.state = parent.array ? VALUE : KEY;
220
+ return index + 1;
143
221
  }
222
+ if (parent && code === (parent.array ? 93 : 125)) {
223
+ return this.close(index);
224
+ }
225
+ throw unexpected();
144
226
  }
145
- private finishScalar(): void {
146
- const scalar = this.scalar;
147
- if (scalar && scalar.limit !== 0) {
148
- this.put(scalar.path, scalar.number ? Number(scalar.text) : scalar.text);
227
+ private structure(text: string, index: number, code: number): number {
228
+ switch (this.state) {
229
+ case VALUE:
230
+ return this.startValue(text, index, code);
231
+ case FIRST_VALUE:
232
+ return code === 93 ? this.close(index) : this.startValue(text, index, code);
233
+ case KEY:
234
+ case FIRST_KEY:
235
+ return this.member(index, code);
236
+ case COLON:
237
+ if (code !== 58) {
238
+ throw unexpected();
239
+ }
240
+ this.state = VALUE;
241
+ return index + 1;
242
+ case AFTER:
243
+ return this.after(index, code);
244
+ default:
245
+ throw unexpected();
149
246
  }
150
- this.scalar = undefined;
151
247
  }
152
- private token(token: Readonly<Token>): void {
153
- switch (token.name) {
154
- case "startKey": {
155
- const parent = this.parent();
156
- this.key = true;
157
- parent.key = "";
158
- this.keyLimit = parent.value === undefined ? 0 : this.keys(parent.path, this.root());
159
- break;
248
+ private observed(decoded: string): void {
249
+ if (decoded === "") {
250
+ return;
251
+ }
252
+ if (this.stringChunk && !this.key) {
253
+ this.stringChunk(this.path, decoded, this.root());
254
+ }
255
+ this.append(decoded);
256
+ }
257
+ private closeString(): void {
258
+ const parent = this.stack.at(-1);
259
+ if (this.key && parent) {
260
+ parent.key = this.text;
261
+ this.state = COLON;
262
+ } else {
263
+ if (this.textCap !== 0) {
264
+ this.put(this.text);
160
265
  }
161
- case "endKey":
162
- this.key = false;
163
- break;
164
- case "startObject":
165
- this.startContainer(false);
166
- break;
167
- case "startArray":
168
- this.startContainer(true);
169
- break;
170
- case "endObject":
171
- case "endArray":
172
- this.stack.pop();
173
- break;
174
- case "startString":
175
- this.startScalar(false);
176
- break;
177
- case "startNumber":
178
- this.startScalar(true);
179
- break;
180
- case "stringChunk":
181
- case "numberChunk":
182
- if (this.key) {
183
- this.appendKey(token.value);
184
- } else {
185
- this.appendScalar(token.value, token.name === "stringChunk");
186
- }
187
- break;
188
- case "endString":
189
- case "endNumber":
190
- this.finishScalar();
191
- break;
192
- case "trueValue":
193
- case "falseValue":
194
- case "nullValue": {
195
- const path = this.nextPath();
196
- const keep = this.selected(path);
197
- if (retained(keep)) {
198
- this.put(path, token.value);
199
- }
200
- break;
266
+ this.ended();
267
+ }
268
+ }
269
+ /** Observed values and unfinished selections need text; others are only validated. */
270
+ private decoding(): boolean {
271
+ return (this.stringChunk !== undefined && !this.key) || this.text.length < this.textCap;
272
+ }
273
+ private string(text: string, start: number): number {
274
+ for (let index = start; ;) {
275
+ BODY.lastIndex = index;
276
+ BODY.test(text);
277
+ const end = BODY.lastIndex;
278
+ if (end > index && this.decoding()) {
279
+ const raw = text.slice(index, end);
280
+ // A matched run is a complete valid string body no longer than this chunk.
281
+ this.observed(raw.includes("\\") ? String(JSON.parse(`"${raw}"`)) : raw);
282
+ }
283
+ if (text.charCodeAt(end) === 34) {
284
+ this.closeString();
285
+ return end + 1;
286
+ }
287
+ if (chunkEnds(text, end, end > index)) {
288
+ this.carry = text.slice(end);
289
+ return text.length;
201
290
  }
202
- case "keyValue":
203
- case "stringValue":
204
- case "numberValue":
205
- case "whitespace":
206
- break; // Packing is disabled; whitespace carries no projected value.
291
+ index = end;
207
292
  }
208
293
  }
209
- write(chunk: string | symbol): void {
210
- if (typeof chunk === "symbol" && chunk !== none) {
211
- throw new TypeError("Unknown JSON tokenizer sentinel");
294
+ private number(text: string, start: number, final: boolean): number {
295
+ let state = this.numberState;
296
+ let index = start;
297
+ for (; index < text.length; index++) {
298
+ const next = NUMBER_NEXT[state]?.[NUMBER_COLUMN[text.charCodeAt(index)] ?? -1] ?? -1;
299
+ if (next < 0) {
300
+ break;
301
+ }
302
+ state = next;
303
+ }
304
+ this.numberState = state;
305
+ this.append(text.slice(start, index));
306
+ if (index === text.length && !final) {
307
+ return index;
308
+ }
309
+ if (!NUMBER_END.has(state)) {
310
+ throw new SyntaxError("Incomplete number in JSON");
212
311
  }
213
- const output = this.parse(typeof chunk === "symbol" ? none : chunk);
214
- if (typeof output !== "symbol") {
215
- for (const token of output.values) {
216
- this.token(token);
312
+ if (this.textCap !== 0) {
313
+ this.put(Number(this.text));
314
+ }
315
+ this.ended();
316
+ return index;
317
+ }
318
+ write(chunk: string): void {
319
+ const text = this.carry + chunk;
320
+ this.carry = "";
321
+ let index = 0;
322
+ while (index < text.length) {
323
+ const code = text.charCodeAt(index);
324
+ if (this.state === STRING) {
325
+ index = this.string(text, index);
326
+ } else if (this.state === NUMBER) {
327
+ index = this.number(text, index, false);
328
+ } else if (code === 32 || code === 10 || code === 13 || code === 9) {
329
+ index++;
330
+ } else {
331
+ index = this.structure(text, index, code);
217
332
  }
218
333
  }
219
334
  }
220
335
  finish(): unknown {
221
- this.write(none);
336
+ if (this.state === NUMBER) {
337
+ this.number("", 0, true);
338
+ }
339
+ if (this.state !== DONE || this.carry !== "") {
340
+ throw new SyntaxError("Unexpected end of JSON input");
341
+ }
222
342
  return this.value;
223
343
  }
224
344
  }