carto-md 2.0.6 → 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 +297 -130
- package/docs/screenshots/claude-code-supabase.png +0 -0
- package/package.json +7 -3
- package/scripts/postinstall.js +46 -0
- package/src/acp/agent.js +6 -13
- package/src/acp/providers/index.js +4 -12
- 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/impact.js +6 -1
- package/src/cli/index.js +14 -2
- package/src/cli/init.js +297 -50
- 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 +181 -1
- 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 +535 -8
- package/src/mcp/validate.js +304 -0
- package/src/security/ignore.js +56 -1
- package/src/store/config-loader.js +77 -0
- package/src/store/path-utils.js +50 -0
- package/src/store/sqlite-store.js +419 -8
- package/src/store/sync-v2.js +422 -96
- package/BENCHMARK_RESULTS.md +0 -34
|
@@ -30,20 +30,12 @@ class ProviderRegistry {
|
|
|
30
30
|
this._config = null;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
list() {
|
|
37
|
-
return SUPPORTED_PROVIDERS.map(p => ({
|
|
38
|
-
id: p.id,
|
|
39
|
-
name: p.name,
|
|
40
|
-
defaultModel: p.defaultModel,
|
|
41
|
-
models: p.models,
|
|
42
|
-
}));
|
|
43
|
-
}
|
|
33
|
+
// list() was removed — it was only called by a dropped custom
|
|
34
|
+
// provider-list ACP method that the SDK never actually dispatched.
|
|
35
|
+
// SUPPORTED_PROVIDERS is still exported for external callers / tests.
|
|
44
36
|
|
|
45
37
|
/**
|
|
46
|
-
* set(params) — Configures the active provider
|
|
38
|
+
* set(params) — Configures the active provider.
|
|
47
39
|
* params: { providerId, apiKey, baseUrl?, model? }
|
|
48
40
|
*/
|
|
49
41
|
set(params) {
|
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 };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bitmap engine orchestrator — freshness, persistence, and process-lifetime cache.
|
|
5
|
+
*
|
|
6
|
+
* Wraps `sidecar.js` with a simple lifecycle: SQLite is the durable
|
|
7
|
+
* source of truth, the bitmap layer is derived + disposable, and a
|
|
8
|
+
* stale or corrupt bitmap.bin must never be a correctness problem —
|
|
9
|
+
* we just rebuild.
|
|
10
|
+
*
|
|
11
|
+
* Public surface:
|
|
12
|
+
* ensureBitmapFresh(cartoDir, store) → sidecar object
|
|
13
|
+
* Returns a fresh sidecar suitable for the bitmap query tools. If
|
|
14
|
+
* `.carto/bitmap.bin` is missing, older than `.carto/carto.db`, or
|
|
15
|
+
* fails to load (corrupt / version mismatch), rebuilds from `store`
|
|
16
|
+
* and re-persists.
|
|
17
|
+
*
|
|
18
|
+
* invalidate(cartoDir?) → void
|
|
19
|
+
* Clears the in-memory cache. If `cartoDir` is given, also deletes
|
|
20
|
+
* the on-disk bitmap.bin so a follow-up `ensureBitmapFresh` from a
|
|
21
|
+
* different process triggers a rebuild even if the carto.db mtime
|
|
22
|
+
* didn't bump (e.g. write within the same mtime granularity).
|
|
23
|
+
*
|
|
24
|
+
* _resetForTests() → void
|
|
25
|
+
* Test-only escape hatch — drops the cache without filesystem side
|
|
26
|
+
* effects. The leading underscore signals "do not use in production."
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const fs = require('fs');
|
|
30
|
+
const path = require('path');
|
|
31
|
+
const {
|
|
32
|
+
buildFromStore,
|
|
33
|
+
saveToDisk,
|
|
34
|
+
loadFromDisk,
|
|
35
|
+
BITMAP_FILENAME,
|
|
36
|
+
} = require('./sidecar');
|
|
37
|
+
|
|
38
|
+
// Process-lifetime cache. The MCP server is one Node process per repo,
|
|
39
|
+
// so a singleton matches the lifecycle correctly. `cartoDir` is part of
|
|
40
|
+
// the key so the (rare) case where one process serves multiple repos
|
|
41
|
+
// — e.g. tests that spin up several MCP servers in sequence — doesn't
|
|
42
|
+
// hand a stale sidecar to the wrong project.
|
|
43
|
+
let cached = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if `bitmapPath` exists and is at least as fresh as `dbPath`.
|
|
47
|
+
* Files with identical mtimes are treated as fresh — the bitmap was almost
|
|
48
|
+
* certainly written immediately after the DB row that triggered the rebuild.
|
|
49
|
+
*/
|
|
50
|
+
function bitmapIsFresh(cartoDir) {
|
|
51
|
+
const dbPath = path.join(cartoDir, 'carto.db');
|
|
52
|
+
const bitmapPath = path.join(cartoDir, BITMAP_FILENAME);
|
|
53
|
+
let bitmapStat, dbStat;
|
|
54
|
+
try { bitmapStat = fs.statSync(bitmapPath); } catch { return false; }
|
|
55
|
+
try { dbStat = fs.statSync(dbPath); } catch { return false; }
|
|
56
|
+
return bitmapStat.mtimeMs >= dbStat.mtimeMs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* ensureBitmapFresh(cartoDir, store) → sidecar
|
|
61
|
+
*
|
|
62
|
+
* Best-effort cache lookup → disk load → rebuild. Throws only if the
|
|
63
|
+
* SQLite store itself is unusable (no `.db`). Callers that can fall back
|
|
64
|
+
* to SQLite query paths should wrap this in try/catch.
|
|
65
|
+
*/
|
|
66
|
+
function ensureBitmapFresh(cartoDir, store) {
|
|
67
|
+
// 1. In-memory cache hit (and still fresh).
|
|
68
|
+
if (cached && cached.cartoDir === cartoDir) {
|
|
69
|
+
if (bitmapIsFresh(cartoDir)) return cached.sidecar;
|
|
70
|
+
// Cache is stale w.r.t. on-disk DB — fall through and rebuild.
|
|
71
|
+
cached = null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. Try loading from disk if the file is fresh.
|
|
75
|
+
if (bitmapIsFresh(cartoDir)) {
|
|
76
|
+
const loaded = loadFromDisk(cartoDir);
|
|
77
|
+
if (loaded) {
|
|
78
|
+
cached = { cartoDir, sidecar: loaded };
|
|
79
|
+
return loaded;
|
|
80
|
+
}
|
|
81
|
+
// loadFromDisk returned null → corrupt or version mismatch. Rebuild.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Rebuild from the SQLite source of truth and re-persist.
|
|
85
|
+
const sidecar = buildFromStore(store);
|
|
86
|
+
try {
|
|
87
|
+
saveToDisk(cartoDir, sidecar);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
// Persistence failure (read-only FS, disk full, race with another
|
|
90
|
+
// writer) is non-fatal — the in-memory sidecar still answers
|
|
91
|
+
// queries correctly. Surface to stderr so it shows up in MCP host
|
|
92
|
+
// logs without killing the request.
|
|
93
|
+
process.stderr.write(
|
|
94
|
+
`[CARTO bitmap] failed to persist sidecar to ${cartoDir}: ` +
|
|
95
|
+
`${err && err.message ? err.message : err}\n`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
cached = { cartoDir, sidecar };
|
|
99
|
+
return sidecar;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* invalidate(cartoDir?) — drop in-memory cache and (optionally) the
|
|
104
|
+
* on-disk file. Called by `runSyncV2` (full sync rebuilds, then we
|
|
105
|
+
* re-prime the cache on next ensureBitmapFresh) and `syncFiles`
|
|
106
|
+
* (partial sync — disk file is now stale).
|
|
107
|
+
*/
|
|
108
|
+
function invalidate(cartoDir) {
|
|
109
|
+
cached = null;
|
|
110
|
+
if (cartoDir) {
|
|
111
|
+
const target = path.join(cartoDir, BITMAP_FILENAME);
|
|
112
|
+
try { fs.unlinkSync(target); } catch {}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Test-only: drop the in-memory cache without touching the filesystem. */
|
|
117
|
+
function _resetForTests() {
|
|
118
|
+
cached = null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { ensureBitmapFresh, invalidate, _resetForTests };
|