purecontext-mcp 1.1.0 → 1.1.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/AGENT_INSTRUCTIONS.md +509 -0
- package/AGENT_INSTRUCTIONS_SHORT.md +97 -0
- package/CHANGELOG.md +212 -0
- package/docs/01-introduction.md +69 -0
- package/docs/02-installation.md +267 -0
- package/docs/03-quick-start.md +135 -0
- package/docs/04-configuration.md +214 -0
- package/docs/05-cli-reference.md +130 -0
- package/docs/06-tools-reference.md +499 -0
- package/docs/07-language-support.md +88 -0
- package/docs/08-framework-adapters.md +324 -0
- package/docs/09-dependency-graph.md +182 -0
- package/docs/10-semantic-search.md +153 -0
- package/docs/11-search-quality.md +110 -0
- package/docs/12-ai-summarization.md +106 -0
- package/docs/13-token-savings.md +110 -0
- package/docs/14-transport-modes.md +167 -0
- package/docs/15-team-setup.md +251 -0
- package/docs/16-docker.md +186 -0
- package/docs/17-web-ui.md +157 -0
- package/docs/18-git-history.md +157 -0
- package/docs/19-cross-repo.md +177 -0
- package/docs/20-architecture-analysis.md +228 -0
- package/docs/21-ecosystem-tools.md +189 -0
- package/docs/22-distribution.md +240 -0
- package/docs/23-performance.md +121 -0
- package/docs/24-security.md +144 -0
- package/docs/25-architecture-overview.md +240 -0
- package/docs/26-troubleshooting.md +234 -0
- package/docs/27-api-stability.md +114 -0
- package/docs/README.md +71 -0
- package/guide/README.md +57 -0
- package/guide/ai-summaries.md +127 -0
- package/guide/code-health.md +190 -0
- package/guide/code-history.md +149 -0
- package/guide/finding-code.md +157 -0
- package/guide/navigating-new-code.md +121 -0
- package/guide/safe-changes.md +156 -0
- package/guide/team-setup.md +191 -0
- package/guide/web-ui.md +154 -0
- package/guide/why-purecontext.md +73 -0
- package/guide/workflow-onboarding.md +114 -0
- package/guide/workflow-pr-review.md +199 -0
- package/guide/workflow-refactoring.md +172 -0
- package/package.json +9 -2
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# Framework Adapters
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Framework adapters layer domain-specific symbol extraction on top of language handlers. They are auto-detected from project config files.
|
|
5
|
+
|
|
6
|
+
## How adapters work
|
|
7
|
+
|
|
8
|
+
Each adapter implements the `FrameworkAdapter` interface:
|
|
9
|
+
|
|
10
|
+
- `detect(projectRoot)` — returns `true` if this framework is present (checks `package.json`, `go.mod`, etc.)
|
|
11
|
+
- `fileFilter(filePath)` — returns `true` for files this adapter should process
|
|
12
|
+
- `preProcess(source, filePath)` — splits multi-language files into parseable blocks (e.g., Vue SFCs)
|
|
13
|
+
- `extractFrameworkSymbols(tree, source, filePath)` — extracts framework-specific symbols from the AST
|
|
14
|
+
- `enrichMetadata(symbol)` — adds `frameworkMeta` to existing symbols
|
|
15
|
+
|
|
16
|
+
Multiple adapters can be active simultaneously. Adapters compose — a Vue + Nuxt project activates both.
|
|
17
|
+
|
|
18
|
+
## Configuring adapters
|
|
19
|
+
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"adapters": "auto" // auto-detect from project files (default)
|
|
23
|
+
"adapters": "none" // disable all adapters
|
|
24
|
+
"adapters": ["vue", "nuxt"] // explicit list
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## JavaScript / TypeScript frameworks
|
|
31
|
+
|
|
32
|
+
### Vue 3
|
|
33
|
+
|
|
34
|
+
**Detected by:** `vue` in `package.json`, or any `.vue` file present.
|
|
35
|
+
|
|
36
|
+
Extracts from `.vue` Single File Components:
|
|
37
|
+
- `component` — the component itself (from filename or `defineComponent`)
|
|
38
|
+
- `composable` — exported `use*` functions
|
|
39
|
+
|
|
40
|
+
`frameworkMeta` includes: `vue_component: true`, `vue_composable: true`.
|
|
41
|
+
|
|
42
|
+
### Nuxt
|
|
43
|
+
|
|
44
|
+
**Detected by:** `nuxt.config.ts` (or `.js`, `.mts`, `.mjs`) in project root.
|
|
45
|
+
|
|
46
|
+
Extracts:
|
|
47
|
+
- `route` from `pages/**/*.vue` (derives route path: `pages/blog/[slug].vue` → `/blog/:slug`)
|
|
48
|
+
- `route` from `server/api/**/*.ts` (HTTP method from filename suffix)
|
|
49
|
+
- `middleware` from `plugins/**` and `middleware/**`
|
|
50
|
+
- Enriches composables in `composables/**` with `nuxt_auto_import: true`
|
|
51
|
+
|
|
52
|
+
### React
|
|
53
|
+
|
|
54
|
+
**Detected by:** `react` in `package.json` dependencies.
|
|
55
|
+
|
|
56
|
+
Enriches TypeScript handler symbols:
|
|
57
|
+
- PascalCase functions returning JSX → `component`
|
|
58
|
+
- `use*` functions → `hook`
|
|
59
|
+
|
|
60
|
+
### Next.js
|
|
61
|
+
|
|
62
|
+
**Detected by:** `next.config.*` or `next` in `package.json`.
|
|
63
|
+
|
|
64
|
+
Extracts:
|
|
65
|
+
- **Pages Router** (`pages/**`): `route` symbols with path derivation
|
|
66
|
+
- `pages/blog/[slug].tsx` → `/blog/:slug`
|
|
67
|
+
- Detects `getServerSideProps` (SSR), `getStaticProps` (SSG)
|
|
68
|
+
- **App Router** (`app/**/page.tsx`): `route` symbols
|
|
69
|
+
- `app/(marketing)/about/page.tsx` → `/about` (route groups stripped)
|
|
70
|
+
- Detects `'use client'` directive
|
|
71
|
+
- **API routes**: `pages/api/**` and `app/**/route.ts` with HTTP method exports
|
|
72
|
+
- **Middleware** (`middleware.ts`): `middleware` symbol with matcher config
|
|
73
|
+
|
|
74
|
+
### Angular
|
|
75
|
+
|
|
76
|
+
**Detected by:** `@angular/core` in `package.json`.
|
|
77
|
+
|
|
78
|
+
Extracts from decorated classes:
|
|
79
|
+
- `@Component` → `component` (with `selector`)
|
|
80
|
+
- `@Injectable` → `class` (service)
|
|
81
|
+
- `@NgModule` → `class` (module)
|
|
82
|
+
- `@Directive` → `component` (with `selector`)
|
|
83
|
+
- `@Pipe` → `component` (with pipe name)
|
|
84
|
+
- `RouterModule.forRoot/forChild` → `route` symbols
|
|
85
|
+
|
|
86
|
+
### NestJS
|
|
87
|
+
|
|
88
|
+
**Detected by:** `@nestjs/core` in `package.json`.
|
|
89
|
+
|
|
90
|
+
Extracts from decorated controllers:
|
|
91
|
+
- `@Controller('prefix')` + `@Get(':id')` → `route` with combined path (`GET /prefix/:id`)
|
|
92
|
+
- `@Injectable` → `class` with `nestjs_provider: true`
|
|
93
|
+
- `@Module` → `class` with `nestjs_module: true`
|
|
94
|
+
- `@Guard` / `CanActivate` → `middleware`
|
|
95
|
+
|
|
96
|
+
### Express
|
|
97
|
+
|
|
98
|
+
**Detected by:** `express` in `package.json`.
|
|
99
|
+
|
|
100
|
+
Extracts string-literal route registrations:
|
|
101
|
+
- `app.get('/path', ...)` → `route`
|
|
102
|
+
- `router.post('/path', ...)` → `route`
|
|
103
|
+
- `app.use('/path', ...)` → `middleware`
|
|
104
|
+
|
|
105
|
+
Dynamic paths (variables, template literals) are skipped.
|
|
106
|
+
|
|
107
|
+
### Fastify
|
|
108
|
+
|
|
109
|
+
**Detected by:** `fastify` in `package.json`.
|
|
110
|
+
|
|
111
|
+
Same pattern as Express: `fastify.get(path, ...)` → `route`.
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Python frameworks
|
|
116
|
+
|
|
117
|
+
### Flask
|
|
118
|
+
|
|
119
|
+
**Detected by:** `Flask` in `requirements.txt` or `pyproject.toml`.
|
|
120
|
+
|
|
121
|
+
Extracts:
|
|
122
|
+
- `@app.route('/path')` → `route`
|
|
123
|
+
- `@app.get('/path')`, `@app.post(...)` → `route`
|
|
124
|
+
- Blueprint routes: `@bp.route(...)` → `route`
|
|
125
|
+
- Class-based views (inheriting `MethodView`) → `view`
|
|
126
|
+
|
|
127
|
+
Flask path parameters (`<int:user_id>`) are preserved as-is.
|
|
128
|
+
|
|
129
|
+
### FastAPI
|
|
130
|
+
|
|
131
|
+
**Detected by:** `fastapi` in `requirements.txt` or `pyproject.toml`.
|
|
132
|
+
|
|
133
|
+
Extracts:
|
|
134
|
+
- `@app.get('/items/{id}')` → `route`
|
|
135
|
+
- `@router.post(...)` (APIRouter) → `route`
|
|
136
|
+
|
|
137
|
+
FastAPI path parameters (`{param}`) are preserved.
|
|
138
|
+
|
|
139
|
+
### Django
|
|
140
|
+
|
|
141
|
+
**Detected by:** `manage.py` at project root, or `django` in requirements.
|
|
142
|
+
|
|
143
|
+
Extracts:
|
|
144
|
+
- `models.Model` subclasses → `model`
|
|
145
|
+
- Class-based views (`APIView`, `ViewSet`, etc.) → `view`
|
|
146
|
+
- Function-based views (with `@login_required`, `@api_view`) → `view`
|
|
147
|
+
- `path(...)` / `re_path(...)` in `urls.py` → `route`
|
|
148
|
+
- `@receiver(post_save)` → `signal`
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Go frameworks
|
|
153
|
+
|
|
154
|
+
### Gin
|
|
155
|
+
|
|
156
|
+
**Detected by:** `github.com/gin-gonic/gin` in `go.mod`.
|
|
157
|
+
|
|
158
|
+
Extracts: `r.GET("/path", handler)`, `r.POST(...)`, etc. → `route`. Groups: `r.Group("/api")` prefix applied to child routes.
|
|
159
|
+
|
|
160
|
+
### Echo
|
|
161
|
+
|
|
162
|
+
**Detected by:** `github.com/labstack/echo` in `go.mod`.
|
|
163
|
+
|
|
164
|
+
Same pattern as Gin: `e.GET("/path", handler)` → `route`.
|
|
165
|
+
|
|
166
|
+
### Fiber
|
|
167
|
+
|
|
168
|
+
**Detected by:** `github.com/gofiber/fiber` in `go.mod`.
|
|
169
|
+
|
|
170
|
+
Title-case methods: `app.Get("/path", handler)` → `route`.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## PHP frameworks
|
|
175
|
+
|
|
176
|
+
### Laravel
|
|
177
|
+
|
|
178
|
+
**Detected by:** `laravel/framework` in `composer.json`.
|
|
179
|
+
|
|
180
|
+
Extracts:
|
|
181
|
+
- `Route::get('/path', ...)` → `route`
|
|
182
|
+
- `Route::resource('users', Controller::class)` → multiple CRUD routes
|
|
183
|
+
- `Route::group(['prefix' => '/api'], ...)` → prefix applied to children
|
|
184
|
+
- `User extends Model` → `model`
|
|
185
|
+
- Controller public methods → `view`
|
|
186
|
+
- Middleware classes → `middleware`
|
|
187
|
+
|
|
188
|
+
### Symfony
|
|
189
|
+
|
|
190
|
+
**Detected by:** `symfony/framework-bundle` in `composer.json`.
|
|
191
|
+
|
|
192
|
+
Extracts:
|
|
193
|
+
- `#[Route('/path', methods: ['GET'])]` → `route` (PHP 8 attributes)
|
|
194
|
+
- `@Route('/path')` in docblock → `route` (annotation style)
|
|
195
|
+
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
## Ruby frameworks
|
|
199
|
+
|
|
200
|
+
### Rails
|
|
201
|
+
|
|
202
|
+
**Detected by:** `gem 'rails'` in `Gemfile`.
|
|
203
|
+
|
|
204
|
+
Extracts:
|
|
205
|
+
- `ApplicationRecord` subclasses → `model` (with `has_many` associations)
|
|
206
|
+
- Controller public methods → `view` (action)
|
|
207
|
+
- `get '/path'`, `resources :users` in `routes.rb` → `route`
|
|
208
|
+
|
|
209
|
+
### Sinatra
|
|
210
|
+
|
|
211
|
+
**Detected by:** `gem 'sinatra'` in `Gemfile` or `require 'sinatra'` in source.
|
|
212
|
+
|
|
213
|
+
Extracts: `get '/path' do ... end` → `route`.
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Kotlin frameworks
|
|
218
|
+
|
|
219
|
+
### Ktor
|
|
220
|
+
|
|
221
|
+
**Detected by:** `io.ktor` in `build.gradle` / `pom.xml`.
|
|
222
|
+
|
|
223
|
+
Extracts from routing DSL:
|
|
224
|
+
- `get("/path") { ... }` → `route`
|
|
225
|
+
- `route("/api") { get("/users") }` → combined path `/api/users`
|
|
226
|
+
- `authenticate { ... }` → `frameworkMeta.authenticated: true`
|
|
227
|
+
|
|
228
|
+
### Spring (Kotlin)
|
|
229
|
+
|
|
230
|
+
**Detected by:** `org.springframework.boot` in `build.gradle` / `pom.xml`.
|
|
231
|
+
|
|
232
|
+
Extracts: `@RestController` + `@GetMapping("/path")` → `route`; `@Service`, `@Component`, `@Repository` → class with metadata.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Rust frameworks
|
|
237
|
+
|
|
238
|
+
### Axum
|
|
239
|
+
|
|
240
|
+
**Detected by:** `axum` in `Cargo.toml`.
|
|
241
|
+
|
|
242
|
+
Extracts: `.route("/path", get(handler))` chains → `route`. Layers: `.layer(...)` → `middleware`.
|
|
243
|
+
|
|
244
|
+
### Actix-web
|
|
245
|
+
|
|
246
|
+
**Detected by:** `actix-web` in `Cargo.toml`.
|
|
247
|
+
|
|
248
|
+
Extracts: `#[get("/path")]` attribute macro → `route`; `.wrap(...)` → `middleware`.
|
|
249
|
+
|
|
250
|
+
### Rocket
|
|
251
|
+
|
|
252
|
+
**Detected by:** `rocket` in `Cargo.toml`.
|
|
253
|
+
|
|
254
|
+
Extracts: `#[get("/path")]` → `route`; `#[catch(404)]` → error catcher; `Fairing` implementations → `middleware`.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Java frameworks
|
|
259
|
+
|
|
260
|
+
### Spring Boot
|
|
261
|
+
|
|
262
|
+
**Detected by:** `spring-boot-starter` in `pom.xml` / `build.gradle`.
|
|
263
|
+
|
|
264
|
+
Extracts: `@GetMapping`, `@PostMapping` (combined with `@RequestMapping` prefix) → `route`; `@Service`, `@Component`, `@Repository` → beans; `@Bean` methods; `@Scheduled` methods → scheduled tasks.
|
|
265
|
+
|
|
266
|
+
### Micronaut
|
|
267
|
+
|
|
268
|
+
**Detected by:** `io.micronaut` in build files.
|
|
269
|
+
|
|
270
|
+
Extracts: `@Get("/path")`, `@Post(...)` → `route`; `@Client` interfaces.
|
|
271
|
+
|
|
272
|
+
### Quarkus
|
|
273
|
+
|
|
274
|
+
**Detected by:** `io.quarkus` in build files.
|
|
275
|
+
|
|
276
|
+
Extracts: JAX-RS `@GET` / `@Path` → `route`; `@ApplicationScoped` → bean.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## ORM adapters
|
|
281
|
+
|
|
282
|
+
### Hibernate (Java)
|
|
283
|
+
|
|
284
|
+
**Detected by:** `hibernate-core` or `jakarta.persistence` in dependencies.
|
|
285
|
+
|
|
286
|
+
Extracts from `@Entity` classes: table name, columns with types and nullability, relationships (`@OneToMany`, etc.), named queries.
|
|
287
|
+
|
|
288
|
+
### SQLAlchemy (Python)
|
|
289
|
+
|
|
290
|
+
**Detected by:** `sqlalchemy` in requirements.
|
|
291
|
+
|
|
292
|
+
Extracts from `Base` / `DeclarativeBase` subclasses: `__tablename__`, `Column(Type)` fields, `relationship()` associations, `@hybrid_property` methods. Supports both SQLAlchemy 1.x and 2.0 (`mapped_column`) styles.
|
|
293
|
+
|
|
294
|
+
### Django ORM
|
|
295
|
+
|
|
296
|
+
**Detected by:** Django project with `manage.py`.
|
|
297
|
+
|
|
298
|
+
Extracts `models.Model` subclasses with field types: `CharField`, `IntegerField`, `ForeignKey`, `ManyToManyField`, etc.
|
|
299
|
+
|
|
300
|
+
### Prisma
|
|
301
|
+
|
|
302
|
+
**Detected by:** `prisma` in `package.json` or `schema.prisma` present.
|
|
303
|
+
|
|
304
|
+
Extracts model definitions and relations from `schema.prisma`.
|
|
305
|
+
|
|
306
|
+
### TypeORM
|
|
307
|
+
|
|
308
|
+
**Detected by:** `typeorm` in `package.json`.
|
|
309
|
+
|
|
310
|
+
Extracts `@Entity` classes and `@Column` / `@Relation` fields.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## Mobile frameworks
|
|
315
|
+
|
|
316
|
+
### Flutter
|
|
317
|
+
|
|
318
|
+
**Detected by:** `sdk: flutter` in `pubspec.yaml`.
|
|
319
|
+
|
|
320
|
+
Extracts from `.dart` files:
|
|
321
|
+
- `StatelessWidget` subclasses → `widget`
|
|
322
|
+
- `StatefulWidget` subclasses → `widget` (with linked State class)
|
|
323
|
+
- `ChangeNotifier` subclasses → `class` with `flutter_notifier: true`
|
|
324
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Dependency Graph Tools
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
The dependency graph tracks import relationships between files. Four tools let agents query it at different granularities — from a single hop to a full transitive walk.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Concepts
|
|
9
|
+
|
|
10
|
+
During indexing, each `import` / `require` / `use` statement is resolved to a dependency edge:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
dep_edge: sourceFile → resolvedTargetFile (via import specifier)
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Edges are stored in the `dep_edges` SQLite table. External packages (e.g., `from 'react'`) produce edges with `resolvedPath: null` and are excluded from graph traversal.
|
|
17
|
+
|
|
18
|
+
Two directions of traversal:
|
|
19
|
+
- **Forward walk** — "what does X depend on?" (imports, transitively)
|
|
20
|
+
- **Reverse walk** — "what depends on X?" (importers, transitively)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## `get_context_bundle`
|
|
25
|
+
|
|
26
|
+
**Purpose:** Forward-walk from a symbol — returns everything an agent needs to understand it (the symbol itself plus its transitive imports).
|
|
27
|
+
|
|
28
|
+
**When to use:** Before modifying a function — understand its full context without reading whole files.
|
|
29
|
+
|
|
30
|
+
**Parameters:**
|
|
31
|
+
|
|
32
|
+
| Parameter | Type | Default | Description |
|
|
33
|
+
|-----------|------|---------|-------------|
|
|
34
|
+
| `repoId` | `string` | required | Target repository |
|
|
35
|
+
| `symbolId` | `string` | required | Starting symbol |
|
|
36
|
+
| `maxDepth` | `number` | `3` | Traversal depth |
|
|
37
|
+
| `maxTokens` | `number` | — | Stop collecting when estimate exceeds this |
|
|
38
|
+
|
|
39
|
+
**Example:**
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
"Give me everything needed to understand the processOrder function."
|
|
43
|
+
|
|
44
|
+
→ get_context_bundle({ symbolId: "processOrder-id", maxDepth: 2 })
|
|
45
|
+
→ Returns: processOrder + validateCart + calculateTax + formatPrice
|
|
46
|
+
_tokenEstimate: 820
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Response:**
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"symbols": [
|
|
54
|
+
{ "id": "...", "name": "processOrder", "signature": "...", "source": "..." },
|
|
55
|
+
{ "id": "...", "name": "validateCart", "signature": "...", "source": "..." }
|
|
56
|
+
],
|
|
57
|
+
"files": ["src/orders/processor.ts", "src/cart/validator.ts"],
|
|
58
|
+
"_tokenEstimate": 820
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## `get_blast_radius`
|
|
65
|
+
|
|
66
|
+
**Purpose:** Reverse-walk — all files that (transitively) import a given symbol. Tells you what would break if you change or delete it.
|
|
67
|
+
|
|
68
|
+
**When to use:** Before modifying or deleting a symbol.
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
|
|
72
|
+
| Parameter | Type | Default | Description |
|
|
73
|
+
|-----------|------|---------|-------------|
|
|
74
|
+
| `repoId` | `string` | required | Target repository |
|
|
75
|
+
| `symbolId` | `string` | required | Symbol to analyze |
|
|
76
|
+
| `maxDepth` | `number` | `5` | Traversal depth |
|
|
77
|
+
|
|
78
|
+
**Example:**
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
"What breaks if I change UserService.authenticate?"
|
|
82
|
+
|
|
83
|
+
→ get_blast_radius({ symbolId: "UserService.authenticate-id" })
|
|
84
|
+
→ Returns: 14 files at depth 1–3
|
|
85
|
+
(AuthController, LoginPage, SessionMiddleware, tests/...)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Response:**
|
|
89
|
+
|
|
90
|
+
```json
|
|
91
|
+
{
|
|
92
|
+
"importers": [
|
|
93
|
+
"src/controllers/auth.ts",
|
|
94
|
+
"src/middleware/session.ts",
|
|
95
|
+
"src/pages/Login.tsx",
|
|
96
|
+
"test/auth.test.ts"
|
|
97
|
+
],
|
|
98
|
+
"count": 14,
|
|
99
|
+
"_tokenEstimate": 120
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## `find_importers`
|
|
106
|
+
|
|
107
|
+
**Purpose:** Direct (one-hop) importers of a file — faster and narrower than `get_blast_radius`.
|
|
108
|
+
|
|
109
|
+
**When to use:** Quick check — "who imports this module directly?"
|
|
110
|
+
|
|
111
|
+
**Parameters:** `{ repoId, filePath }` — `filePath` is relative to repo root.
|
|
112
|
+
|
|
113
|
+
**Response:**
|
|
114
|
+
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"importers": [
|
|
118
|
+
{
|
|
119
|
+
"filePath": "src/controllers/auth.ts",
|
|
120
|
+
"importedNames": ["UserService", "AuthToken"]
|
|
121
|
+
}
|
|
122
|
+
],
|
|
123
|
+
"_tokenEstimate": 80
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## `find_dead_code`
|
|
130
|
+
|
|
131
|
+
**Purpose:** Exported symbols in files that nothing else imports — potential dead code.
|
|
132
|
+
|
|
133
|
+
**When to use:** Cleanup sprints, pre-refactor audits.
|
|
134
|
+
|
|
135
|
+
**Parameters:**
|
|
136
|
+
|
|
137
|
+
| Parameter | Type | Default | Description |
|
|
138
|
+
|-----------|------|---------|-------------|
|
|
139
|
+
| `repoId` | `string` | required | Target repository |
|
|
140
|
+
| `limit` | `number` | `50` | Max results |
|
|
141
|
+
|
|
142
|
+
**Response:**
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"symbols": [
|
|
147
|
+
{
|
|
148
|
+
"id": "...",
|
|
149
|
+
"name": "legacyFormatDate",
|
|
150
|
+
"kind": "function",
|
|
151
|
+
"filePath": "src/utils/date-old.ts",
|
|
152
|
+
"signature": "function legacyFormatDate(d: Date): string"
|
|
153
|
+
}
|
|
154
|
+
],
|
|
155
|
+
"_tokenEstimate": 240
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**False positive sources:**
|
|
160
|
+
- **Dynamic imports** — `import('./module')` are not tracked by the static graph
|
|
161
|
+
- **Side-effect imports** — `import './setup'` (no names imported) create edges but no `importedNames`
|
|
162
|
+
- **External consumers** — if this repo is itself an npm package, external consumers won't appear in the index
|
|
163
|
+
- **Test files** — test imports are included in the graph; symbols only used by tests are not dead
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Combining graph tools
|
|
168
|
+
|
|
169
|
+
A typical refactoring workflow:
|
|
170
|
+
|
|
171
|
+
```
|
|
172
|
+
1. get_blast_radius(symbolId)
|
|
173
|
+
→ See the full impact scope before touching anything
|
|
174
|
+
|
|
175
|
+
2. get_context_bundle(symbolId, maxDepth: 2)
|
|
176
|
+
→ Understand the symbol and its immediate dependencies
|
|
177
|
+
|
|
178
|
+
3. Make the change
|
|
179
|
+
|
|
180
|
+
4. find_dead_code(repoId)
|
|
181
|
+
→ Verify no orphaned exports were left behind
|
|
182
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# Semantic Search
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
Semantic search finds symbols by **meaning** rather than exact name. It uses an HNSW (Hierarchical Navigable Small World) vector index built from symbol summaries and signatures.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## How it works
|
|
9
|
+
|
|
10
|
+
1. During indexing, each symbol's summary + signature is sent to an embedding model (e.g., OpenAI `text-embedding-3-small`)
|
|
11
|
+
2. The resulting vector is stored in an HNSW index persisted at `~/.purecontext/indexes/{repoId}/hnsw.idx`
|
|
12
|
+
3. At search time, the query is embedded using the same model
|
|
13
|
+
4. HNSW performs an approximate nearest-neighbor search by cosine similarity
|
|
14
|
+
5. Results are ranked by similarity score and returned
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## When the HNSW index is built
|
|
19
|
+
|
|
20
|
+
The HNSW index is only built when **all three** conditions are met:
|
|
21
|
+
|
|
22
|
+
1. `semantic.enabled: true` in config
|
|
23
|
+
2. An embedding provider is configured (`semantic.provider`)
|
|
24
|
+
3. The repo has more symbols than `semantic.threshold` (default: 50,000)
|
|
25
|
+
|
|
26
|
+
**For small or medium repos**, set `threshold` low to enable HNSW early:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"semantic": {
|
|
31
|
+
"enabled": true,
|
|
32
|
+
"provider": "openai",
|
|
33
|
+
"threshold": 100
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Below the threshold, all search falls back to FTS5 keyword search — which is fast and accurate for name-based queries.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Enabling semantic search
|
|
43
|
+
|
|
44
|
+
### Using OpenAI embeddings
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"semantic": {
|
|
49
|
+
"enabled": true,
|
|
50
|
+
"provider": "openai",
|
|
51
|
+
"threshold": 50000
|
|
52
|
+
},
|
|
53
|
+
"ai": {
|
|
54
|
+
"openaiApiKey": "${OPENAI_API_KEY}"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Using local Ollama embeddings
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
{
|
|
63
|
+
"semantic": {
|
|
64
|
+
"enabled": true,
|
|
65
|
+
"provider": "local",
|
|
66
|
+
"localEmbeddingEndpoint": "http://localhost:11434",
|
|
67
|
+
"threshold": 1000
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Set the model by passing it in the endpoint config or using Ollama's default embedding model.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Using `search_semantic`
|
|
77
|
+
|
|
78
|
+
```json
|
|
79
|
+
{
|
|
80
|
+
"repo": "my-project",
|
|
81
|
+
"query": "function that validates user credentials",
|
|
82
|
+
"mode": "hybrid",
|
|
83
|
+
"semantic_weight": 0.6,
|
|
84
|
+
"keyword_weight": 0.4,
|
|
85
|
+
"max_results": 10
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**Response:**
|
|
90
|
+
|
|
91
|
+
```json
|
|
92
|
+
{
|
|
93
|
+
"results": [
|
|
94
|
+
{
|
|
95
|
+
"id": "8f3a...",
|
|
96
|
+
"name": "authenticateUser",
|
|
97
|
+
"kind": "function",
|
|
98
|
+
"filePath": "src/auth/validator.ts",
|
|
99
|
+
"signature": "function authenticateUser(creds: Credentials): Promise<User>",
|
|
100
|
+
"summary": "Validates credentials against the database and returns a session token.",
|
|
101
|
+
"scores": {
|
|
102
|
+
"keyword": 0.72,
|
|
103
|
+
"semantic": 0.89,
|
|
104
|
+
"combined": 0.82
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Keyword vs semantic — when to use which
|
|
114
|
+
|
|
115
|
+
| Situation | Best tool |
|
|
116
|
+
|-----------|-----------|
|
|
117
|
+
| You know the name or a fragment of it | `search_symbols` (keyword) |
|
|
118
|
+
| You know what it does, not what it's called | `search_semantic` |
|
|
119
|
+
| You want maximum recall | `search_semantic` with `mode: "hybrid"` |
|
|
120
|
+
| Semantic index not available (small repo) | `search_symbols` always works |
|
|
121
|
+
|
|
122
|
+
The `search_symbols` tool also supports `mode: "hybrid"` — it will use HNSW automatically if the index exists.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## How hybrid search works
|
|
127
|
+
|
|
128
|
+
Hybrid mode runs both searches and merges results using **Reciprocal Rank Fusion (RRF)**:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
score = keyword_weight × (1 / (60 + keyword_rank))
|
|
132
|
+
+ semantic_weight × (1 / (60 + semantic_rank))
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
This combines the precision of exact-name matching with the recall of semantic matching. Adjust weights to bias toward one or the other.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## Performance
|
|
140
|
+
|
|
141
|
+
- HNSW search: < 10ms for k=10 in a 100k vector index
|
|
142
|
+
- Embedding generation: batched at index time, ~50ms per symbol (API latency)
|
|
143
|
+
- Indexes are persisted — no rebuild on each startup
|
|
144
|
+
|
|
145
|
+
## Rebuilding the vector index
|
|
146
|
+
|
|
147
|
+
The HNSW index is rebuilt automatically on full re-index. To force a rebuild without re-parsing all files:
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
Use invalidate_cache with force: true, then index_folder again.
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Or call `index_folder` with `force: true` — this re-embeds all symbols even if their source hasn't changed.
|