pi-supernova 0.8.1 → 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.
- package/README.md +244 -54
- package/docs/CHANGELOG.md +111 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +23 -33
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +18 -2
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -242
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +7 -2
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +35 -68
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +11 -10
- package/src/runtime/runtime.js +94 -261
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
package/src/context/snap.js
CHANGED
|
@@ -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
|
-
|
|
3
|
-
import { truncateChars } from "../output/format.js";
|
|
5
|
+
|
|
4
6
|
import * as fs from "node:fs/promises";
|
|
5
|
-
|
|
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
|
+
}
|
package/src/contract/bash.js
CHANGED
|
@@ -17,7 +17,10 @@ function normalizeArgv(args) {
|
|
|
17
17
|
throw new Error(`${ARGV_ERROR}; args[${i}] is ${type}; check the supplied data fields and pass each argument as a string`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
-
args.args = args.args.map(
|
|
20
|
+
args.args = args.args.map((arg, i) => {
|
|
21
|
+
if (arg.includes("\0")) throw new Error(`bash args[${i}] must not contain null bytes`);
|
|
22
|
+
return String(arg);
|
|
23
|
+
});
|
|
21
24
|
|
|
22
25
|
if (process.platform === "win32") {
|
|
23
26
|
delete args._directArgv;
|
|
@@ -37,9 +40,22 @@ function assertBashOptions(args) {
|
|
|
37
40
|
export function normalizeBash(command, opts) {
|
|
38
41
|
const args = isObject(command) ? { ...opts, ...command } : { command, ...opts };
|
|
39
42
|
assertBashOptions(args);
|
|
43
|
+
if (!isString(args.command) || !args.command.trim()) throw new Error("bash requires a non-empty command string");
|
|
44
|
+
if (args.command.includes("\0")) throw new Error("bash command must not contain null bytes");
|
|
40
45
|
normalizeArgv(args);
|
|
41
46
|
|
|
42
|
-
|
|
47
|
+
normalizeTimeout(args);
|
|
43
48
|
|
|
44
49
|
return args;
|
|
45
50
|
}
|
|
51
|
+
|
|
52
|
+
function normalizeTimeout(args) {
|
|
53
|
+
if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
|
|
54
|
+
// Reject before the host's external-mutation barrier can flush staged files.
|
|
55
|
+
if (args.timeoutMs !== undefined) {
|
|
56
|
+
const timeout = Number(args.timeoutMs);
|
|
57
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new Error("command timeoutMs must be a positive finite number");
|
|
58
|
+
args.timeoutMs = Math.max(1, Math.min(2_147_483_647, Math.floor(timeout)));
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
}
|
package/src/contract/read.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 (
|
|
148
|
+
if (['"', "'", "`"].includes(c)) return consumeQuoted(text, i, stack);
|
|
149
149
|
|
|
150
150
|
if (c !== "/") return null;
|
|
151
151
|
|