pi-supernova 0.6.0 → 0.7.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.
Files changed (49) hide show
  1. package/README.md +27 -3
  2. package/docs/CHANGELOG.md +114 -0
  3. package/docs/TOKEN_COSTS.md +13 -5
  4. package/index.js +120 -79
  5. package/package.json +1 -1
  6. package/src/adapters/bash.js +73 -0
  7. package/src/adapters/edit.js +249 -0
  8. package/src/adapters/errors.js +31 -0
  9. package/src/adapters/index.js +31 -0
  10. package/src/adapters/list.js +102 -0
  11. package/src/adapters/read.js +805 -0
  12. package/src/adapters/refs.js +41 -0
  13. package/src/adapters/write.js +96 -0
  14. package/src/bridge/catalog.js +28 -222
  15. package/src/bridge/host-bridge.js +113 -1668
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -198
  18. package/src/context/evidence.js +140 -76
  19. package/src/context/fuzzy.js +42 -24
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +23 -18
  22. package/src/context/repo-index.js +206 -170
  23. package/src/context/search.js +157 -77
  24. package/src/context/snap.js +240 -136
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +14 -5
  27. package/src/contract/bash.js +31 -0
  28. package/src/contract/edit.js +95 -0
  29. package/src/contract/read.js +220 -0
  30. package/src/fs/check.js +12 -8
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +94 -50
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +205 -175
  37. package/src/fs/workspace.js +119 -108
  38. package/src/output/bottleneck.js +195 -116
  39. package/src/output/format.js +101 -67
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +296 -292
  42. package/src/runtime/parallel.js +97 -64
  43. package/src/runtime/program-batch.js +178 -69
  44. package/src/runtime/reference.js +15 -14
  45. package/src/runtime/runtime.js +327 -187
  46. package/src/shared/decode.js +58 -36
  47. package/src/ui/omp-frame.js +59 -42
  48. package/src/ui/render-measure.js +51 -29
  49. package/src/ui/render.js +241 -145
@@ -29,6 +29,47 @@ function pendingInScope(root, pendingPaths) {
29
29
  });
30
30
  }
31
31
 
32
+ function parseMatchRecord(line, truncated, isLast) {
33
+ if (!line) return { skip: true };
34
+
35
+ try { return { record: JSON.parse(line) }; }
36
+ catch (error) {
37
+ if (truncated && isLast) return { stop: true };
38
+ throw error;
39
+ }
40
+ }
41
+
42
+ function isRgMatch(record) {
43
+ return record.type === "match" && isString(record.data?.path?.text) && isString(record.data.lines?.text);
44
+ }
45
+
46
+ function applyMatchRecord(record, root, overlayText, add) {
47
+ if (!isRgMatch(record)) return;
48
+ const file = path.resolve(root, record.data.path.text);
49
+
50
+ if (overlayText(file) === undefined) add(file, record.data.line_number, record.data.lines.text);
51
+ }
52
+
53
+ function ingestRgMatches(records, result, signal, root, overlayText, add) {
54
+ for (let i = 0; i < records.length; i++) {
55
+ signal?.throwIfAborted();
56
+ const parsed = parseMatchRecord(records[i], result.outputTruncated, i === records.length - 1);
57
+
58
+ if (parsed.stop) break;
59
+
60
+ if (parsed.skip) continue;
61
+ applyMatchRecord(parsed.record, root, overlayText, add);
62
+ }
63
+ }
64
+
65
+ function ingestPendingRefs(root, pendingPaths, overlayText, add) {
66
+ for (const file of pendingInScope(root, pendingPaths)) {
67
+ const text = overlayText(file);
68
+
69
+ if (text !== undefined && Buffer.byteLength(text, "utf8") <= 512 * 1024) text.split("\n").forEach((line, i) => add(file, i + 1, line));
70
+ }
71
+ }
72
+
32
73
  /** One bounded direct search for all changed names; no repository index or per-name spawn. */
33
74
  export async function referencesForNames({ root, names, excludePath, overlayText, pendingPaths, signal, run = runCommand }) {
34
75
  const references = new Map(names.map(name => [name, []]));
@@ -48,85 +89,90 @@ export async function referencesForNames({ root, names, excludePath, overlayText
48
89
  { cwd: root, signal, timeoutMs: 5000, maxOutputChars: 65536 });
49
90
 
50
91
  if (result.exitCode !== 0 && result.exitCode !== 1) throw new Error(result.stderr.trim() || "reference search failed");
51
- const records = result.stdout.split("\n");
52
-
53
- for (let i = 0; i < records.length; i++) {
54
- signal?.throwIfAborted();
55
-
56
- if (!records[i]) continue;
57
- let record;
58
-
59
- try { record = JSON.parse(records[i]); }
60
- catch (error) { if (result.outputTruncated && i === records.length - 1) break; throw error; }
92
+ ingestRgMatches(result.stdout.split("\n"), result, signal, root, overlayText, add);
93
+ ingestPendingRefs(root, pendingPaths, overlayText, add);
61
94
 
62
- if (record.type !== "match" || !isString(record.data?.path?.text) || !isString(record.data.lines?.text)) continue;
63
- const file = path.resolve(root, record.data.path.text);
95
+ return { references, incomplete: result.outputTruncated === true };
96
+ }
64
97
 
65
- if (overlayText(file) === undefined) add(file, record.data.line_number, record.data.lines.text);
66
- }
98
+ function grepCaseSensitive(pattern, params) {
99
+ return params?.caseSensitive === true || (params?.caseSensitive !== false && smartCase(pattern));
100
+ }
67
101
 
68
- for (const file of pendingInScope(root, pendingPaths)) {
69
- const text = overlayText(file);
102
+ function pushGrepFlags(args, pattern, params) {
103
+ if (!grepCaseSensitive(pattern, params)) args.push("--ignore-case");
70
104
 
71
- if (text !== undefined && Buffer.byteLength(text, "utf8") <= 512 * 1024) text.split("\n").forEach((line, i) => add(file, i + 1, line));
72
- }
105
+ if (params?.glob) args.push("--glob", String(params.glob));
106
+ const limit = params?.limit;
73
107
 
74
- return { references, incomplete: result.outputTruncated === true };
108
+ if (Number.isInteger(limit) && limit > 0) args.push("--max-count", String(Math.min(limit, 2000)));
75
109
  }
76
110
 
77
111
  export function rgGrepArgs(pattern, params, searchPath) {
78
112
  const args = ["--line-number", "--no-heading", "--color", "never"];
79
- const caseSensitive = params?.caseSensitive === true || (params?.caseSensitive !== false && smartCase(pattern));
80
-
81
- if (!caseSensitive) args.push("--ignore-case");
82
-
83
- if (params?.glob) args.push("--glob", String(params.glob));
84
- if (Number.isInteger(params?.limit) && params.limit > 0) args.push("--max-count", String(Math.min(params.limit, 2000)));
113
+ pushGrepFlags(args, pattern, params);
85
114
  args.push("--", pattern, searchPath);
86
115
 
87
116
  return args;
88
117
  }
89
118
 
90
- /** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
91
- export async function listWithTools(searchDir, pattern, cwd, signal, pendingPaths = []) {
92
- const stat = await fs.stat(searchDir).catch(() => null);
93
- const pendingAbs = pendingInScope(searchDir, pendingPaths);
94
- const pending = pendingAbs.map(file => relativeSlash(cwd, file));
119
+ function globMatcher(pattern) {
120
+ if (!pattern) return null;
95
121
 
96
- let matcher = null;
122
+ try { return globToRegExp(pattern); }
123
+ catch { return /^$/; }
124
+ }
97
125
 
98
- if (pattern) {
99
- try { matcher = globToRegExp(pattern); }
100
- catch { matcher = /^$/; }
101
- }
126
+ function isDirectList(stat, pendingLength) {
127
+ return !!(stat?.isFile() || (!stat?.isDirectory() && pendingLength));
128
+ }
102
129
 
103
- if (stat?.isFile() || (!stat?.isDirectory() && pending.length)) {
104
- const rel = stat?.isFile() ? relativeSlash(cwd, searchDir) : null;
105
- const rows = [...new Set([...(rel ? [rel] : []), ...pending])].filter(file => !matcher || matcher.test(file));
130
+ function listDirectRows(stat, searchDir, cwd, pending, matcher) {
131
+ const rel = stat?.isFile() ? relativeSlash(cwd, searchDir) : null;
106
132
 
107
- return textResult(rows.length ? rows.join("\n") + "\n" : "", { via: pending.length ? "vfs" : "file" });
108
- }
133
+ return [...new Set([...(rel ? [rel] : []), ...pending])].filter(file => !matcher || matcher.test(file));
134
+ }
109
135
 
110
- const pendingMerged = pendingAbs.filter((_, i) => !matcher || matcher.test(pending[i]));
111
- const mergePending = stdout => {
112
- const diskRows = String(stdout || "").split("\n").filter(Boolean)
113
- .map(row => relativeSlash(cwd, path.isAbsolute(row) ? row : path.resolve(cwd, row)));
114
- const rows = [...new Set([...diskRows, ...pendingMerged])];
136
+ function mergeListStdout(stdout, cwd, pendingMerged) {
137
+ const diskRows = String(stdout || "").split("\n").filter(Boolean)
138
+ .map(row => relativeSlash(cwd, path.isAbsolute(row) ? row : path.resolve(cwd, row)));
139
+ const rows = [...new Set([...diskRows, ...pendingMerged])];
115
140
 
116
- return rows.length ? rows.join("\n") + "\n" : "";
117
- };
141
+ return rows.length ? rows.join("\n") + "\n" : "";
142
+ }
143
+
144
+ async function listDisk(searchDir, pattern, cwd, signal) {
118
145
  const args = ["--files"];
119
146
 
120
147
  if (pattern) args.push("-g", pattern);
121
148
  const res = await runCommand(["rg", ...args, searchDir], { cwd, timeoutMs: 30_000, signal }).catch(() => null);
122
149
 
123
- if (res && (res.exitCode === 0 || res.exitCode === 1)) return textResult(mergePending(res.stdout), { via: "rg", outputTruncated: res.outputTruncated === true });
150
+ if (res && (res.exitCode === 0 || res.exitCode === 1)) return { stdout: res.stdout, via: "rg", outputTruncated: res.outputTruncated === true };
124
151
  const findArgs = [searchDir];
125
152
 
126
153
  if (pattern) findArgs.push("-name", pattern);
127
154
  const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
128
155
 
129
- return textResult(mergePending(findRes.stdout), { via: "find", outputTruncated: findRes.outputTruncated === true });
156
+ return { stdout: findRes.stdout, via: "find", outputTruncated: findRes.outputTruncated === true };
157
+ }
158
+
159
+ /** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
160
+ export async function listWithTools(searchDir, pattern, cwd, signal, pendingPaths = []) {
161
+ const stat = await fs.stat(searchDir).catch(() => null);
162
+ const pendingAbs = pendingInScope(searchDir, pendingPaths);
163
+ const pending = pendingAbs.map(file => relativeSlash(cwd, file));
164
+ const matcher = globMatcher(pattern);
165
+
166
+ if (isDirectList(stat, pending.length)) {
167
+ const rows = listDirectRows(stat, searchDir, cwd, pending, matcher);
168
+
169
+ return textResult(rows.length ? rows.join("\n") + "\n" : "", { via: pending.length ? "vfs" : "file" });
170
+ }
171
+
172
+ const pendingMerged = pendingAbs.filter((_, i) => !matcher || matcher.test(pending[i]));
173
+ const listed = await listDisk(searchDir, pattern, cwd, signal);
174
+
175
+ return textResult(mergeListStdout(listed.stdout, cwd, pendingMerged), { via: listed.via, outputTruncated: listed.outputTruncated });
130
176
  }
131
177
 
132
178
  const GLOB_CHARS = /[*?[\]{}]/;
@@ -153,6 +199,23 @@ export async function fuzzyFind(index, root, cwd, pattern, limit = 20, pendingPa
153
199
  return rows.map((r) => r.path).join("\n") + "\n";
154
200
  }
155
201
 
202
+ function applyGlob(files, params, cwd) {
203
+ if (!params?.glob) return files;
204
+ const matcher = globToRegExp(String(params.glob));
205
+
206
+ return files.filter((f) => matcher.test(relativeSlash(cwd, f)));
207
+ }
208
+
209
+ function filesReadable(index, files, overlayText) {
210
+ for (const file of files) {
211
+ const overlay = overlayText(file);
212
+
213
+ if (overlay === undefined && index.entry(file) === null) return false;
214
+ }
215
+
216
+ return true;
217
+ }
218
+
156
219
  /** fff-style grep: smart-case, definition lines first, fuzzy fallback when the literal has no hits. */
157
220
  export async function grepIndexed(index, pattern, params, searchPath, cwd, overlayText = () => undefined, pendingPaths = []) {
158
221
  const compiled = grepRegex(pattern, params);
@@ -162,17 +225,9 @@ export async function grepIndexed(index, pattern, params, searchPath, cwd, overl
162
225
  let files = [...new Set([...await candidateFileList(index, searchPath), ...pendingInScope(searchPath, pendingPaths)])];
163
226
 
164
227
  if (!index.canScan(files)) return null;
228
+ files = applyGlob(files, params, cwd);
165
229
 
166
- if (params?.glob) {
167
- const matcher = globToRegExp(String(params.glob));
168
- files = files.filter((f) => matcher.test(relativeSlash(cwd, f)));
169
- }
170
-
171
- for (const file of files) {
172
- const overlay = overlayText(file);
173
-
174
- if (overlay === undefined && index.entry(file) === null) return null;
175
- }
230
+ if (!filesReadable(index, files, overlayText)) return null;
176
231
  const rows = index.grepRows(files, regex, cwd, overlayText);
177
232
  const fallback = rows.length === 0 && /^[\w$.-]{4,}$/.test(pattern) ? fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText) : rows;
178
233
 
@@ -193,35 +248,45 @@ function grepRegex(pattern, params) {
193
248
  }
194
249
  }
195
250
 
251
+ function overlaySearchEntry(index, filePath, overlayText) {
252
+ const pending = overlayText(filePath);
253
+
254
+ return pending === undefined
255
+ ? index.entry(filePath)
256
+ : Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(filePath, pending) : null;
257
+ }
258
+
259
+ function fuzzyLineRow(pattern, rawLine, defName, rel, line, maxTypos, caseSensitive) {
260
+ const m = fuzzyMatch(pattern, rawLine, { maxTypos, caseSensitive });
261
+
262
+ if (!m || m.end - m.start > pattern.length + 2) return null;
263
+
264
+ return { rel, line, text: rawLine, def: defName !== "" && fuzzyMatch(pattern, defName, { maxTypos }) !== null };
265
+ }
266
+
196
267
  /** Zero literal hits: retry each line fuzzily (1 typo, 2 for long names) within a tight span, so IsOffTheRecord finds is_off_the_record. */
197
268
  function fuzzyGrepRows(index, files, pattern, cwd, caseSensitive, overlayText = () => undefined) {
198
269
  const maxTypos = pattern.length >= 8 ? 2 : 1;
199
270
  const rows = [];
200
271
 
201
272
  for (const filePath of files) {
202
- const pending = overlayText(filePath);
203
- const e = pending === undefined
204
- ? index.entry(filePath)
205
- : Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(filePath, pending) : null;
273
+ const e = overlaySearchEntry(index, filePath, overlayText);
206
274
 
207
275
  if (!e) continue;
208
276
  const { raw, defNames } = WorkspaceIndex.linesOf(e);
209
277
  const rel = relativeSlash(cwd, filePath);
210
278
 
211
279
  for (let i = 0; i < raw.length && rows.length <= 400; i++) {
212
- const m = fuzzyMatch(pattern, raw[i], { maxTypos, caseSensitive });
280
+ const row = fuzzyLineRow(pattern, raw[i], defNames[i], rel, i + 1, maxTypos, caseSensitive);
213
281
 
214
- if (!m || m.end - m.start > pattern.length + 2) continue;
215
- rows.push({ rel, line: i + 1, text: raw[i], def: defNames[i] !== "" && fuzzyMatch(pattern, defNames[i], { maxTypos }) !== null });
282
+ if (row) rows.push(row);
216
283
  }
217
284
  }
218
285
 
219
286
  return rows;
220
287
  }
221
288
 
222
- /** fff definition-first hinting: files that declare the name come first, declarations first within a file; one header per file. */
223
- function formatGrepRows(rows, limit) {
224
- if (rows.length === 0) return "";
289
+ function groupGrepRows(rows) {
225
290
  const groups = new Map();
226
291
 
227
292
  for (const r of rows) {
@@ -229,19 +294,34 @@ function formatGrepRows(rows, limit) {
229
294
  groups.get(r.rel).push(r);
230
295
  }
231
296
 
232
- const files = [...groups.values()].sort((a, b) => Number(b.some((r) => r.def)) - Number(a.some((r) => r.def)));
297
+ return [...groups.values()].sort((a, b) => Number(b.some((r) => r.def)) - Number(a.some((r) => r.def)));
298
+ }
299
+
300
+ function formatGroup(group, limit, shown) {
301
+ let out = group[0].rel + "\n";
302
+ group.sort((a, b) => Number(b.def) - Number(a.def) || a.line - b.line);
303
+ let n = shown;
304
+
305
+ for (const r of group) {
306
+ if (n++ >= limit) break;
307
+ out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
308
+ }
309
+
310
+ return { out, shown: n };
311
+ }
312
+
313
+ /** fff definition-first hinting: files that declare the name come first, declarations first within a file; one header per file. */
314
+ function formatGrepRows(rows, limit) {
315
+ if (rows.length === 0) return "";
316
+ const files = groupGrepRows(rows);
233
317
  let out = "";
234
318
  let shown = 0;
235
319
 
236
320
  for (const group of files) {
237
321
  if (shown >= limit) break;
238
- out += group[0].rel + "\n";
239
- group.sort((a, b) => Number(b.def) - Number(a.def) || a.line - b.line);
240
-
241
- for (const r of group) {
242
- if (shown++ >= limit) break;
243
- out += " " + r.line + (r.def ? "*" : ":") + " " + r.text.trim() + "\n";
244
- }
322
+ const next = formatGroup(group, limit, shown);
323
+ out += next.out;
324
+ shown = next.shown;
245
325
  }
246
326
 
247
327
  if (rows.length > limit) out += "… " + (rows.length - limit) + " more matches (pass limit or narrow the pattern)\n";