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,220 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { estimateTokens } from "../planner/budget.js";
4
+ import { rankSymbol } from "../planner/rank.js";
5
+ import { renderSkeleton } from "../render/compact-context.js";
6
+ import { composeDependencyContext, composeImportContext, composeJpaContext, composeRouteContext, composeSiblings, composeTransactionContext, } from "../planner/composition.js";
7
+ import { WorkflowError } from "./errors.js";
8
+ /**
9
+ * Composition may use at most this share of the budget, so sibling context
10
+ * fills spare capacity and never crowds out the target, callers or callees.
11
+ */
12
+ export const COMPOSITION_BUDGET_SHARE = 0.35;
13
+ /** Reads each distinct included file once, in full, as the "without context-slice" cost.
14
+ * A file that can't be read (renamed/deleted since indexing) is skipped rather than guessed —
15
+ * the resulting reduction then understates savings, never overstates them. */
16
+ function wholeFileBaseline(index, included, estimatedTokens) {
17
+ const filePaths = new Set(included.map((item) => item.filePath));
18
+ let wholeFileTokens = 0;
19
+ for (const filePath of filePaths) {
20
+ try {
21
+ wholeFileTokens += estimateTokens(readFileSync(join(index.root, filePath), "utf8"));
22
+ }
23
+ catch {
24
+ // Excluded, not guessed — see doc comment above.
25
+ }
26
+ }
27
+ const reduction = wholeFileTokens > 0 ? Math.min(1, Math.max(0, 1 - estimatedTokens / wholeFileTokens)) : 0;
28
+ return { files: filePaths.size, wholeFileTokens, reduction };
29
+ }
30
+ function parameterCount(symbol) {
31
+ const parameters = symbol.signature?.match(/\(([^)]*)\)/)?.[1].trim() ?? "";
32
+ return parameters ? parameters.split(",").length : 0;
33
+ }
34
+ /** Test sources answer "how is this tested", not "how does this work". */
35
+ const isTestPath = (filePath) => /(^|\/)tests?\//i.test(filePath) ||
36
+ /(^|\/)test_[^/]*$/.test(filePath) ||
37
+ /_test\.[^/]+$/.test(filePath) ||
38
+ /Tests?\.java$/.test(filePath) ||
39
+ /\.(test|spec)\.[jt]sx?$/.test(filePath);
40
+ /** Keep production candidates when there are any; otherwise keep everything. */
41
+ function preferProduction(symbols) {
42
+ const production = symbols.filter((symbol) => !isTestPath(symbol.filePath));
43
+ return production.length ? production : symbols;
44
+ }
45
+ function chooseTarget(index, task) {
46
+ const exact = index.resolveSymbol(task);
47
+ if (exact.length === 1)
48
+ return exact[0];
49
+ const normalized = task.toLowerCase();
50
+ const named = new RegExp(`\\b(${task.match(/[A-Za-z_][\w$]*/g)?.join("|") ?? ""})\\b`);
51
+ const exactName = preferProduction(index.symbols.filter((symbol) => (symbol.kind === "method" ||
52
+ symbol.kind === "constructor" ||
53
+ symbol.kind === "function") &&
54
+ normalized.includes(symbol.name.toLowerCase()) &&
55
+ named.test(symbol.name))).sort((a, b) => b.name.length - a.name.length ||
56
+ parameterCount(a) - parameterCount(b) ||
57
+ a.id.localeCompare(b.id));
58
+ if (exactName.length)
59
+ return exactName[0];
60
+ const ranked = index
61
+ .search(task, 10)
62
+ .map((result) => index.symbols.find((symbol) => symbol.id === result.id))
63
+ .filter((symbol) => Boolean(symbol));
64
+ const target = preferProduction(ranked)[0];
65
+ if (target)
66
+ return target;
67
+ throw new WorkflowError("SYMBOL_NOT_FOUND", `No indexed symbol matches task: ${task}`, "Run context-slice index, then use a task that names a method, type, or qualified symbol.");
68
+ }
69
+ function renderedSkeleton(index, symbol, relation) {
70
+ const calls = index.calls
71
+ .filter((call) => call.callerId === symbol.id)
72
+ .map((call) => `${call.receiverText ? `${call.receiverText}.` : ""}${call.calleeName}(…)${call.externalPackage ? ` [external: ${call.externalPackage}]` : ""}`);
73
+ return `// ${relation}\n${renderSkeleton(symbol, calls)}`;
74
+ }
75
+ function ranked(symbols, task) {
76
+ return [...symbols].sort((a, b) => rankSymbol(b, task, task) - rankSymbol(a, task, task) ||
77
+ a.id.localeCompare(b.id));
78
+ }
79
+ export function buildPreview(index, task, options = {}) {
80
+ if (!task.trim()) {
81
+ throw new WorkflowError("SYMBOL_NOT_FOUND", "Preview requires a non-empty developer task.", 'Pass a task such as: context-slice preview "explain retryPayment".');
82
+ }
83
+ if (!index.symbols.length) {
84
+ throw new WorkflowError("INDEX_STALE", "No loaded index is available for preview.", "Run context-slice index before requesting a preview.");
85
+ }
86
+ const target = chooseTarget(index, task);
87
+ const targetTokens = estimateTokens(target.source);
88
+ const budget = options.budget ?? Math.max(1200, targetTokens);
89
+ if (budget < targetTokens) {
90
+ throw new WorkflowError("BUDGET_TOO_SMALL", `Budget ${budget} cannot include target ${target.qualifiedName ?? target.name} (${targetTokens} tokens).`, `Increase --budget to at least ${targetTokens}, or select a smaller target.`);
91
+ }
92
+ const included = [];
93
+ const omitted = [];
94
+ const composition = {
95
+ "task target": 0,
96
+ "direct caller": 0,
97
+ "direct callee": 0,
98
+ "enclosing type": 0,
99
+ "enterprise relation": 0,
100
+ "file imports": 0,
101
+ };
102
+ let estimatedTokens = 0;
103
+ const add = (symbol, reason, rendered, explanation, extra = {}) => {
104
+ const tokens = estimateTokens(rendered);
105
+ if (estimatedTokens + tokens > budget) {
106
+ omitted.push({
107
+ symbolId: symbol.id,
108
+ symbol: symbol.qualifiedName ?? symbol.name,
109
+ reason: "context budget",
110
+ estimatedTokens: tokens,
111
+ });
112
+ return;
113
+ }
114
+ included.push({
115
+ symbolId: symbol.id,
116
+ symbol: symbol.qualifiedName ?? symbol.name,
117
+ filePath: symbol.filePath,
118
+ reason,
119
+ explanation,
120
+ estimatedTokens: tokens,
121
+ rendered,
122
+ ...extra,
123
+ });
124
+ estimatedTokens += tokens;
125
+ composition[reason] += tokens;
126
+ };
127
+ add(target, "task target", target.source, `Selected because the task names ${target.name}.`);
128
+ const related = [
129
+ ...ranked(index.callersAtDepth(target, options.depth ?? 1), task).map((symbol) => ({
130
+ symbol,
131
+ reason: "direct caller",
132
+ relation: "Direct caller",
133
+ })),
134
+ ...ranked(index.dependenciesAtDepth(target, options.depth ?? 1), task).map((symbol) => ({
135
+ symbol,
136
+ reason: "direct callee",
137
+ relation: "Direct callee",
138
+ })),
139
+ ].sort((a, b) => rankSymbol(b.symbol, task, options.intent ?? "") -
140
+ rankSymbol(a.symbol, task, options.intent ?? "") ||
141
+ a.symbol.id.localeCompare(b.symbol.id));
142
+ const includedIds = new Set([target.id]);
143
+ for (const item of related) {
144
+ if (includedIds.has(item.symbol.id))
145
+ continue;
146
+ includedIds.add(item.symbol.id);
147
+ add(item.symbol, item.reason, renderedSkeleton(index, item.symbol, item.relation), `${item.relation} of ${target.qualifiedName ?? target.name}.`);
148
+ }
149
+ const relatedFiles = new Set([...includedIds]
150
+ .map((id) => index.symbols.find((s) => s.id === id)?.filePath)
151
+ .filter((file) => Boolean(file) && file !== target.filePath));
152
+ // Same-enclosing-type and import-context composition run after callers and
153
+ // callees, so they can only use budget they left, and never replace them.
154
+ let compositionTokens = 0;
155
+ const compositionAllowance = Math.floor(budget * COMPOSITION_BUDGET_SHARE);
156
+ const siblings = options.composition === false
157
+ ? []
158
+ : composeSiblings(index, target, includedIds);
159
+ const importCandidates = options.composition === false
160
+ ? []
161
+ : composeImportContext(index, target, relatedFiles, includedIds);
162
+ const relatedIds = new Set([...includedIds].filter((id) => id !== target.id));
163
+ const routeCandidates = options.composition === false
164
+ ? []
165
+ : composeRouteContext(index, target, relatedIds, includedIds);
166
+ const dependencyCandidates = options.composition === false
167
+ ? []
168
+ : composeDependencyContext(index, target, relatedIds, includedIds);
169
+ const transactionCandidates = options.composition === false
170
+ ? []
171
+ : composeTransactionContext(index, target, relatedIds, includedIds);
172
+ const jpaCandidates = options.composition === false
173
+ ? []
174
+ : composeJpaContext(index, target, relatedIds, includedIds);
175
+ for (const candidate of [
176
+ ...siblings,
177
+ ...importCandidates,
178
+ ...routeCandidates,
179
+ ...dependencyCandidates,
180
+ ...transactionCandidates,
181
+ ...jpaCandidates,
182
+ ]) {
183
+ if (candidate.symbol && includedIds.has(candidate.symbol.id))
184
+ continue;
185
+ if (compositionTokens + candidate.estimatedTokens > compositionAllowance) {
186
+ omitted.push({
187
+ symbolId: candidate.symbol?.id,
188
+ symbol: candidate.label,
189
+ reason: "composition budget share",
190
+ estimatedTokens: candidate.estimatedTokens,
191
+ evidence: candidate.evidence,
192
+ });
193
+ continue;
194
+ }
195
+ if (candidate.symbol)
196
+ includedIds.add(candidate.symbol.id);
197
+ compositionTokens += candidate.estimatedTokens;
198
+ add(candidate.symbol ?? target, candidate.reason, candidate.rendered, candidate.evidence.join("; "), {
199
+ evidence: candidate.evidence,
200
+ });
201
+ }
202
+ const unresolved = index.calls
203
+ .filter((call) => call.callerId === target.id && !call.resolvedTargetId)
204
+ .map((call) => ({ calleeName: call.calleeName, evidence: call.evidence }))
205
+ .sort((a, b) => a.calleeName.localeCompare(b.calleeName));
206
+ return {
207
+ task,
208
+ target,
209
+ budget,
210
+ estimatedTokens,
211
+ rendered: included.map((item) => item.rendered).join("\n\n"),
212
+ included,
213
+ omitted,
214
+ unresolved,
215
+ confidence: unresolved.length ? "mixed" : "high",
216
+ freshness: index.inspect(),
217
+ composition,
218
+ baseline: wholeFileBaseline(index, included, estimatedTokens),
219
+ };
220
+ }
@@ -0,0 +1,56 @@
1
+ import { existsSync, readdirSync } from "node:fs";
2
+ import { dirname, resolve } from "node:path";
3
+ import { adapterFor, ignoredDirectories } from "../languages/adapter.js";
4
+ import "../languages/java.js";
5
+ import "../languages/typescript/index.js";
6
+ import "../languages/javascript/index.js";
7
+ import "../languages/python/index.js";
8
+ import "../languages/go/index.js";
9
+ import { WorkflowError } from "./errors.js";
10
+ const ignored = new Set([
11
+ ".git",
12
+ "node_modules",
13
+ "target",
14
+ "build",
15
+ "dist",
16
+ "out",
17
+ ".gradle",
18
+ ".idea",
19
+ ".vscode",
20
+ ".context-slice",
21
+ ...ignoredDirectories(),
22
+ ]);
23
+ function hasSupportedSource(directory) {
24
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
25
+ if (ignored.has(entry.name))
26
+ continue;
27
+ const path = resolve(directory, entry.name);
28
+ if (entry.isFile() && adapterFor(entry.name))
29
+ return true;
30
+ if (entry.isDirectory() && hasSupportedSource(path))
31
+ return true;
32
+ }
33
+ return false;
34
+ }
35
+ function nearestGitRoot(start) {
36
+ let current = resolve(start);
37
+ while (true) {
38
+ if (existsSync(resolve(current, ".git")))
39
+ return current;
40
+ const parent = dirname(current);
41
+ if (parent === current)
42
+ return undefined;
43
+ current = parent;
44
+ }
45
+ }
46
+ export function resolveRepositoryRoot(options = {}) {
47
+ const cwd = resolve(options.cwd ?? process.cwd());
48
+ const root = options.repository
49
+ ? resolve(options.repository)
50
+ : (nearestGitRoot(cwd) ?? cwd);
51
+ if (!existsSync(root))
52
+ throw new WorkflowError("REPOSITORY_NOT_FOUND", `Repository not found: ${root}`, "Pass an existing directory with --repo.");
53
+ if (!hasSupportedSource(root))
54
+ throw new WorkflowError("NO_SUPPORTED_SOURCE", `No supported source found in: ${root}`, "Run ContextSlice in a Java or TypeScript repository, or pass --repo.");
55
+ return root;
56
+ }
package/mcp.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "context-slice": {
5
+ "type": "stdio",
6
+ "command": "node",
7
+ "args": ["${PLUGIN_ROOT}/dist/src/server/mcp-server.js"]
8
+ }
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "context-slice",
3
+ "version": "1.8.2",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/nvxtien/context-slice.git"
9
+ },
10
+ "bugs": {
11
+ "url": "https://github.com/nvxtien/context-slice/issues"
12
+ },
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "files": [
17
+ "dist/src",
18
+ "queries",
19
+ "skills",
20
+ "plugin.json",
21
+ "mcp.json",
22
+ "README.md",
23
+ "LICENSE"
24
+ ],
25
+ "bin": {
26
+ "context-slice": "dist/src/cli.js"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "prepack": "npm run build",
31
+ "start": "node dist/src/server/mcp-server.js",
32
+ "cli": "tsx src/cli.ts",
33
+ "package-smoke": "tsx scripts/package-smoke-test.ts",
34
+ "release:rc": "tsx scripts/clean-room-rc.ts",
35
+ "test": "tsx --test tests/**/*.test.ts",
36
+ "benchmark": "tsx benchmarks/benchmark.ts",
37
+ "benchmark:checkouts": "tsx benchmarks/fetch-checkouts.ts",
38
+ "benchmark:v03": "npm run benchmark:checkouts && tsx benchmarks/v0.3-real-repositories.ts",
39
+ "benchmark:v04": "npm run benchmark:v03 && tsx benchmarks/v0.4-symbol-index-hardening.ts",
40
+ "benchmark:v05": "npm run benchmark:v04 && tsx benchmarks/v0.5-semantic-call-resolution.ts",
41
+ "benchmark:v06": "npm run benchmark:v05 && tsx benchmarks/v0.6-developer-context-efficiency.ts",
42
+ "benchmark:v07": "tsx benchmarks/v0.7-developer-workflow.ts",
43
+ "benchmark:v08": "npm run package-smoke",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
46
+ "benchmark:v11": "npm run benchmark:checkouts && tsx benchmarks/v1.1-typescript-support.ts",
47
+ "benchmark:v12": "npm run benchmark:checkouts && tsx benchmarks/v1.2-context-composition.ts",
48
+ "benchmark:v13": "npm run benchmark:checkouts && tsx benchmarks/v1.3-python-support.ts",
49
+ "benchmark:v16": "npm run benchmark:checkouts && tsx benchmarks/v1.6-go-support.ts",
50
+ "benchmark:v17": "npm run benchmark:checkouts && tsx benchmarks/v1.7-javascript-support.ts",
51
+ "benchmark:v15-phase1": "npm run benchmark:checkouts && tsx benchmarks/v1.5-rust-real-repositories.ts",
52
+ "benchmark:v15-phase3": "npm run benchmark:checkouts && tsx benchmarks/v1.5-rust-tasks.ts",
53
+ "benchmark:v14-phase1": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase1-spring-mvc.ts",
54
+ "benchmark:v14-phase2": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase2-dependency-injection.ts",
55
+ "benchmark:v14-phase3": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase3-transactions.ts",
56
+ "benchmark:v14-phase4": "npm run benchmark:checkouts && tsx benchmarks/v1.4-phase4-jpa-spring-data.ts"
57
+ },
58
+ "dependencies": {
59
+ "@modelcontextprotocol/sdk": "^1.12.0",
60
+ "better-sqlite3": "^11.8.1",
61
+ "tree-sitter": "^0.21.1",
62
+ "tree-sitter-go": "^0.23.4",
63
+ "tree-sitter-java": "^0.23.5",
64
+ "tree-sitter-python": "^0.21.0",
65
+ "tree-sitter-rust": "0.21.0",
66
+ "tree-sitter-typescript": "^0.23.2",
67
+ "zod": "^3.25.76"
68
+ },
69
+ "devDependencies": {
70
+ "@types/better-sqlite3": "^7.6.12",
71
+ "@types/node": "^22.15.21",
72
+ "prettier": "^3.9.8",
73
+ "tsx": "^4.19.4",
74
+ "typescript": "^5.8.3"
75
+ }
76
+ }
package/plugin.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "context-slice",
4
+ "version": "1.8.2",
5
+ "description": "Structural code-context slicing for Java, TypeScript/TSX, JavaScript, Python, Rust, and Go. Serves a budget-bounded, relevance-ranked slice of a repository instead of whole files.",
6
+ "skills": "./skills/",
7
+ "mcpServers": "./mcp.json",
8
+ "author": {
9
+ "name": "tien.nguyen"
10
+ },
11
+ "homepage": "https://github.com/nvxtien/context-slice",
12
+ "repository": "https://github.com/nvxtien/context-slice",
13
+ "license": "MIT",
14
+ "keywords": ["context", "code-search", "mcp", "tree-sitter", "context-window"]
15
+ }
@@ -0,0 +1 @@
1
+ (annotation) @annotation
@@ -0,0 +1 @@
1
+ (method_invocation name: (identifier) @call.name)
@@ -0,0 +1 @@
1
+ (import_declaration) @import
@@ -0,0 +1,2 @@
1
+ (class_declaration name: (identifier) @class.name)
2
+ (method_declaration name: (identifier) @method.name)
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: context-slice
3
+ description: Use before reading source files to understand, implement, or explain something in a Java, TypeScript/TSX, JavaScript, Python, Rust, or Go repository. Serves a budget-bounded, relevance-ranked slice of the codebase (target symbol plus ranked callers/callees) through MCP tools instead of whole-file reads, shrinking how much context window the task consumes.
4
+ ---
5
+
6
+ # Context Slice
7
+
8
+ Context Slice is an MCP server that indexes a repository's symbols and call
9
+ graph (via Tree-sitter, no LLM involved) and serves a compact, explainable
10
+ slice of it — not whole files. Prefer its tools over `Read`/`Grep` on source
11
+ files whenever the task is about a specific symbol, function, class, or
12
+ behavior in a supported repository, so the model spends tokens on the
13
+ relevant code instead of re-reading entire files to find it.
14
+
15
+ **Supported languages:** Java, TypeScript, TSX, JavaScript, Python, Rust, Go.
16
+ For any other language, or a question with no clear target symbol (e.g.
17
+ "what does this project do overall"), fall back to normal file reading.
18
+
19
+ ## Workflow
20
+
21
+ 1. **Start with `context.preview`** for any implementation or explanation
22
+ task: `{ task: "<the task in the user's own words>" }`. It returns a
23
+ selected target symbol, its rendered body, ranked direct callers/callees
24
+ included under the token budget, inclusion/omission explanations, and any
25
+ unresolved calls. Read the explanations — they say *why* each piece was
26
+ included, and what was left out and why.
27
+ 2. **Do not assume an unresolved call has a concrete implementation.**
28
+ Tree-sitter analysis cannot prove runtime dispatch (reflection, DI
29
+ proxies, framework-generated code, Python/Go dynamic dispatch). An
30
+ unresolved call in the result means exactly that — unresolved, not
31
+ "has no implementation."
32
+ 3. **Drill in with the other tools once you know the target symbol's id or
33
+ name:**
34
+ - `context.symbol` — read one symbol as `signature` / `skeleton` / `body`
35
+ / `full` source. Use the smallest `detail` level that answers the
36
+ question; only use `full` when the exact source text matters.
37
+ - `context.callers` — bounded-depth callers of a symbol (`depth` 1-5).
38
+ - `context.slice` — a strict-budget slice centered on one symbol instead
39
+ of a task description.
40
+ - `context.search` — find candidate symbols by name/text when you don't
41
+ yet know the exact target.
42
+ - `context.diff` — a `git diff` under a strict token budget, when the
43
+ task is about recent changes.
44
+ 4. **Only fall back to `Read`/`Grep` on raw source files** when: the
45
+ language isn't supported, the repository has no index yet and a quick
46
+ one-off answer is needed, or a tool result's explanation says the
47
+ information isn't in the index (e.g. a file outside the checked-out
48
+ source, or an unresolved dynamic call you must manually verify).
49
+
50
+ ## Notes
51
+
52
+ - The index lives in `.context-slice/` inside the target repository and
53
+ refreshes automatically on every tool call — changed files are never
54
+ served stale, and there is no separate "build the index" step to run
55
+ first.
56
+ - Every tool call targets the project Claude Code currently has open
57
+ (`CONTEXT_SLICE_ROOT`), not the context-slice plugin's own source.
58
+ - All tool output is structural (symbols, call edges, source text) derived
59
+ from the repository itself — never benchmark answers, expected results, or
60
+ anything from outside the checked-out source.