universal-ast-mapper 1.27.0 → 2.0.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/BLUEPRINT.md +230 -230
- package/CHANGELOG.md +466 -321
- package/README.md +878 -877
- package/package.json +48 -47
- package/scripts/install-skill.mjs +187 -187
- package/dist/analysis.js +0 -134
- package/dist/callgraph.js +0 -467
- package/dist/check.js +0 -112
- package/dist/cli.js +0 -1275
- package/dist/complexity.js +0 -98
- package/dist/config.js +0 -53
- package/dist/contextpack.js +0 -79
- package/dist/coupling.js +0 -35
- package/dist/crosslang.js +0 -425
- package/dist/diskcache.js +0 -97
- package/dist/explorer.js +0 -123
- package/dist/extractors/c.js +0 -204
- package/dist/extractors/common.js +0 -56
- package/dist/extractors/cpp.js +0 -272
- package/dist/extractors/csharp.js +0 -209
- package/dist/extractors/go.js +0 -212
- package/dist/extractors/java.js +0 -152
- package/dist/extractors/kotlin.js +0 -159
- package/dist/extractors/php.js +0 -208
- package/dist/extractors/python.js +0 -153
- package/dist/extractors/ruby.js +0 -146
- package/dist/extractors/rust.js +0 -249
- package/dist/extractors/swift.js +0 -192
- package/dist/extractors/typescript.js +0 -577
- package/dist/gitdiff.js +0 -178
- package/dist/graph-analysis.js +0 -279
- package/dist/graph.js +0 -165
- package/dist/html.js +0 -326
- package/dist/index.js +0 -1407
- package/dist/layers.js +0 -36
- package/dist/modulecoupling.js +0 -0
- package/dist/parser.js +0 -84
- package/dist/pool.js +0 -114
- package/dist/prompts.js +0 -67
- package/dist/registry.js +0 -87
- package/dist/report.js +0 -187
- package/dist/resolver.js +0 -222
- package/dist/roots.js +0 -47
- package/dist/search.js +0 -68
- package/dist/semantic.js +0 -365
- package/dist/sfc.js +0 -27
- package/dist/skeleton.js +0 -132
- package/dist/sourcemap.js +0 -60
- package/dist/testmap.js +0 -167
- package/dist/tsconfig.js +0 -212
- package/dist/typeflow.js +0 -124
- package/dist/types.js +0 -5
- package/dist/unused-params.js +0 -127
- package/dist/worker.js +0 -27
- package/dist/workspace.js +0 -330
package/dist/semantic.js
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Semantic symbol search — find symbols by *meaning*, not exact name.
|
|
3
|
-
*
|
|
4
|
-
* No embeddings, no network, no model downloads. Pure lexical semantics:
|
|
5
|
-
* 1. Identifier tokenization — camelCase / PascalCase / snake_case /
|
|
6
|
-
* kebab-case / digits / acronym boundaries ("HTTPServer" → http, server).
|
|
7
|
-
* 2. Concept expansion — a built-in thesaurus of programming
|
|
8
|
-
* synonym groups (fetch≈get≈load≈retrieve, remove≈delete≈destroy, …).
|
|
9
|
-
* 3. Light stemming — plural/gerund/past suffixes folded so
|
|
10
|
-
* "parsing" matches "parse", "users" matches "user".
|
|
11
|
-
* 4. BM25-style ranking — rare tokens weigh more (IDF over the
|
|
12
|
-
* scanned corpus); name hits outweigh doc/signature/path hits;
|
|
13
|
-
* direct hits outweigh synonym hits outweigh fuzzy hits.
|
|
14
|
-
*/
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { buildSkeleton, collectSourceFiles } from "./skeleton.js";
|
|
17
|
-
import { resolveOptions, loadProjectConfig } from "./config.js";
|
|
18
|
-
// ─── Synonym groups (programming thesaurus) ────────────────────────────────────
|
|
19
|
-
// Tokens in the same group are considered semantically equivalent (at a small
|
|
20
|
-
// penalty vs. a direct match). Keep each group tight — over-broad groups cause
|
|
21
|
-
// noisy results.
|
|
22
|
-
const SYNONYM_GROUPS = [
|
|
23
|
-
["get", "fetch", "load", "retrieve", "read", "lookup", "resolve"],
|
|
24
|
-
["set", "update", "write", "assign", "put", "patch", "modify", "change", "edit"],
|
|
25
|
-
["create", "make", "build", "new", "generate", "construct", "init", "initialize", "spawn"],
|
|
26
|
-
["delete", "remove", "destroy", "drop", "clear", "purge", "erase"],
|
|
27
|
-
["find", "search", "query", "locate", "match", "scan", "discover"],
|
|
28
|
-
["send", "dispatch", "emit", "publish", "post", "broadcast", "notify"],
|
|
29
|
-
["receive", "consume", "subscribe", "listen", "handle", "process"],
|
|
30
|
-
["start", "begin", "launch", "run", "execute", "invoke", "trigger"],
|
|
31
|
-
["stop", "end", "halt", "kill", "terminate", "cancel", "abort", "shutdown", "close"],
|
|
32
|
-
["check", "validate", "verify", "test", "assert", "ensure", "confirm"],
|
|
33
|
-
["parse", "decode", "deserialize", "unmarshal", "extract", "tokenize"],
|
|
34
|
-
["format", "encode", "serialize", "marshal", "stringify", "render", "print"],
|
|
35
|
-
["convert", "transform", "map", "translate", "cast", "normalize"],
|
|
36
|
-
["user", "account", "member", "person", "profile", "customer"],
|
|
37
|
-
["auth", "authenticate", "login", "signin", "authorize", "session", "credential"],
|
|
38
|
-
["config", "configuration", "settings", "options", "preferences", "setup"],
|
|
39
|
-
["error", "exception", "fault", "failure", "err", "panic"],
|
|
40
|
-
["log", "logger", "logging", "trace", "audit"],
|
|
41
|
-
["cache", "memo", "memoize", "store", "buffer"],
|
|
42
|
-
["list", "enumerate", "all", "collection", "array", "items"],
|
|
43
|
-
["count", "total", "sum", "aggregate", "tally"],
|
|
44
|
-
["file", "document", "path", "filename"],
|
|
45
|
-
["dir", "directory", "folder"],
|
|
46
|
-
["request", "req", "call", "http"],
|
|
47
|
-
["response", "res", "reply", "result", "output"],
|
|
48
|
-
["message", "msg", "event", "signal"],
|
|
49
|
-
["connect", "connection", "link", "attach", "bind", "join"],
|
|
50
|
-
["disconnect", "detach", "unbind", "release", "unsubscribe"],
|
|
51
|
-
["save", "persist", "commit", "flush", "sync"],
|
|
52
|
-
["copy", "clone", "duplicate", "snapshot"],
|
|
53
|
-
["merge", "combine", "concat", "union", "join"],
|
|
54
|
-
["split", "divide", "partition", "chunk", "segment"],
|
|
55
|
-
["sort", "order", "rank", "arrange"],
|
|
56
|
-
["filter", "select", "exclude", "where"],
|
|
57
|
-
["compare", "diff", "equal", "equals", "cmp"],
|
|
58
|
-
["compute", "calculate", "calc", "derive", "evaluate", "measure"],
|
|
59
|
-
["watch", "observe", "monitor", "track", "poll"],
|
|
60
|
-
["wait", "sleep", "delay", "debounce", "throttle", "defer"],
|
|
61
|
-
["retry", "attempt", "backoff"],
|
|
62
|
-
["lock", "mutex", "semaphore", "guard"],
|
|
63
|
-
["queue", "stack", "heap", "pool", "buffer"],
|
|
64
|
-
["graph", "tree", "node", "edge", "vertex"],
|
|
65
|
-
["dependency", "dep", "import", "require"],
|
|
66
|
-
["token", "symbol", "identifier", "ident", "name"],
|
|
67
|
-
["database", "db", "storage", "repository", "repo", "dao"],
|
|
68
|
-
["key", "id", "identifier", "uuid", "guid"],
|
|
69
|
-
["string", "str", "text", "char"],
|
|
70
|
-
["number", "num", "int", "integer", "float", "numeric"],
|
|
71
|
-
["boolean", "bool", "flag", "toggle"],
|
|
72
|
-
["helper", "util", "utility", "utils", "tool", "common"],
|
|
73
|
-
["test", "spec", "mock", "stub", "fixture"],
|
|
74
|
-
["render", "draw", "paint", "display", "show", "view"],
|
|
75
|
-
["hide", "conceal", "mask", "suppress"],
|
|
76
|
-
["enable", "activate", "on"],
|
|
77
|
-
["disable", "deactivate", "off"],
|
|
78
|
-
["add", "insert", "append", "push", "register"],
|
|
79
|
-
["pop", "shift", "dequeue", "take"],
|
|
80
|
-
["circular", "cycle", "cyclic", "loop", "recursive"],
|
|
81
|
-
["dead", "unused", "orphan", "unreachable", "stale"],
|
|
82
|
-
["complexity", "complex", "cyclomatic", "cognitive"],
|
|
83
|
-
["coupling", "cohesion", "instability", "afferent", "efferent"],
|
|
84
|
-
];
|
|
85
|
-
const GROUP_OF = new Map();
|
|
86
|
-
SYNONYM_GROUPS.forEach((group, gi) => {
|
|
87
|
-
for (const word of group) {
|
|
88
|
-
// Register both raw and stemmed forms so stemmed corpus/query tokens
|
|
89
|
-
// ("setting", "item") still hit groups declared as "settings", "items".
|
|
90
|
-
for (const form of new Set([word, stem(word)])) {
|
|
91
|
-
const list = GROUP_OF.get(form);
|
|
92
|
-
if (list) {
|
|
93
|
-
if (!list.includes(gi))
|
|
94
|
-
list.push(gi);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
GROUP_OF.set(form, [gi]);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
});
|
|
102
|
-
// ─── Tokenization ──────────────────────────────────────────────────────────────
|
|
103
|
-
/** Light stemmer: fold common English suffixes so "parsing"→"parse", "users"→"user". */
|
|
104
|
-
export function stem(word) {
|
|
105
|
-
let w = word;
|
|
106
|
-
if (w.length > 4 && w.endsWith("ies"))
|
|
107
|
-
return w.slice(0, -3) + "y";
|
|
108
|
-
if (w.length > 4 && w.endsWith("ing")) {
|
|
109
|
-
w = w.slice(0, -3);
|
|
110
|
-
// "mapping" → "mapp" → "map"; "parsing" → "pars" → add back "e"? keep both simple:
|
|
111
|
-
if (w.length > 2 && w[w.length - 1] === w[w.length - 2])
|
|
112
|
-
w = w.slice(0, -1);
|
|
113
|
-
return w;
|
|
114
|
-
}
|
|
115
|
-
if (w.length > 4 && w.endsWith("ed")) {
|
|
116
|
-
w = w.slice(0, -2);
|
|
117
|
-
if (w.length > 2 && w[w.length - 1] === w[w.length - 2])
|
|
118
|
-
w = w.slice(0, -1);
|
|
119
|
-
return w;
|
|
120
|
-
}
|
|
121
|
-
if (w.length > 3 && w.endsWith("es"))
|
|
122
|
-
return w.slice(0, -2);
|
|
123
|
-
if (w.length > 3 && w.endsWith("s") && !w.endsWith("ss"))
|
|
124
|
-
return w.slice(0, -1);
|
|
125
|
-
return w;
|
|
126
|
-
}
|
|
127
|
-
/**
|
|
128
|
-
* Split an identifier into lowercase word tokens.
|
|
129
|
-
* Handles camelCase, PascalCase, snake_case, kebab-case, dots, digits and
|
|
130
|
-
* acronym boundaries: "getHTTPServerByID" → [get, http, server, by, id].
|
|
131
|
-
*/
|
|
132
|
-
export function splitIdentifier(identifier) {
|
|
133
|
-
const out = [];
|
|
134
|
-
for (const chunk of identifier.split(/[^A-Za-z0-9]+/)) {
|
|
135
|
-
if (!chunk)
|
|
136
|
-
continue;
|
|
137
|
-
// Insert boundaries: aA | AAa (acronym→word) | letter↔digit
|
|
138
|
-
const spaced = chunk
|
|
139
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
140
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
141
|
-
.replace(/([A-Za-z])([0-9])/g, "$1 $2")
|
|
142
|
-
.replace(/([0-9])([A-Za-z])/g, "$1 $2");
|
|
143
|
-
for (const word of spaced.split(" ")) {
|
|
144
|
-
if (word)
|
|
145
|
-
out.push(word.toLowerCase());
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
return out;
|
|
149
|
-
}
|
|
150
|
-
/** Levenshtein distance with early exit when > max. */
|
|
151
|
-
function editDistance(a, b, max) {
|
|
152
|
-
if (Math.abs(a.length - b.length) > max)
|
|
153
|
-
return max + 1;
|
|
154
|
-
const prev = new Array(b.length + 1);
|
|
155
|
-
const curr = new Array(b.length + 1);
|
|
156
|
-
for (let j = 0; j <= b.length; j++)
|
|
157
|
-
prev[j] = j;
|
|
158
|
-
for (let i = 1; i <= a.length; i++) {
|
|
159
|
-
curr[0] = i;
|
|
160
|
-
let rowMin = curr[0];
|
|
161
|
-
for (let j = 1; j <= b.length; j++) {
|
|
162
|
-
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
163
|
-
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
164
|
-
if (curr[j] < rowMin)
|
|
165
|
-
rowMin = curr[j];
|
|
166
|
-
}
|
|
167
|
-
if (rowMin > max)
|
|
168
|
-
return max + 1;
|
|
169
|
-
for (let j = 0; j <= b.length; j++)
|
|
170
|
-
prev[j] = curr[j];
|
|
171
|
-
}
|
|
172
|
-
return prev[b.length];
|
|
173
|
-
}
|
|
174
|
-
function sharesGroup(a, b) {
|
|
175
|
-
const ga = GROUP_OF.get(a);
|
|
176
|
-
if (!ga)
|
|
177
|
-
return false;
|
|
178
|
-
const gb = GROUP_OF.get(b);
|
|
179
|
-
if (!gb)
|
|
180
|
-
return false;
|
|
181
|
-
return ga.some((g) => gb.includes(g));
|
|
182
|
-
}
|
|
183
|
-
const FIELD_WEIGHT = { name: 3, doc: 2, signature: 1.5, path: 1, kind: 1 };
|
|
184
|
-
function addToken(doc, raw, weight) {
|
|
185
|
-
const t = stem(raw);
|
|
186
|
-
if (t.length < 2)
|
|
187
|
-
return;
|
|
188
|
-
const existing = doc.tokens.get(t);
|
|
189
|
-
if (existing === undefined || weight > existing)
|
|
190
|
-
doc.tokens.set(t, weight);
|
|
191
|
-
}
|
|
192
|
-
function* flattenDocs(symbols, file, parentName) {
|
|
193
|
-
for (const sym of symbols) {
|
|
194
|
-
const fullName = parentName ? `${parentName}.${sym.name}` : sym.name;
|
|
195
|
-
yield { sym, fullName };
|
|
196
|
-
if (sym.children.length > 0)
|
|
197
|
-
yield* flattenDocs(sym.children, file, fullName);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
function buildDoc(sym, fullName, file) {
|
|
201
|
-
const doc = {
|
|
202
|
-
match: {
|
|
203
|
-
file,
|
|
204
|
-
symbol: fullName,
|
|
205
|
-
kind: sym.kind,
|
|
206
|
-
exported: sym.exported ?? false,
|
|
207
|
-
range: sym.range,
|
|
208
|
-
...(sym.signature ? { signature: sym.signature } : {}),
|
|
209
|
-
},
|
|
210
|
-
tokens: new Map(),
|
|
211
|
-
nameTokens: new Set(),
|
|
212
|
-
};
|
|
213
|
-
for (const t of splitIdentifier(fullName)) {
|
|
214
|
-
addToken(doc, t, FIELD_WEIGHT.name);
|
|
215
|
-
doc.nameTokens.add(stem(t));
|
|
216
|
-
}
|
|
217
|
-
addToken(doc, sym.kind, FIELD_WEIGHT.kind);
|
|
218
|
-
if (sym.doc) {
|
|
219
|
-
for (const t of splitIdentifier(sym.doc))
|
|
220
|
-
addToken(doc, t, FIELD_WEIGHT.doc);
|
|
221
|
-
}
|
|
222
|
-
if (sym.signature) {
|
|
223
|
-
for (const t of splitIdentifier(sym.signature))
|
|
224
|
-
addToken(doc, t, FIELD_WEIGHT.signature);
|
|
225
|
-
}
|
|
226
|
-
for (const seg of file.split("/")) {
|
|
227
|
-
for (const t of splitIdentifier(seg))
|
|
228
|
-
addToken(doc, t, FIELD_WEIGHT.path);
|
|
229
|
-
}
|
|
230
|
-
return doc;
|
|
231
|
-
}
|
|
232
|
-
// ─── Scoring ───────────────────────────────────────────────────────────────────
|
|
233
|
-
const MATCH_WEIGHT = { direct: 1, synonym: 0.7, fuzzy: 0.45 };
|
|
234
|
-
// English/query stopwords — ignored as query concepts.
|
|
235
|
-
const STOPWORDS = new Set([
|
|
236
|
-
"a", "an", "the", "of", "in", "on", "for", "to", "with", "that", "this",
|
|
237
|
-
"is", "are", "be", "and", "or", "by", "from", "at", "it", "its", "as",
|
|
238
|
-
"do", "does", "how", "what", "which", "where", "when", "i", "we", "you",
|
|
239
|
-
"function", "method", "code", "thing", "stuff", "something",
|
|
240
|
-
]);
|
|
241
|
-
/**
|
|
242
|
-
* Search for symbols by meaning across all source files in a directory.
|
|
243
|
-
*
|
|
244
|
-
* @param dirAbs Absolute path of directory to scan.
|
|
245
|
-
* @param query Natural-language-ish query, e.g. "remove expired sessions".
|
|
246
|
-
* @param root Project root (for relative paths in results).
|
|
247
|
-
* @param options limit, kind filter, exportedOnly.
|
|
248
|
-
*/
|
|
249
|
-
export async function semanticSearch(dirAbs, query, root, options = {}) {
|
|
250
|
-
const { limit = 20, kind, exportedOnly = false } = options;
|
|
251
|
-
// Query concepts: tokenized, stopword-filtered, stemmed (dedup, keep order).
|
|
252
|
-
const concepts = [];
|
|
253
|
-
for (const raw of splitIdentifier(query)) {
|
|
254
|
-
if (STOPWORDS.has(raw))
|
|
255
|
-
continue;
|
|
256
|
-
const t = stem(raw);
|
|
257
|
-
if (t.length >= 2 && !concepts.includes(t))
|
|
258
|
-
concepts.push(t);
|
|
259
|
-
}
|
|
260
|
-
if (concepts.length === 0)
|
|
261
|
-
return [];
|
|
262
|
-
// Build corpus (detail "full" so doc comments and signatures are available).
|
|
263
|
-
const opts = resolveOptions({ detail: "full", emitHtml: false }, loadProjectConfig(root));
|
|
264
|
-
const files = collectSourceFiles(dirAbs, opts);
|
|
265
|
-
const docs = [];
|
|
266
|
-
for (const file of files) {
|
|
267
|
-
const fileRel = path.relative(root, file).split(path.sep).join("/");
|
|
268
|
-
try {
|
|
269
|
-
const skel = await buildSkeleton(file, fileRel, opts);
|
|
270
|
-
for (const { sym, fullName } of flattenDocs(skel.symbols, skel.file)) {
|
|
271
|
-
if (kind && sym.kind !== kind)
|
|
272
|
-
continue;
|
|
273
|
-
if (exportedOnly && !(sym.exported ?? false))
|
|
274
|
-
continue;
|
|
275
|
-
docs.push(buildDoc(sym, fullName, skel.file));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
// skip unreadable / unparseable files
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
if (docs.length === 0)
|
|
283
|
-
return [];
|
|
284
|
-
// Document frequency per concept (direct-token presence) → BM25-ish IDF.
|
|
285
|
-
const N = docs.length;
|
|
286
|
-
const idf = new Map();
|
|
287
|
-
for (const concept of concepts) {
|
|
288
|
-
let df = 0;
|
|
289
|
-
for (const doc of docs)
|
|
290
|
-
if (doc.tokens.has(concept))
|
|
291
|
-
df++;
|
|
292
|
-
idf.set(concept, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
|
|
293
|
-
}
|
|
294
|
-
const scored = [];
|
|
295
|
-
for (const doc of docs) {
|
|
296
|
-
let score = 0;
|
|
297
|
-
const matchedTerms = [];
|
|
298
|
-
let nameHits = 0;
|
|
299
|
-
for (const concept of concepts) {
|
|
300
|
-
let best = 0;
|
|
301
|
-
let how = null;
|
|
302
|
-
for (const [token, fieldWeight] of doc.tokens) {
|
|
303
|
-
let mw = 0;
|
|
304
|
-
let label = null;
|
|
305
|
-
if (token === concept) {
|
|
306
|
-
mw = MATCH_WEIGHT.direct;
|
|
307
|
-
label = concept;
|
|
308
|
-
}
|
|
309
|
-
else if (sharesGroup(token, concept)) {
|
|
310
|
-
mw = MATCH_WEIGHT.synonym;
|
|
311
|
-
label = `${concept}≈${token}`;
|
|
312
|
-
}
|
|
313
|
-
else if (concept.length >= 4 &&
|
|
314
|
-
token.length >= 4 &&
|
|
315
|
-
editDistance(token, concept, 1) <= 1) {
|
|
316
|
-
mw = MATCH_WEIGHT.fuzzy;
|
|
317
|
-
label = `${concept}~${token}`;
|
|
318
|
-
}
|
|
319
|
-
const contribution = mw * fieldWeight;
|
|
320
|
-
if (contribution > best) {
|
|
321
|
-
best = contribution;
|
|
322
|
-
how = label;
|
|
323
|
-
if (fieldWeight >= FIELD_WEIGHT.name && mw === MATCH_WEIGHT.direct)
|
|
324
|
-
break; // can't beat this
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
if (best > 0 && how) {
|
|
328
|
-
score += best * (idf.get(concept) ?? 1);
|
|
329
|
-
matchedTerms.push(how);
|
|
330
|
-
if (doc.nameTokens.has(concept))
|
|
331
|
-
nameHits++;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
if (matchedTerms.length === 0)
|
|
335
|
-
continue;
|
|
336
|
-
// Bonuses: all concepts matched; full query substring of name; coverage ratio.
|
|
337
|
-
const coverage = matchedTerms.length / concepts.length;
|
|
338
|
-
score *= 0.5 + 0.5 * coverage;
|
|
339
|
-
if (nameHits === concepts.length)
|
|
340
|
-
score *= 1.25;
|
|
341
|
-
const flatQuery = concepts.join("");
|
|
342
|
-
if (doc.match.symbol.toLowerCase().includes(flatQuery))
|
|
343
|
-
score *= 1.2;
|
|
344
|
-
// Length normalization: prefer focused names — "login" beats "handleLogin"
|
|
345
|
-
// when both match the same concepts. Penalize name tokens no concept explains.
|
|
346
|
-
let unmatchedNameTokens = 0;
|
|
347
|
-
for (const t of doc.nameTokens) {
|
|
348
|
-
const explained = concepts.some((c) => t === c ||
|
|
349
|
-
sharesGroup(t, c) ||
|
|
350
|
-
(c.length >= 4 && t.length >= 4 && editDistance(t, c, 1) <= 1));
|
|
351
|
-
if (!explained)
|
|
352
|
-
unmatchedNameTokens++;
|
|
353
|
-
}
|
|
354
|
-
score /= 1 + 0.15 * unmatchedNameTokens;
|
|
355
|
-
scored.push({ ...doc.match, score, matchedTerms });
|
|
356
|
-
}
|
|
357
|
-
scored.sort((a, b) => b.score - a.score || a.symbol.localeCompare(b.symbol));
|
|
358
|
-
const top = scored.slice(0, limit);
|
|
359
|
-
// Normalize scores to 0–1 within the result set.
|
|
360
|
-
const max = top.length > 0 ? top[0].score : 1;
|
|
361
|
-
if (max > 0)
|
|
362
|
-
for (const m of top)
|
|
363
|
-
m.score = Math.round((m.score / max) * 1000) / 1000;
|
|
364
|
-
return top;
|
|
365
|
-
}
|
package/dist/sfc.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
const SCRIPT_RE = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
|
|
2
|
-
export function isSfcExt(ext) {
|
|
3
|
-
return ext === ".vue" || ext === ".svelte";
|
|
4
|
-
}
|
|
5
|
-
export function extractScript(source) {
|
|
6
|
-
// Start from an all-blank canvas the same shape as the source (newlines kept,
|
|
7
|
-
// every other character turned into a space) to preserve line/column offsets.
|
|
8
|
-
let out = source.replace(/[^\n]/g, " ");
|
|
9
|
-
let hasScript = false;
|
|
10
|
-
let lang = "js";
|
|
11
|
-
let m;
|
|
12
|
-
SCRIPT_RE.lastIndex = 0;
|
|
13
|
-
while ((m = SCRIPT_RE.exec(source)) !== null) {
|
|
14
|
-
hasScript = true;
|
|
15
|
-
const attrs = m[1] ?? "";
|
|
16
|
-
if (/lang\s*=\s*["'](ts|typescript)["']/i.test(attrs))
|
|
17
|
-
lang = "ts";
|
|
18
|
-
const inner = m[2] ?? "";
|
|
19
|
-
const innerStart = m.index + m[0].indexOf(inner, m[1] ? m[1].length : 0);
|
|
20
|
-
out = out.slice(0, innerStart) + inner + out.slice(innerStart + inner.length);
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
code: hasScript ? out : "",
|
|
24
|
-
grammar: lang === "ts" ? "typescript" : "javascript",
|
|
25
|
-
hasScript,
|
|
26
|
-
};
|
|
27
|
-
}
|
package/dist/skeleton.js
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { detectLanguage, supportedExtensions } from "./registry.js";
|
|
4
|
-
import { parseSource } from "./parser.js";
|
|
5
|
-
import { countSymbols, toOutline } from "./extractors/common.js";
|
|
6
|
-
import { extractScript } from "./sfc.js";
|
|
7
|
-
import { diskCacheKey, diskCacheGet, diskCachePut } from "./diskcache.js";
|
|
8
|
-
export const SCHEMA_VERSION = "1.1";
|
|
9
|
-
export const GRAMMAR_SOURCE = "tree-sitter-wasms@0.1.13";
|
|
10
|
-
const parseCache = new Map();
|
|
11
|
-
function cacheKey(absPath, detail) { return `${absPath}|${detail}`; }
|
|
12
|
-
function getCached(absPath, detail) {
|
|
13
|
-
try {
|
|
14
|
-
const entry = parseCache.get(cacheKey(absPath, detail));
|
|
15
|
-
if (!entry)
|
|
16
|
-
return null;
|
|
17
|
-
const mtime = fs.statSync(absPath).mtimeMs;
|
|
18
|
-
return entry.mtime === mtime ? entry.result : null;
|
|
19
|
-
}
|
|
20
|
-
catch {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
function setCached(absPath, detail, result) {
|
|
25
|
-
try {
|
|
26
|
-
const mtime = fs.statSync(absPath).mtimeMs;
|
|
27
|
-
parseCache.set(cacheKey(absPath, detail), { mtime, result });
|
|
28
|
-
}
|
|
29
|
-
catch { /* skip if stat fails */ }
|
|
30
|
-
}
|
|
31
|
-
export class UnsupportedLanguageError extends Error {
|
|
32
|
-
ext;
|
|
33
|
-
constructor(ext) {
|
|
34
|
-
super(`Unsupported file type "${ext}". Supported: ${supportedExtensions().join(", ")}`);
|
|
35
|
-
this.ext = ext;
|
|
36
|
-
this.name = "UnsupportedLanguageError";
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Build a skeleton for a single file.
|
|
41
|
-
* @param absPath absolute path on disk (already validated to be within root)
|
|
42
|
-
* @param relPath path relative to root, used as the displayed `file`
|
|
43
|
-
*/
|
|
44
|
-
export async function buildSkeleton(absPath, relPath, opts) {
|
|
45
|
-
const ext = path.extname(absPath).toLowerCase();
|
|
46
|
-
const entry = detectLanguage(absPath);
|
|
47
|
-
if (!entry)
|
|
48
|
-
throw new UnsupportedLanguageError(ext);
|
|
49
|
-
const stat = fs.statSync(absPath);
|
|
50
|
-
if (stat.size > opts.maxFileBytes) {
|
|
51
|
-
throw new Error(`File is ${stat.size} bytes, exceeds maxFileBytes (${opts.maxFileBytes}). Increase the limit to parse it.`);
|
|
52
|
-
}
|
|
53
|
-
// Return cached result if file hasn't changed. The cached SkeletonFile's
|
|
54
|
-
// `.file` is whatever relPath the first caller used; the same absolute file
|
|
55
|
-
// can be requested under a different root (different relPath), so override
|
|
56
|
-
// `.file` per call to avoid leaking a stale rel path into callers/indexes.
|
|
57
|
-
const cached = getCached(absPath, opts.detail);
|
|
58
|
-
if (cached) {
|
|
59
|
-
const wantFile = relPath.split(path.sep).join("/");
|
|
60
|
-
return cached.file === wantFile ? cached : { ...cached, file: wantFile };
|
|
61
|
-
}
|
|
62
|
-
const rawSource = fs.readFileSync(absPath, "utf8");
|
|
63
|
-
// Persistent cache (content-hash keyed, see diskcache.ts). A hit skips
|
|
64
|
-
// parsing entirely; entries can never be stale because the key embeds the
|
|
65
|
-
// file's bytes + detail + schema/grammar versions.
|
|
66
|
-
const dKey = diskCacheKey(rawSource, opts.detail, SCHEMA_VERSION, GRAMMAR_SOURCE);
|
|
67
|
-
const fromDisk = diskCacheGet(dKey);
|
|
68
|
-
if (fromDisk) {
|
|
69
|
-
const wantFile = relPath.split(path.sep).join("/");
|
|
70
|
-
const hit = fromDisk.file === wantFile ? fromDisk : { ...fromDisk, file: wantFile };
|
|
71
|
-
setCached(absPath, opts.detail, hit);
|
|
72
|
-
return hit;
|
|
73
|
-
}
|
|
74
|
-
let source = rawSource;
|
|
75
|
-
let grammar = entry.grammar;
|
|
76
|
-
if (entry.sfc) {
|
|
77
|
-
const script = extractScript(source);
|
|
78
|
-
source = script.code; // blank-padded script-only source (offsets preserved)
|
|
79
|
-
grammar = script.grammar;
|
|
80
|
-
}
|
|
81
|
-
const root = await parseSource(grammar, source);
|
|
82
|
-
let symbols = entry.extract(root, source);
|
|
83
|
-
if (opts.detail === "outline")
|
|
84
|
-
symbols = toOutline(symbols);
|
|
85
|
-
const directives = entry.extractDirectives ? entry.extractDirectives(root, source) : [];
|
|
86
|
-
const imports = entry.extractImports ? entry.extractImports(root, source) : [];
|
|
87
|
-
const result = {
|
|
88
|
-
schemaVersion: SCHEMA_VERSION,
|
|
89
|
-
file: relPath.split(path.sep).join("/"),
|
|
90
|
-
language: entry.language,
|
|
91
|
-
generatedAt: new Date().toISOString(),
|
|
92
|
-
parser: { engine: "tree-sitter", grammar: `${grammar} (${GRAMMAR_SOURCE})` },
|
|
93
|
-
symbolCount: countSymbols(symbols),
|
|
94
|
-
...(directives.length > 0 ? { directives } : {}),
|
|
95
|
-
...(imports.length > 0 ? { imports } : {}),
|
|
96
|
-
symbols,
|
|
97
|
-
};
|
|
98
|
-
setCached(absPath, opts.detail, result);
|
|
99
|
-
diskCachePut(dKey, result);
|
|
100
|
-
return result;
|
|
101
|
-
}
|
|
102
|
-
/** Recursively collect supported source files under a directory. */
|
|
103
|
-
export function collectSourceFiles(absDir, opts) {
|
|
104
|
-
const supported = new Set(supportedExtensions());
|
|
105
|
-
const ignore = new Set(opts.ignore);
|
|
106
|
-
const results = [];
|
|
107
|
-
const walk = (dir) => {
|
|
108
|
-
let entries;
|
|
109
|
-
try {
|
|
110
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
for (const e of entries) {
|
|
116
|
-
if (e.name.startsWith(".")) {
|
|
117
|
-
if (e.isDirectory())
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const full = path.join(dir, e.name);
|
|
121
|
-
if (e.isDirectory()) {
|
|
122
|
-
if (!ignore.has(e.name))
|
|
123
|
-
walk(full);
|
|
124
|
-
}
|
|
125
|
-
else if (e.isFile() && supported.has(path.extname(e.name).toLowerCase())) {
|
|
126
|
-
results.push(full);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
walk(absDir);
|
|
131
|
-
return results.sort();
|
|
132
|
-
}
|
package/dist/sourcemap.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
function decodeInline(url) {
|
|
4
|
-
const b64 = url.match(/base64,(.+)$/);
|
|
5
|
-
try {
|
|
6
|
-
if (b64)
|
|
7
|
-
return JSON.parse(Buffer.from(b64[1], "base64").toString("utf8"));
|
|
8
|
-
const comma = url.indexOf(",");
|
|
9
|
-
if (comma >= 0)
|
|
10
|
-
return JSON.parse(decodeURIComponent(url.slice(comma + 1)));
|
|
11
|
-
}
|
|
12
|
-
catch { /* malformed */ }
|
|
13
|
-
return null;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Read the source map for a compiled JS/CSS file: handles an inline
|
|
17
|
-
* `//# sourceMappingURL=data:...` comment or an external `.map` file, and
|
|
18
|
-
* returns the original source paths it maps back to.
|
|
19
|
-
*/
|
|
20
|
-
export function readSourceMap(absPath, relPath) {
|
|
21
|
-
let src;
|
|
22
|
-
try {
|
|
23
|
-
src = fs.readFileSync(absPath, "utf8");
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
const matches = [...src.matchAll(/[#@]\s*sourceMappingURL=([^\s'"]+)/g)];
|
|
29
|
-
if (matches.length === 0)
|
|
30
|
-
return null;
|
|
31
|
-
const url = matches[matches.length - 1][1];
|
|
32
|
-
let map = null;
|
|
33
|
-
let mapKind;
|
|
34
|
-
let mapFile;
|
|
35
|
-
if (url.startsWith("data:")) {
|
|
36
|
-
mapKind = "inline";
|
|
37
|
-
map = decodeInline(url);
|
|
38
|
-
}
|
|
39
|
-
else {
|
|
40
|
-
mapKind = "external";
|
|
41
|
-
mapFile = url;
|
|
42
|
-
try {
|
|
43
|
-
map = JSON.parse(fs.readFileSync(path.resolve(path.dirname(absPath), url), "utf8"));
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
map = null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (!map || !Array.isArray(map.sources))
|
|
50
|
-
return null;
|
|
51
|
-
const root = typeof map.sourceRoot === "string" ? map.sourceRoot.replace(/\/$/, "") : "";
|
|
52
|
-
const sources = map.sources.map((s) => root && !s.startsWith("/") ? root + "/" + s : s);
|
|
53
|
-
return {
|
|
54
|
-
file: relPath,
|
|
55
|
-
mapKind,
|
|
56
|
-
...(mapFile ? { mapFile } : {}),
|
|
57
|
-
sources,
|
|
58
|
-
hasContent: Array.isArray(map.sourcesContent) && map.sourcesContent.length > 0,
|
|
59
|
-
};
|
|
60
|
-
}
|