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 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
- One command from your project root:
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: `~/Library/Application Support/Claude/claude_desktop_config.json`
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": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
83
+ { "servers": { "carto": { "type": "stdio", "command": "carto", "args": ["serve"] } } }
65
84
  ```
66
85
 
67
86
  ### Windsurf
68
87
 
69
- `~/.windsurf/mcp.json`
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
  ![Claude Code reviewing the supabase repo through carto's MCP server](docs/screenshots/claude-code-supabase.png)
@@ -192,7 +235,7 @@ Carto supports any LLM provider — configure in your editor:
192
235
 
193
236
  ---
194
237
 
195
- ## The 16 MCP tools
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 watch` | Incremental live re-index on every file save (<50ms) |
247
- | `carto serve` | Start MCP server (called by your AI tool usually you don't run this directly) |
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 | **649ms** | **110ms** | 0.7 MB | 1,387 |
262
- | [supabase/supabase](https://github.com/supabase/supabase) | TypeScript | 6,259 | **4.8s** | **751ms** | 4.0 MB | 5,321 |
263
- | [microsoft/vscode](https://github.com/microsoft/vscode) | TypeScript | 7,567 | **7.8s** | **387ms** | 6.7 MB | 13,420 |
264
- | [zed-industries/zed](https://github.com/zed-industries/zed) | Rust | 1,752 | **3.0s** | **182ms** | 4.4 MB | 2,113 |
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 run** = only changed files re-parsed. mtime+size checked before reading content — if nothing changed, nothing is re-parsed.
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 · EVENTS · AUTH |
275
- | supabase | CORE · AUTH · DATABASE · PAYMENTS · EVENTS · NOTIFICATIONS · TRPC |
276
- | vscode | CORE · DATABASE · AUTH · EVENTS · NOTIFICATIONS |
277
- | zed (Rust) | CORE · DATABASE · AUTH · EVENTS · PAYMENTS · NOTIFICATIONS · TRPC |
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 / carto sync
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
- carto watch (optional)
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.7",
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 <26.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 in 2.0.7 (Spec 7 Bug 3). The ACP SDK only dispatches the
75
- // methods declared in its AGENT_METHODS constant, so those custom routes
76
- // returned -32601 "Method not found" anyway. Provider config flows
77
- // through env vars / editor settings; ProviderRegistry remains as the
78
- // internal configuration carrier.
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 in 2.0.7 (Spec 7 Bug 3). It was only called by the
34
- // dropped custom provider-list ACP method, which the SDK never dispatched.
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
  /**
@@ -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
- let name = nameCommunity(members, keywordSeeds);
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 };