context-slice 1.8.2
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/LICENSE +21 -0
- package/README.md +349 -0
- package/dist/src/cli.js +199 -0
- package/dist/src/indexer/index.js +325 -0
- package/dist/src/languages/adapter.js +23 -0
- package/dist/src/languages/go/index.js +15 -0
- package/dist/src/languages/go/parse.js +443 -0
- package/dist/src/languages/go/resolve.js +313 -0
- package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
- package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
- package/dist/src/languages/java/enterprise/registry.js +22 -0
- package/dist/src/languages/java/enterprise/spring-data.js +239 -0
- package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
- package/dist/src/languages/java/enterprise/transactions.js +110 -0
- package/dist/src/languages/java.js +84 -0
- package/dist/src/languages/javascript/index.js +25 -0
- package/dist/src/languages/python/index.js +29 -0
- package/dist/src/languages/python/parse.js +415 -0
- package/dist/src/languages/python/resolve.js +413 -0
- package/dist/src/languages/rust/calls-resolve.js +1405 -0
- package/dist/src/languages/rust/index.js +12 -0
- package/dist/src/languages/rust/parse.js +545 -0
- package/dist/src/languages/rust/resolve.js +284 -0
- package/dist/src/languages/typescript/index.js +25 -0
- package/dist/src/languages/typescript/parse.js +793 -0
- package/dist/src/languages/typescript/resolve.js +463 -0
- package/dist/src/package-info.js +16 -0
- package/dist/src/parser/java-parser.js +339 -0
- package/dist/src/planner/budget.js +1 -0
- package/dist/src/planner/composition.js +372 -0
- package/dist/src/planner/rank.js +10 -0
- package/dist/src/render/compact-context.js +11 -0
- package/dist/src/server/mcp-server.js +154 -0
- package/dist/src/storage/sqlite.js +105 -0
- package/dist/src/types/enterprise.js +1 -0
- package/dist/src/types/model.js +1 -0
- package/dist/src/workflow/errors.js +10 -0
- package/dist/src/workflow/preview.js +220 -0
- package/dist/src/workflow/repository.js +56 -0
- package/mcp.json +10 -0
- package/package.json +76 -0
- package/plugin.json +15 -0
- package/queries/java/annotations.scm +1 -0
- package/queries/java/calls.scm +1 -0
- package/queries/java/imports.scm +1 -0
- package/queries/java/symbols.scm +2 -0
- package/skills/context-slice/SKILL.md +60 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
import { rankSymbol } from "../planner/rank.js";
|
|
5
|
+
import { INDEX_VERSION, IndexStorage, } from "../storage/sqlite.js";
|
|
6
|
+
import { adapterFor, ignoredDirectories, languages, } from "../languages/adapter.js";
|
|
7
|
+
import { extractEnterpriseRelations, resolveEnterpriseRelations, } from "../languages/java/enterprise/registry.js";
|
|
8
|
+
import "../languages/java.js";
|
|
9
|
+
import "../languages/typescript/index.js";
|
|
10
|
+
import "../languages/javascript/index.js";
|
|
11
|
+
import "../languages/python/index.js";
|
|
12
|
+
import "../languages/rust/index.js";
|
|
13
|
+
import "../languages/go/index.js";
|
|
14
|
+
import "../languages/java/enterprise/spring-mvc.js";
|
|
15
|
+
import "../languages/java/enterprise/dependency-injection.js";
|
|
16
|
+
import "../languages/java/enterprise/transactions.js";
|
|
17
|
+
import "../languages/java/enterprise/jpa-entity.js";
|
|
18
|
+
import "../languages/java/enterprise/spring-data.js";
|
|
19
|
+
const coreIgnored = new Set([
|
|
20
|
+
".git",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"build",
|
|
23
|
+
"dist",
|
|
24
|
+
"out",
|
|
25
|
+
".idea",
|
|
26
|
+
".vscode",
|
|
27
|
+
".context-slice",
|
|
28
|
+
]);
|
|
29
|
+
const ignored = new Set([...coreIgnored, ...ignoredDirectories()]);
|
|
30
|
+
/** Display suffix: `.d.ts` and `.tsx` are counted separately from `.ts`. */
|
|
31
|
+
function fileSuffix(filePath) {
|
|
32
|
+
const lower = filePath.toLowerCase();
|
|
33
|
+
return lower.endsWith(".d.ts") ? ".d.ts" : extname(lower);
|
|
34
|
+
}
|
|
35
|
+
export class ProjectIndex {
|
|
36
|
+
root;
|
|
37
|
+
symbols = [];
|
|
38
|
+
calls = [];
|
|
39
|
+
imports = [];
|
|
40
|
+
exports = [];
|
|
41
|
+
enterpriseRelations = [];
|
|
42
|
+
hashes = new Map();
|
|
43
|
+
storage;
|
|
44
|
+
constructor(root) {
|
|
45
|
+
this.root = resolve(root);
|
|
46
|
+
this.storage = new IndexStorage(this.root);
|
|
47
|
+
}
|
|
48
|
+
files(dir) {
|
|
49
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
50
|
+
if (ignored.has(entry.name))
|
|
51
|
+
return [];
|
|
52
|
+
const path = join(dir, entry.name);
|
|
53
|
+
return entry.isDirectory()
|
|
54
|
+
? this.files(path)
|
|
55
|
+
: entry.isFile() && adapterFor(entry.name)
|
|
56
|
+
? [path]
|
|
57
|
+
: [];
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
counts(files) {
|
|
61
|
+
const byExtension = {};
|
|
62
|
+
const byLanguage = {};
|
|
63
|
+
for (const file of files) {
|
|
64
|
+
const suffix = fileSuffix(file);
|
|
65
|
+
byExtension[suffix] = (byExtension[suffix] ?? 0) + 1;
|
|
66
|
+
const language = adapterFor(file)?.label ?? "unknown";
|
|
67
|
+
byLanguage[language] = (byLanguage[language] ?? 0) + 1;
|
|
68
|
+
}
|
|
69
|
+
return { byExtension, byLanguage };
|
|
70
|
+
}
|
|
71
|
+
inspect() {
|
|
72
|
+
const files = this.files(this.root);
|
|
73
|
+
const previous = this.storage.load();
|
|
74
|
+
const hashes = new Map();
|
|
75
|
+
for (const file of files) {
|
|
76
|
+
const filePath = relative(this.root, file);
|
|
77
|
+
hashes.set(filePath, createHash("sha256").update(readFileSync(file, "utf8")).digest("hex"));
|
|
78
|
+
}
|
|
79
|
+
const hasIndex = previous.files.size > 0;
|
|
80
|
+
const stale = hasIndex &&
|
|
81
|
+
(hashes.size !== previous.files.size ||
|
|
82
|
+
[...hashes].some(([path, hash]) => previous.files.get(path)?.hash !== hash));
|
|
83
|
+
const metadata = this.storage.metadata();
|
|
84
|
+
const counts = this.counts(files);
|
|
85
|
+
return {
|
|
86
|
+
state: hasIndex
|
|
87
|
+
? stale
|
|
88
|
+
? "STALE"
|
|
89
|
+
: "CURRENT"
|
|
90
|
+
: "UNINITIALIZED",
|
|
91
|
+
sourceFiles: files.length,
|
|
92
|
+
filesByExtension: counts.byExtension,
|
|
93
|
+
filesByLanguage: counts.byLanguage,
|
|
94
|
+
languages: Object.keys(counts.byLanguage),
|
|
95
|
+
indexedFiles: previous.files.size,
|
|
96
|
+
schemaVersion: metadata.schema_version ?? INDEX_VERSION,
|
|
97
|
+
lastRefreshedAt: metadata.last_refreshed_at,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
refresh() {
|
|
101
|
+
const summary = this.rebuild();
|
|
102
|
+
return { summary, freshness: this.inspect() };
|
|
103
|
+
}
|
|
104
|
+
rebuild() {
|
|
105
|
+
const started = Date.now();
|
|
106
|
+
const files = this.files(this.root);
|
|
107
|
+
const previous = this.storage.load();
|
|
108
|
+
this.symbols = [];
|
|
109
|
+
this.calls = [];
|
|
110
|
+
this.imports = [];
|
|
111
|
+
this.exports = [];
|
|
112
|
+
this.enterpriseRelations = [];
|
|
113
|
+
this.hashes = new Map();
|
|
114
|
+
let filesParsed = 0;
|
|
115
|
+
let cacheHits = 0;
|
|
116
|
+
let parseErrors = 0;
|
|
117
|
+
for (const file of files) {
|
|
118
|
+
const filePath = relative(this.root, file);
|
|
119
|
+
const adapter = adapterFor(filePath);
|
|
120
|
+
if (!adapter)
|
|
121
|
+
continue;
|
|
122
|
+
const source = readFileSync(file, "utf8");
|
|
123
|
+
const hash = createHash("sha256").update(source).digest("hex");
|
|
124
|
+
this.hashes.set(filePath, { hash, language: adapter.id });
|
|
125
|
+
let fileSymbols;
|
|
126
|
+
if (previous.files.get(filePath)?.hash === hash) {
|
|
127
|
+
fileSymbols = previous.symbols.filter((symbol) => symbol.filePath === filePath);
|
|
128
|
+
this.symbols.push(...fileSymbols);
|
|
129
|
+
this.calls.push(...previous.calls.filter((call) => call.filePath === filePath));
|
|
130
|
+
this.imports.push(...previous.imports.filter((record) => record.filePath === filePath));
|
|
131
|
+
this.exports.push(...previous.exports.filter((record) => record.filePath === filePath));
|
|
132
|
+
cacheHits++;
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
const parsed = adapter.parse(filePath, source);
|
|
136
|
+
fileSymbols = parsed.symbols;
|
|
137
|
+
this.symbols.push(...parsed.symbols);
|
|
138
|
+
this.calls.push(...parsed.calls);
|
|
139
|
+
this.imports.push(...parsed.imports);
|
|
140
|
+
this.exports.push(...parsed.exports);
|
|
141
|
+
if (parsed.parseError)
|
|
142
|
+
parseErrors++;
|
|
143
|
+
filesParsed++;
|
|
144
|
+
}
|
|
145
|
+
if (adapter.id === "java") {
|
|
146
|
+
this.enterpriseRelations.push(...extractEnterpriseRelations(fileSymbols, filePath, source));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
this.enterpriseRelations = resolveEnterpriseRelations(this.enterpriseRelations, this.symbols.filter((symbol) => symbol.language === "java"));
|
|
150
|
+
// Each language resolves only its own edges; cross-language calls stay unresolved.
|
|
151
|
+
for (const adapter of languages()) {
|
|
152
|
+
const context = {
|
|
153
|
+
root: this.root,
|
|
154
|
+
symbols: this.symbols.filter((symbol) => symbol.language === adapter.id),
|
|
155
|
+
calls: this.calls.filter((call) => (call.language ?? "java") === adapter.id && !call.resolvedTargetId),
|
|
156
|
+
imports: this.imports.filter((record) => record.language === adapter.id),
|
|
157
|
+
exports: this.exports.filter((record) => record.language === adapter.id),
|
|
158
|
+
sourceOf: (symbol) => this.sourceFor(symbol),
|
|
159
|
+
};
|
|
160
|
+
if (context.symbols.length)
|
|
161
|
+
adapter.resolveCalls(context);
|
|
162
|
+
}
|
|
163
|
+
this.storage.save(this.hashes, this.symbols, this.calls, this.imports, this.exports);
|
|
164
|
+
const counts = this.counts(files);
|
|
165
|
+
return {
|
|
166
|
+
files: files.length,
|
|
167
|
+
filesScanned: files.length,
|
|
168
|
+
filesParsed,
|
|
169
|
+
parseErrors,
|
|
170
|
+
cacheHits,
|
|
171
|
+
filesByExtension: counts.byExtension,
|
|
172
|
+
filesByLanguage: counts.byLanguage,
|
|
173
|
+
symbols: this.symbols.length,
|
|
174
|
+
symbolsUpdated: this.symbols.length,
|
|
175
|
+
calls: this.calls.length,
|
|
176
|
+
imports: this.imports.length,
|
|
177
|
+
exports: this.exports.length,
|
|
178
|
+
elapsedMs: Date.now() - started,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
search(query, limit = 10) {
|
|
182
|
+
return this.symbols
|
|
183
|
+
.map((symbol) => ({
|
|
184
|
+
id: symbol.id,
|
|
185
|
+
kind: symbol.kind,
|
|
186
|
+
language: symbol.language,
|
|
187
|
+
name: symbol.name,
|
|
188
|
+
qualifiedName: symbol.qualifiedName,
|
|
189
|
+
signature: symbol.signature,
|
|
190
|
+
filePath: symbol.filePath,
|
|
191
|
+
score: rankSymbol(symbol, query),
|
|
192
|
+
}))
|
|
193
|
+
.filter((item) => item.score > 0)
|
|
194
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
195
|
+
.slice(0, limit);
|
|
196
|
+
}
|
|
197
|
+
resolveSymbol(input) {
|
|
198
|
+
// An overload signature is API surface; the implementation is the target.
|
|
199
|
+
const preferImplementation = (matches) => {
|
|
200
|
+
const implementations = matches.filter((symbol) => !symbol.metadata?.overloadSignature);
|
|
201
|
+
return implementations.length ? implementations : matches;
|
|
202
|
+
};
|
|
203
|
+
const exact = this.symbols.filter((s) => s.id === input ||
|
|
204
|
+
s.canonicalIdentity === input ||
|
|
205
|
+
s.qualifiedName === input ||
|
|
206
|
+
s.signature === input);
|
|
207
|
+
if (exact.length)
|
|
208
|
+
return preferImplementation(exact);
|
|
209
|
+
const qualifiedSuffix = this.symbols.filter((s) => s.qualifiedName?.endsWith(`.${input}`));
|
|
210
|
+
return preferImplementation(qualifiedSuffix.length
|
|
211
|
+
? qualifiedSuffix
|
|
212
|
+
: this.symbols.filter((s) => s.name === input));
|
|
213
|
+
}
|
|
214
|
+
callers(target) {
|
|
215
|
+
// A cfg-gated call (spec §40) carries every alternative in runtimeTargetIds, not just the
|
|
216
|
+
// resolvedTargetId it settled on — a non-first alternative must still be reachable as a
|
|
217
|
+
// caller/dependency edge, or context composition can never include it (see docs/rust-support.md).
|
|
218
|
+
return this.calls
|
|
219
|
+
.filter((call) => (call.runtimeTargetIds ?? [call.resolvedTargetId]).includes(target.id))
|
|
220
|
+
.map((call) => this.symbols.find((s) => s.id === call.callerId))
|
|
221
|
+
.filter((s) => Boolean(s));
|
|
222
|
+
}
|
|
223
|
+
callersAtDepth(target, depth) {
|
|
224
|
+
let frontier = [target];
|
|
225
|
+
const found = new Map();
|
|
226
|
+
for (let level = 0; level < depth; level++) {
|
|
227
|
+
const next = frontier
|
|
228
|
+
.flatMap((symbol) => this.callers(symbol))
|
|
229
|
+
.filter((symbol) => !found.has(symbol.id));
|
|
230
|
+
for (const symbol of next)
|
|
231
|
+
found.set(symbol.id, symbol);
|
|
232
|
+
frontier = next;
|
|
233
|
+
}
|
|
234
|
+
return [...found.values()];
|
|
235
|
+
}
|
|
236
|
+
dependencies(target) {
|
|
237
|
+
return this.calls
|
|
238
|
+
.filter((call) => call.callerId === target.id)
|
|
239
|
+
.flatMap((call) => call.runtimeTargetIds ?? (call.resolvedTargetId ? [call.resolvedTargetId] : []))
|
|
240
|
+
.map((id) => this.symbols.find((s) => s.id === id))
|
|
241
|
+
.filter((s) => Boolean(s));
|
|
242
|
+
}
|
|
243
|
+
dependenciesAtDepth(target, depth) {
|
|
244
|
+
let frontier = [target];
|
|
245
|
+
const found = new Map();
|
|
246
|
+
for (let level = 0; level < depth; level++) {
|
|
247
|
+
const next = frontier
|
|
248
|
+
.flatMap((symbol) => this.dependencies(symbol))
|
|
249
|
+
.filter((symbol) => !found.has(symbol.id));
|
|
250
|
+
for (const symbol of next)
|
|
251
|
+
found.set(symbol.id, symbol);
|
|
252
|
+
frontier = next;
|
|
253
|
+
}
|
|
254
|
+
return [...found.values()];
|
|
255
|
+
}
|
|
256
|
+
ambiguousCalls() {
|
|
257
|
+
return this.calls.filter((call) => !call.resolvedTargetId &&
|
|
258
|
+
this.symbols.filter((symbol) => symbol.kind === "method" && symbol.name === call.calleeName).length > 1);
|
|
259
|
+
}
|
|
260
|
+
diagnostics() {
|
|
261
|
+
const simpleNames = new Map();
|
|
262
|
+
for (const symbol of this.symbols)
|
|
263
|
+
simpleNames.set(symbol.name, (simpleNames.get(symbol.name) ?? 0) + 1);
|
|
264
|
+
const ids = new Set();
|
|
265
|
+
let collisions = 0;
|
|
266
|
+
for (const symbol of this.symbols) {
|
|
267
|
+
if (ids.has(symbol.id))
|
|
268
|
+
collisions++;
|
|
269
|
+
ids.add(symbol.id);
|
|
270
|
+
}
|
|
271
|
+
const resolutionKindCounts = Object.fromEntries([...new Set(this.calls.map((call) => call.resolutionKind))].map((kind) => [
|
|
272
|
+
kind,
|
|
273
|
+
this.calls.filter((call) => call.resolutionKind === kind).length,
|
|
274
|
+
]));
|
|
275
|
+
const byLanguage = Object.fromEntries(languages().map((adapter) => [
|
|
276
|
+
adapter.id,
|
|
277
|
+
{
|
|
278
|
+
symbols: this.symbols.filter((symbol) => symbol.language === adapter.id).length,
|
|
279
|
+
calls: this.calls.filter((call) => (call.language ?? "java") === adapter.id).length,
|
|
280
|
+
imports: this.imports.filter((record) => record.language === adapter.id).length,
|
|
281
|
+
exports: this.exports.filter((record) => record.language === adapter.id).length,
|
|
282
|
+
},
|
|
283
|
+
]));
|
|
284
|
+
const kindCount = (kind) => this.symbols.filter((symbol) => symbol.kind === kind).length;
|
|
285
|
+
return {
|
|
286
|
+
filesIndexed: new Set(this.symbols.map((symbol) => symbol.filePath)).size,
|
|
287
|
+
symbolsIndexed: this.symbols.length,
|
|
288
|
+
methodsIndexed: kindCount("method"),
|
|
289
|
+
constructorsIndexed: kindCount("constructor"),
|
|
290
|
+
classesIndexed: kindCount("class"),
|
|
291
|
+
interfacesIndexed: kindCount("interface"),
|
|
292
|
+
recordsIndexed: kindCount("record"),
|
|
293
|
+
enumsIndexed: kindCount("enum"),
|
|
294
|
+
functionsIndexed: kindCount("function"),
|
|
295
|
+
typesIndexed: kindCount("type"),
|
|
296
|
+
componentsIndexed: this.symbols.filter((symbol) => symbol.metadata?.reactComponent).length,
|
|
297
|
+
duplicateSimpleNames: [...simpleNames.values()].filter((count) => count > 1).length,
|
|
298
|
+
ambiguousLookups: 0,
|
|
299
|
+
symbolIdCollisions: collisions,
|
|
300
|
+
callEdgesTotal: this.calls.length,
|
|
301
|
+
callEdgesExact: this.calls.filter((call) => call.confidence === "exact")
|
|
302
|
+
.length,
|
|
303
|
+
callEdgesProbable: this.calls.filter((call) => call.confidence === "probable").length,
|
|
304
|
+
callEdgesUnresolved: this.calls.filter((call) => call.confidence === "unresolved").length,
|
|
305
|
+
externalCallEdges: this.calls.filter((call) => call.externalPackage)
|
|
306
|
+
.length,
|
|
307
|
+
importsTotal: this.imports.length,
|
|
308
|
+
importsResolved: this.imports.filter((record) => record.resolvedFile)
|
|
309
|
+
.length,
|
|
310
|
+
externalImports: this.imports.filter((record) => record.externalPackage)
|
|
311
|
+
.length,
|
|
312
|
+
reexportsTotal: this.exports.filter((record) => record.fromModule).length,
|
|
313
|
+
reexportsResolved: this.exports.filter((record) => record.fromModule && record.resolvedFile).length,
|
|
314
|
+
byLanguage,
|
|
315
|
+
resolutionKindCounts,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
sourceFor(symbol) {
|
|
319
|
+
const full = resolve(this.root, symbol.filePath);
|
|
320
|
+
const fromRoot = relative(this.root, full);
|
|
321
|
+
if (fromRoot === ".." || fromRoot.startsWith(`..${sep}`) || isAbsolute(fromRoot))
|
|
322
|
+
throw new Error("Path nằm ngoài repository root");
|
|
323
|
+
return readFileSync(full, "utf8");
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const registry = [];
|
|
2
|
+
export function registerLanguage(adapter) {
|
|
3
|
+
registry.push(adapter);
|
|
4
|
+
return adapter;
|
|
5
|
+
}
|
|
6
|
+
export function languages() {
|
|
7
|
+
return registry;
|
|
8
|
+
}
|
|
9
|
+
export function adapterFor(filePath) {
|
|
10
|
+
const lower = filePath.toLowerCase();
|
|
11
|
+
let best;
|
|
12
|
+
for (const adapter of registry)
|
|
13
|
+
for (const extension of adapter.extensions)
|
|
14
|
+
if (lower.endsWith(extension) && (best?.length ?? 0) < extension.length)
|
|
15
|
+
best = { adapter, length: extension.length };
|
|
16
|
+
return best?.adapter;
|
|
17
|
+
}
|
|
18
|
+
export function supportedExtensions() {
|
|
19
|
+
return registry.flatMap((adapter) => adapter.extensions);
|
|
20
|
+
}
|
|
21
|
+
export function ignoredDirectories() {
|
|
22
|
+
return new Set(registry.flatMap((adapter) => adapter.ignoredDirectories ?? []));
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { registerLanguage } from "../adapter.js";
|
|
2
|
+
import { LANGUAGE_ID, parseGo } from "./parse.js";
|
|
3
|
+
import { resolveGoCalls } from "./resolve.js";
|
|
4
|
+
export const goAdapter = {
|
|
5
|
+
id: LANGUAGE_ID,
|
|
6
|
+
label: "Go",
|
|
7
|
+
extensions: [".go"],
|
|
8
|
+
ignoredDirectories: ["vendor"],
|
|
9
|
+
parse: parseGo,
|
|
10
|
+
resolveCalls: (context) => {
|
|
11
|
+
resolveGoCalls(context);
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
registerLanguage(goAdapter);
|
|
15
|
+
export { LANGUAGE_ID as GO_LANGUAGE_ID };
|