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/context/surface.js
CHANGED
|
@@ -8,14 +8,20 @@ function scanPython(lines) {
|
|
|
8
8
|
const line = lines[i];
|
|
9
9
|
const match = /^([ \t]*)(def|class|async def)\s+([a-zA-Z0-9_]+)(\(.*?\))?:?/.exec(line);
|
|
10
10
|
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
if (match) {
|
|
12
|
+
items.push({
|
|
13
|
+
kind: match[2].includes("def") ? "function" : "class",
|
|
14
|
+
name: match[3],
|
|
15
|
+
signature: match[0].trim(),
|
|
16
|
+
line: i + 1,
|
|
17
|
+
depth: Math.floor(match[1].length / 4),
|
|
18
|
+
});
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const constant = /^([ \t]*)([A-Z][A-Z0-9_]*)\s*(?::[^=\n]+)?=/.exec(line);
|
|
23
|
+
|
|
24
|
+
if (constant && constant[1].length === 0) items.push({ kind: "constant", name: constant[2], signature: constant[0].replace(/=\s*$/, "=").trim(), line: i + 1, depth: 0 });
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
return items;
|
|
@@ -110,15 +116,24 @@ function declarationItem(line, rawLine, lineNumber) {
|
|
|
110
116
|
return null;
|
|
111
117
|
}
|
|
112
118
|
|
|
119
|
+
function skipJsNoise(line) {
|
|
120
|
+
return !line || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function jsItemAt(lines, i) {
|
|
124
|
+
const line = lines[i].trim();
|
|
125
|
+
|
|
126
|
+
if (skipJsNoise(line)) return null;
|
|
127
|
+
const indent = lines[i].length - lines[i].trimStart().length;
|
|
128
|
+
|
|
129
|
+
return declarationItem(line, lines[i], i + 1) || (indent > 0 && indent <= 8 ? methodItem(line, i + 1, Math.max(1, Math.floor(indent / 2))) : null);
|
|
130
|
+
}
|
|
131
|
+
|
|
113
132
|
function scanJavaScript(lines) {
|
|
114
133
|
const items = [];
|
|
115
134
|
|
|
116
135
|
for (let i = 0; i < lines.length; i++) {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
if (!line || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) continue;
|
|
120
|
-
const indent = lines[i].length - lines[i].trimStart().length;
|
|
121
|
-
const item = declarationItem(line, lines[i], i + 1) || (indent > 0 && indent <= 8 ? methodItem(line, i + 1, Math.max(1, Math.floor(indent / 2))) : null);
|
|
136
|
+
const item = jsItemAt(lines, i);
|
|
122
137
|
|
|
123
138
|
if (item) items.push(item);
|
|
124
139
|
}
|
|
@@ -141,7 +156,7 @@ const SCANNERS = {
|
|
|
141
156
|
export function extractStructuralSurface(code, extension = "js") {
|
|
142
157
|
if (!isString(code) || !code.trim()) return { items: [], lineCount: 0 };
|
|
143
158
|
const lines = code.split("\n");
|
|
144
|
-
const ext = extension.replace(/^\./, "").toLowerCase();
|
|
159
|
+
const ext = String(extension ?? "").replace(/^\./, "").toLowerCase();
|
|
145
160
|
const scanner = SCANNERS[ext] || SCANNERS.js;
|
|
146
161
|
const items = scanner(lines);
|
|
147
162
|
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { isString, isObject } from "../shared/decode.js";
|
|
2
|
+
|
|
3
|
+
const ARGV_ERROR = "bash argv requires a command string and an array of string args";
|
|
4
|
+
|
|
5
|
+
function quoteShellArg(value) {
|
|
6
|
+
return "'" + String(value).replaceAll("'", "'\\''") + "'";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Normalize guest bash(command, opts) / bash({command, args}) into host args. */
|
|
10
|
+
function normalizeArgv(args) {
|
|
11
|
+
if (args.args === undefined) return;
|
|
12
|
+
if (!isString(args.command) || !Array.isArray(args.args)) throw new Error(ARGV_ERROR);
|
|
13
|
+
|
|
14
|
+
for (let i = 0; i < args.args.length; i++) if (!isString(args.args[i])) throw new Error(ARGV_ERROR);
|
|
15
|
+
args.args = args.args.map(String);
|
|
16
|
+
|
|
17
|
+
if (process.platform === "win32") {
|
|
18
|
+
delete args._directArgv;
|
|
19
|
+
args.command = [args.command, ...args.args].map(quoteShellArg).join(" ");
|
|
20
|
+
delete args.args;
|
|
21
|
+
} else args._directArgv = true;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeBash(command, opts) {
|
|
25
|
+
const args = isObject(command) ? { ...opts, ...command } : { command, ...opts };
|
|
26
|
+
normalizeArgv(args);
|
|
27
|
+
|
|
28
|
+
if (args.timeout !== undefined && args.timeoutMs === undefined) args.timeoutMs = args.timeout * 1000;
|
|
29
|
+
|
|
30
|
+
return args;
|
|
31
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { isString, isObject, isFunction, isNumber } from "../shared/decode.js";
|
|
2
|
+
|
|
3
|
+
export const EDIT_USAGE = 'invalid edit signature; use edit(path,oldText,newText), edit({path,edits:[{oldText,newText}]}), or edit({path,patch:"@@ -1 +1 @@\n-old\n+new\n"})';
|
|
4
|
+
|
|
5
|
+
function spanStart(value) {
|
|
6
|
+
return isNumber(value.start) ? value.start : Array.isArray(value.lines) ? value.lines[0] : value.line;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function spanEnd(value, start) {
|
|
10
|
+
return isNumber(value.end) ? value.end : Array.isArray(value.lines) && value.lines.length > 1 ? value.lines[1] : start;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function viewSpan(value) {
|
|
14
|
+
const start = spanStart(value);
|
|
15
|
+
const end = spanEnd(value, start);
|
|
16
|
+
|
|
17
|
+
if (!isNumber(start) || !isNumber(end) || start < 1 || end < start) return null;
|
|
18
|
+
|
|
19
|
+
return { start: Math.floor(start), end: Math.floor(end) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isEditView(value) {
|
|
23
|
+
return isObject(value) && !Array.isArray(value) && isString(value.path) && value.path.trim() && isString(value.text) && (value.status === undefined || value.status === "found") && viewSpan(value);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function classifyViewEdit(p, oldText, newText) {
|
|
27
|
+
if (isNumber(p.nextOffset)) throw new Error("edit view is incomplete");
|
|
28
|
+
const span = viewSpan(p);
|
|
29
|
+
const args = { path: p.path, viewStart: span.start, viewEnd: span.end, viewText: p.text, newText: newText === undefined ? oldText : newText };
|
|
30
|
+
|
|
31
|
+
if (newText !== undefined) args.oldText = oldText;
|
|
32
|
+
|
|
33
|
+
return { kind: "view", command: "edit", args };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function namedEditObject(p, oldText, newText) {
|
|
37
|
+
if (isObject(p) && (Array.isArray(p) || oldText !== undefined || newText !== undefined)) throw new Error(EDIT_USAGE);
|
|
38
|
+
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function namedEditPositional(p, oldText, newText) {
|
|
43
|
+
if (isObject(oldText) && !Array.isArray(oldText)) throw new Error(EDIT_USAGE);
|
|
44
|
+
|
|
45
|
+
return Array.isArray(oldText) ? { path: p, edits: oldText } : { path: p, oldText, newText };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeEditArgs(p, oldText, newText) {
|
|
49
|
+
return isObject(p) ? namedEditObject(p, oldText, newText) : namedEditPositional(p, oldText, newText);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function namedEditArgs(p, oldText, newText) {
|
|
53
|
+
const args = normalizeEditArgs(p, oldText, newText);
|
|
54
|
+
|
|
55
|
+
if (!isString(args.path) || !args.path.trim()) throw new Error(EDIT_USAGE);
|
|
56
|
+
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function assertNamedEditMode(args, oldText, newText) {
|
|
61
|
+
const modes = Number(args.patch !== undefined) + Number(args.edits !== undefined) + Number(args.oldText !== undefined || args.newText !== undefined);
|
|
62
|
+
|
|
63
|
+
if (modes !== 1 || (Array.isArray(oldText) && newText !== undefined)) throw new Error(EDIT_USAGE);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function classifyPatch(args) {
|
|
67
|
+
if (!isString(args.patch) || !args.patch.trim()) throw new Error(EDIT_USAGE);
|
|
68
|
+
|
|
69
|
+
return { kind: "patch", command: "apply_patch", args };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function classifyReplacements(args) {
|
|
73
|
+
const edits = args.edits === undefined ? [args] : args.edits;
|
|
74
|
+
|
|
75
|
+
if (!Array.isArray(edits) || !edits.length) throw new Error(EDIT_USAGE);
|
|
76
|
+
|
|
77
|
+
for (const e of edits) if (!isString(e?.oldText) || !e.oldText.length || !isString(e?.newText)) throw new Error(EDIT_USAGE + "; replacements require non-empty oldText and string newText");
|
|
78
|
+
|
|
79
|
+
return { kind: "edits", command: "edit", args };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function classifyNamedEdit(p, oldText, newText) {
|
|
83
|
+
const args = namedEditArgs(p, oldText, newText);
|
|
84
|
+
assertNamedEditMode(args, oldText, newText);
|
|
85
|
+
|
|
86
|
+
return args.patch !== undefined ? classifyPatch(args) : classifyReplacements(args);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Guest signature → { command, args } for one host call. */
|
|
90
|
+
export function classifyEdit(p, oldText, newText) {
|
|
91
|
+
if (isFunction(p)) return { kind: "checkpoint", fn: p };
|
|
92
|
+
if (isEditView(p) && isString(oldText) && (newText === undefined || isString(newText))) return classifyViewEdit(p, oldText, newText);
|
|
93
|
+
|
|
94
|
+
return classifyNamedEdit(p, oldText, newText);
|
|
95
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { isString, isObject, isNumber, looksLikePath } from "../shared/decode.js";
|
|
2
|
+
import { sessionJsonArgs, validateJsonRead } from "../fs/json-read.js";
|
|
3
|
+
|
|
4
|
+
export const SESSION_URI = /^(?:agent|artifact):\/\//i;
|
|
5
|
+
|
|
6
|
+
const BOOL_KEYS = ["resolve", "complete", "outline", "evidence"];
|
|
7
|
+
|
|
8
|
+
export function isSessionUri(value) {
|
|
9
|
+
return isString(value) && SESSION_URI.test(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Guest call shape → one options object. */
|
|
13
|
+
export function gatherReadArgs(p, a, b) {
|
|
14
|
+
if (isObject(p) && !Array.isArray(p)) {
|
|
15
|
+
const args = { ...p, path: p.path ?? p.target ?? p.query };
|
|
16
|
+
|
|
17
|
+
if (p.path === undefined && p.target !== undefined) delete args.target;
|
|
18
|
+
|
|
19
|
+
return args;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function assertReadPaths(targetParam) {
|
|
26
|
+
if (!Array.isArray(targetParam)) return;
|
|
27
|
+
if (targetParam.length > 64) throw new Error("read accepts at most 64 paths per batch");
|
|
28
|
+
|
|
29
|
+
for (const item of targetParam) if (!isString(item) || !item.trim()) throw new Error("read paths must be non-empty strings");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function assertReadFlags(args) {
|
|
33
|
+
for (const key of BOOL_KEYS) {
|
|
34
|
+
if (args[key] !== undefined && args[key] !== true && args[key] !== false) throw new Error("read " + key + " must be a boolean");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (args.about !== undefined && !isString(args.about)) throw new Error("read about must be a string");
|
|
38
|
+
if (args.query !== undefined && !isString(args.query)) throw new Error("read query must be a string");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertExclusiveRead(args) {
|
|
42
|
+
const focusModes = [args.about !== undefined, args.query !== undefined, args.outline === true].filter(Boolean).length;
|
|
43
|
+
|
|
44
|
+
if (focusModes > 1 || (args.outline === true && args.evidence === true)) throw new Error("read accepts only one of about, query, outline, or evidence");
|
|
45
|
+
if (args.resolve === true && args.complete === true) throw new Error("read accepts either resolve or complete, not both");
|
|
46
|
+
if ((focusModes === 1 || args.evidence === true) && args.complete === true) throw new Error("complete:true requires a raw file read, not a source view");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function autoResolve(args) {
|
|
50
|
+
if (!isString(args.path) || args.resolve !== undefined || args.complete === true || args.json !== undefined) return args;
|
|
51
|
+
if (args.about !== undefined || args.query !== undefined || looksLikePath(args.path) || isSessionUri(args.path)) return args;
|
|
52
|
+
|
|
53
|
+
return { ...args, resolve: true };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Exclusive-mode + JSON + auto-resolve. Same rules for guest and host. */
|
|
57
|
+
export function normalizeRead(params) {
|
|
58
|
+
if (!isObject(params)) throw new Error("read requires an options object");
|
|
59
|
+
if (params.path !== undefined && params.target !== undefined && params.path !== params.target) {
|
|
60
|
+
throw new Error("read accepts either path or target, not both");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const args = sessionJsonArgs({ ...params, path: params.path ?? params.target });
|
|
64
|
+
validateJsonRead(args);
|
|
65
|
+
assertReadFlags(args);
|
|
66
|
+
assertExclusiveRead(args);
|
|
67
|
+
assertReadPaths(args.path);
|
|
68
|
+
|
|
69
|
+
return autoResolve(args);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function needsProbe(params) {
|
|
73
|
+
if (isSessionUri(params.path)) return false;
|
|
74
|
+
if (params.evidence === true) return false;
|
|
75
|
+
if (isString(params.query)) return false;
|
|
76
|
+
if (params.outline === true) return false;
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Kind of read after exclusive modes are already validated.
|
|
83
|
+
* `existing` is the probe result, or null/undefined when needsProbe is false or the path is missing.
|
|
84
|
+
*/
|
|
85
|
+
function classifyExisting(params, existing) {
|
|
86
|
+
if (existing.directory) {
|
|
87
|
+
if (params.json !== undefined) throw new Error("JSON read requires a file, not a directory");
|
|
88
|
+
|
|
89
|
+
return isString(params.about)
|
|
90
|
+
? { kind: "snap", query: params.about, scoped: true, existing }
|
|
91
|
+
: { kind: "dir", existing };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (params.resolve === true && isString(params.about)) {
|
|
95
|
+
throw new Error("resolve:true cannot combine with about on a file; use about for a focused outline or resolve for source text");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isString(params.about) && existing.size > 512 * 1024) return { kind: "focus", existing, about: params.about };
|
|
99
|
+
|
|
100
|
+
return { kind: params.resolve ? "open" : "file", existing };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function classifySession(params) {
|
|
104
|
+
if (params.about !== undefined || params.query !== undefined || params.outline === true || params.evidence === true) {
|
|
105
|
+
throw new Error("session resources do not support about/query/outline/evidence views");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { kind: "session" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function classifyEvidence(params, target) {
|
|
112
|
+
const query = params.about ?? params.query ?? target;
|
|
113
|
+
const scope = target !== query || looksLikePath(target) ? target : undefined;
|
|
114
|
+
|
|
115
|
+
return { kind: "evidence", query, scope };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function classifyBarePath(params, target) {
|
|
119
|
+
if (params.json === undefined && !looksLikePath(target)) {
|
|
120
|
+
return { kind: "snap", query: isString(params.about) ? params.about : target, scoped: isString(params.about) };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (params.resolve === true && params.complete !== true) return { kind: "missing" };
|
|
124
|
+
|
|
125
|
+
return { kind: "file", existing: null };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function classifyRead(params, existing) {
|
|
129
|
+
const target = params.path;
|
|
130
|
+
|
|
131
|
+
if (isSessionUri(target)) return classifySession(params);
|
|
132
|
+
if (params.evidence === true) return classifyEvidence(params, target);
|
|
133
|
+
if (isString(params.query)) return { kind: "snap", query: params.query, scoped: Boolean(target && target !== params.query) };
|
|
134
|
+
if (params.outline === true) return { kind: "outline" };
|
|
135
|
+
if (existing) return classifyExisting(params, existing);
|
|
136
|
+
|
|
137
|
+
return classifyBarePath(params, target);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function jsonSelectorNote(args) {
|
|
141
|
+
return args.json === undefined ? "" : " (" + (Array.isArray(args.json) ? args.json.join(", ") : String(args.json)) + ")";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const ROUTING_STATUS = "too_large";
|
|
145
|
+
|
|
146
|
+
const ROUTING_PREFIX = '{"status":"too_large",';
|
|
147
|
+
|
|
148
|
+
export const ROUTING_KEYS_MAX = 32;
|
|
149
|
+
|
|
150
|
+
export function isRoutingPayload(value) {
|
|
151
|
+
return isString(value) && value.startsWith(ROUTING_PREFIX);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isRoutingObject(parsed) {
|
|
155
|
+
return isObject(parsed) && parsed.status === ROUTING_STATUS && isString(parsed.path)
|
|
156
|
+
&& isNumber(parsed.chars) && (Array.isArray(parsed.keys) || isNumber(parsed.length));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Shape of an over-bound JSON document for in-band routing. Throws when text is not JSON. */
|
|
160
|
+
export function buildJsonRouting(rel, text) {
|
|
161
|
+
const document = JSON.parse(text);
|
|
162
|
+
const base = { status: ROUTING_STATUS, path: rel, chars: text.length };
|
|
163
|
+
|
|
164
|
+
if (Array.isArray(document)) return { ...base, length: document.length };
|
|
165
|
+
|
|
166
|
+
if (isObject(document)) {
|
|
167
|
+
const keys = Object.keys(document);
|
|
168
|
+
|
|
169
|
+
return keys.length > ROUTING_KEYS_MAX
|
|
170
|
+
? { ...base, keys: keys.slice(0, ROUTING_KEYS_MAX), keysTruncated: true }
|
|
171
|
+
: { ...base, keys };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return base;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function routingText(routing) {
|
|
178
|
+
const text = JSON.stringify(routing);
|
|
179
|
+
|
|
180
|
+
if (!isRoutingPayload(text)) throw new Error("routing payload must start with the shared marker");
|
|
181
|
+
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Shape of one over-budget selection for in-band routing; value is already parsed. */
|
|
186
|
+
export function buildSelectionRouting(rel, selector, value, chars) {
|
|
187
|
+
const base = { status: ROUTING_STATUS, path: rel, selector, chars };
|
|
188
|
+
|
|
189
|
+
if (Array.isArray(value)) return { ...base, length: value.length };
|
|
190
|
+
|
|
191
|
+
if (isObject(value)) {
|
|
192
|
+
const keys = Object.keys(value);
|
|
193
|
+
|
|
194
|
+
return keys.length > ROUTING_KEYS_MAX
|
|
195
|
+
? { ...base, keys: keys.slice(0, ROUTING_KEYS_MAX), keysTruncated: true }
|
|
196
|
+
: { ...base, keys };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return base;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function decodeByArgs(args, value) {
|
|
203
|
+
return (args.resolve || args.json !== undefined || args.outline || args.evidence) && isString(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function decodeReadValue(args, value) {
|
|
207
|
+
const sniffed = isRoutingPayload(value);
|
|
208
|
+
|
|
209
|
+
if (!sniffed && !decodeByArgs(args, value)) return value;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const parsed = JSON.parse(value);
|
|
213
|
+
|
|
214
|
+
return sniffed && !isRoutingObject(parsed) ? value : parsed;
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (sniffed && !decodeByArgs(args, value)) return value;
|
|
217
|
+
|
|
218
|
+
throw new Error("JSON read failed for " + String(args.path ?? args.target ?? "resource") + jsonSelectorNote(args) + ": " + (error instanceof Error ? error.message : String(error)));
|
|
219
|
+
}
|
|
220
|
+
}
|
package/src/fs/check.js
CHANGED
|
@@ -60,7 +60,7 @@ function skipComment(text, i) {
|
|
|
60
60
|
|
|
61
61
|
const end = text.indexOf("*/", i + 2);
|
|
62
62
|
|
|
63
|
-
return end < 0 ?
|
|
63
|
+
return end < 0 ? -1 : end + 2;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function skipRegex(text, i) {
|
|
@@ -118,6 +118,21 @@ function consumeQuoted(text, i, stack) {
|
|
|
118
118
|
return end < 0 ? { error: "unterminated string", at: i } : { end, prev: "value" };
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
function consumeSlash(text, i, prev) {
|
|
122
|
+
if (text[i + 1] === "/" || text[i + 1] === "*") {
|
|
123
|
+
const end = skipComment(text, i);
|
|
124
|
+
|
|
125
|
+
if (end < 0) return { error: "unterminated comment", at: i };
|
|
126
|
+
|
|
127
|
+
return { end, prev };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (prev !== "" && !REGEX_PRECEDERS.has(prev)) return null;
|
|
131
|
+
const end = skipRegex(text, i);
|
|
132
|
+
|
|
133
|
+
return end > 0 ? { end, prev: "value" } : null;
|
|
134
|
+
}
|
|
135
|
+
|
|
121
136
|
/** Try to consume a comment, string, template, or regex at i. Returns { end, prev } | { error, at } | null. */
|
|
122
137
|
function consumeLiteral(text, i, stack, prev) {
|
|
123
138
|
const c = text[i];
|
|
@@ -126,12 +141,7 @@ function consumeLiteral(text, i, stack, prev) {
|
|
|
126
141
|
|
|
127
142
|
if (c !== "/") return null;
|
|
128
143
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (prev !== "" && !REGEX_PRECEDERS.has(prev)) return null;
|
|
132
|
-
const end = skipRegex(text, i);
|
|
133
|
-
|
|
134
|
-
return end > 0 ? { end, prev: "value" } : null;
|
|
144
|
+
return consumeSlash(text, i, prev);
|
|
135
145
|
}
|
|
136
146
|
|
|
137
147
|
/** Push/pop a bracket; returns an error, a stop, or null to continue. */
|
|
@@ -190,6 +200,8 @@ const CODE_EXT = new Set([".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts",
|
|
|
190
200
|
|
|
191
201
|
/** { ok: true } | { ok: false, message }; message names the problem and line. */
|
|
192
202
|
export function quickCheck(text, ext) {
|
|
203
|
+
ext = String(ext ?? "").toLowerCase();
|
|
204
|
+
|
|
193
205
|
if (ext === ".json") {
|
|
194
206
|
try {
|
|
195
207
|
JSON.parse(text);
|
package/src/fs/diff.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
|
|
2
2
|
import { isString } from "../shared/decode.js";
|
|
3
3
|
|
|
4
|
+
/** Receipts render at most this many matches; totals still cover every match. */
|
|
5
|
+
export const MAX_DIFF_MATCHES = 32;
|
|
6
|
+
|
|
4
7
|
export function buildEditDiff(filePath, originalText, oldText, newText) {
|
|
5
8
|
const fileLines = contentLines(originalText);
|
|
6
9
|
const idx = isString(originalText) ? originalText.indexOf(oldText) : -1;
|
|
@@ -39,7 +42,8 @@ export function buildEditDiff(filePath, originalText, oldText, newText) {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
export function buildMultiEditDiff(filePath, originalText, replacements) {
|
|
42
|
-
const
|
|
45
|
+
const rendered = replacements.slice(0, MAX_DIFF_MATCHES);
|
|
46
|
+
const parts = rendered.map(({ oldText, newText }) =>
|
|
43
47
|
buildEditDiff(filePath, originalText, oldText, newText),
|
|
44
48
|
);
|
|
45
49
|
|
|
@@ -53,15 +57,16 @@ export function buildMultiEditDiff(filePath, originalText, replacements) {
|
|
|
53
57
|
: { ...line, newLineNum: Math.max(1, line.lineNum + shift) });
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
shift +=
|
|
60
|
+
shift += rendered[index].newText.split("\n").length - rendered[index].oldText.split("\n").length;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
return {
|
|
60
64
|
path: filePath,
|
|
61
65
|
op: "edit",
|
|
62
|
-
added:
|
|
63
|
-
removed:
|
|
66
|
+
added: replacements.reduce((sum, r) => sum + contentLines(r.newText).length, 0),
|
|
67
|
+
removed: replacements.reduce((sum, r) => sum + contentLines(r.oldText).length, 0),
|
|
64
68
|
lines,
|
|
69
|
+
omittedMatches: replacements.length - rendered.length,
|
|
65
70
|
};
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -71,22 +76,28 @@ function classifyPatchLine(line) {
|
|
|
71
76
|
return PATCH_LINE_KINDS[line[0]] || null;
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
export function buildPatchDiff(filePath, patchText) {
|
|
79
|
+
export function buildPatchDiff(filePath, patchText, relocations = []) {
|
|
75
80
|
const patchLines = isString(patchText) ? patchText.replace(/\r\n/g, "\n").split("\n") : [];
|
|
81
|
+
const shiftByHunk = new Map(relocations.map(entry => [entry.hunk, entry.offset]));
|
|
76
82
|
const lines = [];
|
|
77
83
|
let added = 0;
|
|
78
84
|
let removed = 0;
|
|
79
85
|
let oldLineNum = 1;
|
|
80
86
|
let newLineNum = 1;
|
|
81
87
|
let inHunk = false;
|
|
88
|
+
let hunkIndex = 0;
|
|
89
|
+
let relocatedBy = 0;
|
|
82
90
|
|
|
83
91
|
for (const patchLine of patchLines) {
|
|
84
92
|
const headerMatch = (/^@@\s+-(\d+)(?:,(\d+))?\s+\+(\d+)(?:,(\d+))?/).exec(patchLine);
|
|
85
93
|
|
|
86
94
|
if (headerMatch) {
|
|
95
|
+
// Headers already carry length deltas, so only drift relocates the receipt.
|
|
96
|
+
hunkIndex += 1;
|
|
97
|
+
relocatedBy += shiftByHunk.get(hunkIndex) ?? 0;
|
|
87
98
|
// A zero-length range names the line before the insertion/deletion point.
|
|
88
|
-
oldLineNum = Number(headerMatch[1]) + Number(headerMatch[2] === "0");
|
|
89
|
-
newLineNum = Number(headerMatch[3]) + Number(headerMatch[4] === "0");
|
|
99
|
+
oldLineNum = Number(headerMatch[1]) + Number(headerMatch[2] === "0") + relocatedBy;
|
|
100
|
+
newLineNum = Number(headerMatch[3]) + Number(headerMatch[4] === "0") + relocatedBy;
|
|
90
101
|
inHunk = true;
|
|
91
102
|
continue;
|
|
92
103
|
}
|
package/src/fs/json-read.js
CHANGED
|
@@ -2,7 +2,37 @@ import { isString, isObject } from "../shared/decode.js";
|
|
|
2
2
|
|
|
3
3
|
export const MAX_JSON_BYTES = 16 * 1024 * 1024;
|
|
4
4
|
|
|
5
|
-
const SELECTOR_HELP = 'JSON selector supports .field, .nested[0], .items[0:3], .["quoted.key"], or . (whole value); not full jq';
|
|
5
|
+
const SELECTOR_HELP = 'JSON selector supports .field, .nested[0], .items[0:3], .items.length, .["quoted.key"], or . (whole value); not full jq';
|
|
6
|
+
|
|
7
|
+
function parseIdentStep(rest, first) {
|
|
8
|
+
const match = (first ? /^([A-Za-z_$][\w$]*)/ : /^\.([A-Za-z_$][\w$]*)/).exec(rest);
|
|
9
|
+
|
|
10
|
+
return match ? { step: { key: match[1] }, match } : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function parseQuotedStep(rest) {
|
|
14
|
+
const match = /^\[("(?:[^"\\]|\\.)*")\]/.exec(rest);
|
|
15
|
+
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
|
|
18
|
+
try { return { step: { key: JSON.parse(match[1]) }, match }; } catch { throw new Error(SELECTOR_HELP); }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseIndexBounds(start, end) {
|
|
22
|
+
if (!Number.isSafeInteger(start) || (end !== undefined && (!Number.isSafeInteger(end) || end < start))) throw new Error(SELECTOR_HELP);
|
|
23
|
+
|
|
24
|
+
return end === undefined ? { index: start } : { start, end };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseIndexStep(rest) {
|
|
28
|
+
const match = /^\[(\d+)(?::(\d+))?\]/.exec(rest);
|
|
29
|
+
|
|
30
|
+
if (!match) return null;
|
|
31
|
+
const start = Number(match[1]);
|
|
32
|
+
const end = match[2] === undefined ? undefined : Number(match[2]);
|
|
33
|
+
|
|
34
|
+
return { step: parseIndexBounds(start, end), match };
|
|
35
|
+
}
|
|
6
36
|
|
|
7
37
|
/** Parse a small, non-evaluating selector language. No dynamic code or prototype lookup. */
|
|
8
38
|
function parseSelector(selector) {
|
|
@@ -12,25 +42,46 @@ function parseSelector(selector) {
|
|
|
12
42
|
let first = true;
|
|
13
43
|
|
|
14
44
|
while (rest) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
try { steps.push({ key: JSON.parse(match[1]) }); } catch { throw new Error(SELECTOR_HELP); }
|
|
21
|
-
} else if ((match = /^\[(\d+)(?::(\d+))?\]/.exec(rest))) {
|
|
22
|
-
const start = Number(match[1]), end = match[2] === undefined ? undefined : Number(match[2]);
|
|
23
|
-
|
|
24
|
-
if (!Number.isSafeInteger(start) || (end !== undefined && (!Number.isSafeInteger(end) || end < start))) throw new Error(SELECTOR_HELP);
|
|
25
|
-
steps.push(end === undefined ? { index: start } : { start, end });
|
|
26
|
-
} else throw new Error(SELECTOR_HELP);
|
|
27
|
-
rest = rest.slice(match[0].length);
|
|
45
|
+
const parsed = parseIdentStep(rest, first) || parseQuotedStep(rest) || parseIndexStep(rest);
|
|
46
|
+
|
|
47
|
+
if (!parsed) throw new Error(SELECTOR_HELP);
|
|
48
|
+
steps.push(parsed.step);
|
|
49
|
+
rest = rest.slice(parsed.match[0].length);
|
|
28
50
|
first = false;
|
|
29
51
|
}
|
|
30
52
|
|
|
31
53
|
return steps;
|
|
32
54
|
}
|
|
33
55
|
|
|
56
|
+
function missingJsonField(value, key) {
|
|
57
|
+
const keys = isObject(value) ? Object.keys(value) : [];
|
|
58
|
+
const preview = keys.length ? "; available keys: " + keys.slice(0, 24).map(key => JSON.stringify(key)).join(", ") + (keys.length > 24 ? ", …" : "") : "";
|
|
59
|
+
|
|
60
|
+
return new Error("JSON field not found: " + JSON.stringify(key) + preview);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selectKey(value, key) {
|
|
64
|
+
if (Array.isArray(value) && key === "length") return value.length;
|
|
65
|
+
|
|
66
|
+
if (!isObject(value) || !Object.hasOwn(value, key)) throw missingJsonField(value, key);
|
|
67
|
+
|
|
68
|
+
return value[key];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function selectStep(value, step) {
|
|
72
|
+
if (step.key !== undefined) return selectKey(value, step.key);
|
|
73
|
+
|
|
74
|
+
if (!Array.isArray(value)) throw new Error("JSON index requires an array");
|
|
75
|
+
|
|
76
|
+
if (step.index !== undefined) {
|
|
77
|
+
if (step.index >= value.length) throw new Error("JSON index out of range: " + step.index);
|
|
78
|
+
|
|
79
|
+
return value[step.index];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value.slice(step.start, step.end);
|
|
83
|
+
}
|
|
84
|
+
|
|
34
85
|
export function jsonProjector(json) {
|
|
35
86
|
const many = Array.isArray(json);
|
|
36
87
|
const selectors = many ? json : [json === true ? "." : json];
|
|
@@ -41,27 +92,7 @@ export function jsonProjector(json) {
|
|
|
41
92
|
// Yield one selection at a time so the caller can budget it before the next
|
|
42
93
|
// slice allocation. Eagerly mapping 64 large slices can exhaust the host heap.
|
|
43
94
|
return function* (root) {
|
|
44
|
-
for (const steps of plans) yield steps.reduce(
|
|
45
|
-
if (step.key !== undefined) {
|
|
46
|
-
if (!isObject(value) || !Object.hasOwn(value, step.key)) {
|
|
47
|
-
const keys = isObject(value) ? Object.keys(value) : [];
|
|
48
|
-
const preview = keys.length ? "; available keys: " + keys.slice(0, 24).map(key => JSON.stringify(key)).join(", ") + (keys.length > 24 ? ", …" : "") : "";
|
|
49
|
-
throw new Error("JSON field not found: " + JSON.stringify(step.key) + preview);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return value[step.key];
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
if (!Array.isArray(value)) throw new Error("JSON index requires an array");
|
|
56
|
-
|
|
57
|
-
if (step.index !== undefined) {
|
|
58
|
-
if (step.index >= value.length) throw new Error("JSON index out of range: " + step.index);
|
|
59
|
-
|
|
60
|
-
return value[step.index];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return value.slice(step.start, step.end);
|
|
64
|
-
}, root);
|
|
95
|
+
for (const steps of plans) yield steps.reduce(selectStep, root);
|
|
65
96
|
};
|
|
66
97
|
}
|
|
67
98
|
|