pi-supernova 0.0.11 → 0.1.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/CHANGELOG.md +74 -3
- package/README.md +72 -21
- package/bottleneck.js +98 -97
- package/catalog.js +12 -36
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -2
- package/decode.js +57 -6
- package/evidence.js +27 -35
- package/format.js +16 -17
- package/fuzzy.js +182 -0
- package/guest-worker.js +38 -157
- package/host-bridge.js +246 -124
- package/index.js +95 -123
- package/ledger.js +150 -0
- package/omp-frame.js +7 -23
- package/outline.js +80 -0
- package/package.json +11 -2
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +46 -144
- package/render.js +43 -158
- package/repo-index.js +106 -11
- package/runtime.js +204 -227
- package/search.js +141 -0
- package/snap.js +1 -6
- package/surface.js +22 -13
- package/vfs.js +114 -75
- package/workspace.js +51 -33
package/guest-worker.js
CHANGED
|
@@ -1,95 +1,15 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
2
|
import { parallel as runParallel, pipeline as runPipeline } from "./parallel.js";
|
|
3
|
-
import { isString,
|
|
3
|
+
import { isString, isObject, toPlain } from "./decode.js";
|
|
4
|
+
import { truncateChars } from "./format.js";
|
|
4
5
|
|
|
5
6
|
// Guest programs run here, off the host thread. The host can terminate() this
|
|
6
7
|
// worker mid-loop, so a runaway "while (true) {}" or process.exit() in guest
|
|
7
8
|
// code cannot take the harness down with it.
|
|
8
9
|
|
|
9
10
|
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
10
|
-
const
|
|
11
|
-
const COMPILED_CACHE_MAX = 256;
|
|
12
|
-
const PARAMS = [
|
|
13
|
-
"nova", "tools", "console", "parallel", "pipeline",
|
|
14
|
-
"read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate",
|
|
15
|
-
];
|
|
16
|
-
// V8 and JSC both place the body on the line after the synthesized header.
|
|
11
|
+
const PARAMS = ["nova", "console", "parallel", "pipeline", "read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate"];
|
|
17
12
|
const BODY_LINE_OFFSET = 2;
|
|
18
|
-
|
|
19
|
-
function skipLeadingComments(src) {
|
|
20
|
-
let i = 0;
|
|
21
|
-
for (;;) {
|
|
22
|
-
while (/\s/.test(src[i])) i++;
|
|
23
|
-
if (src.startsWith("//", i)) {
|
|
24
|
-
const nl = src.indexOf("\n", i);
|
|
25
|
-
if (nl < 0) return src.length;
|
|
26
|
-
i = nl + 1;
|
|
27
|
-
} else if (src.startsWith("/*", i)) {
|
|
28
|
-
const end = src.indexOf("*/", i + 2);
|
|
29
|
-
if (end < 0) return src.length;
|
|
30
|
-
i = end + 2;
|
|
31
|
-
} else {
|
|
32
|
-
return i;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function skipString(src, j, quote) {
|
|
38
|
-
for (j++; j < src.length && src[j] !== quote; j++) if (src[j] === "\\") j++;
|
|
39
|
-
return j;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function skipBalancedParens(src, i) {
|
|
43
|
-
let depth = 0;
|
|
44
|
-
for (let j = i; j < src.length; j++) {
|
|
45
|
-
const ch = src[j];
|
|
46
|
-
if (ch === "(") {
|
|
47
|
-
depth++;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (ch === ")") {
|
|
51
|
-
depth--;
|
|
52
|
-
if (depth === 0) return j + 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if ("\"'\u0060".includes(ch)) j = skipString(src, j, ch);
|
|
56
|
-
}
|
|
57
|
-
return -1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** True when src (after comments) is a single arrow or function expression. */
|
|
61
|
-
function isFunctionExpression(src) {
|
|
62
|
-
let i = skipLeadingComments(src);
|
|
63
|
-
const rest = src.slice(i);
|
|
64
|
-
if (/^(async\s+)?function\b/.test(rest)) return true;
|
|
65
|
-
const asyncMatch = /^async\s+/.exec(rest);
|
|
66
|
-
if (asyncMatch) i += asyncMatch[0].length;
|
|
67
|
-
if (/^[A-Za-z_$][\w$]*\s*=>/.test(src.slice(i))) return true;
|
|
68
|
-
if (src[i] !== "(") return false;
|
|
69
|
-
const after = skipBalancedParens(src, i);
|
|
70
|
-
if (after < 0) return false;
|
|
71
|
-
return /^\s*=>/.test(src.slice(after));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function wrapBody(code) {
|
|
75
|
-
const trimmed = String(code).trim();
|
|
76
|
-
if (!trimmed) throw new Error("code must be a non-empty string");
|
|
77
|
-
if (isFunctionExpression(trimmed)) return "const __fn = (" + trimmed + ");\nreturn await __fn();";
|
|
78
|
-
return trimmed;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function compile(code) {
|
|
82
|
-
const body = wrapBody(code);
|
|
83
|
-
let compiled = compiledCache.get(body);
|
|
84
|
-
if (compiled) return compiled;
|
|
85
|
-
compiled = new AsyncFunction(...PARAMS, body);
|
|
86
|
-
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
87
|
-
compiledCache.delete(compiledCache.keys().next().value);
|
|
88
|
-
}
|
|
89
|
-
compiledCache.set(body, compiled);
|
|
90
|
-
return compiled;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
13
|
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
94
14
|
function guestLocation(err) {
|
|
95
15
|
const stack = String(err?.stack);
|
|
@@ -100,64 +20,8 @@ function guestLocation(err) {
|
|
|
100
20
|
return { line, col: Number(m[2]) };
|
|
101
21
|
}
|
|
102
22
|
|
|
103
|
-
const MAX_DEPTH = 64;
|
|
104
|
-
const MAX_TYPED_ARRAY = 4096;
|
|
105
|
-
|
|
106
|
-
function plainFromBinary(value) {
|
|
107
|
-
const bytes = value.byteLength;
|
|
108
|
-
if (value instanceof ArrayBuffer) value = new Uint8Array(value);
|
|
109
|
-
if (value.length > MAX_TYPED_ARRAY) return "[" + value.constructor.name + " " + bytes + " bytes]";
|
|
110
|
-
return Array.from(value, (x) => (typeof x === "bigint" ? x.toString() + "n" : x));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function plainFromMap(value, seen, depth) {
|
|
114
|
-
const allStringKeys = [...value.keys()].every((k) => typeof k === "string");
|
|
115
|
-
if (!allStringKeys) return [...value].map(([k, v]) => [toPlain(k, seen, depth + 1), toPlain(v, seen, depth + 1)]);
|
|
116
|
-
const out = {};
|
|
117
|
-
for (const [k, v] of value) out[k] = toPlain(v, seen, depth + 1);
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function plainFromCollection(value, seen, depth) {
|
|
122
|
-
if (Array.isArray(value)) return value.map((x) => toPlain(x, seen, depth + 1));
|
|
123
|
-
if (value instanceof Set) return [...value].map((x) => toPlain(x, seen, depth + 1));
|
|
124
|
-
if (value instanceof Map) return plainFromMap(value, seen, depth);
|
|
125
|
-
const out = {};
|
|
126
|
-
for (const k of Object.keys(value)) out[k] = toPlain(value[k], seen, depth + 1);
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Convert any guest value to structured-clone-safe, JSON-shaped data. */
|
|
131
|
-
function toPlain(value, seen = new Set(), depth = 0) {
|
|
132
|
-
if (value === null || value === undefined) return value;
|
|
133
|
-
const t = typeof value;
|
|
134
|
-
if (t === "string" || t === "number" || t === "boolean") return value;
|
|
135
|
-
if (t === "bigint") return value.toString() + "n";
|
|
136
|
-
if (t === "function") return "[Function" + (value.name ? " " + value.name : "") + "]";
|
|
137
|
-
if (t === "symbol") return value.toString();
|
|
138
|
-
if (depth > MAX_DEPTH) return "[Depth]";
|
|
139
|
-
if (seen.has(value)) return "[Circular]";
|
|
140
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
141
|
-
if (value instanceof RegExp) return value.toString();
|
|
142
|
-
if (value instanceof Error) {
|
|
143
|
-
const out = { name: value.name, message: value.message };
|
|
144
|
-
if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
if (value instanceof Promise) return "[Promise]";
|
|
148
|
-
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
|
|
149
|
-
if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
|
|
150
|
-
seen.add(value);
|
|
151
|
-
try {
|
|
152
|
-
return plainFromCollection(value, seen, depth);
|
|
153
|
-
} finally {
|
|
154
|
-
seen.delete(value);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---- RPC to the host thread ----
|
|
159
|
-
|
|
160
23
|
let activeRunId = 0;
|
|
24
|
+
let runActive = false;
|
|
161
25
|
let rpcSeq = 0;
|
|
162
26
|
const pendingRpc = new Map();
|
|
163
27
|
|
|
@@ -165,12 +29,13 @@ function post(msg) {
|
|
|
165
29
|
parentPort.postMessage(msg);
|
|
166
30
|
}
|
|
167
31
|
|
|
168
|
-
function
|
|
32
|
+
function callRpc(runId, method, args) {
|
|
33
|
+
if (!runActive || runId !== activeRunId) return Promise.reject(new Error("program is already complete"));
|
|
169
34
|
return new Promise((resolve, reject) => {
|
|
170
35
|
const id = ++rpcSeq;
|
|
171
36
|
pendingRpc.set(id, { resolve, reject });
|
|
172
37
|
try {
|
|
173
|
-
post({ op: "rpc", id, runId
|
|
38
|
+
post({ op: "rpc", id, runId, method, args });
|
|
174
39
|
} catch (err) {
|
|
175
40
|
pendingRpc.delete(id);
|
|
176
41
|
reject(new Error("nova." + method + " arguments are not transferable: " + err?.message));
|
|
@@ -179,6 +44,7 @@ function rpc(method, args) {
|
|
|
179
44
|
}
|
|
180
45
|
|
|
181
46
|
function unwrapValue(res) {
|
|
47
|
+
if (res?.ok === false) throw new Error(String(res.value ?? res.error ?? "host tool failed"));
|
|
182
48
|
if ("value" in Object(res)) return res.value;
|
|
183
49
|
return res;
|
|
184
50
|
}
|
|
@@ -201,7 +67,8 @@ function leanEnvelope(res) {
|
|
|
201
67
|
return res;
|
|
202
68
|
}
|
|
203
69
|
|
|
204
|
-
function buildGuestApi(available) {
|
|
70
|
+
function buildGuestApi(available, batchRead, runId) {
|
|
71
|
+
const rpc = (method, args) => callRpc(runId, method, args);
|
|
205
72
|
const availableSet = new Set(available);
|
|
206
73
|
const nova = {
|
|
207
74
|
search: (query, limit) => rpc("search", [query, limit]),
|
|
@@ -228,20 +95,24 @@ function buildGuestApi(available) {
|
|
|
228
95
|
return { ok: false, committed: false, error: err instanceof Error ? err.message : String(err) };
|
|
229
96
|
}
|
|
230
97
|
},
|
|
231
|
-
surface: async (filePath) => unwrapJsonValue(await rpc("surface",
|
|
98
|
+
surface: async (filePath) => unwrapJsonValue(await rpc("call", ["surface", { path: filePath }])),
|
|
232
99
|
evidence: async (query, opts) => unwrapJsonValue(await rpc("call", ["evidence", { query, ...opts }])),
|
|
233
|
-
snap: async (query, targetPath) => unwrapJsonValue(await rpc("snap",
|
|
100
|
+
snap: async (query, targetPath) => unwrapJsonValue(await rpc("call", ["snap", { query, path: targetPath }])),
|
|
234
101
|
has: (name) => availableSet.has(name),
|
|
235
102
|
};
|
|
236
103
|
|
|
237
|
-
|
|
104
|
+
// read(path, offset?, limit?) or read(path, { about, offset, limit, maxChars })
|
|
105
|
+
const readArgs = (p, a, b) => (isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b });
|
|
106
|
+
const read = async (p, a, b) => {
|
|
238
107
|
if (Array.isArray(p)) {
|
|
239
|
-
|
|
108
|
+
if (!batchRead) return Promise.all(p.map(item => read(item, a, b)));
|
|
109
|
+
const res = await nova.call("read", readArgs(p, a, b));
|
|
110
|
+
unwrapValue(res);
|
|
240
111
|
if (Array.isArray(res?.items)) return res.items;
|
|
241
112
|
// Captured host executor without batch support: fan out.
|
|
242
|
-
return Promise.all(p.map((item) => read(item,
|
|
113
|
+
return Promise.all(p.map((item) => read(item, a, b)));
|
|
243
114
|
}
|
|
244
|
-
return unwrapValue(await nova.call("read",
|
|
115
|
+
return unwrapValue(await nova.call("read", readArgs(p, a, b)));
|
|
245
116
|
};
|
|
246
117
|
const write = async (p, content) => unwrapValue(await nova.call("write", { path: p, content }));
|
|
247
118
|
const edit = async (p, oldText, newText) => unwrapValue(await nova.call("edit", { path: p, oldText, newText }));
|
|
@@ -275,8 +146,13 @@ function buildGuestApi(available) {
|
|
|
275
146
|
|
|
276
147
|
function makeConsole(runId, limits) {
|
|
277
148
|
let count = 0;
|
|
149
|
+
let truncated = false;
|
|
150
|
+
const markTruncated = () => {
|
|
151
|
+
if (!truncated) post({ op: "logTruncated", runId });
|
|
152
|
+
truncated = true;
|
|
153
|
+
};
|
|
278
154
|
const emit = (...args) => {
|
|
279
|
-
if (count >= limits.maxLogLines) return;
|
|
155
|
+
if (count >= limits.maxLogLines) { markTruncated(); return; }
|
|
280
156
|
count++;
|
|
281
157
|
const line = args
|
|
282
158
|
.map((a) => {
|
|
@@ -288,31 +164,35 @@ function makeConsole(runId, limits) {
|
|
|
288
164
|
}
|
|
289
165
|
})
|
|
290
166
|
.join(" ");
|
|
291
|
-
|
|
167
|
+
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
168
|
+
if (clipped.truncated) markTruncated();
|
|
169
|
+
post({ op: "log", runId, line: clipped.text, truncated: clipped.truncated });
|
|
292
170
|
};
|
|
293
171
|
return { log: emit, warn: emit, error: emit, info: emit, debug: emit };
|
|
294
172
|
}
|
|
295
173
|
|
|
296
174
|
function postFailure(runId, err, location) {
|
|
175
|
+
runActive = false;
|
|
297
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
298
177
|
post({ op: "error", runId, message, location });
|
|
299
178
|
}
|
|
300
179
|
|
|
301
180
|
async function handleRun(msg) {
|
|
302
|
-
const { runId,
|
|
181
|
+
const { runId, prepared, limits, available, batchRead = true } = msg;
|
|
303
182
|
activeRunId = runId;
|
|
183
|
+
runActive = true;
|
|
304
184
|
let compiled;
|
|
305
185
|
try {
|
|
306
|
-
compiled =
|
|
186
|
+
compiled = { fn: new AsyncFunction(...PARAMS, prepared.body), hasReturn: prepared.hasReturn };
|
|
307
187
|
} catch (err) {
|
|
308
188
|
postFailure(runId, err);
|
|
309
189
|
return;
|
|
310
190
|
}
|
|
311
|
-
const api = buildGuestApi(available);
|
|
191
|
+
const api = buildGuestApi(available, batchRead, runId);
|
|
312
192
|
const scopedConsole = makeConsole(runId, limits);
|
|
313
193
|
try {
|
|
314
|
-
const value = await compiled(
|
|
315
|
-
api.nova,
|
|
194
|
+
const value = await compiled.fn(
|
|
195
|
+
api.nova, scopedConsole, runParallel, runPipeline,
|
|
316
196
|
api.read, api.write, api.edit, api.patch, api.surface, api.snap, api.evidence, api.bash, api.exec, api.speculate,
|
|
317
197
|
);
|
|
318
198
|
if (runId !== activeRunId) return;
|
|
@@ -322,7 +202,8 @@ async function handleRun(msg) {
|
|
|
322
202
|
} catch (err) {
|
|
323
203
|
plain = "[unserializable: " + (err?.message || err) + "]";
|
|
324
204
|
}
|
|
325
|
-
|
|
205
|
+
runActive = false;
|
|
206
|
+
post({ op: "done", runId, value: plain, undefinedReturn: value === undefined && !compiled.hasReturn, hasReturn: compiled.hasReturn });
|
|
326
207
|
} catch (err) {
|
|
327
208
|
if (runId !== activeRunId) return;
|
|
328
209
|
postFailure(runId, err, guestLocation(err));
|