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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/src/cli.js +199 -0
  4. package/dist/src/indexer/index.js +325 -0
  5. package/dist/src/languages/adapter.js +23 -0
  6. package/dist/src/languages/go/index.js +15 -0
  7. package/dist/src/languages/go/parse.js +443 -0
  8. package/dist/src/languages/go/resolve.js +313 -0
  9. package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
  10. package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
  11. package/dist/src/languages/java/enterprise/registry.js +22 -0
  12. package/dist/src/languages/java/enterprise/spring-data.js +239 -0
  13. package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
  14. package/dist/src/languages/java/enterprise/transactions.js +110 -0
  15. package/dist/src/languages/java.js +84 -0
  16. package/dist/src/languages/javascript/index.js +25 -0
  17. package/dist/src/languages/python/index.js +29 -0
  18. package/dist/src/languages/python/parse.js +415 -0
  19. package/dist/src/languages/python/resolve.js +413 -0
  20. package/dist/src/languages/rust/calls-resolve.js +1405 -0
  21. package/dist/src/languages/rust/index.js +12 -0
  22. package/dist/src/languages/rust/parse.js +545 -0
  23. package/dist/src/languages/rust/resolve.js +284 -0
  24. package/dist/src/languages/typescript/index.js +25 -0
  25. package/dist/src/languages/typescript/parse.js +793 -0
  26. package/dist/src/languages/typescript/resolve.js +463 -0
  27. package/dist/src/package-info.js +16 -0
  28. package/dist/src/parser/java-parser.js +339 -0
  29. package/dist/src/planner/budget.js +1 -0
  30. package/dist/src/planner/composition.js +372 -0
  31. package/dist/src/planner/rank.js +10 -0
  32. package/dist/src/render/compact-context.js +11 -0
  33. package/dist/src/server/mcp-server.js +154 -0
  34. package/dist/src/storage/sqlite.js +105 -0
  35. package/dist/src/types/enterprise.js +1 -0
  36. package/dist/src/types/model.js +1 -0
  37. package/dist/src/workflow/errors.js +10 -0
  38. package/dist/src/workflow/preview.js +220 -0
  39. package/dist/src/workflow/repository.js +56 -0
  40. package/mcp.json +10 -0
  41. package/package.json +76 -0
  42. package/plugin.json +15 -0
  43. package/queries/java/annotations.scm +1 -0
  44. package/queries/java/calls.scm +1 -0
  45. package/queries/java/imports.scm +1 -0
  46. package/queries/java/symbols.scm +2 -0
  47. package/skills/context-slice/SKILL.md +60 -0
@@ -0,0 +1,284 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { leaveUnresolvedOnError, resolveCallsA } from "./calls-resolve.js";
4
+ import { modulePathFor } from "./parse.js";
5
+ function cargoPackageName(tomlText) {
6
+ const section = tomlText.match(/\[package\]([\s\S]*?)(?:\n\[|$)/);
7
+ const body = section ? section[1] : tomlText;
8
+ return body.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
9
+ }
10
+ function cargoWorkspaceMembers(tomlText) {
11
+ const section = tomlText.match(/\[workspace\]([\s\S]*?)(?:\n\[|$)/);
12
+ if (!section)
13
+ return [];
14
+ const members = section[1].match(/members\s*=\s*\[([\s\S]*?)\]/);
15
+ if (!members)
16
+ return [];
17
+ return [...members[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
18
+ }
19
+ /** Only the simple "dir/*" trailing-glob form is supported — real Cargo globs (nested
20
+ * wildcards, exclude patterns) are rare enough in practice not to special-case here. */
21
+ function expandMemberPattern(root, pattern) {
22
+ const clean = pattern.replace(/\/$/, "");
23
+ if (!clean.endsWith("/*"))
24
+ return [clean];
25
+ const base = clean.slice(0, -2);
26
+ try {
27
+ return readdirSync(join(root, base), { withFileTypes: true })
28
+ .filter((entry) => entry.isDirectory())
29
+ .map((entry) => `${base}/${entry.name}`);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ }
35
+ /** Every crate this project can resolve `use` paths into: a root Cargo.toml's own
36
+ * `[package]` (if any) plus every `[workspace]` member's own `[package]`. No Cargo.toml
37
+ * means no known crates — callers fall back to treating the whole project as one crate,
38
+ * which is the project's existing, correct single-crate behavior. */
39
+ function discoverCrates(root) {
40
+ let rootToml;
41
+ try {
42
+ rootToml = readFileSync(join(root, "Cargo.toml"), "utf8");
43
+ }
44
+ catch {
45
+ return [];
46
+ }
47
+ const crates = [];
48
+ const rootName = cargoPackageName(rootToml);
49
+ if (rootName)
50
+ crates.push({ name: rootName, dir: "" });
51
+ for (const pattern of cargoWorkspaceMembers(rootToml)) {
52
+ for (const memberDir of expandMemberPattern(root, pattern)) {
53
+ let memberToml;
54
+ try {
55
+ memberToml = readFileSync(join(root, memberDir, "Cargo.toml"), "utf8");
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ const name = cargoPackageName(memberToml);
61
+ if (name)
62
+ crates.push({ name, dir: memberDir });
63
+ }
64
+ }
65
+ return crates;
66
+ }
67
+ /**
68
+ * Map every indexed .rs file to its crate-relative module path, and back.
69
+ * A module-path key claimed by two different files (e.g. two crates' lib.rs
70
+ * in a workspace) is ambiguous: it is dropped from `byModule` and recorded in
71
+ * `ambiguous`, so it resolves to nothing rather than to whichever came first.
72
+ */
73
+ export function rustModuleIndex(files) {
74
+ const byModule = new Map();
75
+ const ambiguous = new Set();
76
+ const byFile = new Map();
77
+ for (const file of files) {
78
+ const path = modulePathFor(file);
79
+ byFile.set(file, path);
80
+ const key = path.join("::");
81
+ if (ambiguous.has(key))
82
+ continue;
83
+ const existing = byModule.get(key);
84
+ if (existing !== undefined && existing !== file) {
85
+ byModule.delete(key);
86
+ ambiguous.add(key);
87
+ }
88
+ else
89
+ byModule.set(key, file);
90
+ }
91
+ return { byModule, byFile, ambiguous };
92
+ }
93
+ /**
94
+ * Resolve a `use` target's path segments to an indexed file, or report it
95
+ * as external. Deterministic and conservative.
96
+ *
97
+ * Anchored (crate/self/super) targets can only mean "somewhere in this
98
+ * crate", so a miss is `{}`, never external. At file granularity a
99
+ * module-only anchored path whose module has no file of its own (e.g.
100
+ * ["crate","nope"], where `nope` may be an inline `mod` or an item in the
101
+ * crate root) falls back to the containing module's file via `asContaining`
102
+ * (here the crate root); that is the documented file-granularity fallback.
103
+ *
104
+ * Non-anchored targets follow Rust 2018: first a descendant module of the
105
+ * CURRENT module (`[...here, ...segments]`), else an extern crate. Only a
106
+ * match strictly deeper than the current module counts, so a path can never
107
+ * fall back to the current file or an ancestor (e.g. lib.rs).
108
+ *
109
+ * A module-path key claimed by several files is ambiguous and never guessed:
110
+ * the result is `{}`.
111
+ *
112
+ * Accepts EITHER a containing-module path (what `resolveRustCalls` passes,
113
+ * since `ImportRecord.module` is everything but the final segment) OR a full
114
+ * path with the trailing item name (used by the unit tests). Both work via
115
+ * the two-step lookup `asModule` (exact) / `asContaining` (drop last segment).
116
+ */
117
+ export function resolveRustModule(segments, fromFile, index,
118
+ /** Names bound in `fromFile` (declared items, imports). A non-anchored path starting with one is not an external crate. */
119
+ boundNames,
120
+ /** Other crates in this workspace, by Cargo.toml package name — lets a non-anchored path
121
+ * whose first segment names a sibling crate resolve into THAT crate's own module index,
122
+ * instead of being reported as an external (non-workspace) package. */
123
+ otherCrates) {
124
+ const [anchor, ...rest] = segments;
125
+ const anchored = anchor === "crate" || anchor === "self" || anchor === "super";
126
+ const here = index.byFile.get(fromFile) ?? [];
127
+ let absolute;
128
+ let minLen = 0; // non-anchored: matches must be strictly deeper than `here`
129
+ if (anchor === "crate") {
130
+ absolute = rest;
131
+ }
132
+ else if (anchor === "self" || anchor === "super") {
133
+ if (!index.byFile.has(fromFile))
134
+ return {};
135
+ const base = anchor === "super" ? here.slice(0, Math.max(0, here.length - 1)) : here;
136
+ absolute = [...base, ...rest];
137
+ }
138
+ else {
139
+ absolute = [...here, ...segments];
140
+ minLen = here.length + 1;
141
+ }
142
+ for (const path of [absolute, absolute.slice(0, -1)]) {
143
+ if (path.length < minLen)
144
+ continue;
145
+ const key = path.join("::");
146
+ if (index.ambiguous.has(key))
147
+ return {};
148
+ const file = index.byModule.get(key);
149
+ if (file)
150
+ return { file };
151
+ }
152
+ if (anchored)
153
+ return {};
154
+ if (anchor && boundNames?.has(anchor))
155
+ return {}; // bound locally: unresolved, not external
156
+ const sibling = anchor ? otherCrates?.get(anchor) : undefined;
157
+ if (sibling) {
158
+ for (const path of [rest, rest.slice(0, -1)]) {
159
+ const key = path.join("::");
160
+ if (sibling.ambiguous.has(key))
161
+ return {};
162
+ const file = sibling.byModule.get(key);
163
+ if (file)
164
+ return { file };
165
+ }
166
+ }
167
+ return { externalPackage: anchor };
168
+ }
169
+ /**
170
+ * Resolves every Rust ImportRecord's `resolvedFile`/`externalPackage` in
171
+ * place, then resolves CallEdges to symbols (structural targets, see
172
+ * calls-resolve.ts).
173
+ */
174
+ export function resolveRustCalls(context) {
175
+ const files = [...new Set(context.symbols.map((symbol) => symbol.filePath))];
176
+ for (const record of [...context.imports, ...context.exports])
177
+ files.push(record.filePath);
178
+ const allFiles = [...new Set(files)];
179
+ // Partition files by crate (longest directory-prefix match) and build one module index per
180
+ // crate, so two crates' own src/lib.rs (both module path []) never collide into one "ambiguous"
181
+ // key the way a single project-wide index would. No Cargo.toml, or a Cargo.toml with no
182
+ // crates discovered, falls back to exactly one crate covering the whole project — the
183
+ // project's prior, unpartitioned behavior.
184
+ const discovered = discoverCrates(context.root);
185
+ const crateDirs = discovered.length ? discovered : [{ name: "", dir: "" }];
186
+ if (!crateDirs.some((c) => c.dir === ""))
187
+ crateDirs.push({ name: "", dir: "" });
188
+ const dirsByLengthDesc = [...crateDirs].sort((a, b) => b.dir.length - a.dir.length);
189
+ const crateFor = (file) => dirsByLengthDesc.find((c) => c.dir === "" || file === c.dir || file.startsWith(c.dir + "/")) ??
190
+ dirsByLengthDesc[dirsByLengthDesc.length - 1];
191
+ const filesByCrateDir = new Map();
192
+ for (const file of allFiles) {
193
+ const dir = crateFor(file).dir;
194
+ const list = filesByCrateDir.get(dir) ?? [];
195
+ list.push(file);
196
+ filesByCrateDir.set(dir, list);
197
+ }
198
+ const indexByCrateDir = new Map();
199
+ const indexByCrateName = new Map();
200
+ for (const crate of crateDirs) {
201
+ const crateIndex = rustModuleIndex(filesByCrateDir.get(crate.dir) ?? []);
202
+ indexByCrateDir.set(crate.dir, crateIndex);
203
+ if (crate.name)
204
+ indexByCrateName.set(crate.name, crateIndex);
205
+ }
206
+ const indexFor = (file) => indexByCrateDir.get(crateFor(file).dir);
207
+ // Per-file bound names: declared items at any nesting + names bound by imports.
208
+ // A bare unaliased `use foo;` binds the crate name itself, so it does not count.
209
+ const boundByFile = new Map();
210
+ const bind = (file, name) => {
211
+ if (!name)
212
+ return;
213
+ let set = boundByFile.get(file);
214
+ if (!set)
215
+ boundByFile.set(file, (set = new Set()));
216
+ set.add(name);
217
+ };
218
+ // Only type-namespace items (struct/enum/trait/type/mod) can start a `use` path;
219
+ // a fn/const/static with the same name as a crate does not shadow it.
220
+ for (const symbol of context.symbols)
221
+ if (symbol.kind !== "function" && symbol.kind !== "variable")
222
+ bind(symbol.filePath, symbol.name);
223
+ for (const record of context.imports) {
224
+ if (record.wildcard)
225
+ continue;
226
+ const bareCrate = record.module === record.importedName && record.localName === record.importedName;
227
+ if (!bareCrate)
228
+ bind(record.filePath, record.localName);
229
+ }
230
+ for (const record of context.imports) {
231
+ const resolved = resolveRustModule(record.module.split("::"), record.filePath, indexFor(record.filePath), boundByFile.get(record.filePath), indexByCrateName);
232
+ record.resolvedFile = resolved.file;
233
+ record.externalPackage = resolved.externalPackage;
234
+ }
235
+ const exportsByFile = new Map();
236
+ for (const record of context.exports) {
237
+ if (record.fromModule) {
238
+ record.resolvedFile = resolveRustModule(record.fromModule.split("::"), record.filePath, indexFor(record.filePath), undefined, indexByCrateName).file;
239
+ }
240
+ const list = exportsByFile.get(record.filePath) ?? [];
241
+ list.push(record);
242
+ exportsByFile.set(record.filePath, list);
243
+ }
244
+ const symbolsByFileAndName = new Map(); // `${file}#${name}` -> symbolId
245
+ for (const symbol of context.symbols)
246
+ symbolsByFileAndName.set(`${symbol.filePath}#${symbol.name}`, symbol.id);
247
+ /** Follows `pub use` re-export chains; `seen` breaks cycles (mirrors python resolve.ts's `lookup`). */
248
+ const lookup = (file, name, seen) => {
249
+ const key = `${file}#${name}`;
250
+ if (seen.has(key))
251
+ return undefined;
252
+ seen.add(key);
253
+ const direct = symbolsByFileAndName.get(key);
254
+ if (direct)
255
+ return direct;
256
+ const records = exportsByFile.get(file) ?? [];
257
+ const reexport = records.find((record) => record.exportedName === name && record.resolvedFile);
258
+ if (reexport?.resolvedFile)
259
+ return lookup(reexport.resolvedFile, reexport.sourceName ?? name, seen);
260
+ for (const wildcard of records.filter((record) => record.wildcard))
261
+ if (wildcard.resolvedFile) {
262
+ const found = lookup(wildcard.resolvedFile, name, seen);
263
+ if (found)
264
+ return found;
265
+ }
266
+ return undefined;
267
+ };
268
+ for (const record of context.exports) {
269
+ record.symbolId = undefined; // drop any stale id loaded from the cache
270
+ if (record.resolvedFile && record.sourceName)
271
+ record.symbolId = lookup(record.resolvedFile, record.sourceName, new Set());
272
+ }
273
+ // Exact module file for a containing-module path: the dummy trailing segment makes
274
+ // resolveRustModule's "drop last segment" fallback land on `segments` itself, never a parent.
275
+ try {
276
+ resolveCallsA(context, {
277
+ moduleOf: (segments, fromFile) => resolveRustModule([...segments, "\u0000"], fromFile, indexFor(fromFile), boundByFile.get(fromFile), indexByCrateName),
278
+ });
279
+ }
280
+ catch {
281
+ // A throw outside the per-edge guard (impl pre-pass): no Rust edge is trusted, the rebuild continues.
282
+ context.calls.forEach(leaveUnresolvedOnError);
283
+ }
284
+ }
@@ -0,0 +1,25 @@
1
+ import { registerLanguage } from "../adapter.js";
2
+ import { LANGUAGE_ID, parseTypeScript } from "./parse.js";
3
+ import { resolveTypeScriptCalls, typeScriptDiagnostics } from "./resolve.js";
4
+ export const typeScriptAdapter = {
5
+ id: LANGUAGE_ID,
6
+ label: "TypeScript",
7
+ // Longest-suffix matching means ".d.ts" is recognized before ".ts".
8
+ extensions: [".ts", ".tsx", ".mts", ".cts", ".d.ts"],
9
+ ignoredDirectories: [
10
+ "node_modules",
11
+ ".next",
12
+ ".nuxt",
13
+ ".turbo",
14
+ ".cache",
15
+ "coverage",
16
+ "storybook-static",
17
+ ],
18
+ parse: parseTypeScript,
19
+ resolveCalls: (context) => {
20
+ resolveTypeScriptCalls(context);
21
+ },
22
+ };
23
+ registerLanguage(typeScriptAdapter);
24
+ export { typeScriptDiagnostics };
25
+ export { LANGUAGE_ID as TYPESCRIPT_LANGUAGE_ID };