pi-supernova 0.0.1
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +133 -0
- package/bottleneck.js +132 -0
- package/catalog.js +113 -0
- package/config.default.json +14 -0
- package/config.js +75 -0
- package/decode.js +16 -0
- package/diff.js +107 -0
- package/host-bridge.js +822 -0
- package/index.js +227 -0
- package/package.json +89 -0
- package/parallel.js +48 -0
- package/render.js +584 -0
- package/runtime.js +243 -0
- package/snap.js +225 -0
- package/surface.js +127 -0
package/runtime.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
|
|
2
|
+
import { packageFinalReturn } from "./bottleneck.js";
|
|
3
|
+
import { parallel as runParallel, pipeline as runPipeline } from "./parallel.js";
|
|
4
|
+
import { performance } from "node:perf_hooks";
|
|
5
|
+
import { isString, isFunction, isObject } from "./decode.js";
|
|
6
|
+
|
|
7
|
+
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
8
|
+
|
|
9
|
+
const compiledCache = new Map();
|
|
10
|
+
const COMPILED_CACHE_MAX = 256;
|
|
11
|
+
|
|
12
|
+
function wrapBody(code) {
|
|
13
|
+
const trimmed = String(code || "").trim();
|
|
14
|
+
if (!trimmed) throw new Error("code must be a non-empty string");
|
|
15
|
+
|
|
16
|
+
if (/^(async\s*)?(?:\([^)]*\)|[A-Za-z_$][\w$]*)\s*=>/.test(trimmed)) {
|
|
17
|
+
return `const __fn = (${trimmed});\nreturn await __fn();`;
|
|
18
|
+
}
|
|
19
|
+
if (/^async\s+function\b/.test(trimmed) || /^function\b/.test(trimmed)) {
|
|
20
|
+
return `const __fn = (${trimmed});\nreturn await __fn();`;
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function runGuestProgram(options) {
|
|
26
|
+
const { code, nova, config, signal, onTimeout } = options;
|
|
27
|
+
const maxCode = config.maxCodeChars ?? 48000;
|
|
28
|
+
if (code.length > maxCode) {
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
error: `code exceeds ${maxCode} characters`,
|
|
32
|
+
logs: [],
|
|
33
|
+
wallMs: 0,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const logs = [];
|
|
38
|
+
const started = performance.now();
|
|
39
|
+
const timeoutMs = config.timeoutMs ?? 60000;
|
|
40
|
+
|
|
41
|
+
const scopedConsole = {
|
|
42
|
+
log: (...args) => pushLog(logs, args, config),
|
|
43
|
+
warn: (...args) => pushLog(logs, args, config),
|
|
44
|
+
error: (...args) => pushLog(logs, args, config),
|
|
45
|
+
info: (...args) => pushLog(logs, args, config),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let body;
|
|
49
|
+
try {
|
|
50
|
+
body = wrapBody(code);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
ok: false,
|
|
54
|
+
error: err instanceof Error ? err.message : String(err),
|
|
55
|
+
logs,
|
|
56
|
+
wallMs: Math.round(performance.now() - started),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let compiled = compiledCache.get(body);
|
|
61
|
+
if (!compiled) {
|
|
62
|
+
compiled = new AsyncFunction(
|
|
63
|
+
"nova",
|
|
64
|
+
"tools",
|
|
65
|
+
"console",
|
|
66
|
+
"parallel",
|
|
67
|
+
"pipeline",
|
|
68
|
+
"read",
|
|
69
|
+
"write",
|
|
70
|
+
"edit",
|
|
71
|
+
"patch",
|
|
72
|
+
"surface",
|
|
73
|
+
"snap",
|
|
74
|
+
"bash",
|
|
75
|
+
"exec",
|
|
76
|
+
"speculate",
|
|
77
|
+
body,
|
|
78
|
+
);
|
|
79
|
+
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
80
|
+
const first = compiledCache.keys().next().value;
|
|
81
|
+
if (first !== undefined) compiledCache.delete(first);
|
|
82
|
+
}
|
|
83
|
+
compiledCache.set(body, compiled);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const abortError = new Error("supernova timed out or aborted");
|
|
87
|
+
const timeoutPromise = sleepReject(timeoutMs, abortError, signal, () => {
|
|
88
|
+
try {
|
|
89
|
+
onTimeout?.();
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const unwrapValue = (res) => {
|
|
95
|
+
if (res && isObject(res) && "value" in res) {
|
|
96
|
+
if (res.details?.isSnap && isString(res.value)) {
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(res.value);
|
|
99
|
+
} catch {
|
|
100
|
+
return res.value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (res.details?.batch && Array.isArray(res.details?.items)) {
|
|
104
|
+
return res.details.items;
|
|
105
|
+
}
|
|
106
|
+
return res.value;
|
|
107
|
+
}
|
|
108
|
+
return res;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const guestRead = async (p, off, lim) => {
|
|
112
|
+
if (Array.isArray(p)) {
|
|
113
|
+
return await Promise.all(p.map((item) => guestRead(item, off, lim)));
|
|
114
|
+
}
|
|
115
|
+
const res = await nova.call("read", { path: p, offset: off, limit: lim });
|
|
116
|
+
return unwrapValue(res);
|
|
117
|
+
};
|
|
118
|
+
const guestWrite = async (p, c) => unwrapValue(await nova.call("write", { path: p, content: c }));
|
|
119
|
+
const guestEdit = async (p, oldOrDiff, newText) => {
|
|
120
|
+
const res = await nova.call("edit", { path: p, oldText: oldOrDiff, newText });
|
|
121
|
+
return unwrapValue(res);
|
|
122
|
+
};
|
|
123
|
+
const guestPatch = async (p, d) => unwrapValue(await nova.call("apply_patch", { path: p, patch: d }));
|
|
124
|
+
const guestSurface = async (p) => {
|
|
125
|
+
const res = await (isFunction(nova.surface) ? nova.surface(p) : nova.call("surface", { path: p }));
|
|
126
|
+
return unwrapValue(res);
|
|
127
|
+
};
|
|
128
|
+
const guestSnap = async (q, p) => {
|
|
129
|
+
const res = await (isFunction(nova.snap) ? nova.snap(q, p) : nova.call("snap", { query: q, path: p }));
|
|
130
|
+
return unwrapValue(res);
|
|
131
|
+
};
|
|
132
|
+
const guestBash = async (cmd, opts) => {
|
|
133
|
+
const res = await nova.call("bash", { command: cmd, ...opts });
|
|
134
|
+
if (res?.ok === false) {
|
|
135
|
+
const detail = isString(res?.details) ? res.details : "";
|
|
136
|
+
throw new Error(res?.value || detail || `command failed: ${cmd}`);
|
|
137
|
+
}
|
|
138
|
+
return unwrapValue(res);
|
|
139
|
+
};
|
|
140
|
+
const quoteShellArg = (value) => `'${String(value).replaceAll("'", "'\\''")}'`;
|
|
141
|
+
const guestExec = async (cmd, args, opts) => {
|
|
142
|
+
const argv = [cmd, ...(Array.isArray(args) ? args : [])].map(quoteShellArg).join(" ");
|
|
143
|
+
return guestBash(argv, opts);
|
|
144
|
+
};
|
|
145
|
+
const guestSpeculate = async (fn) => (isFunction(nova.speculate) ? nova.speculate(fn) : fn());
|
|
146
|
+
|
|
147
|
+
let settled = false;
|
|
148
|
+
const runPromise = Promise.resolve(
|
|
149
|
+
compiled(
|
|
150
|
+
nova,
|
|
151
|
+
nova,
|
|
152
|
+
scopedConsole,
|
|
153
|
+
runParallel,
|
|
154
|
+
runPipeline,
|
|
155
|
+
guestRead,
|
|
156
|
+
guestWrite,
|
|
157
|
+
guestEdit,
|
|
158
|
+
guestPatch,
|
|
159
|
+
guestSurface,
|
|
160
|
+
guestSnap,
|
|
161
|
+
guestBash,
|
|
162
|
+
guestExec,
|
|
163
|
+
guestSpeculate,
|
|
164
|
+
),
|
|
165
|
+
);
|
|
166
|
+
runPromise.catch((err) => {
|
|
167
|
+
if (!settled) return;
|
|
168
|
+
pushLog(logs, [`[late guest error] ${err instanceof Error ? err.message : String(err)}`], config);
|
|
169
|
+
});
|
|
170
|
+
timeoutPromise.catch(() => {
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const resultValue = await Promise.race([runPromise, timeoutPromise]);
|
|
175
|
+
settled = true;
|
|
176
|
+
const packaged = packageFinalReturn(resultValue, logs, config);
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
result: packaged.returnValue,
|
|
180
|
+
resultText: packaged.returnText,
|
|
181
|
+
returnTruncated: packaged.returnTruncated,
|
|
182
|
+
logs: packaged.logs,
|
|
183
|
+
logTruncated: packaged.logTruncated,
|
|
184
|
+
wallMs: Math.round(performance.now() - started),
|
|
185
|
+
};
|
|
186
|
+
} catch (err) {
|
|
187
|
+
settled = true;
|
|
188
|
+
return {
|
|
189
|
+
ok: false,
|
|
190
|
+
error: err instanceof Error ? err.message : String(err),
|
|
191
|
+
logs,
|
|
192
|
+
wallMs: Math.round(performance.now() - started),
|
|
193
|
+
};
|
|
194
|
+
} finally {
|
|
195
|
+
settled = true;
|
|
196
|
+
timeoutPromise.clear();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function pushLog(logs, args, config) {
|
|
201
|
+
const maxLines = config.maxLogLines ?? 100;
|
|
202
|
+
if (logs.length >= maxLines) return;
|
|
203
|
+
const line = args
|
|
204
|
+
.map((a) => {
|
|
205
|
+
if (isString(a)) return a;
|
|
206
|
+
try {
|
|
207
|
+
return JSON.stringify(a);
|
|
208
|
+
} catch {
|
|
209
|
+
return String(a);
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
.join(" ");
|
|
213
|
+
logs.push(line);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sleepReject(ms, error, signal, onFire) {
|
|
217
|
+
let timer;
|
|
218
|
+
let onAbort;
|
|
219
|
+
const fire = (reject) => {
|
|
220
|
+
try {
|
|
221
|
+
onFire?.();
|
|
222
|
+
} catch {
|
|
223
|
+
}
|
|
224
|
+
reject(error);
|
|
225
|
+
};
|
|
226
|
+
const promise = new Promise((_, reject) => {
|
|
227
|
+
timer = setTimeout(() => fire(reject), ms);
|
|
228
|
+
if (timer.unref) timer.unref();
|
|
229
|
+
if (signal) {
|
|
230
|
+
onAbort = () => {
|
|
231
|
+
clearTimeout(timer);
|
|
232
|
+
fire(reject);
|
|
233
|
+
};
|
|
234
|
+
if (signal.aborted) onAbort();
|
|
235
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
promise.clear = () => {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
if (signal && onAbort) signal.removeEventListener("abort", onAbort);
|
|
241
|
+
};
|
|
242
|
+
return promise;
|
|
243
|
+
}
|
package/snap.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { isString } from "./decode.js";
|
|
4
|
+
import { extractStructuralSurface } from "./surface.js";
|
|
5
|
+
|
|
6
|
+
const STOP_WORDS = new Set([
|
|
7
|
+
"the", "a", "an", "and", "or", "in", "on", "at", "to", "for", "of", "with",
|
|
8
|
+
"by", "from", "is", "it", "this", "that", "where", "how", "what", "which",
|
|
9
|
+
"file", "code", "function", "class", "method", "find", "get", "look",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
export function tokenizeQuery(query) {
|
|
13
|
+
if (!isString(query) || !query.trim()) {
|
|
14
|
+
return { tokens: [], wantsTest: false, wantsType: false, wantsDoc: false };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const raw = query
|
|
18
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.split(/[^a-zA-Z0-9_]+/);
|
|
21
|
+
|
|
22
|
+
const tokens = raw.filter((t) => t.length > 1 && !STOP_WORDS.has(t));
|
|
23
|
+
const queryLower = query.toLowerCase();
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
tokens: [...new Set(tokens)],
|
|
27
|
+
wantsTest: queryLower.includes("test") || queryLower.includes("spec"),
|
|
28
|
+
wantsType: queryLower.includes("type") || queryLower.includes("interface") || queryLower.includes("schema"),
|
|
29
|
+
wantsDoc: queryLower.includes("doc") || queryLower.includes("readme"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function scorePathTopology(filePath, tokens, { wantsTest, wantsDoc, wantsType }) {
|
|
34
|
+
const norm = filePath.replace(/\\/g, "/").toLowerCase();
|
|
35
|
+
const basename = path.basename(norm);
|
|
36
|
+
const ext = path.extname(norm);
|
|
37
|
+
|
|
38
|
+
const isTest = norm.includes("test") || norm.includes("spec") || norm.includes("__tests__");
|
|
39
|
+
if (isTest && !wantsTest) return -50;
|
|
40
|
+
if (!isTest && wantsTest) return -20;
|
|
41
|
+
|
|
42
|
+
if (norm.includes("node_modules/") || norm.includes("dist/") || norm.includes("target/")) {
|
|
43
|
+
return -100;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let score = 0;
|
|
47
|
+
const pathParts = norm.split(/[^a-zA-Z0-9]+/);
|
|
48
|
+
|
|
49
|
+
for (const token of tokens) {
|
|
50
|
+
if (basename === token || basename.startsWith(token + ".")) score += 60;
|
|
51
|
+
else if (basename.includes(token)) score += 30;
|
|
52
|
+
else if (pathParts.includes(token)) score += 15;
|
|
53
|
+
else if (norm.includes(token)) score += 5;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if ([".ts", ".js", ".mjs", ".rs", ".py", ".go"].includes(ext) && !wantsDoc) {
|
|
57
|
+
score += 5;
|
|
58
|
+
}
|
|
59
|
+
if (wantsType && [".ts", ".d.ts", ".rs", ".go"].includes(ext)) {
|
|
60
|
+
score += 10;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return score;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function scoreContentDefinitions(content, tokens) {
|
|
67
|
+
const lines = content.split("\n");
|
|
68
|
+
let score = 0;
|
|
69
|
+
let bestLine = 1;
|
|
70
|
+
let bestLineScore = 0;
|
|
71
|
+
|
|
72
|
+
const defRegex = /^(?:pub\s+)?(?:export\s+)?(?:async\s+)?(?:default\s+)?(function|class|def|fn|const|let|interface|type|struct|enum)\s+([a-zA-Z0-9_$]+)/;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i].trim();
|
|
76
|
+
if (!line || line.startsWith("//") || line.startsWith("#") || line.startsWith("*")) continue;
|
|
77
|
+
|
|
78
|
+
let lineScore = 0;
|
|
79
|
+
const isDef = defRegex.test(line);
|
|
80
|
+
|
|
81
|
+
for (const token of tokens) {
|
|
82
|
+
if (line.toLowerCase().includes(token)) {
|
|
83
|
+
lineScore += isDef ? 40 : 5;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (lineScore > bestLineScore) {
|
|
88
|
+
bestLineScore = lineScore;
|
|
89
|
+
bestLine = i + 1;
|
|
90
|
+
}
|
|
91
|
+
score += lineScore;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { totalScore: score, bestLine, bestLineScore };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function executeSnap({ query, searchDir, vfs, runCommand }) {
|
|
98
|
+
const { tokens, wantsTest, wantsType, wantsDoc } = tokenizeQuery(query);
|
|
99
|
+
if (tokens.length === 0) {
|
|
100
|
+
throw new Error("snap requires at least one searchable concept keyword");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const dir = searchDir || process.cwd();
|
|
104
|
+
let fileList = [];
|
|
105
|
+
try {
|
|
106
|
+
const res = await runCommand(["rg", "--files", dir], { timeoutMs: 15_000 });
|
|
107
|
+
fileList = res.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
108
|
+
} catch {
|
|
109
|
+
fileList = [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (fileList.length === 0) {
|
|
113
|
+
throw new Error(`no files found to search in ${dir}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const scoredPaths = [];
|
|
117
|
+
for (const f of fileList) {
|
|
118
|
+
const score = scorePathTopology(f, tokens, { wantsTest, wantsDoc, wantsType });
|
|
119
|
+
if (score > 0) scoredPaths.push({ path: f, score });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
scoredPaths.sort((a, b) => b.score - a.score);
|
|
123
|
+
|
|
124
|
+
let candidates = scoredPaths.filter((p) => p.score >= 25).slice(0, 10).map((p) => p.path);
|
|
125
|
+
|
|
126
|
+
if (candidates.length < 5) {
|
|
127
|
+
try {
|
|
128
|
+
const grepArgs = ["-l", "--max-count=1"];
|
|
129
|
+
if (!wantsTest) {
|
|
130
|
+
grepArgs.push("-g", "!test/**", "-g", "!tests/**", "-g", "!*.test.*", "-g", "!*.spec.*");
|
|
131
|
+
}
|
|
132
|
+
const salient = tokens.filter((t) => t.length > 2).slice(0, 4);
|
|
133
|
+
for (const t of salient) grepArgs.push("-e", t);
|
|
134
|
+
const res = await runCommand(["rg", ...grepArgs, dir], { timeoutMs: 15_000 });
|
|
135
|
+
const grepHits = res.stdout.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
136
|
+
const seen = new Set(candidates);
|
|
137
|
+
for (const h of grepHits) {
|
|
138
|
+
if (!seen.has(h)) {
|
|
139
|
+
seen.add(h);
|
|
140
|
+
candidates.push(h);
|
|
141
|
+
if (candidates.length >= 15) break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
} catch {
|
|
145
|
+
if (candidates.length === 0) candidates = fileList.slice(0, 5);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const candidateScores = [];
|
|
150
|
+
|
|
151
|
+
for (const filePath of candidates) {
|
|
152
|
+
let content = "";
|
|
153
|
+
try {
|
|
154
|
+
content = await vfs.read(filePath);
|
|
155
|
+
} catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const { totalScore, bestLine, bestLineScore } = scoreContentDefinitions(content, tokens);
|
|
160
|
+
const ext = path.extname(filePath);
|
|
161
|
+
const surface = extractStructuralSurface(content, ext);
|
|
162
|
+
|
|
163
|
+
let surfaceBonus = 0;
|
|
164
|
+
let signature = "";
|
|
165
|
+
let anchorLine = bestLine;
|
|
166
|
+
|
|
167
|
+
for (const item of surface.items) {
|
|
168
|
+
const nameLower = item.name.toLowerCase();
|
|
169
|
+
for (const token of tokens) {
|
|
170
|
+
if (nameLower.includes(token)) {
|
|
171
|
+
surfaceBonus += item.isExport ? 80 : 50;
|
|
172
|
+
if (!signature) {
|
|
173
|
+
signature = item.signature;
|
|
174
|
+
anchorLine = item.line;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const isTestFile = filePath.toLowerCase().includes("test") || filePath.toLowerCase().includes("spec");
|
|
181
|
+
const testAdjustment = isTestFile && !wantsTest ? -200 : (isTestFile && wantsTest ? 100 : 0);
|
|
182
|
+
const finalScore = totalScore + surfaceBonus + (bestLineScore * 2) + testAdjustment;
|
|
183
|
+
candidateScores.push({
|
|
184
|
+
path: filePath,
|
|
185
|
+
score: finalScore,
|
|
186
|
+
anchorLine,
|
|
187
|
+
signature,
|
|
188
|
+
content,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
candidateScores.sort((a, b) => b.score - a.score);
|
|
193
|
+
|
|
194
|
+
if (candidateScores.length === 0 || candidateScores[0].score <= 0) {
|
|
195
|
+
const fallbackPath = candidates[0] || fileList[0];
|
|
196
|
+
return {
|
|
197
|
+
path: fallbackPath,
|
|
198
|
+
line: 1,
|
|
199
|
+
signature: "",
|
|
200
|
+
confidence: 0.3,
|
|
201
|
+
context: [],
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const best = candidateScores[0];
|
|
206
|
+
const lines = best.content.split("\n");
|
|
207
|
+
const startLine = Math.max(1, best.anchorLine - 3);
|
|
208
|
+
const endLine = Math.min(lines.length, best.anchorLine + 8);
|
|
209
|
+
|
|
210
|
+
const contextLines = [];
|
|
211
|
+
for (let l = startLine; l <= endLine; l++) {
|
|
212
|
+
const marker = l === best.anchorLine ? "►" : " ";
|
|
213
|
+
contextLines.push(`${marker} ${String(l).padStart(4)} │ ${lines[l - 1]}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const confidence = Math.min(0.98, Math.max(0.65, best.score / 150));
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
path: best.path,
|
|
220
|
+
line: best.anchorLine,
|
|
221
|
+
signature: best.signature,
|
|
222
|
+
confidence: Number(confidence.toFixed(2)),
|
|
223
|
+
context: contextLines,
|
|
224
|
+
};
|
|
225
|
+
}
|
package/surface.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
|
|
2
|
+
import { isString } from "./decode.js";
|
|
3
|
+
|
|
4
|
+
function scanPython(lines) {
|
|
5
|
+
const items = [];
|
|
6
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7
|
+
const line = lines[i];
|
|
8
|
+
const match = /^([ \t]*)(def|class|async def)\s+([a-zA-Z0-9_]+)(\(.*?\))?:?/.exec(line);
|
|
9
|
+
if (!match) continue;
|
|
10
|
+
items.push({
|
|
11
|
+
kind: match[2].includes("def") ? "function" : "class",
|
|
12
|
+
name: match[3],
|
|
13
|
+
signature: match[0].trim(),
|
|
14
|
+
line: i + 1,
|
|
15
|
+
depth: Math.floor(match[1].length / 4),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return items;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function scanRust(lines) {
|
|
22
|
+
const items = [];
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
const line = lines[i].trim();
|
|
25
|
+
const match = /^(pub\s+)?(async\s+)?(fn|struct|enum|trait|impl|type|const)\s+([a-zA-Z0-9_]+)(<.*?>)?(\(.*?\))?/.exec(line);
|
|
26
|
+
if (!match) continue;
|
|
27
|
+
items.push({
|
|
28
|
+
kind: match[3],
|
|
29
|
+
name: match[4],
|
|
30
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
31
|
+
line: i + 1,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return items;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function scanGo(lines) {
|
|
38
|
+
const items = [];
|
|
39
|
+
for (let i = 0; i < lines.length; i++) {
|
|
40
|
+
const line = lines[i].trim();
|
|
41
|
+
const funcMatch = /^func\s+(\(.*?\)\s+)?([a-zA-Z0-9_]+)(\(.*?\))/.exec(line);
|
|
42
|
+
if (funcMatch) {
|
|
43
|
+
items.push({
|
|
44
|
+
kind: "function",
|
|
45
|
+
name: funcMatch[2],
|
|
46
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
47
|
+
line: i + 1,
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const typeMatch = /^type\s+([a-zA-Z0-9_]+)\s+(struct|interface)/.exec(line);
|
|
52
|
+
if (typeMatch) {
|
|
53
|
+
items.push({
|
|
54
|
+
kind: typeMatch[2],
|
|
55
|
+
name: typeMatch[1],
|
|
56
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
57
|
+
line: i + 1,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return items;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function scanJavaScript(lines) {
|
|
65
|
+
const items = [];
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const line = lines[i].trim();
|
|
68
|
+
if (!line || line.startsWith("//") || line.startsWith("/*") || line.startsWith("*")) continue;
|
|
69
|
+
|
|
70
|
+
const expMatch = /^export\s+(?:default\s+)?(?:async\s+)?(function\*?|class|const|let|var|type|interface|enum)\s+([a-zA-Z0-9_$]+)/.exec(line);
|
|
71
|
+
if (expMatch) {
|
|
72
|
+
items.push({
|
|
73
|
+
kind: expMatch[1],
|
|
74
|
+
name: expMatch[2],
|
|
75
|
+
isExport: true,
|
|
76
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
77
|
+
line: i + 1,
|
|
78
|
+
});
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const declMatch = /^(?:async\s+)?(function\*?|class)\s+([a-zA-Z0-9_$]+)/.exec(line);
|
|
83
|
+
if (declMatch) {
|
|
84
|
+
items.push({
|
|
85
|
+
kind: declMatch[1],
|
|
86
|
+
name: declMatch[2],
|
|
87
|
+
isExport: false,
|
|
88
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
89
|
+
line: i + 1,
|
|
90
|
+
});
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const tsMatch = /^(interface|type)\s+([a-zA-Z0-9_$]+)/.exec(line);
|
|
95
|
+
if (tsMatch) {
|
|
96
|
+
items.push({
|
|
97
|
+
kind: tsMatch[1],
|
|
98
|
+
name: tsMatch[2],
|
|
99
|
+
isExport: false,
|
|
100
|
+
signature: line.replace(/\{.*$/, "").trim(),
|
|
101
|
+
line: i + 1,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return items;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const SCANNERS = {
|
|
109
|
+
py: scanPython,
|
|
110
|
+
rs: scanRust,
|
|
111
|
+
go: scanGo,
|
|
112
|
+
js: scanJavaScript,
|
|
113
|
+
ts: scanJavaScript,
|
|
114
|
+
jsx: scanJavaScript,
|
|
115
|
+
tsx: scanJavaScript,
|
|
116
|
+
mjs: scanJavaScript,
|
|
117
|
+
cjs: scanJavaScript,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export function extractStructuralSurface(code, extension = "js") {
|
|
121
|
+
if (!isString(code) || !code.trim()) return { items: [], lineCount: 0 };
|
|
122
|
+
const lines = code.split("\n");
|
|
123
|
+
const ext = extension.replace(/^\./, "").toLowerCase();
|
|
124
|
+
const scanner = SCANNERS[ext] || SCANNERS.js;
|
|
125
|
+
const items = scanner(lines);
|
|
126
|
+
return { items, lineCount: lines.length };
|
|
127
|
+
}
|