pi-supernova 0.5.0 → 0.7.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 (50) hide show
  1. package/README.md +97 -11
  2. package/docs/CHANGELOG.md +150 -0
  3. package/docs/TOKEN_COSTS.md +71 -29
  4. package/index.js +126 -82
  5. package/package.json +2 -2
  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 +30 -220
  15. package/src/bridge/host-bridge.js +142 -1032
  16. package/src/bridge/invoke.js +35 -0
  17. package/src/bridge/native-tools.js +1 -188
  18. package/src/context/evidence.js +142 -70
  19. package/src/context/fuzzy.js +61 -22
  20. package/src/context/ledger.js +43 -24
  21. package/src/context/outline.js +26 -12
  22. package/src/context/repo-index.js +242 -71
  23. package/src/context/search.js +189 -56
  24. package/src/context/snap.js +306 -150
  25. package/src/context/spans.js +2 -1
  26. package/src/context/surface.js +29 -14
  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 +19 -7
  31. package/src/fs/diff.js +18 -7
  32. package/src/fs/json-read.js +66 -35
  33. package/src/fs/patch.js +97 -51
  34. package/src/fs/source-window.js +82 -0
  35. package/src/fs/text-ops.js +512 -0
  36. package/src/fs/vfs.js +289 -162
  37. package/src/fs/workspace.js +122 -105
  38. package/src/output/bottleneck.js +211 -107
  39. package/src/output/format.js +112 -63
  40. package/src/runtime/guest-deny-imports.js +34 -0
  41. package/src/runtime/guest-worker.js +306 -213
  42. package/src/runtime/parallel.js +99 -63
  43. package/src/runtime/program-batch.js +189 -69
  44. package/src/runtime/program-file.js +6 -3
  45. package/src/runtime/reference.js +13 -12
  46. package/src/runtime/runtime.js +327 -176
  47. package/src/shared/decode.js +61 -27
  48. package/src/ui/omp-frame.js +70 -46
  49. package/src/ui/render-measure.js +51 -29
  50. package/src/ui/render.js +242 -146
package/src/fs/patch.js CHANGED
@@ -9,6 +9,36 @@ function parseHunkHeader(line) {
9
9
  newStart: Number(match[3]), newLength: match[4] === undefined ? 1 : Number(match[4]), lines: [], noNewline: [] };
10
10
  }
11
11
 
12
+ function applyNoNewlineMarker(current) {
13
+ if (!current.lines.length) throw new Error("newline marker requires a preceding hunk line");
14
+ current.noNewline.push(current.lines.length - 1);
15
+ }
16
+
17
+ function pushHunkLine(current, line, oldCount, newCount) {
18
+ if (oldCount === current.oldLength && newCount === current.newLength && /^--- |^\+\+\+ /.test(line)) {
19
+ throw new Error("apply_patch accepts one file at a time");
20
+ }
21
+
22
+ current.lines.push(line);
23
+
24
+ return {
25
+ oldCount: line[0] !== "+" ? oldCount + 1 : oldCount,
26
+ newCount: line[0] !== "-" ? newCount + 1 : newCount,
27
+ };
28
+ }
29
+
30
+ function consumeHunkLine(current, line, oldCount, newCount) {
31
+ if (line.startsWith("\")) {
32
+ applyNoNewlineMarker(current);
33
+
34
+ return { oldCount, newCount };
35
+ }
36
+
37
+ if (!/^[+ -]/.test(line)) return { oldCount, newCount };
38
+
39
+ return pushHunkLine(current, line, oldCount, newCount);
40
+ }
41
+
12
42
  export function parsePatchHunks(patchText) {
13
43
  const hunks = [];
14
44
  let current;
@@ -23,19 +53,8 @@ export function parsePatchHunks(patchText) {
23
53
  hunks.push(current);
24
54
  oldCount = 0;
25
55
  newCount = 0;
26
- } else if (current && line.startsWith("\")) {
27
- if (!current.lines.length) throw new Error("newline marker requires a preceding hunk line");
28
- current.noNewline.push(current.lines.length - 1);
29
- } else if (current && /^[+ -]/.test(line)) {
30
- if (oldCount === current.oldLength && newCount === current.newLength && /^--- |^\+\+\+ /.test(line)) {
31
- throw new Error("apply_patch accepts one file at a time");
32
- }
33
-
34
- current.lines.push(line);
35
-
36
- if (line[0] !== "+") oldCount++;
37
-
38
- if (line[0] !== "-") newCount++;
56
+ } else if (current) {
57
+ ({ oldCount, newCount } = consumeHunkLine(current, line, oldCount, newCount));
39
58
  }
40
59
  }
41
60
 
@@ -59,21 +78,73 @@ function splitFile(text) {
59
78
  });
60
79
  }
61
80
 
62
- function findHunkMatch(fileLines, expectedOld, nominal) {
81
+ function findHunkMatch(fileLines, expectedOld, nominal, hunk, oldStart) {
63
82
  const matchAt = index => index >= 0 && index + expectedOld.length <= fileLines.length
64
83
  && expectedOld.every((line, i) => fileLines[index + i].text === line);
65
84
 
66
- if (matchAt(nominal)) return nominal;
85
+ if (matchAt(nominal)) return { index: nominal, relocated: 0 };
86
+
87
+ if (!expectedOld.length) return { index: -1, relocated: 0 };
88
+
89
+ const maxDrift = Math.min(Math.max(fileLines.length, 100), 200);
90
+ const hits = [];
91
+
92
+ for (let delta = 1; delta <= maxDrift; delta++) {
93
+ if (matchAt(nominal + delta)) hits.push(nominal + delta);
94
+
95
+ if (matchAt(nominal - delta)) hits.push(nominal - delta);
96
+ }
97
+
98
+ if (hits.length > 1) throw new Error("patch hunk " + hunk + " near line " + oldStart + " matches " + hits.length + " locations; add context lines to disambiguate");
99
+
100
+ if (hits.length === 1) return { index: hits[0], relocated: hits[0] - nominal };
101
+
102
+ return { index: -1, relocated: 0 };
103
+ }
104
+
105
+ function hunkLineText(line) {
106
+ return line.slice(1).replace(/\r$/, "");
107
+ }
67
108
 
68
- if (!expectedOld.length) return -1;
109
+ function applyHunkLines(hunk, fileLines, matchIndex, ending) {
110
+ const replacement = [];
111
+ let oldIndex = matchIndex;
69
112
 
70
- for (let delta = 1; delta <= Math.max(fileLines.length, 100); delta++) {
71
- if (matchAt(nominal + delta)) return nominal + delta;
113
+ for (let i = 0; i < hunk.lines.length; i++) {
114
+ const line = hunk.lines[i];
72
115
 
73
- if (matchAt(nominal - delta)) return nominal - delta;
116
+ if (line[0] === "+") {
117
+ replacement.push({ text: hunkLineText(line), ending: hunk.noNewline.includes(i) ? "" : ending });
118
+ } else {
119
+ const original = fileLines[oldIndex++];
120
+
121
+ if (hunk.noNewline.includes(i) && original.ending) throw new Error("patch newline marker does not match the file");
122
+
123
+ if (line[0] === " ") replacement.push(original);
124
+ }
74
125
  }
75
126
 
76
- return -1;
127
+ return replacement;
128
+ }
129
+
130
+ function applyOneHunk(hunk, h, fileLines, offsetShift, relocationShift, ending) {
131
+ const expectedOld = hunk.lines.filter(line => line[0] !== "+").map(hunkLineText);
132
+ const newCount = hunk.lines.filter(line => line[0] !== "-").length;
133
+
134
+ if (expectedOld.length !== hunk.oldLength || newCount !== hunk.newLength) throw new Error("patch hunk " + (h + 1) + " length does not match its header");
135
+ // The new coordinate also handles BSD diff's -1,0 header at file start.
136
+ const nominal = hunk.oldLength === 0 ? hunk.newStart - 1 + relocationShift : hunk.oldStart - 1 + offsetShift;
137
+ const match = findHunkMatch(fileLines, expectedOld, nominal, h + 1, hunk.oldStart);
138
+
139
+ if (match.index < 0) throw new Error("patch hunk " + (h + 1) + " rejected at line " + hunk.oldStart + ": context did not match");
140
+ const replacement = applyHunkLines(hunk, fileLines, match.index, ending);
141
+ fileLines.splice(match.index, expectedOld.length, ...replacement);
142
+
143
+ return {
144
+ relocationShift: relocationShift + match.relocated,
145
+ offsetShift: offsetShift + match.relocated + replacement.length - expectedOld.length,
146
+ relocated: match.relocated,
147
+ };
77
148
  }
78
149
 
79
150
  export function applyPatchToText(originalText, patchText) {
@@ -81,42 +152,17 @@ export function applyPatchToText(originalText, patchText) {
81
152
  const hunks = parsePatchHunks(patchText);
82
153
  const fileLines = splitFile(originalText);
83
154
  const ending = fileLines.find(line => line.ending)?.ending ?? "\n";
155
+ const relocations = [];
84
156
  let offsetShift = 0;
85
157
  let relocationShift = 0;
86
158
 
87
159
  for (let h = 0; h < hunks.length; h++) {
88
- const hunk = hunks[h];
89
- const textOf = line => line.slice(1).replace(/\r$/, "");
90
- const expectedOld = hunk.lines.filter(line => line[0] !== "+").map(textOf);
91
- const newCount = hunk.lines.filter(line => line[0] !== "-").length;
92
-
93
- if (expectedOld.length !== hunk.oldLength || newCount !== hunk.newLength) throw new Error("patch hunk " + (h + 1) + " length does not match its header");
94
- // The new coordinate also handles BSD diff's -1,0 header at file start.
95
- const nominal = hunk.oldLength === 0 ? hunk.newStart - 1 + relocationShift : hunk.oldStart - 1 + offsetShift;
96
- const matchIndex = findHunkMatch(fileLines, expectedOld, nominal);
97
-
98
- if (matchIndex < 0) throw new Error("patch hunk " + (h + 1) + " rejected at line " + hunk.oldStart + ": context did not match");
99
- const replacement = [];
100
- let oldIndex = matchIndex;
101
-
102
- for (let i = 0; i < hunk.lines.length; i++) {
103
- const line = hunk.lines[i];
104
-
105
- if (line[0] === "+") {
106
- replacement.push({ text: textOf(line), ending: hunk.noNewline.includes(i) ? "" : line.endsWith("\r") ? "\r\n" : ending });
107
- } else {
108
- const original = fileLines[oldIndex++];
109
-
110
- if (hunk.noNewline.includes(i) && original.ending) throw new Error("patch newline marker does not match the file");
111
-
112
- if (line[0] === " ") replacement.push(original);
113
- }
114
- }
160
+ const applied = applyOneHunk(hunks[h], h, fileLines, offsetShift, relocationShift, ending);
161
+ offsetShift = applied.offsetShift;
162
+ relocationShift = applied.relocationShift;
115
163
 
116
- fileLines.splice(matchIndex, expectedOld.length, ...replacement);
117
- relocationShift += matchIndex - nominal;
118
- offsetShift += matchIndex - nominal + replacement.length - expectedOld.length;
164
+ if (applied.relocated !== 0) relocations.push({ hunk: h + 1, offset: applied.relocated });
119
165
  }
120
166
 
121
- return { resultText: fileLines.map(line => line.text + line.ending).join(""), hunkCount: hunks.length };
167
+ return { resultText: fileLines.map(line => line.text + line.ending).join(""), hunkCount: hunks.length, relocations };
122
168
  }
@@ -0,0 +1,82 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import { relativeSlash } from "./workspace.js";
4
+
5
+ export const SOURCE_REF = /((?:\/|[A-Za-z]:[\\/])?(?:[\w.@-]+[\\/])*[\w.@-]+\.(?:m?[jt]sx?|c[jt]s|py|rs|go|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|json|ya?ml|toml))(?::|\()(\d+)/g;
6
+
7
+ function prefix(p) {
8
+ return p.endsWith(path.sep) ? p : p + path.sep;
9
+ }
10
+
11
+ async function readBoundedFile(real, rootPrefix, signal) {
12
+ const handle = await fs.open(real, fs.constants.O_RDONLY | (fs.constants.O_NONBLOCK ?? 0));
13
+
14
+ try {
15
+ const stat = await handle.stat();
16
+
17
+ if (!real.startsWith(rootPrefix) || !stat.isFile() || stat.size > 1024 * 1024) return null;
18
+ const buffer = Buffer.alloc(Math.min(stat.size, 1024 * 1024));
19
+ let offset = 0;
20
+
21
+ while (offset < buffer.length) {
22
+ const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
23
+
24
+ if (bytesRead <= 0) break;
25
+ offset += bytesRead;
26
+ signal?.throwIfAborted();
27
+ }
28
+
29
+ return buffer.subarray(0, offset).toString("utf8");
30
+ } finally { await handle.close(); }
31
+ }
32
+
33
+ function windowRows(text, lineNo, ledger, rel) {
34
+ const raw = text.split("\n");
35
+
36
+ if (lineNo < 1 || lineNo > raw.length) return null;
37
+ const start = Math.max(1, lineNo - 2);
38
+ const rows = [];
39
+
40
+ for (let l = start; l <= Math.min(raw.length, lineNo + 2); l++) rows.push((l === lineNo ? "►" : " ") + String(l).padStart(4) + " " + raw[l - 1]);
41
+ ledger.recordOrigin(rel, start, rows);
42
+
43
+ return rel + ":" + lineNo + "\n" + rows.join("\n");
44
+ }
45
+
46
+ /** Fresh bounded source window for a diagnostic; no index warmup or stale cached bodies. */
47
+ export async function sourceWindow(cwd, commandCwd, file, lineNo, signal, ledger) {
48
+ const candidate = path.resolve(commandCwd, file);
49
+
50
+ try {
51
+ const root = await fs.realpath(cwd);
52
+ const cwdPrefix = prefix(path.resolve(cwd));
53
+ const rootPrefix = prefix(root);
54
+
55
+ if (!candidate.startsWith(cwdPrefix) && !candidate.startsWith(rootPrefix)) return null;
56
+ const real = await fs.realpath(candidate);
57
+ const text = await readBoundedFile(real, rootPrefix, signal);
58
+
59
+ if (text === null) return null;
60
+
61
+ return windowRows(text, lineNo, ledger, relativeSlash(root, real));
62
+ } catch { return null; }
63
+ }
64
+
65
+ /** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
66
+ export async function sourceForReferences(cwd, commandCwd, output, signal, ledger) {
67
+ const seen = new Set();
68
+ const blocks = [];
69
+
70
+ for (const m of output.matchAll(SOURCE_REF)) {
71
+ const key = m[1] + ":" + m[2];
72
+
73
+ if (seen.has(key)) continue;
74
+ if (seen.size >= 4) break;
75
+ seen.add(key);
76
+ const block = await sourceWindow(cwd, commandCwd, m[1], Number(m[2]), signal, ledger);
77
+
78
+ if (block) blocks.push(block);
79
+ }
80
+
81
+ return blocks.length ? "\n--- source\n" + blocks.join("\n") : "";
82
+ }