carto-md 2.0.7 → 2.0.8
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/README.md +147 -23
- package/package.json +8 -4
- package/scripts/postinstall.js +46 -0
- package/src/acp/agent.js +5 -5
- package/src/acp/providers/index.js +2 -2
- package/src/agents/leiden.js +7 -13
- package/src/bitmap/bitset.js +190 -0
- package/src/bitmap/index.js +121 -0
- package/src/bitmap/sidecar.js +545 -0
- package/src/bitmap/tools.js +310 -0
- package/src/cli/check.js +57 -0
- package/src/cli/index.js +14 -2
- package/src/cli/init.js +297 -65
- package/src/cli/inspect.js +295 -0
- package/src/cli/serve.js +1 -1
- package/src/cli/watch.js +6 -0
- package/src/engine/worker.js +24 -4
- package/src/extractors/imports.js +176 -0
- package/src/extractors/languages/html.js +4 -1
- package/src/extractors/languages/javascript.js +5 -0
- package/src/extractors/languages/prisma.js +4 -1
- package/src/extractors/languages/python.js +5 -1
- package/src/extractors/languages/r.js +4 -1
- package/src/extractors/languages/typescript.js +2 -0
- package/src/extractors/tree-sitter-parser.js +15 -0
- package/src/mcp/diff-parser.js +246 -0
- package/src/mcp/server-v2.js +489 -8
- package/src/mcp/validate.js +304 -0
- package/src/store/config-loader.js +77 -0
- package/src/store/sqlite-store.js +389 -4
- package/src/store/sync-v2.js +422 -96
- package/BENCHMARK_RESULTS.md +0 -34
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ carto init
|
|
|
17
17
|
|
|
18
18
|
That's it. Carto auto-wires itself into every AI tool you have installed. Restart your AI tool and it now knows your codebase architecturally.
|
|
19
19
|
|
|
20
|
-
**Works with:** Cursor · Claude Code · Kiro · Claude Desktop · Windsurf · VS Code Copilot · JetBrains · Zed
|
|
20
|
+
**Works with:** Cursor · Claude Code · Codex · Kiro · Claude Desktop · Windsurf · VS Code Copilot · JetBrains · Zed
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ That's it. Carto auto-wires itself into every AI tool you have installed. Restar
|
|
|
34
34
|
|
|
35
35
|
### Claude Code (CLI)
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
`carto init` writes `<project>/.mcp.json` for you when Claude Code is detected (`claude` binary on PATH or `~/.claude/` exists). Manual:
|
|
38
38
|
```bash
|
|
39
39
|
claude mcp add carto -- carto serve
|
|
40
40
|
```
|
|
@@ -43,6 +43,21 @@ Or create `.mcp.json` at the project root:
|
|
|
43
43
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"] } } }
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
+
### Codex (CLI)
|
|
47
|
+
|
|
48
|
+
`carto init` writes `~/.codex/config.toml` for you when Codex is detected (`codex` binary on PATH or `~/.codex/` exists). Manual:
|
|
49
|
+
```bash
|
|
50
|
+
codex mcp add carto -- carto serve
|
|
51
|
+
```
|
|
52
|
+
Or edit `~/.codex/config.toml`:
|
|
53
|
+
```toml
|
|
54
|
+
[mcp_servers.carto]
|
|
55
|
+
command = "carto"
|
|
56
|
+
args = ["serve"]
|
|
57
|
+
cwd = "/your/project"
|
|
58
|
+
enabled = true
|
|
59
|
+
```
|
|
60
|
+
|
|
46
61
|
### Kiro
|
|
47
62
|
|
|
48
63
|
`carto init` writes this for you. Manual: `~/.kiro/settings/mcp.json`
|
|
@@ -52,21 +67,25 @@ Or create `.mcp.json` at the project root:
|
|
|
52
67
|
|
|
53
68
|
### Claude Desktop
|
|
54
69
|
|
|
55
|
-
`carto init` writes this for you. Manual:
|
|
70
|
+
`carto init` writes this for you (cross-platform). Manual paths:
|
|
71
|
+
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
72
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
73
|
+
- Linux: `~/.config/Claude/claude_desktop_config.json` (community Linux builds)
|
|
74
|
+
|
|
56
75
|
```json
|
|
57
76
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
58
77
|
```
|
|
59
78
|
|
|
60
79
|
### VS Code Copilot
|
|
61
80
|
|
|
62
|
-
`.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
|
|
81
|
+
`carto init` writes `<project>/.vscode/mcp.json` for you when the `code` binary is on PATH. Manual: `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`. Note: VS Code uses `servers` (not `mcpServers`) and requires `"type": "stdio"`.
|
|
63
82
|
```json
|
|
64
|
-
{ "servers": { "carto": { "
|
|
83
|
+
{ "servers": { "carto": { "type": "stdio", "command": "carto", "args": ["serve"] } } }
|
|
65
84
|
```
|
|
66
85
|
|
|
67
86
|
### Windsurf
|
|
68
87
|
|
|
69
|
-
`~/.windsurf/
|
|
88
|
+
`carto init` writes this for you when Windsurf is detected. Manual: `~/.codeium/windsurf/mcp_config.json`
|
|
70
89
|
```json
|
|
71
90
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
72
91
|
```
|
|
@@ -94,6 +113,30 @@ Without carto, your AI greps text and guesses. With carto, it queries structural
|
|
|
94
113
|
|
|
95
114
|
Your AI's response goes from "here's a guess" to "here's the right change with the architectural context."
|
|
96
115
|
|
|
116
|
+
### Catching bad changes before save
|
|
117
|
+
|
|
118
|
+
The harder problem isn't finding the right file — it's stopping a confident-sounding AI from writing a refactor that breaks the rest of the repo. Carto exposes a diff-shaped query the AI can call before the user accepts a change:
|
|
119
|
+
|
|
120
|
+
> *AI proposes a 12-line patch to `packages/pg-meta/src/pg-format/index.ts`. Before showing the diff, it calls `validate_diff(patch)`.*
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
# Diff Validation
|
|
124
|
+
|
|
125
|
+
**Risk:** 🔴 HIGH
|
|
126
|
+
**Files changed:** 1
|
|
127
|
+
**Union blast radius:** 83 transitive dependents
|
|
128
|
+
|
|
129
|
+
## Violations (1)
|
|
130
|
+
|
|
131
|
+
| Severity | Kind | File | Detail |
|
|
132
|
+
|----------|-------------|----------------------------------------|-----------------------------------------------------------------|
|
|
133
|
+
| HIGH | high_blast | `packages/pg-meta/src/pg-format/index.ts` | Modifying this file affects 83 transitive dependents (>50). |
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The AI sees this *before* it proposes the diff. It revises its plan, splits the change, or asks the user. The bad refactor never makes it to the screen. Sub-millisecond on a 7,000-file repo — see the **Benchmarks** section below.
|
|
137
|
+
|
|
138
|
+
Every `validate_diff` call is also written to a local SQLite log, so a session that runs five hours later can ask `did_we_discuss_this("snake_case naming")` and get back the prior decision. The AI stops re-deciding settled questions.
|
|
139
|
+
|
|
97
140
|
### In the wild
|
|
98
141
|
|
|
99
142
|

|
|
@@ -192,7 +235,7 @@ Carto supports any LLM provider — configure in your editor:
|
|
|
192
235
|
|
|
193
236
|
---
|
|
194
237
|
|
|
195
|
-
## The
|
|
238
|
+
## The 22 MCP tools
|
|
196
239
|
|
|
197
240
|
Once carto is wired in, your AI tool can call any of these mid-task. You don't need to memorize them — your AI will pick the right ones.
|
|
198
241
|
|
|
@@ -201,6 +244,8 @@ Once carto is wired in, your AI tool can call any of these mid-task. You don't n
|
|
|
201
244
|
| `get_architecture()` | 500-word project overview: domains, entry points, stack, key patterns. **Use this first.** |
|
|
202
245
|
| `get_change_plan(intent)` | Given "add rate limiting to /api/users" → files to touch, domains affected, blast radius, similar patterns |
|
|
203
246
|
| `get_blast_radius(file)` | All files affected by changing a given file, with hop distance |
|
|
247
|
+
| `simulate_change_impact(files)` | Union of all files transitively affected by changing **multiple** files at once. Powered by the bitmap engine — sub-millisecond on 7K-file repos. |
|
|
248
|
+
| `validate_diff(diff)` | Given a unified diff: violations (cross-domain imports, high-blast files), blast radius per file, risk level (SAFE/LOW/MEDIUM/HIGH), suggestions. Each call is recorded in the **episodic memory** log so other tools can ask "did we discuss this?". Sub-15ms p99. |
|
|
204
249
|
| `get_context(file)` | Everything about a file: domain, blast radius, neighbors, routes, models |
|
|
205
250
|
| `get_file_summary(file)` | What a file does, its role, key deps and dependents |
|
|
206
251
|
| `get_similar_patterns(file)` | Files with same domain, route shape, or shared deps — find conventions before writing new code |
|
|
@@ -214,15 +259,25 @@ Once carto is wired in, your AI tool can call any of these mid-task. You don't n
|
|
|
214
259
|
| `get_high_impact_files(n)` | Top N files by blast radius |
|
|
215
260
|
| `get_env_vars(domain?)` | All env vars with domain mapping |
|
|
216
261
|
| `get_domains_list()` | All detected domains with file, route, model counts |
|
|
262
|
+
| `get_recent_decisions(time_range, kind?)` | Recent validation decisions and architectural choices the AI has made in this project |
|
|
263
|
+
| `get_session_context(session_id?)` | Full context for an AI session: every decision and intervention, ordered chronologically |
|
|
264
|
+
| `did_we_discuss_this(topic)` | Substring search over the episodic memory log — avoid re-deciding settled questions |
|
|
265
|
+
| `get_intervention_history(file?)` | Past Carto-issued violations and suggestions, optionally filtered by file |
|
|
217
266
|
|
|
218
267
|
**All MCP queries:** **<5ms** on every benchmarked repo.
|
|
219
268
|
|
|
269
|
+
## Episodic Memory
|
|
270
|
+
|
|
271
|
+
Carto remembers every diff it validates. The `validate_diff` tool writes one row per call into a local SQLite log (`ai_sessions`/`decisions`/`interventions` tables) — so a session that runs five hours later can still ask `did_we_discuss_this("snake_case naming")` and get back the prior decision. The log lives next to the index in `.carto/carto.db` — never sent over the network, never shared between projects.
|
|
272
|
+
|
|
220
273
|
---
|
|
221
274
|
|
|
222
275
|
## Domain detection
|
|
223
276
|
|
|
224
277
|
Carto uses **Leiden+CPM graph clustering** — files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns (AUTH, PAYMENTS, DATABASE, etc.).
|
|
225
278
|
|
|
279
|
+
**Adaptive strategy:** Repos under 100 files use keyword-only clustering (avoids over-fragmentation). Larger repos with dense import graphs get graph-based clustering with a gamma that scales continuously with repo size.
|
|
280
|
+
|
|
226
281
|
Works on any repo — not just SaaS apps. vscode gets AUTH/EVENTS/DATABASE. zed (Rust) gets DATABASE/AUTH/EVENTS. A game engine would get RENDERER/PHYSICS/AUDIO.
|
|
227
282
|
|
|
228
283
|
Custom domains via `carto.config.json`:
|
|
@@ -235,55 +290,101 @@ Custom domains via `carto.config.json`:
|
|
|
235
290
|
}
|
|
236
291
|
```
|
|
237
292
|
|
|
293
|
+
Full schema with anchor pinning (forces files into a domain regardless of clustering):
|
|
294
|
+
```json
|
|
295
|
+
{
|
|
296
|
+
"domains": {
|
|
297
|
+
"AUTH": {
|
|
298
|
+
"keywords": ["auth", "login", "session"],
|
|
299
|
+
"anchor": ["src/auth/session.ts", "src/auth/middleware.ts"]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
**Stability tracking:** Carto tracks domain assignments across syncs. If >5% of files change domain, `carto check` flags it as unstable. Two consecutive syncs with no code changes always produce 0% drift.
|
|
306
|
+
|
|
238
307
|
---
|
|
239
308
|
|
|
240
309
|
## CLI commands
|
|
241
310
|
|
|
242
311
|
| Command | What it does |
|
|
243
312
|
|---------|-------------|
|
|
244
|
-
| `carto init` | Detect project, index codebase, generate AGENTS.md, auto-wire MCP into every AI tool found |
|
|
245
|
-
| `carto sync` | Full re-index (skips unchanged files via mtime+size cache) |
|
|
246
|
-
| `carto
|
|
247
|
-
| `carto
|
|
313
|
+
| `carto init` | Detect project, index codebase, generate AGENTS.md, install git hooks (pre-commit, post-checkout, post-merge, post-rewrite), auto-wire MCP into every AI tool found |
|
|
314
|
+
| `carto sync` | Full re-index (skips unchanged files via mtime+size cache). Called automatically by git hooks on commit/checkout/merge/rebase. |
|
|
315
|
+
| `carto serve` | Start MCP server (called by your AI tool — usually you don't run this directly). On every file-aware query the server mtime+size-checks the file and re-parses inline if stale. |
|
|
316
|
+
| `carto watch` | **Optional.** Live re-index on every file save. Not required — git hooks + lazy MCP re-parse keep the index fresh by default. Use only for AI-heavy workflows that write 50+ files between commits. |
|
|
248
317
|
| `carto agent` | Start ACP agent mode (for Zed / JetBrains / VS Code) |
|
|
249
318
|
| `carto impact <file>` | Blast radius: risk level, affected files, routes at risk |
|
|
250
319
|
| `carto check` | Cross-domain violations, high-risk uncommitted changes, domain health |
|
|
320
|
+
| `carto inspect` | Read-only diagnostic: index paths, sizes, freshness, bitmap sidecar shape, top-impact files, schema version, sync timestamps. `--json` for piping into `jq`. Never triggers a rebuild. |
|
|
251
321
|
| `carto remove` | Remove AGENTS.md and .carto/ from project |
|
|
252
322
|
|
|
253
323
|
---
|
|
254
324
|
|
|
255
325
|
## Benchmarks
|
|
256
326
|
|
|
257
|
-
Measured on real open-source repos. Apple M-series, 8 CPUs, 8GB RAM. SHAs pinned in `~/carto-test-repos`.
|
|
327
|
+
Measured on real open-source repos. Apple M-series, 8 CPUs, 8GB RAM. SHAs pinned in `~/carto-test-repos`. Reproducible via `~/carto-test-repos/run-bench.sh`.
|
|
328
|
+
|
|
329
|
+
### Indexing speed
|
|
258
330
|
|
|
259
331
|
| Repo | Language | Indexed Files | First Run | Second Run | DB Size | Import Edges |
|
|
260
332
|
|------|----------|---------------|-----------|------------|---------|--------------|
|
|
261
|
-
| [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 961 | **
|
|
262
|
-
| [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,
|
|
263
|
-
| [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 7,567 | **7.
|
|
264
|
-
| [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,752 | **3.0s** | **
|
|
333
|
+
| [prisma/prisma](https://github.com/prisma/prisma) | TypeScript | 961 | **961ms** | **431ms** | 0.7 MB | 1,387 |
|
|
334
|
+
| [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,330 | **5.4s** | **1.2s** | 4.0 MB | 5,321 |
|
|
335
|
+
| [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 7,567 | **7.7s** | **1.0s** | 6.7 MB | 13,420 |
|
|
336
|
+
| [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,752 | **3.0s** | **491ms** | 4.4 MB | 2,113 |
|
|
265
337
|
|
|
266
338
|
**Indexed Files** counts what Carto actually parses — `.ts/.js/.py/.go/.rs/...` after excluding `node_modules`, build output, and per-file `*.test.*` / `*.spec.*` / `*.stories.*` / `test_*.py` patterns. The on-disk file count of the repo is larger.
|
|
267
339
|
|
|
268
|
-
**Second
|
|
340
|
+
**Second Run** = `carto sync` after no changes. mtime+size checked before reading content — if nothing changed, nothing is re-parsed.
|
|
341
|
+
|
|
342
|
+
### MCP query latency (bitmap engine vs SQLite)
|
|
343
|
+
|
|
344
|
+
Carto's MCP query path is bitmap-backed on five tools, plus a sixth (`simulate_change_impact`) that's only feasible with bitmap OR-aggregation. Speedups measured against the same data, same DB, same machine — bitmap path vs the equivalent SQLite path.
|
|
345
|
+
|
|
346
|
+
| Tool | vscode (7,567 files) | Speedup vs SQLite |
|
|
347
|
+
|------|---------------------:|------------------:|
|
|
348
|
+
| `get_blast_radius` | sub-ms | **10.7×** |
|
|
349
|
+
| `get_cross_domain` | 2.1ms | **6.2×** |
|
|
350
|
+
| `get_high_impact_files` | sub-ms | **559×** |
|
|
351
|
+
| `get_similar_patterns` | sub-ms | **73×** |
|
|
352
|
+
| `simulate_change_impact` | sub-ms | **6.5×** (no SQLite equivalent at this latency) |
|
|
353
|
+
|
|
354
|
+
Median speedup across all five tools on vscode: **10.7×**. Smaller repos hit higher peaks — laravel-framework `get_high_impact_files` clocks at 155× on the smaller graph. Reproducible via `npm run bench:bitmap -- --repo <path>`.
|
|
355
|
+
|
|
356
|
+
### `validate_diff` latency
|
|
357
|
+
|
|
358
|
+
The new diff-shaped query that lets the AI ask "is this patch safe?" before showing it to the user. Profiled with a representative 20-line diff against 20 random mid-blast-radius files per repo, 1000 calls each.
|
|
359
|
+
|
|
360
|
+
| Repo | Files | p50 | p99 |
|
|
361
|
+
|------|-------|----:|----:|
|
|
362
|
+
| supabase | 6,259 | **0.082ms** | **0.298ms** |
|
|
363
|
+
| vscode | 7,567 | **0.084ms** | **0.489ms** |
|
|
364
|
+
|
|
365
|
+
Budget was p50 ≤ 5ms, p99 ≤ 15ms. Both targets are cleared by 30-60×. The bitmap engine handles every blast-radius and cross-domain query in microseconds; what's left is diff parsing + result aggregation. Reproducible via `node bench/validation-perf/index.js --repo <path>`.
|
|
269
366
|
|
|
270
367
|
### Domains detected
|
|
271
368
|
|
|
272
369
|
| Repo | Domains |
|
|
273
370
|
|------|---------|
|
|
274
|
-
| prisma | CORE · DATABASE ·
|
|
275
|
-
| supabase | CORE · AUTH · DATABASE · PAYMENTS ·
|
|
276
|
-
| vscode |
|
|
277
|
-
| zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS ·
|
|
371
|
+
| prisma | CORE · DATABASE · AUTH · EVENTS |
|
|
372
|
+
| supabase | CORE · AUTH · DATABASE · PAYMENTS · NOTIFICATIONS · EVENTS · TRPC |
|
|
373
|
+
| vscode | EXTENSIONS · AUTH · EVENTS · DATABASE · EXTENSION · CLI · CORE |
|
|
374
|
+
| zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · TRPC · NOTIFICATIONS |
|
|
278
375
|
|
|
279
376
|
vscode at 7,567 indexed files in under 8 seconds. Rust import graph working on zed (2,113 edges from `mod` declarations and `use crate::` paths).
|
|
280
377
|
|
|
378
|
+
### Accuracy
|
|
379
|
+
|
|
380
|
+
12 corpus repos pass `node test/accuracy-corpus.js --samples 100` — full parity between the bitmap path and the SQLite path on `blastRadius`, `crossDomain`, `highImpactFiles`, and `simulateChangeImpact`. The bitmap layer is a speedup, never a behavior change.
|
|
381
|
+
|
|
281
382
|
---
|
|
282
383
|
|
|
283
384
|
## How it works
|
|
284
385
|
|
|
285
386
|
```
|
|
286
|
-
carto init
|
|
387
|
+
carto init
|
|
287
388
|
↓
|
|
288
389
|
Discovers all files (no cap — SQLite handles the volume)
|
|
289
390
|
mtime+size check → skip unchanged files
|
|
@@ -293,9 +394,22 @@ Leiden+CPM graph clustering → auto-detects domains
|
|
|
293
394
|
Computes reverse deps → blast radius for every file
|
|
294
395
|
Writes AGENTS.md + .carto/context/*.md (lazy, on-demand)
|
|
295
396
|
Auto-wires MCP into every AI tool found
|
|
397
|
+
Installs 4 git hooks: pre-commit, post-checkout, post-merge, post-rewrite
|
|
398
|
+
↓
|
|
399
|
+
[no daemon, no watcher, no background process]
|
|
400
|
+
↓
|
|
401
|
+
─── Freshness mechanism 1: git hooks (90% of cases) ───
|
|
402
|
+
You commit / pull / switch branches / rebase
|
|
403
|
+
→ hook runs `carto sync` quietly in <1s
|
|
404
|
+
→ only changed files re-parsed
|
|
296
405
|
↓
|
|
297
|
-
|
|
406
|
+
─── Freshness mechanism 2: lazy mtime check (the gap) ───
|
|
407
|
+
You edit files between commits, AI asks "blast radius of db.ts?"
|
|
408
|
+
→ MCP server stats db.ts (mtime+size vs DB row)
|
|
409
|
+
→ if stale, re-parses just that file inline (<50ms)
|
|
410
|
+
→ returns fresh answer
|
|
298
411
|
↓
|
|
412
|
+
─── Optional: carto watch (AI-heavy workflows only) ───
|
|
299
413
|
File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
300
414
|
```
|
|
301
415
|
|
|
@@ -310,6 +424,16 @@ File saved → debounce 50ms → re-parse 1 file → SQLite write → <50ms
|
|
|
310
424
|
|
|
311
425
|
---
|
|
312
426
|
|
|
427
|
+
## Origin
|
|
428
|
+
|
|
429
|
+
I was building [Emfirge](https://www.emfirge.cloud) — a cloud security agent that maps AWS infrastructure into a graph and simulates the blast radius of every change.
|
|
430
|
+
|
|
431
|
+
To make the AI inside Emfirge understand infrastructure, I wrote a module called `cartography.py`. It mapped AWS resources, built a graph of how they connected, and wrote it into a structured map. The AI stopped hallucinating. It worked with facts, not guesses.
|
|
432
|
+
|
|
433
|
+
Carto is the same idea, applied to source code. Same insight: AI agents stop guessing once they can query the architecture.
|
|
434
|
+
|
|
435
|
+
---
|
|
436
|
+
|
|
313
437
|
## License
|
|
314
438
|
|
|
315
439
|
MIT. Free forever.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carto-md",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Structural intelligence layer for AI coding tools. Indexes your codebase into SQLite — routes, models, import graph, blast radius, domains — and exposes 16 MCP tools for Kiro, Cursor, and Claude.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"carto": "src/cli/index.js"
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"test": "node test/test.js",
|
|
11
11
|
"test:correctness": "node test/correctness.js",
|
|
12
12
|
"test:benchmark": "node test/benchmark.js",
|
|
13
|
-
"test:bench-ci": "node test/bench-ci.js"
|
|
13
|
+
"test:bench-ci": "node test/bench-ci.js",
|
|
14
|
+
"postinstall": "node scripts/postinstall.js",
|
|
15
|
+
"bench:bitmap": "node bench/bitmap-validation/index.js"
|
|
14
16
|
},
|
|
15
17
|
"dependencies": {
|
|
16
18
|
"@agentclientprotocol/sdk": "^0.22.1",
|
|
@@ -18,7 +20,9 @@
|
|
|
18
20
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
21
|
"better-sqlite3": "11.7.0",
|
|
20
22
|
"chokidar": "^3.6.0",
|
|
21
|
-
"tree-sitter": "0.25.0"
|
|
23
|
+
"tree-sitter": "0.25.0"
|
|
24
|
+
},
|
|
25
|
+
"optionalDependencies": {
|
|
22
26
|
"tree-sitter-c-sharp": "0.21.3",
|
|
23
27
|
"tree-sitter-cpp": "0.23.4",
|
|
24
28
|
"tree-sitter-go": "0.25.0",
|
|
@@ -29,7 +33,7 @@
|
|
|
29
33
|
"tree-sitter-typescript": "0.23.2"
|
|
30
34
|
},
|
|
31
35
|
"engines": {
|
|
32
|
-
"node": ">=18.0.0
|
|
36
|
+
"node": ">=18.0.0"
|
|
33
37
|
},
|
|
34
38
|
"author": {
|
|
35
39
|
"name": "Ansh Sonkar",
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// Postinstall script — checks tree-sitter grammar availability and prints
|
|
5
|
+
// actionable guidance if any failed to install. Always exits 0.
|
|
6
|
+
|
|
7
|
+
if (process.env.CARTO_NO_POSTINSTALL === '1') process.exit(0);
|
|
8
|
+
|
|
9
|
+
const GRAMMARS = [
|
|
10
|
+
{ pkg: 'tree-sitter-javascript', langs: 'JavaScript' },
|
|
11
|
+
{ pkg: 'tree-sitter-typescript', langs: 'TypeScript' },
|
|
12
|
+
{ pkg: 'tree-sitter-python', langs: 'Python' },
|
|
13
|
+
{ pkg: 'tree-sitter-go', langs: 'Go' },
|
|
14
|
+
{ pkg: 'tree-sitter-rust', langs: 'Rust' },
|
|
15
|
+
{ pkg: 'tree-sitter-java', langs: 'Java' },
|
|
16
|
+
{ pkg: 'tree-sitter-cpp', langs: 'C/C++' },
|
|
17
|
+
{ pkg: 'tree-sitter-c-sharp', langs: 'C#' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const failed = [];
|
|
21
|
+
for (const g of GRAMMARS) {
|
|
22
|
+
try { require(g.pkg); } catch { failed.push(g); }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (failed.length === 0) process.exit(0);
|
|
26
|
+
|
|
27
|
+
// Print guidance
|
|
28
|
+
const langs = failed.map(g => g.langs).join(', ');
|
|
29
|
+
const isWin = process.platform === 'win32';
|
|
30
|
+
const isMac = process.platform === 'darwin';
|
|
31
|
+
|
|
32
|
+
let fix;
|
|
33
|
+
if (isWin) {
|
|
34
|
+
fix = 'Install "Desktop development with C++" from https://aka.ms/vs/17/release/vs_BuildTools.exe then re-run: npm rebuild';
|
|
35
|
+
} else if (isMac) {
|
|
36
|
+
fix = 'Run: xcode-select --install && npm rebuild';
|
|
37
|
+
} else {
|
|
38
|
+
fix = 'Run: sudo apt-get install -y build-essential && npm rebuild (or equivalent for your distro)';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log('');
|
|
42
|
+
console.log('[CARTO] ⚠️ Some tree-sitter grammars failed to install.');
|
|
43
|
+
console.log(`[CARTO] Affected languages: ${langs}`);
|
|
44
|
+
console.log('[CARTO] These languages will use regex-only extraction (less accurate).');
|
|
45
|
+
console.log(`[CARTO] To fix: ${fix}`);
|
|
46
|
+
console.log('');
|
package/src/acp/agent.js
CHANGED
|
@@ -71,11 +71,11 @@ class CartoAgent {
|
|
|
71
71
|
|
|
72
72
|
// Provider methods
|
|
73
73
|
// The custom `unstable_*` provider-management methods (list / set / disable)
|
|
74
|
-
// were removed
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
74
|
+
// were removed. The ACP SDK only dispatches the methods declared in
|
|
75
|
+
// its AGENT_METHODS constant, so those custom routes returned -32601
|
|
76
|
+
// "Method not found" anyway. Provider config flows through env vars /
|
|
77
|
+
// editor settings; ProviderRegistry remains as the internal
|
|
78
|
+
// configuration carrier.
|
|
79
79
|
|
|
80
80
|
// Session list/load stubs
|
|
81
81
|
async listSessions(_params) { return { sessions: [] }; }
|
|
@@ -30,8 +30,8 @@ class ProviderRegistry {
|
|
|
30
30
|
this._config = null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
// list() was removed
|
|
34
|
-
//
|
|
33
|
+
// list() was removed — it was only called by a dropped custom
|
|
34
|
+
// provider-list ACP method that the SDK never actually dispatched.
|
|
35
35
|
// SUPPORTED_PROVIDERS is still exported for external callers / tests.
|
|
36
36
|
|
|
37
37
|
/**
|
package/src/agents/leiden.js
CHANGED
|
@@ -300,24 +300,18 @@ function clusterByGraph(importGraph, gamma = 0.03, keywordSeeds = {}) {
|
|
|
300
300
|
|
|
301
301
|
// Name each community
|
|
302
302
|
const commNames = new Map();
|
|
303
|
-
const usedNames = new Map(); // name → count (for deduplication)
|
|
304
303
|
|
|
305
304
|
for (const [commId, members] of communities) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
// Deduplicate: if name already used, append a number
|
|
309
|
-
if (usedNames.has(name)) {
|
|
310
|
-
const count = usedNames.get(name) + 1;
|
|
311
|
-
usedNames.set(name, count);
|
|
312
|
-
name = `${name}_${count}`;
|
|
313
|
-
} else {
|
|
314
|
-
usedNames.set(name, 1);
|
|
315
|
-
}
|
|
316
|
-
|
|
305
|
+
const name = nameCommunity(members, keywordSeeds);
|
|
317
306
|
commNames.set(commId, name);
|
|
318
307
|
}
|
|
319
308
|
|
|
320
|
-
// Build final file → domain map
|
|
309
|
+
// Build final file → domain map.
|
|
310
|
+
// When multiple communities resolve to the same base name (e.g. with a
|
|
311
|
+
// dense import graph, several disjoint sub-systems can each rank highest
|
|
312
|
+
// for AUTH), merge them into a single domain. The keyword that names the
|
|
313
|
+
// domain is the user-visible truth; preserving "AUTH_2", "AUTH_3", ...
|
|
314
|
+
// suffixes leaks Leiden's internal community count into the UI.
|
|
321
315
|
const result = new Map();
|
|
322
316
|
for (const [node, commId] of communityMap) {
|
|
323
317
|
result.set(node, commNames.get(commId) || 'CORE');
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bitset — Uint32Array-backed dense bitmap.
|
|
5
|
+
*
|
|
6
|
+
* 60-line implementation,
|
|
7
|
+
* zero deps. Used by Carto's bitmap engine (`src/bitmap/`) for sub-millisecond
|
|
8
|
+
* forward/reverse adjacency queries on the import graph.
|
|
9
|
+
*
|
|
10
|
+
* Operations are word-level (32-bit lanes) — `or`, `and`, `andNot`, `popcount`,
|
|
11
|
+
* `iterate` all run at one cycle per 32 bits on modern CPUs. For Carto's typical
|
|
12
|
+
* 10K-file repos this is 312 words per bitmap → tens of nanoseconds for a
|
|
13
|
+
* popcount, microseconds for a 1-hop frontier expansion.
|
|
14
|
+
*
|
|
15
|
+
* Layout: bit `i` lives in `words[i >>> 5]` at lane `i & 31`. Out-of-range reads
|
|
16
|
+
* return false; out-of-range writes are caller's responsibility (the bitset
|
|
17
|
+
* does not auto-grow — it's sized at construction to the max file id + 1).
|
|
18
|
+
*/
|
|
19
|
+
class Bitset {
|
|
20
|
+
/**
|
|
21
|
+
* @param {number} size — Number of bits (i.e. max file id + 1).
|
|
22
|
+
*/
|
|
23
|
+
constructor(size) {
|
|
24
|
+
this.size = size;
|
|
25
|
+
this.words = new Uint32Array(Math.ceil(size / 32));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Set bit `i` to 1. */
|
|
29
|
+
set(i) { this.words[i >>> 5] |= (1 << (i & 31)); }
|
|
30
|
+
|
|
31
|
+
/** Clear bit `i` (set to 0). */
|
|
32
|
+
clear(i) { this.words[i >>> 5] &= ~(1 << (i & 31)); }
|
|
33
|
+
|
|
34
|
+
/** Return true if bit `i` is set. */
|
|
35
|
+
has(i) { return (this.words[i >>> 5] & (1 << (i & 31))) !== 0; }
|
|
36
|
+
|
|
37
|
+
/** Bitwise OR. Returns a new Bitset sized to the larger of the two. */
|
|
38
|
+
or(other) {
|
|
39
|
+
const r = new Bitset(Math.max(this.size, other.size));
|
|
40
|
+
for (let i = 0; i < this.words.length; i++) r.words[i] = this.words[i];
|
|
41
|
+
for (let i = 0; i < other.words.length; i++) r.words[i] |= other.words[i];
|
|
42
|
+
return r;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Bitwise AND. Returns a new Bitset sized to `this`. */
|
|
46
|
+
and(other) {
|
|
47
|
+
const r = new Bitset(this.size);
|
|
48
|
+
const len = Math.min(this.words.length, other.words.length);
|
|
49
|
+
for (let i = 0; i < len; i++) r.words[i] = this.words[i] & other.words[i];
|
|
50
|
+
return r;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Bitwise AND-NOT (this & ~other). Returns a new Bitset sized to `this`. */
|
|
54
|
+
andNot(other) {
|
|
55
|
+
const r = new Bitset(this.size);
|
|
56
|
+
for (let i = 0; i < this.words.length; i++) {
|
|
57
|
+
r.words[i] = this.words[i] & ~(other.words[i] || 0);
|
|
58
|
+
}
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* In-place bitwise OR. Mutates `this` to `this | other` and returns `this`.
|
|
64
|
+
*
|
|
65
|
+
* Used by hot BFS loops (`blastRadius`, `simulateChangeImpact`)
|
|
66
|
+
* to avoid the per-hop Uint32Array allocation that `.or()` performs.
|
|
67
|
+
* If `other` is wider than `this`, words beyond `this.size` are dropped —
|
|
68
|
+
* callers that care about size growth should resize before calling.
|
|
69
|
+
*/
|
|
70
|
+
orInPlace(other) {
|
|
71
|
+
const n = Math.min(this.words.length, other.words.length);
|
|
72
|
+
for (let i = 0; i < n; i++) this.words[i] |= other.words[i];
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* In-place bitwise AND-NOT. Mutates `this` to `this & ~other` and
|
|
78
|
+
* returns `this`.
|
|
79
|
+
*/
|
|
80
|
+
andNotInPlace(other) {
|
|
81
|
+
const n = Math.min(this.words.length, other.words.length);
|
|
82
|
+
for (let i = 0; i < n; i++) this.words[i] &= ~other.words[i];
|
|
83
|
+
// If `other` is shorter than `this`, the trailing words stay as-is —
|
|
84
|
+
// andNot with implicit-zero is a no-op.
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Copy bits from `other` into `this` (mutates and returns `this`).
|
|
90
|
+
*
|
|
91
|
+
* The receiver's word array is reused — no new allocation.
|
|
92
|
+
* If sizes differ, the overlapping prefix is copied and the suffix of
|
|
93
|
+
* `this` (if any) is zeroed so stale bits don't leak.
|
|
94
|
+
*/
|
|
95
|
+
copyFrom(other) {
|
|
96
|
+
if (this.words.length === other.words.length) {
|
|
97
|
+
this.words.set(other.words);
|
|
98
|
+
} else {
|
|
99
|
+
const n = Math.min(this.words.length, other.words.length);
|
|
100
|
+
for (let i = 0; i < n; i++) this.words[i] = other.words[i];
|
|
101
|
+
for (let i = n; i < this.words.length; i++) this.words[i] = 0;
|
|
102
|
+
}
|
|
103
|
+
return this;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set every word to `value` (defaults to 0). Mutates and returns `this`.
|
|
108
|
+
*
|
|
109
|
+
* The hot use case is `setAll(0)` to clear the transient
|
|
110
|
+
* frontier bitset between BFS hops without reallocating its 32-bit lanes.
|
|
111
|
+
*/
|
|
112
|
+
setAll(value = 0) {
|
|
113
|
+
this.words.fill(value >>> 0);
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Population count (number of set bits).
|
|
119
|
+
* Hamming weight via parallel bit-fold — same kernel as POPCNT but portable
|
|
120
|
+
* across Node runtimes that don't expose hardware popcount.
|
|
121
|
+
*/
|
|
122
|
+
popcount() {
|
|
123
|
+
let c = 0;
|
|
124
|
+
for (let i = 0; i < this.words.length; i++) {
|
|
125
|
+
let v = this.words[i];
|
|
126
|
+
v = v - ((v >>> 1) & 0x55555555);
|
|
127
|
+
v = (v & 0x33333333) + ((v >>> 2) & 0x33333333);
|
|
128
|
+
c += (((v + (v >>> 4)) & 0x0F0F0F0F) * 0x01010101) >>> 24;
|
|
129
|
+
}
|
|
130
|
+
return c;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Returns an array of every set bit's index (file id), in ascending order.
|
|
135
|
+
* Uses the `v & -v` LSB trick to skip zero lanes early.
|
|
136
|
+
*/
|
|
137
|
+
iterate() {
|
|
138
|
+
const result = [];
|
|
139
|
+
for (let w = 0; w < this.words.length; w++) {
|
|
140
|
+
let v = this.words[w];
|
|
141
|
+
while (v) {
|
|
142
|
+
const bit = v & (-v);
|
|
143
|
+
result.push((w << 5) + (31 - Math.clz32(bit)));
|
|
144
|
+
v ^= bit;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Deep copy. */
|
|
151
|
+
clone() {
|
|
152
|
+
const r = new Bitset(this.size);
|
|
153
|
+
r.words.set(this.words);
|
|
154
|
+
return r;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Serialize to a Buffer. Layout: just the raw word bytes (LE on x86/ARM
|
|
159
|
+
* Node hosts; Carto runs Node so endianness matches the running machine —
|
|
160
|
+
* the on-disk file is per-machine just like SQLite's `carto.db`).
|
|
161
|
+
*
|
|
162
|
+
* Size in bytes: `Math.ceil(size / 32) * 4`. Caller persists the `size`
|
|
163
|
+
* separately so deserialize knows how many bits to expect.
|
|
164
|
+
*/
|
|
165
|
+
serialize() {
|
|
166
|
+
return Buffer.from(this.words.buffer, this.words.byteOffset, this.words.byteLength);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Reconstruct a Bitset from a Buffer + the original size.
|
|
171
|
+
*
|
|
172
|
+
* @param {Buffer} buf — Bytes produced by `serialize()`.
|
|
173
|
+
* @param {number} size — Bit count (must match the serialized bitset).
|
|
174
|
+
* @returns {Bitset}
|
|
175
|
+
*/
|
|
176
|
+
static deserialize(buf, size) {
|
|
177
|
+
const r = new Bitset(size);
|
|
178
|
+
// Copy through a Uint8Array view so we don't alias buf's underlying
|
|
179
|
+
// ArrayBuffer (which may be shared with Node's internal pool).
|
|
180
|
+
const expected = r.words.byteLength;
|
|
181
|
+
if (buf.length < expected) {
|
|
182
|
+
throw new Error(`Bitset.deserialize: buffer too small (got ${buf.length}, need ${expected})`);
|
|
183
|
+
}
|
|
184
|
+
const u8 = new Uint8Array(r.words.buffer, r.words.byteOffset, expected);
|
|
185
|
+
for (let i = 0; i < expected; i++) u8[i] = buf[i];
|
|
186
|
+
return r;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { Bitset };
|