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.
- package/README.md +97 -11
- package/docs/CHANGELOG.md +150 -0
- package/docs/TOKEN_COSTS.md +71 -29
- package/index.js +126 -82
- package/package.json +2 -2
- package/src/adapters/bash.js +73 -0
- package/src/adapters/edit.js +249 -0
- package/src/adapters/errors.js +31 -0
- package/src/adapters/index.js +31 -0
- package/src/adapters/list.js +102 -0
- package/src/adapters/read.js +805 -0
- package/src/adapters/refs.js +41 -0
- package/src/adapters/write.js +96 -0
- package/src/bridge/catalog.js +30 -220
- package/src/bridge/host-bridge.js +142 -1032
- package/src/bridge/invoke.js +35 -0
- package/src/bridge/native-tools.js +1 -188
- package/src/context/evidence.js +142 -70
- package/src/context/fuzzy.js +61 -22
- package/src/context/ledger.js +43 -24
- package/src/context/outline.js +26 -12
- package/src/context/repo-index.js +242 -71
- package/src/context/search.js +189 -56
- package/src/context/snap.js +306 -150
- package/src/context/spans.js +2 -1
- package/src/context/surface.js +29 -14
- package/src/contract/bash.js +31 -0
- package/src/contract/edit.js +95 -0
- package/src/contract/read.js +220 -0
- package/src/fs/check.js +19 -7
- package/src/fs/diff.js +18 -7
- package/src/fs/json-read.js +66 -35
- package/src/fs/patch.js +97 -51
- package/src/fs/source-window.js +82 -0
- package/src/fs/text-ops.js +512 -0
- package/src/fs/vfs.js +289 -162
- package/src/fs/workspace.js +122 -105
- package/src/output/bottleneck.js +211 -107
- package/src/output/format.js +112 -63
- package/src/runtime/guest-deny-imports.js +34 -0
- package/src/runtime/guest-worker.js +306 -213
- package/src/runtime/parallel.js +99 -63
- package/src/runtime/program-batch.js +189 -69
- package/src/runtime/program-file.js +6 -3
- package/src/runtime/reference.js +13 -12
- package/src/runtime/runtime.js +327 -176
- package/src/shared/decode.js +61 -27
- package/src/ui/omp-frame.js +70 -46
- package/src/ui/render-measure.js +51 -29
- 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
|
|
27
|
-
|
|
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
|
-
|
|
109
|
+
function applyHunkLines(hunk, fileLines, matchIndex, ending) {
|
|
110
|
+
const replacement = [];
|
|
111
|
+
let oldIndex = matchIndex;
|
|
69
112
|
|
|
70
|
-
for (let
|
|
71
|
-
|
|
113
|
+
for (let i = 0; i < hunk.lines.length; i++) {
|
|
114
|
+
const line = hunk.lines[i];
|
|
72
115
|
|
|
73
|
-
if (
|
|
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
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
+
}
|