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/index.js
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { isString, isFunction
|
|
2
|
+
import { isString, isFunction } from "./decode.js";
|
|
3
3
|
import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions } from "./catalog.js";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { createHostBridge } from "./host-bridge.js";
|
|
6
6
|
import { runGuestProgram, warmGuestWorker } from "./runtime.js";
|
|
7
|
-
import {
|
|
8
|
-
extractOperationsFromCode,
|
|
9
|
-
renderSupernovaCall,
|
|
10
|
-
renderSupernovaResult,
|
|
11
|
-
SafeText,
|
|
12
|
-
} from "./render.js";
|
|
7
|
+
import { renderSupernovaCall, renderSupernovaResult } from "./render.js";
|
|
13
8
|
|
|
14
|
-
export {
|
|
9
|
+
export { renderSupernovaCall, renderSupernovaResult };
|
|
15
10
|
|
|
16
|
-
// Sync only
|
|
11
|
+
// Sync only, never top-level await. Dynamic import of host/deps hung OMP plugin load.
|
|
17
12
|
const require = createRequire(import.meta.url);
|
|
18
13
|
let Type;
|
|
19
14
|
try {
|
|
@@ -71,48 +66,44 @@ function progressEmitter(onUpdate) {
|
|
|
71
66
|
return emit;
|
|
72
67
|
}
|
|
73
68
|
|
|
69
|
+
function sessionStats({ programs, returnedChars, collapsedChars, collapsedRuns }) {
|
|
70
|
+
const total = returnedChars + collapsedChars;
|
|
71
|
+
const pct = total ? Math.round((collapsedChars / total) * 100) : 0;
|
|
72
|
+
return `this session: ${programs} programs · ~${Math.round(returnedChars / 4)} tokens returned · ~${Math.round(collapsedChars / 4)} already-seen tokens not re-sent (${pct}%, ${collapsedRuns} runs)`;
|
|
73
|
+
}
|
|
74
|
+
|
|
74
75
|
function logsBlock(outcome, tail = "") {
|
|
75
76
|
return outcome.logs?.length ? `\n--- logs\n${outcome.logs.join("\n")}${tail}` : "";
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function errorText(outcome) {
|
|
79
|
-
return `error ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
79
|
+
function errorText(outcome, call) {
|
|
80
|
+
return `error #${call} ${outcome.wallMs}ms: ${outcome.error}${logsBlock(outcome)}`;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
function successText(outcome) {
|
|
83
|
+
function successText(outcome, call) {
|
|
83
84
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
84
|
-
const hint = outcome.undefinedReturn ? " (no return statement
|
|
85
|
-
return `ok ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
86
|
-
}
|
|
87
|
-
function unwrapStructuredResult(response, operation) {
|
|
88
|
-
if (response?.ok === false) {
|
|
89
|
-
throw new Error(response.value || response.error || `${operation} failed`);
|
|
90
|
-
}
|
|
91
|
-
const value = isObject(response) && "value" in response ? response.value : response;
|
|
92
|
-
if (!isString(value)) return value;
|
|
93
|
-
try {
|
|
94
|
-
return JSON.parse(value);
|
|
95
|
-
} catch {
|
|
96
|
-
return value;
|
|
97
|
-
}
|
|
85
|
+
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
86
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
98
87
|
}
|
|
99
|
-
|
|
100
88
|
const TOOL_DESCRIPTION = `Run one JavaScript program that composes host tools. Async body or arrow; \`return\` a small shaped value (compact literal, capped; strings raw; console.log is captured).
|
|
101
89
|
|
|
102
90
|
Globals (async):
|
|
103
|
-
read(path|paths, offset?, limit?) → text | text[]
|
|
104
|
-
write(path, text) · edit(path, oldText, newText) · patch(path, unifiedDiff)
|
|
91
|
+
read(path|paths, offset?, limit?) → text | text[] · read(path, {about}) → whole-file outline, only relevant bodies expanded
|
|
92
|
+
write(path, text) · edit(path, oldText, newText) → post-edit lines (no re-read needed) · patch(path, unifiedDiff)
|
|
105
93
|
bash(cmd, {cwd?, timeoutMs?}) → output, throws on non-zero exit · exec(cmd, argv?) quotes argv
|
|
106
|
-
evidence(query, {k?}) → {spans: [{path, lines, name, text}]} top-K spans that answer a question
|
|
94
|
+
evidence(query, {k?}) → {spans: [{path, lines, name, text}]} top-K spans that answer a question; use before read
|
|
107
95
|
snap(query, root?) → {path, line, signature, context} · surface(path) → {items: [{name, kind, line}]}
|
|
108
96
|
nova.call(name, args) → {ok, value} for any host tool · nova.callMany([{name, args}]) parallel when read-only
|
|
109
97
|
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
110
|
-
parallel(thunks) · pipeline(items, ...stages)
|
|
98
|
+
parallel(thunks) · pipeline(items, ...stages)
|
|
99
|
+
|
|
100
|
+
Already-seen lines collapse to "⋯ N lines same as #12 · path:a–b ⋯"; read(path, a, n) re-shows them.`;
|
|
111
101
|
|
|
112
102
|
export default function piSupernova(pi) {
|
|
113
103
|
const config = loadConfig();
|
|
114
104
|
let cwd = process.cwd();
|
|
115
105
|
let catalog = [];
|
|
106
|
+
let programSeq = 0;
|
|
116
107
|
|
|
117
108
|
const bridge = createHostBridge({
|
|
118
109
|
pi,
|
|
@@ -120,56 +111,38 @@ export default function piSupernova(pi) {
|
|
|
120
111
|
getCwd: () => cwd,
|
|
121
112
|
});
|
|
122
113
|
|
|
123
|
-
function refreshCatalog() {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
114
|
+
function refreshCatalog(target = bridge) {
|
|
115
|
+
const tools = target.refreshTools();
|
|
116
|
+
const discoverable = mergeNativeToolDefinitions(tools, target.externalNames())
|
|
117
|
+
.filter(tool => target.isCallable(tool.name))
|
|
118
|
+
.map(tool => {
|
|
119
|
+
const schema = tool.parameters;
|
|
120
|
+
try {
|
|
121
|
+
if (isFunction(schema?.toJsonSchema)) return { ...tool, parameters: schema.toJsonSchema() };
|
|
122
|
+
if (schema && !schema.type && pi.zod?.toJSONSchema && (schema._zod || schema._def)) {
|
|
123
|
+
return { ...tool, parameters: pi.zod.toJSONSchema(schema, { io: "input" }) };
|
|
124
|
+
}
|
|
125
|
+
return tool;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { ...tool, parameters: undefined, schemaError: error.message };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
133
130
|
catalog = buildCatalog(discoverable, config.excludeTools || []);
|
|
134
131
|
return catalog;
|
|
135
132
|
}
|
|
136
133
|
|
|
137
|
-
function makeNovaApi() {
|
|
134
|
+
function makeNovaApi(runBridge, runCatalog, cancel) {
|
|
138
135
|
return {
|
|
139
|
-
async
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
return bridge.call(name, args);
|
|
150
|
-
},
|
|
151
|
-
async callMany(calls) {
|
|
152
|
-
return bridge.callMany(calls);
|
|
153
|
-
},
|
|
154
|
-
speculateBegin() {
|
|
155
|
-
bridge.beginSpeculation();
|
|
156
|
-
},
|
|
157
|
-
async speculateCommit() {
|
|
158
|
-
await bridge.commitSpeculation();
|
|
159
|
-
},
|
|
160
|
-
speculateRollback() {
|
|
161
|
-
bridge.rollbackSpeculation();
|
|
162
|
-
},
|
|
163
|
-
names() {
|
|
164
|
-
const cat = catalog.length ? catalog : refreshCatalog();
|
|
165
|
-
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
166
|
-
},
|
|
167
|
-
async surface(filePath) {
|
|
168
|
-
return unwrapStructuredResult(await bridge.call("surface", { path: filePath }), "surface");
|
|
169
|
-
},
|
|
170
|
-
async snap(query, targetPath) {
|
|
171
|
-
return unwrapStructuredResult(await bridge.call("snap", { query, path: targetPath }), "snap");
|
|
172
|
-
},
|
|
136
|
+
search: async (query, limit) => searchCatalog(runCatalog, query, Number.isInteger(limit) ? limit : config.maxSearchResults),
|
|
137
|
+
describe: async (name) => describeTool(runCatalog, name),
|
|
138
|
+
call: (name, args) => runBridge.call(name, args),
|
|
139
|
+
callMany: (calls) => runBridge.callMany(calls),
|
|
140
|
+
speculateBegin: () => runBridge.beginSpeculation(),
|
|
141
|
+
speculateCommit: () => runBridge.commitSpeculation(),
|
|
142
|
+
speculateRollback: () => runBridge.rollbackSpeculation(),
|
|
143
|
+
names: () => runCatalog.map(tool => tool.name),
|
|
144
|
+
batchRead: runBridge.supportsBatchRead(),
|
|
145
|
+
cancel,
|
|
173
146
|
};
|
|
174
147
|
}
|
|
175
148
|
|
|
@@ -179,7 +152,7 @@ export default function piSupernova(pi) {
|
|
|
179
152
|
description: TOOL_DESCRIPTION,
|
|
180
153
|
promptSnippet: "Compose host tools in one JavaScript program",
|
|
181
154
|
promptGuidelines: [
|
|
182
|
-
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code
|
|
155
|
+
"Use supernova for multi-step tool work: loops, filtering, parallel reads, read→edit chains. To understand code: evidence(question) across the repo, or read(path, {about: question}) for one file: full structure, only relevant bodies expanded. Plain read(path) only for lines you will edit. Return a compact shaped value; keep raw tool output inside the program.",
|
|
183
156
|
],
|
|
184
157
|
parameters: Type.Object({
|
|
185
158
|
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
@@ -192,82 +165,81 @@ export default function piSupernova(pi) {
|
|
|
192
165
|
renderCall: renderSupernovaCall,
|
|
193
166
|
renderResult: renderSupernovaResult,
|
|
194
167
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
195
|
-
|
|
168
|
+
const runCwd = ctx?.cwd || cwd;
|
|
196
169
|
const runController = new AbortController();
|
|
197
170
|
const abortRun = () => runController.abort(signal?.reason);
|
|
198
171
|
if (signal?.aborted) abortRun();
|
|
199
172
|
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
173
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd });
|
|
174
|
+
runBridge.bindCallContext(ctx, runController.signal);
|
|
175
|
+
runBridge.resetCallBudget();
|
|
200
176
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
refreshCatalog();
|
|
204
|
-
bridge.beginSpeculation();
|
|
177
|
+
const call = ++programSeq;
|
|
178
|
+
runBridge.ledger.beginProgram(call);
|
|
205
179
|
const emitProgress = progressEmitter(onUpdate);
|
|
206
|
-
|
|
180
|
+
runBridge.setCallListener((_record, trace) => emitProgress(trace));
|
|
207
181
|
emitProgress([]);
|
|
208
|
-
|
|
182
|
+
const started = performance.now();
|
|
209
183
|
let outcome;
|
|
210
184
|
try {
|
|
211
|
-
|
|
185
|
+
const runCatalog = refreshCatalog(runBridge);
|
|
186
|
+
runBridge.beginSpeculation();
|
|
187
|
+
outcome = await runGuestProgram({
|
|
188
|
+
code: params?.code,
|
|
189
|
+
nova: makeNovaApi(runBridge, runCatalog, abortRun),
|
|
190
|
+
config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
191
|
+
signal: runController.signal,
|
|
192
|
+
onTimeout: abortRun,
|
|
193
|
+
});
|
|
194
|
+
runBridge.close();
|
|
195
|
+
if (outcome.ok) {
|
|
196
|
+
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished nova.speculate branch; await it before returning");
|
|
197
|
+
await runBridge.commitSpeculation();
|
|
198
|
+
}
|
|
199
|
+
else runBridge.rollbackSpeculation();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
abortRun();
|
|
202
|
+
runBridge.close();
|
|
203
|
+
runBridge.rollbackSpeculation();
|
|
204
|
+
outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
|
|
212
205
|
} finally {
|
|
213
|
-
|
|
206
|
+
runBridge.setCallListener(null);
|
|
214
207
|
emitProgress.flush();
|
|
215
208
|
signal?.removeEventListener("abort", abortRun);
|
|
216
209
|
}
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
await bridge.commitSpeculation();
|
|
224
|
-
return result(successText(outcome), {
|
|
225
|
-
ok: true,
|
|
226
|
-
wallMs: outcome.wallMs,
|
|
227
|
-
returnTruncated: outcome.returnTruncated,
|
|
228
|
-
logTruncated: outcome.logTruncated,
|
|
229
|
-
logs: outcome.logs,
|
|
230
|
-
result: outcome.result,
|
|
231
|
-
trace,
|
|
210
|
+
const trace = runBridge.getTrace();
|
|
211
|
+
const text = outcome.ok ? successText(outcome, call) : errorText(outcome, call);
|
|
212
|
+
return result(runBridge.ledger.dedupe(text, call), {
|
|
213
|
+
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
214
|
+
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
215
|
+
logs: outcome.logs, result: outcome.result, trace,
|
|
232
216
|
});
|
|
233
217
|
},
|
|
234
218
|
});
|
|
235
219
|
|
|
236
|
-
async function runProgram(params, signal, onTimeout) {
|
|
237
|
-
const runStartedAt = performance.now();
|
|
238
|
-
const runConfig = {
|
|
239
|
-
...config,
|
|
240
|
-
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
241
|
-
};
|
|
242
|
-
try {
|
|
243
|
-
return await runGuestProgram({ code: String(params?.code || ""), nova: makeNovaApi(), config: runConfig, signal, onTimeout });
|
|
244
|
-
} catch (error) {
|
|
245
|
-
return {
|
|
246
|
-
ok: false,
|
|
247
|
-
error: error instanceof Error ? error.message : String(error),
|
|
248
|
-
logs: [],
|
|
249
|
-
wallMs: Math.round(performance.now() - runStartedAt),
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
220
|
pi.on("session_start", (_event, ctx) => {
|
|
255
221
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
222
|
+
// A new session is a new model context: nothing has been seen yet.
|
|
223
|
+
bridge.bindCallContext(ctx);
|
|
224
|
+
bridge.ledger.reset();
|
|
225
|
+
programSeq = 0;
|
|
256
226
|
refreshCatalog();
|
|
257
227
|
warmGuestWorker(config).catch(() => {});
|
|
258
228
|
});
|
|
259
229
|
|
|
260
230
|
pi.registerCommand("supernova", {
|
|
261
|
-
description: "Show pi-supernova status (
|
|
231
|
+
description: "Show pi-supernova status (callable tools and session statistics)",
|
|
262
232
|
handler: async (_args, ctx) => {
|
|
233
|
+
bridge.bindCallContext(ctx);
|
|
263
234
|
refreshCatalog();
|
|
264
|
-
const
|
|
265
|
-
const natives = Object.keys(bridge.natives).sort();
|
|
235
|
+
const external = bridge.externalNames().filter(bridge.isCallable).sort();
|
|
236
|
+
const natives = Object.keys(bridge.natives).filter(name => bridge.isCallable(name) && !external.includes(name)).sort();
|
|
266
237
|
const lines = [
|
|
267
238
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
268
|
-
`
|
|
239
|
+
`external tools: ${external.length ? external.join(", ") : "(none)"}`,
|
|
269
240
|
`native adapters: ${natives.join(", ")}`,
|
|
270
241
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
242
|
+
sessionStats(bridge.ledger.stats),
|
|
271
243
|
];
|
|
272
244
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
273
245
|
},
|
package/ledger.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const MIN_RUN = 6;
|
|
2
|
+
const MIN_SUBSTANTIVE = 4;
|
|
3
|
+
const MAX_CANDIDATES = 8;
|
|
4
|
+
const DEFAULT_WINDOW = 40;
|
|
5
|
+
const MAX_STORED_LINES = 200_000;
|
|
6
|
+
|
|
7
|
+
function hashLine(line) {
|
|
8
|
+
let hash = 0x811c9dc5;
|
|
9
|
+
for (let i = 0; i < line.length; i++) hash = Math.imul(hash ^ line.charCodeAt(i), 0x01000193);
|
|
10
|
+
return hash >>> 0;
|
|
11
|
+
}
|
|
12
|
+
function substantive(line) { return line.trim().length >= 8; }
|
|
13
|
+
function newHistory() {
|
|
14
|
+
return { results: new Map(), occurrences: new Map(), storedLines: 0, latestCall: 0,
|
|
15
|
+
stats: { programs: 0, returnedChars: 0, collapsedChars: 0, collapsedRuns: 0 } };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class SeenLedger {
|
|
19
|
+
constructor({ window = DEFAULT_WINDOW, history } = {}) {
|
|
20
|
+
this.window = window;
|
|
21
|
+
this.history = history ?? newHistory();
|
|
22
|
+
this.origins = new Map();
|
|
23
|
+
this.pinned = new Set();
|
|
24
|
+
}
|
|
25
|
+
get results() { return this.history.results; }
|
|
26
|
+
get occurrences() { return this.history.occurrences; }
|
|
27
|
+
get storedLines() { return this.history.storedLines; }
|
|
28
|
+
get stats() { return this.history.stats; }
|
|
29
|
+
fork() { return new SeenLedger({ window: this.window, history: this.history }); }
|
|
30
|
+
|
|
31
|
+
reset() {
|
|
32
|
+
Object.assign(this.history, newHistory());
|
|
33
|
+
this.origins.clear();
|
|
34
|
+
this.pinned.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
beginProgram(call) {
|
|
38
|
+
this.origins.clear();
|
|
39
|
+
this.pinned.clear();
|
|
40
|
+
this.stats.programs++;
|
|
41
|
+
this.history.latestCall = Math.max(this.history.latestCall, call);
|
|
42
|
+
for (const old of this.results.keys()) if (old <= this.history.latestCall - this.window) this.forget(old);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
forget(call) {
|
|
46
|
+
const entry = this.results.get(call);
|
|
47
|
+
if (!entry) return;
|
|
48
|
+
for (const hash of new Set(entry.hashes)) {
|
|
49
|
+
const kept = (this.occurrences.get(hash) ?? []).filter(item => item.call !== call);
|
|
50
|
+
if (kept.length) this.occurrences.set(hash, kept);
|
|
51
|
+
else this.occurrences.delete(hash);
|
|
52
|
+
}
|
|
53
|
+
this.history.storedLines -= entry.lines.length;
|
|
54
|
+
this.results.delete(call);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
recordOrigin(path, firstLine, lines, pin = false) {
|
|
58
|
+
if (this.window === 0) return;
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const text = lines[i];
|
|
61
|
+
if (!substantive(text)) continue;
|
|
62
|
+
if (this.origins.size >= MAX_STORED_LINES && !this.origins.has(text)) this.origins.delete(this.origins.keys().next().value);
|
|
63
|
+
this.origins.set(text, { path, line: firstLine + i });
|
|
64
|
+
if (pin) this.pinned.add(text);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
runLength(lines, hashes, candidate, index) {
|
|
69
|
+
const earlier = this.results.get(candidate.call);
|
|
70
|
+
if (!earlier) return 0;
|
|
71
|
+
let count = 0;
|
|
72
|
+
while (index + count < lines.length && candidate.index + count < earlier.lines.length
|
|
73
|
+
&& hashes[index + count] === earlier.hashes[candidate.index + count]
|
|
74
|
+
&& lines[index + count] === earlier.lines[candidate.index + count]
|
|
75
|
+
&& !this.pinned.has(lines[index + count])) count++;
|
|
76
|
+
return count;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
longestRun(lines, hashes, index, call) {
|
|
80
|
+
const candidates = this.occurrences.get(hashes[index]);
|
|
81
|
+
if (!candidates) return null;
|
|
82
|
+
let best = null;
|
|
83
|
+
for (const candidate of candidates.filter(item => item.call < call).slice(-MAX_CANDIDATES)) {
|
|
84
|
+
const length = this.runLength(lines, hashes, candidate, index);
|
|
85
|
+
if (length >= MIN_RUN && (!best || length > best.length)) best = { ...candidate, length };
|
|
86
|
+
}
|
|
87
|
+
if (!best) return null;
|
|
88
|
+
let count = 0;
|
|
89
|
+
for (let i = index; i < index + best.length; i++) if (substantive(lines[i])) count++;
|
|
90
|
+
return count >= MIN_SUBSTANTIVE ? best : null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
citation(lines, index, run) {
|
|
94
|
+
const earlier = this.results.get(run.call);
|
|
95
|
+
const origin = offset => this.origins.get(lines[index + offset]) ?? earlier?.origins[run.index + offset];
|
|
96
|
+
const first = origin(0);
|
|
97
|
+
if (!first) return "";
|
|
98
|
+
let expected = first.line;
|
|
99
|
+
for (let i = 0; i < run.length; i++) {
|
|
100
|
+
const item = origin(i);
|
|
101
|
+
if (item && (item.path !== first.path || item.line !== expected)) return "";
|
|
102
|
+
expected++;
|
|
103
|
+
}
|
|
104
|
+
return first.path + ":" + first.line + "–" + (expected - 1);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
dedupe(text, call) {
|
|
108
|
+
if (this.window === 0) {
|
|
109
|
+
this.stats.returnedChars += text.length;
|
|
110
|
+
return text;
|
|
111
|
+
}
|
|
112
|
+
const lines = text.split("\n");
|
|
113
|
+
const hashes = Uint32Array.from(lines, hashLine);
|
|
114
|
+
const out = [];
|
|
115
|
+
let collapsedChars = 0;
|
|
116
|
+
for (let i = 0; i < lines.length;) {
|
|
117
|
+
const run = substantive(lines[i]) ? this.longestRun(lines, hashes, i, call) : null;
|
|
118
|
+
if (!run) { out.push(lines[i++]); continue; }
|
|
119
|
+
const cite = this.citation(lines, i, run);
|
|
120
|
+
out.push("⋯ " + run.length + " lines same as #" + run.call + (cite ? " · " + cite : "") + " ⋯");
|
|
121
|
+
for (let k = 0; k < run.length; k++) collapsedChars += lines[i + k].length + 1;
|
|
122
|
+
this.stats.collapsedRuns++;
|
|
123
|
+
i += run.length;
|
|
124
|
+
}
|
|
125
|
+
const sent = out.join("\n");
|
|
126
|
+
this.stats.returnedChars += sent.length;
|
|
127
|
+
this.stats.collapsedChars += collapsedChars;
|
|
128
|
+
this.remember(call, out);
|
|
129
|
+
return sent;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
remember(call, lines) {
|
|
133
|
+
if (this.window === 0 || call <= this.history.latestCall - this.window || lines.length > MAX_STORED_LINES) return;
|
|
134
|
+
if (this.results.has(call)) this.forget(call);
|
|
135
|
+
for (const old of [...this.results.keys()].sort((a, b) => a - b)) {
|
|
136
|
+
if (this.storedLines + lines.length <= MAX_STORED_LINES) break;
|
|
137
|
+
this.forget(old);
|
|
138
|
+
}
|
|
139
|
+
const hashes = Uint32Array.from(lines, hashLine);
|
|
140
|
+
const origins = lines.map(line => this.origins.get(line));
|
|
141
|
+
for (let i = 0; i < lines.length; i++) {
|
|
142
|
+
if (!substantive(lines[i])) continue;
|
|
143
|
+
let list = this.occurrences.get(hashes[i]);
|
|
144
|
+
if (!list) this.occurrences.set(hashes[i], (list = []));
|
|
145
|
+
list.push({ call, index: i });
|
|
146
|
+
}
|
|
147
|
+
this.results.set(call, { hashes, lines, origins });
|
|
148
|
+
this.history.storedLines += lines.length;
|
|
149
|
+
}
|
|
150
|
+
}
|
package/omp-frame.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared Pi/OMP rounded tool chrome for supernova.
|
|
3
3
|
*
|
|
4
|
-
* Intentionally self-contained
|
|
4
|
+
* Intentionally self-contained: never dynamic-imports `@oh-my-pi/pi-coding-agent`
|
|
5
5
|
* (that hung OMP plugin load). Portable geometry matches native edit/write cards.
|
|
6
6
|
*/
|
|
7
7
|
|
|
@@ -43,30 +43,15 @@ function borderPaint(theme, state, borderColor) {
|
|
|
43
43
|
return (text) => text;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
if (iconOverride !== undefined) return undefined;
|
|
48
|
-
if (icon !== undefined) return icon;
|
|
49
|
-
return state === "error" ? "error" : undefined;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const STATUS_GLYPH = { error: "✗ ", running: "… " };
|
|
53
|
-
const STATUS_COLOR = { error: "error", running: "dim" };
|
|
46
|
+
const STATUS_PREFIX = { error: ["error", "✗ "], running: ["dim", "… "] };
|
|
54
47
|
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
if (!glyph) return "";
|
|
60
|
-
return theme?.fg ? theme.fg(STATUS_COLOR[key], glyph) : glyph;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function statusHeader(theme, { title, description, state, spinnerFrame, icon, iconOverride }) {
|
|
64
|
-
const resolvedIcon = resolveStatusIcon({ icon, iconOverride, state });
|
|
48
|
+
function statusHeader(theme, { title, description, state, icon }) {
|
|
49
|
+
const resolved = icon ?? (state === "error" ? "error" : undefined);
|
|
50
|
+
const prefixSpec = STATUS_PREFIX[resolved];
|
|
51
|
+
const prefix = prefixSpec ? (theme?.fg ? theme.fg(prefixSpec[0], prefixSpec[1]) : prefixSpec[1]) : "";
|
|
65
52
|
const titleText = theme?.fg ? theme.fg("accent", title) : title;
|
|
66
53
|
const descText = description ? (theme?.fg ? theme.fg("muted", description) : description) : "";
|
|
67
|
-
|
|
68
|
-
if (!descText) return `${prefix}${titleText}`;
|
|
69
|
-
return `${prefix}${titleText}: ${descText}`;
|
|
54
|
+
return descText ? `${prefix}${titleText}: ${descText}` : `${prefix}${titleText}`;
|
|
70
55
|
}
|
|
71
56
|
|
|
72
57
|
function padLine(line, width, bgFn) {
|
|
@@ -142,7 +127,6 @@ function renderPortableFrame(theme, { header, sections = [], state = "pending",
|
|
|
142
127
|
const border = borderPaint(theme, state, borderColor);
|
|
143
128
|
const bgFn = bgFnForState(theme, state);
|
|
144
129
|
const h = box.horizontal;
|
|
145
|
-
const v = box.vertical;
|
|
146
130
|
const cap = h.repeat(3);
|
|
147
131
|
|
|
148
132
|
const paintBar = (leftChar, rightChar, label) => {
|
package/outline.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { WorkspaceIndex } from "./repo-index.js";
|
|
2
|
+
import { tokenizeQuery } from "./snap.js";
|
|
3
|
+
import { stem } from "./evidence.js";
|
|
4
|
+
|
|
5
|
+
// read(path, { about }): one call, whole-file structure, only the relevant bodies expanded.
|
|
6
|
+
// L0 (names) and L1 (signatures + line ranges) for every declaration; L2 (full text) for the
|
|
7
|
+
// spans that match the question, within a character budget. The model reads a 600-line file
|
|
8
|
+
// in ~15% of its tokens and knows the exact read(path, offset, limit) to issue for anything folded.
|
|
9
|
+
|
|
10
|
+
const OUTLINE_DEFAULTS = { maxChars: 8000, maxExpanded: 6, headerLines: 30, maxRefs: 5, references: null };
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
function relevance(span, lower, stems) {
|
|
14
|
+
if (stems.length === 0) return 0;
|
|
15
|
+
const nameLower = span.name.toLowerCase();
|
|
16
|
+
let score = 0;
|
|
17
|
+
for (const s of stems) if (nameLower.includes(s)) score += 40;
|
|
18
|
+
for (let i = span.start - 1; i < span.end; i++) {
|
|
19
|
+
let hits = 0;
|
|
20
|
+
for (const s of stems) if (lower[i].includes(s)) hits++;
|
|
21
|
+
score += hits * hits * 5;
|
|
22
|
+
}
|
|
23
|
+
return score;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function chooseExpanded(spans, lower, stems, raw, opts) {
|
|
27
|
+
const scored = spans.map((s, i) => ({ i, r: relevance(s, lower, stems), chars: raw.slice(s.start - 1, s.end).join("\n").length }));
|
|
28
|
+
scored.sort((a, b) => b.r - a.r || a.i - b.i);
|
|
29
|
+
const expanded = new Set();
|
|
30
|
+
let budget = opts.maxChars;
|
|
31
|
+
// Weak-match cutoff (as fff's weak-match detector): stop once relevance falls below 40% of the best span.
|
|
32
|
+
const floor = stems.length > 0 ? Math.max(1, scored[0].r * 0.4) : 0;
|
|
33
|
+
for (const { i, r, chars } of scored) {
|
|
34
|
+
if (expanded.size >= opts.maxExpanded || r < floor) break;
|
|
35
|
+
if (chars > budget) continue;
|
|
36
|
+
expanded.add(i);
|
|
37
|
+
budget -= chars;
|
|
38
|
+
}
|
|
39
|
+
return expanded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function foldedLine(span) {
|
|
43
|
+
const body = span.end - span.start;
|
|
44
|
+
const sig = span.signature.replace(/\s*\{\s*$/, "");
|
|
45
|
+
return String(span.start).padStart(5) + " " + sig + (body > 0 ? " … " + body + " lines" : "");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function expandedBlock(span, raw, opts) {
|
|
49
|
+
const out = [];
|
|
50
|
+
for (let l = span.start; l <= span.end; l++) out.push(String(l).padStart(5) + " " + raw[l - 1]);
|
|
51
|
+
const refs = opts.references ? opts.references(span.name, span.start) : [];
|
|
52
|
+
// Who uses this declaration: the relation a reader would otherwise grep for next.
|
|
53
|
+
if (refs.length) out.push(" // used by: " + refs.slice(0, opts.maxRefs).join(", ") + (refs.length > opts.maxRefs ? " (+" + (refs.length - opts.maxRefs) + ")" : ""));
|
|
54
|
+
return out.join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param entry index entry (text + cached lines/surface)
|
|
59
|
+
* @param about question or symbol; empty ⇒ pure skeleton (every body folded)
|
|
60
|
+
*/
|
|
61
|
+
export function outlineFile(entry, relPath, about, options = {}) {
|
|
62
|
+
const opts = { ...OUTLINE_DEFAULTS, ...options };
|
|
63
|
+
const { raw, lower } = WorkspaceIndex.linesOf(entry);
|
|
64
|
+
const lineCount = raw.length;
|
|
65
|
+
const spans = WorkspaceIndex.spansOf(entry).map((s) => ({ ...s, signature: raw[s.start - 1].trim() }));
|
|
66
|
+
if (spans.length === 0) return null; // no structure: caller falls back to plain text
|
|
67
|
+
const stems = [...new Set(tokenizeQuery(about || "").tokens.map(stem))];
|
|
68
|
+
const expanded = chooseExpanded(spans, lower, stems, raw, opts);
|
|
69
|
+
|
|
70
|
+
const parts = [];
|
|
71
|
+
const headerEnd = Math.min(spans[0].start - 1, opts.headerLines);
|
|
72
|
+
if (headerEnd > 0) {
|
|
73
|
+
const header = raw.slice(0, headerEnd).filter((l) => l.trim());
|
|
74
|
+
if (header.length) parts.push(header.map((l, i) => String(i + 1).padStart(5) + " " + l).join("\n"));
|
|
75
|
+
if (spans[0].start - 1 > opts.headerLines) parts.push(" … " + (spans[0].start - 1 - opts.headerLines) + " more header lines");
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < spans.length; i++) parts.push(expanded.has(i) ? expandedBlock(spans[i], raw, opts) : foldedLine(spans[i]));
|
|
78
|
+
const title = "// " + relPath + " · " + lineCount + " lines · " + spans.length + " declarations · " + expanded.size + " expanded" + (about ? " for \"" + about + "\"" : "") + " · read(path, line, count) for a folded body";
|
|
79
|
+
return { text: title + "\n" + parts.join("\n"), expanded: expanded.size, declarations: spans.length };
|
|
80
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Dual-host CodeMode for Pi/OMP: progressive tool discovery, result bottleneck, and Amdahl Auto parallel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AdityaVG13",
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
"guest-worker.js",
|
|
33
33
|
"repo-index.js",
|
|
34
34
|
"evidence.js",
|
|
35
|
+
"fuzzy.js",
|
|
36
|
+
"outline.js",
|
|
37
|
+
"ledger.js",
|
|
38
|
+
"check.js",
|
|
39
|
+
"search.js",
|
|
35
40
|
"format.js",
|
|
36
41
|
"patch.js",
|
|
37
42
|
"vfs.js",
|
|
@@ -85,5 +90,9 @@
|
|
|
85
90
|
"bugs": {
|
|
86
91
|
"url": "https://github.com/AdityaVG13/pi-stack/issues"
|
|
87
92
|
},
|
|
88
|
-
"homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme"
|
|
93
|
+
"homepage": "https://github.com/AdityaVG13/pi-stack/tree/main/packages/pi-supernova#readme",
|
|
94
|
+
"dependencies": {
|
|
95
|
+
"acorn": "^8.18.0",
|
|
96
|
+
"string-width": "^8.2.2"
|
|
97
|
+
}
|
|
89
98
|
}
|