pi-supernova 0.8.2 → 0.9.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 (66) hide show
  1. package/README.md +188 -51
  2. package/docs/CHANGELOG.md +86 -1
  3. package/docs/TOKEN_COSTS.md +38 -0
  4. package/index.js +10 -175
  5. package/package.json +2 -1
  6. package/src/adapters/bash.js +14 -30
  7. package/src/adapters/errors.js +1 -9
  8. package/src/adapters/read-focus.js +98 -0
  9. package/src/adapters/read-image.js +51 -0
  10. package/src/adapters/read-json.js +42 -0
  11. package/src/adapters/read-text.js +71 -0
  12. package/src/adapters/read.js +66 -635
  13. package/src/bridge/catalog.js +3 -2
  14. package/src/bridge/host-bridge.js +35 -167
  15. package/src/bridge/tool-registry.js +104 -0
  16. package/src/bridge/trace.js +41 -0
  17. package/src/context/evidence-graph.js +249 -0
  18. package/src/context/evidence-rank.js +153 -0
  19. package/src/context/evidence.js +10 -424
  20. package/src/context/query.js +71 -0
  21. package/src/context/repo-index.js +8 -162
  22. package/src/context/search-files.js +19 -0
  23. package/src/context/search.js +2 -24
  24. package/src/context/snap-search.js +202 -0
  25. package/src/context/snap.js +5 -266
  26. package/src/context/source-entry.js +112 -0
  27. package/src/contract/bash.js +6 -1
  28. package/src/contract/program.js +36 -0
  29. package/src/contract/read.js +8 -53
  30. package/src/fs/check.js +1 -1
  31. package/src/fs/commit.js +161 -0
  32. package/src/fs/diff.js +11 -15
  33. package/src/fs/directory.js +79 -0
  34. package/src/fs/file-io.js +100 -0
  35. package/src/fs/glob.js +54 -0
  36. package/src/fs/json-size.js +54 -0
  37. package/src/fs/lines.js +117 -0
  38. package/src/fs/read-window.js +74 -0
  39. package/src/fs/session-resource.js +50 -0
  40. package/src/fs/text-ops.js +7 -227
  41. package/src/fs/vfs.js +5 -239
  42. package/src/fs/workspace.js +2 -1
  43. package/src/output/bottleneck.js +13 -67
  44. package/src/output/final.js +114 -0
  45. package/src/output/format.js +94 -5
  46. package/src/output/outcome.js +91 -0
  47. package/src/runtime/batch-input.js +68 -0
  48. package/src/runtime/guest-api.js +281 -0
  49. package/src/runtime/guest-worker.js +62 -333
  50. package/src/runtime/parallel.js +41 -39
  51. package/src/runtime/program-batch.js +21 -75
  52. package/src/runtime/program-file.js +3 -11
  53. package/src/runtime/program.js +141 -0
  54. package/src/runtime/reference.js +6 -5
  55. package/src/runtime/runtime.js +77 -253
  56. package/src/runtime/worker-pool.js +91 -0
  57. package/src/shared/decode.js +22 -8
  58. package/src/shared/image-worker.js +30 -0
  59. package/src/shared/image.js +78 -0
  60. package/src/shared/png.js +57 -0
  61. package/src/shared/result.js +77 -0
  62. package/src/shared/syntax-context.js +61 -3
  63. package/src/ui/host-render.js +104 -0
  64. package/src/ui/progress.js +51 -0
  65. package/src/ui/render.js +21 -421
  66. package/src/ui/trace.js +277 -0
@@ -1,278 +1,17 @@
1
+ import {inScope,makeCandidate,contentCandidates,MAX_SEARCH_CHARS} from './snap-search.js';
2
+ import { tokenizeQuery, stem } from "./query.js";
3
+ export {tokenizeQuery,scorePathTopology,stem} from './query.js';
1
4
  import * as path from "node:path";
2
- import { isString } from "../shared/decode.js";
3
- import { truncateChars } from "../output/format.js";
5
+
4
6
  import * as fs from "node:fs/promises";
5
- import { extractStructuralSurface } from "./surface.js";
7
+
6
8
  import { WorkspaceIndex } from "./repo-index.js";
7
9
  import { pickSpan, spanCandidate, spanWindow } from "./spans.js";
8
10
  import { rankPaths } from "./fuzzy.js";
9
11
  import { isTestPath, runCommand, relativeSlash } from "../fs/workspace.js";
10
12
 
11
- const STOP_WORDS = new Set([
12
- "the", "a", "an", "and", "or", "in", "on", "at", "to", "for", "of", "with",
13
- "by", "from", "is", "it", "this", "that", "where", "how", "what", "which",
14
- "file", "code", "function", "class", "method", "find", "get", "look", "are", "does", "do",
15
- ]);
16
-
17
- const SOURCE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go"]);
18
-
19
- const TYPED_EXT = new Set([".ts", ".tsx", ".rs", ".go"]);
20
-
21
- const MAX_SEARCH_CHARS = 2 * 1024 * 1024;
22
-
23
- const MAX_NEEDLE_CHARS = 128;
24
-
25
13
  const MAX_ALTERNATIVES = 3;
26
14
 
27
- /** Light suffix stripping so "terminated" ⊇ "terminat" matches "terminate"; deterministic, no dictionary. */
28
- export function stem(token) {
29
- if (token.length < 5) return token;
30
-
31
- return token.replace(/(ations?|ings?|ed|es|e|s|ly|ers?)$/, (m) => (token.length - m.length >= 4 ? "" : m));
32
- }
33
-
34
- export function tokenizeQuery(query) {
35
- if (!isString(query) || !query.trim()) return { tokens: [], wantsTest: false, wantsType: false, wantsDoc: false };
36
- const words = query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-zA-Z0-9_]+/);
37
-
38
- return {
39
- tokens: [...new Set(words.filter(word => word.length > 1 && !STOP_WORDS.has(word)))],
40
- wantsTest: words.some(word => ["test", "tests", "testing", "spec", "specs"].includes(word)),
41
- wantsType: words.some(word => ["type", "types", "interface", "interfaces", "schema", "schemas"].includes(word)),
42
- wantsDoc: words.some(word => ["doc", "docs", "documentation", "readme"].includes(word)),
43
- };
44
- }
45
-
46
- function tokenPathScore(base, words, normalized, tokens) {
47
- let score = 0;
48
-
49
- for (const token of tokens) {
50
- if (base === token || base.startsWith(token + ".")) score += 60;
51
- else if (base.includes(token)) score += 30;
52
- else if (words.includes(token)) score += 15;
53
- else if (normalized.includes(token)) score += 5;
54
- }
55
-
56
- return score;
57
- }
58
-
59
- function topologyPenalty(normalized, flags) {
60
- const parts = normalized.split("/");
61
-
62
- if (parts.some(part => ["node_modules", "dist", "target"].includes(part))) return -100;
63
- const test = isTestPath(normalized);
64
-
65
- if (test && !flags.wantsTest) return -50;
66
- if (!test && flags.wantsTest) return -20;
67
- }
68
-
69
- export function scorePathTopology(filePath, tokens, flags) {
70
- const normalized = filePath.replaceAll("\\", "/").toLowerCase();
71
- const penalty = topologyPenalty(normalized, flags);
72
-
73
- if (penalty !== undefined) return penalty;
74
- const ext = path.extname(normalized);
75
- let score = SOURCE_EXT.has(ext) && !flags.wantsDoc ? 5 : 0;
76
-
77
- if (flags.wantsType && TYPED_EXT.has(ext)) score += 10;
78
-
79
- return score + tokenPathScore(path.basename(normalized), normalized.split(/[^a-zA-Z0-9]+/), normalized, tokens);
80
- }
81
-
82
- function inScope(filePath, dir, includeHidden) {
83
- const relative = path.relative(dir, filePath);
84
-
85
- if (relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) return false;
86
- const parts = relative.split(path.sep);
87
-
88
- return !parts.includes(".git") && (includeHidden || !parts.some(part => part.startsWith(".") && part.length > 1));
89
- }
90
-
91
- function makeCandidate(filePath, dir, query, tokens, flags) {
92
- const relative = path.relative(dir, filePath);
93
- const lower = relative.toLowerCase();
94
- const base = path.basename(lower);
95
-
96
- const extension = path.extname(base);
97
- const stemBase = extension ? base.slice(0, -extension.length) : base;
98
- const exactPath = lower === query.toLowerCase() || base === query.toLowerCase()
99
- || stemBase === query.toLowerCase();
100
-
101
- const needles = tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
102
-
103
- return { path: filePath, pathScore: scorePathTopology(relative, tokens, flags), exactPath,
104
- pathCoverage: tokens.filter((token, index) => lower.includes(needles[index] ?? token)).length,
105
- matched: new Set(), exactDefinition: false, definitionCoverage: 0, lineCoverage: 0,
106
- line: 1, signature: "", context: new Map(), recent: [], anchorScore: -1, exactLines: new Set() };
107
- }
108
-
109
- function bestDeclaration(items, query, tokens, needles) {
110
- let declaration;
111
- let definitionCoverage = 0;
112
- let exact = false;
113
- const queryLower = query.toLowerCase();
114
-
115
- for (const item of items) {
116
- const name = item.name.toLowerCase();
117
- const itemExact = name === queryLower;
118
- const coverage = tokens.filter((token, index) => name.includes(needles[index] ?? token)).length;
119
-
120
- if (itemExact || coverage > definitionCoverage) { declaration = item; definitionCoverage = coverage; exact = itemExact; }
121
- if (exact) break;
122
- }
123
-
124
- return { declaration, definitionCoverage, exact };
125
- }
126
-
127
- function applyMatch(candidate, lineNumber, text, query, tokens, needles, lower) {
128
- const matches = tokens.filter((token, index) => lower.includes(needles[index] ?? token));
129
-
130
- for (const token of matches) candidate.matched.add(token);
131
- const ext = path.extname(candidate.path).toLowerCase();
132
- const items = SOURCE_EXT.has(ext) ? extractStructuralSurface(text, ext).items : [];
133
- const { declaration, definitionCoverage, exact } = bestDeclaration(items, query, tokens, needles);
134
- const score = (exact ? 10000 : 0) + definitionCoverage * 40 + matches.length;
135
-
136
- if (exact) candidate.exactLines.add(lineNumber);
137
-
138
- if (score > candidate.anchorScore) {
139
- candidate.anchorScore = score;
140
- candidate.line = lineNumber;
141
- candidate.signature = truncateChars(declaration?.signature ?? "", 240, "signature").text;
142
- candidate.exactDefinition = exact;
143
- candidate.definitionCoverage = definitionCoverage;
144
- candidate.lineCoverage = matches.length;
145
- candidate.context.clear();
146
-
147
- for (const [number, line] of candidate.recent) if (number >= lineNumber - 2) candidate.context.set(number, line);
148
- }
149
- }
150
-
151
- function inspectLine(candidate, lineNumber, raw, query, tokens, needles, isMatch) {
152
- const text = raw.replace(/\r?\n$/, "");
153
- const lower = text.toLowerCase();
154
-
155
- if (isMatch) applyMatch(candidate, lineNumber, text, query, tokens, needles, lower);
156
- const excerpt = truncateChars(text, 240, "source line").text;
157
-
158
- if (lineNumber >= candidate.line - 2 && lineNumber <= candidate.line + 4) candidate.context.set(lineNumber, excerpt);
159
- candidate.recent.push([lineNumber, excerpt]);
160
-
161
- if (candidate.recent.length > 2) candidate.recent.shift();
162
- }
163
-
164
- function inspectOverlay(candidate, text, needles, query, tokens, signal) {
165
- let start = 0, line = 1, truncated = false;
166
-
167
- // Keep only the candidate and its short context, not another copy of every
168
- // line in a staged document. Oversized individual lines disclose uncertainty.
169
- while (start < text.length) {
170
- if ((line & 127) === 0) signal?.throwIfAborted();
171
- const newline = text.indexOf("\n", start);
172
- const end = newline < 0 ? text.length : newline + 1;
173
-
174
- if (end - start > MAX_SEARCH_CHARS) truncated = true;
175
- else {
176
- const row = text.slice(start, end);
177
- const lower = row.toLowerCase();
178
- inspectLine(candidate, line, row, query, tokens, needles, needles.some(needle => lower.includes(needle)));
179
- }
180
- start = end;
181
- line++;
182
- }
183
-
184
- return truncated;
185
- }
186
-
187
- function parseRgRecord(line, truncated, isLast) {
188
- if (!line) return null;
189
-
190
- try { return JSON.parse(line); } catch (error) {
191
- if (truncated && isLast) return undefined;
192
- throw error;
193
- }
194
- }
195
-
196
- function absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles) {
197
- if (record.type !== "match" && record.type !== "context") return;
198
- const data = record.data;
199
-
200
- if (!data?.path?.text || !isString(data.lines?.text)) return;
201
- const filePath = path.resolve(dir, data.path.text);
202
-
203
- if (!inScope(filePath, dir, includeHidden) || overlayText(filePath) !== undefined) return;
204
- let candidate = candidates.get(filePath);
205
-
206
- if (!candidate) {
207
- candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
208
- candidates.set(filePath, candidate);
209
- }
210
-
211
- inspectLine(candidate, data.line_number, data.lines.text, query, tokens, needles, record.type === "match");
212
- }
213
-
214
- function overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
215
- let overlayTruncated = false;
216
-
217
- for (const filePath of pendingPaths) {
218
- const pending = overlayText(filePath);
219
-
220
- if (pending === undefined) continue;
221
- const candidate = makeCandidate(filePath, candidateRoot, query, tokens, flags);
222
- overlayTruncated = inspectOverlay(candidate, pending, needles, query, tokens, signal) || overlayTruncated;
223
-
224
- if (candidate.matched.size) candidates.set(filePath, candidate);
225
- }
226
-
227
- return overlayTruncated;
228
- }
229
-
230
- function rgSearchArgs(includeHidden, searchNeedles, focusFile, dir) {
231
- const args = ["rg", "--json", "--fixed-strings", "--ignore-case", "--before-context", "2", "--after-context", "4"];
232
-
233
- if (includeHidden) args.push("--hidden");
234
- args.push("-g", "!.git/**", "-g", "!**/.git/**");
235
- for (const needle of searchNeedles) args.push("-e", needle);
236
- args.push("--", focusFile ?? dir);
237
-
238
- return args;
239
- }
240
-
241
- async function runContentSearch({ dir, includeHidden, searchNeedles, run, overlayText, signal, diskFiles, focusFile }) {
242
- const args = rgSearchArgs(includeHidden, searchNeedles, focusFile, dir);
243
- const response = diskFiles || (focusFile && overlayText(focusFile) === undefined)
244
- ? await run(args, { cwd: focusFile ? path.dirname(focusFile) : dir, timeoutMs: 15000, maxOutputChars: MAX_SEARCH_CHARS, signal })
245
- : { stdout: "", stderr: "", exitCode: 1 };
246
-
247
- if (response.exitCode !== 0 && response.exitCode !== 1) throw new Error("source search failed: " + response.stderr.trim());
248
-
249
- return response;
250
- }
251
-
252
- function absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal) {
253
- const records = response.stdout.split("\n");
254
-
255
- for (let i = 0; i < records.length; i++) {
256
- if ((i & 127) === 0) signal?.throwIfAborted();
257
- const record = parseRgRecord(records[i], response.outputTruncated, i === records.length - 1);
258
-
259
- if (record === undefined) break;
260
- if (!record) continue;
261
- absorbRgHit(candidates, record, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles);
262
- }
263
- }
264
-
265
- async function contentCandidates({ dir, includeHidden, query, tokens, flags, pendingPaths, run, overlayText, signal, exact, diskFiles, focusFile }) {
266
- const needles = exact ? [query.toLowerCase().slice(0, MAX_NEEDLE_CHARS)] : tokens.map(token => stem(token).slice(0, MAX_NEEDLE_CHARS));
267
- const candidateRoot = focusFile ? path.dirname(focusFile) : dir;
268
- const candidates = new Map();
269
- const response = await runContentSearch({ dir, includeHidden, searchNeedles: [...new Set(needles)], run, overlayText, signal, diskFiles, focusFile });
270
- absorbRgRecords(candidates, response, dir, includeHidden, overlayText, candidateRoot, query, tokens, flags, needles, signal);
271
- const overlayTruncated = overlayCandidates(candidates, pendingPaths, overlayText, candidateRoot, query, tokens, flags, needles, signal);
272
-
273
- return { candidates, truncated: response.outputTruncated === true || overlayTruncated };
274
- }
275
-
276
15
  function rankScore(candidate, tokenCount) {
277
16
  return (candidate.exactDefinition ? 10000 : 0) + (candidate.exactPath ? 500 : 0)
278
17
  + candidate.definitionCoverage / tokenCount * 100 + candidate.matched.size / tokenCount * 30
@@ -0,0 +1,112 @@
1
+ import * as path from 'node:path';
2
+ import {extractStructuralSurface} from './surface.js';
3
+
4
+ const IDENT_TOKEN = /[A-Za-z_$][\w$]*/g;
5
+
6
+ const EMPTY = Object.freeze([]);
7
+
8
+ const DEF_PATTERN = /^(?:pub\s+)?(?:export\s+)?(?:async\s+)?(?:default\s+)?(?:(function|class|def|fn|const|let|interface|type|struct|enum)\s+([a-zA-Z0-9_$]+)|([A-Z][A-Z0-9_$]*)\s*(?::[^=\n]+)?=)/;
9
+
10
+ /** Declared identifier on a line (function/class/UPPER_CASE constant/…), or ""; the same rule snap and grep use. */
11
+ export function declaredName(line) {
12
+ const match = DEF_PATTERN.exec(String(line).trim());
13
+
14
+ return match?.[2] ?? match?.[3] ?? "";
15
+ }
16
+
17
+ function lineIndent(raw, i) {
18
+ return raw[i].length - raw[i].trimStart().length;
19
+ }
20
+
21
+ function pythonDeclarationEnd(raw, lower, start, lineCount) {
22
+ const base = lineIndent(raw, start - 1);
23
+ let end = start;
24
+
25
+ for (let i = start; i < lineCount; i++) {
26
+ if (lower[i] === "") { end = i + 1; continue; }
27
+ if (lineIndent(raw, i) <= base) break;
28
+ end = i + 1;
29
+ }
30
+
31
+ return Math.min(end, lineCount);
32
+ }
33
+
34
+ function braceDelta(text) {
35
+ let depth = 0;
36
+
37
+ for (const ch of text) {
38
+ if (ch === "{") depth++;
39
+ else if (ch === "}") depth--;
40
+ }
41
+
42
+ return depth;
43
+ }
44
+
45
+ function braceDeclarationEnd(raw, start, lineCount) {
46
+ let depth = braceDelta(raw[start - 1] ?? "");
47
+
48
+ if (depth <= 0) return start;
49
+
50
+ for (let i = start; i < raw.length; i++) {
51
+ depth += braceDelta(raw[i]);
52
+
53
+ if (depth <= 0) return i + 1;
54
+ }
55
+
56
+ return lineCount;
57
+ }
58
+
59
+ function declarationEnd(raw, lower, start, lineCount, ext) {
60
+ if (ext === ".py") return pythonDeclarationEnd(raw, lower, start, lineCount);
61
+
62
+ return braceDeclarationEnd(raw, start, lineCount);
63
+ }
64
+
65
+ export function fromText(filePath, text) {
66
+ return { text, lower: text.toLowerCase(), ext: path.extname(filePath), surface: undefined, lines: undefined, spans: undefined };
67
+ }
68
+
69
+ export function linesOf(entry) {
70
+ if (entry.lines) return entry.lines;
71
+ const raw = entry.text.split("\n");
72
+ const lower = [];
73
+ const defNames = [];
74
+ const idents = [];
75
+
76
+ for (let i = 0; i < raw.length; i++) {
77
+ const trimmed = raw[i].trim();
78
+ lower[i] = trimmed.toLowerCase();
79
+ const declared = DEF_PATTERN.exec(trimmed);
80
+ defNames[i] = (declared?.[2] ?? declared?.[3] ?? "").toLowerCase();
81
+ idents[i] = trimmed.match(IDENT_TOKEN) || EMPTY;
82
+ }
83
+
84
+ entry.lines = { raw, lower, defNames, idents };
85
+
86
+ return entry.lines;
87
+ }
88
+
89
+ export function spansOf(entry) {
90
+ if (entry.spans) return entry.spans;
91
+ const { items, lineCount } = surfaceOf(entry);
92
+ const { lower, raw } = linesOf(entry);
93
+ const spans = [];
94
+
95
+ for (let i = 0; i < items.length; i++) {
96
+ const start = items[i].line;
97
+ let end = declarationEnd(raw, lower, start, lineCount, entry.ext);
98
+
99
+ while (end > start && lower[end - 1] === "") end--;
100
+ spans.push({ start, end, name: items[i].name, kind: items[i].kind, isExport: items[i].isExport === true });
101
+ }
102
+
103
+ entry.spans = spans;
104
+
105
+ return spans;
106
+ }
107
+
108
+ export function surfaceOf(entry) {
109
+ if (!entry.surface) entry.surface = extractStructuralSurface(entry.text, entry.ext);
110
+
111
+ return entry.surface;
112
+ }
@@ -44,6 +44,12 @@ export function normalizeBash(command, opts) {
44
44
  if (args.command.includes("\0")) throw new Error("bash command must not contain null bytes");
45
45
  normalizeArgv(args);
46
46
 
47
+ normalizeTimeout(args);
48
+
49
+ return args;
50
+ }
51
+
52
+ function normalizeTimeout(args) {
47
53
  if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
48
54
  // Reject before the host's external-mutation barrier can flush staged files.
49
55
  if (args.timeoutMs !== undefined) {
@@ -52,5 +58,4 @@ export function normalizeBash(command, opts) {
52
58
  args.timeoutMs = Math.max(1, Math.min(2_147_483_647, Math.floor(timeout)));
53
59
  }
54
60
 
55
- return args;
56
61
  }
@@ -0,0 +1,36 @@
1
+ import {createRequire} from 'node:module';
2
+
3
+ // Sync only, never top-level await. Dynamic import of host/deps hung OMP plugin load.
4
+ const require = createRequire(import.meta.url);
5
+
6
+ let Type;
7
+
8
+ try {
9
+ Type = require("typebox").Type;
10
+ } catch {
11
+ Type = {
12
+ Object: (props, opts) => ({ type: "object", properties: props || {}, additionalProperties: false, ...opts }),
13
+ String: (opts) => ({ type: "string", ...opts }),
14
+ Unknown: (opts) => ({ ...opts }),
15
+ Array: (items, opts) => ({ type: "array", items, ...opts }),
16
+ Integer: (opts) => ({ type: "integer", ...opts }),
17
+ Optional: (s) => ({ ...s }),
18
+ Boolean: (opts) => ({ type: "boolean", ...opts }),
19
+ };
20
+ }
21
+
22
+ export function programParameters(config) {
23
+ return Type.Object({
24
+ code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
25
+ file: Type.Optional(Type.String({ minLength: 1 })),
26
+ data: Type.Optional(Type.Unknown()),
27
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1000 })),
28
+ programs: Type.Optional(Type.Array(Type.Object({
29
+ code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
30
+ file: Type.Optional(Type.String({ minLength: 1 })),
31
+ data: Type.Optional(Type.Unknown()),
32
+ }, {additionalProperties:false}), {minItems:1,maxItems:32})),
33
+ parallel: Type.Optional(Type.Boolean()),
34
+ mergeData: Type.Optional(Type.Boolean()),
35
+ });
36
+ }
@@ -1,4 +1,4 @@
1
- import { isString, isObject, isNumber, looksLikePath } from "../shared/decode.js";
1
+ import { errorMessage, isString, isObject, isNumber, looksLikePath } from "../shared/decode.js";
2
2
  import { sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
3
3
 
4
4
  export const SESSION_URI = /^(?:agent|artifact):\/\//i;
@@ -85,12 +85,7 @@ export function normalizeRead(params) {
85
85
  }
86
86
 
87
87
  export function needsProbe(params) {
88
- if (isSessionUri(params.path)) return false;
89
- if (params.evidence === true) return false;
90
- if (isString(params.query)) return false;
91
- if (params.outline === true) return false;
92
-
93
- return true;
88
+ return !(isSessionUri(params.path) || params.evidence === true || isString(params.query) || params.outline === true);
94
89
  }
95
90
 
96
91
  /**
@@ -160,8 +155,6 @@ export const ROUTING_STATUS = "too_large";
160
155
 
161
156
  const ROUTING_PREFIX = '{"status":"too_large",';
162
157
 
163
- export const ROUTING_KEYS_MAX = 32;
164
-
165
158
  export function isRoutingPayload(value) {
166
159
  return isString(value) && value.startsWith(ROUTING_PREFIX);
167
160
  }
@@ -171,49 +164,6 @@ function isRoutingObject(parsed) {
171
164
  && isNumber(parsed.chars) && (Array.isArray(parsed.keys) || isNumber(parsed.length));
172
165
  }
173
166
 
174
- /** Shape of an over-bound JSON document for in-band routing. Throws when text is not JSON. */
175
- export function buildJsonRouting(rel, text) {
176
- const document = JSON.parse(text);
177
- const base = { status: ROUTING_STATUS, path: rel, chars: text.length };
178
-
179
- if (Array.isArray(document)) return { ...base, length: document.length };
180
-
181
- if (isObject(document)) {
182
- const keys = Object.keys(document);
183
-
184
- return keys.length > ROUTING_KEYS_MAX
185
- ? { ...base, keys: keys.slice(0, ROUTING_KEYS_MAX), keysTruncated: true }
186
- : { ...base, keys };
187
- }
188
-
189
- return base;
190
- }
191
-
192
- export function routingText(routing) {
193
- const text = JSON.stringify(routing);
194
-
195
- if (!isRoutingPayload(text)) throw new Error("routing payload must start with the shared marker");
196
-
197
- return text;
198
- }
199
-
200
- /** Shape of one over-budget selection for in-band routing; value is already parsed. */
201
- export function buildSelectionRouting(rel, selector, value, chars) {
202
- const base = { status: ROUTING_STATUS, path: rel, selector, chars };
203
-
204
- if (Array.isArray(value)) return { ...base, length: value.length };
205
-
206
- if (isObject(value)) {
207
- const keys = Object.keys(value);
208
-
209
- return keys.length > ROUTING_KEYS_MAX
210
- ? { ...base, keys: keys.slice(0, ROUTING_KEYS_MAX), keysTruncated: true }
211
- : { ...base, keys };
212
- }
213
-
214
- return base;
215
- }
216
-
217
167
  function decodeByArgs(args, value) {
218
168
  return (args.resolve || args.json !== undefined || args.outline || args.evidence) && isString(value);
219
169
  }
@@ -230,6 +180,11 @@ export function decodeReadValue(args, value) {
230
180
  } catch (error) {
231
181
  if (sniffed && !decodeByArgs(args, value)) return value;
232
182
 
233
- throw new Error("JSON read failed for " + String(args.path ?? args.target ?? "resource") + jsonSelectorNote(args) + ": " + (error instanceof Error ? error.message : String(error)));
183
+ throw jsonReadError(args, error);
234
184
  }
235
185
  }
186
+
187
+ function jsonReadError(args, error) {
188
+ const target = String(args.path ?? args.target ?? "resource");
189
+ return new Error("JSON read failed for " + target + jsonSelectorNote(args) + ": " + errorMessage(error));
190
+ }
package/src/fs/check.js CHANGED
@@ -145,7 +145,7 @@ function consumeLiteral(text, i, stack, prev, rust) {
145
145
  if (lifetime && text[end] !== "'") return { end, prev: "value" };
146
146
  }
147
147
 
148
- if (c === '"' || c === "'" || c === "`") return consumeQuoted(text, i, stack);
148
+ if (['"', "'", "`"].includes(c)) return consumeQuoted(text, i, stack);
149
149
 
150
150
  if (c !== "/") return null;
151
151