carto-md 2.0.5 → 2.0.6
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/CONTRIBUTING.md +5 -5
- package/README.md +13 -20
- package/index.js +17 -7
- package/package.json +1 -1
- package/src/acp/session.js +9 -11
- package/src/cli/impact.js +5 -5
- package/src/cli/init.js +1 -19
- package/src/cli/serve.js +11 -12
- package/src/cli/sync.js +2 -14
- package/src/store/sqlite-store.js +35 -6
- package/src/store/store-adapter.js +169 -0
- package/src/store/sync-v2.js +37 -4
- package/src/cache/file-hash.js +0 -84
- package/src/cache/graph-cache.js +0 -77
- package/src/detector/files.js +0 -289
- package/src/engine/carto.js +0 -606
- package/src/engine/incremental.js +0 -149
- package/src/mcp/server.js +0 -431
- package/src/sync.js +0 -317
package/CONTRIBUTING.md
CHANGED
|
@@ -44,9 +44,9 @@ Framework-specific route and model extraction lives inside the language plugins.
|
|
|
44
44
|
|
|
45
45
|
---
|
|
46
46
|
|
|
47
|
-
## How to add a language (
|
|
47
|
+
## How to add a language (tree-sitter based)
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Carto uses tree-sitter for import and symbol extraction. Babel is only used for deep JS/TS route/model extraction on API handler files.
|
|
50
50
|
|
|
51
51
|
### Step 1: Install the grammar
|
|
52
52
|
|
|
@@ -177,7 +177,7 @@ Test on at least 2 real open-source projects using the framework.
|
|
|
177
177
|
|
|
178
178
|
---
|
|
179
179
|
|
|
180
|
-
## How domain clustering works
|
|
180
|
+
## How domain clustering works
|
|
181
181
|
|
|
182
182
|
Domain detection uses **Leiden+CPM graph clustering** (`src/agents/leiden.js`). Files that import each other heavily cluster together. Domain names are inferred from path tokens, with keyword hints for well-known patterns.
|
|
183
183
|
|
|
@@ -215,7 +215,7 @@ cd carto
|
|
|
215
215
|
npm install
|
|
216
216
|
node src/cli/index.js init # test in any project
|
|
217
217
|
node src/cli/index.js serve # test MCP server
|
|
218
|
-
npm test # run test suite (
|
|
218
|
+
npm test # run test suite (62 tests)
|
|
219
219
|
node test/correctness.js # run correctness tests (31 tests)
|
|
220
220
|
node test/benchmark.js # run benchmarks against real repos
|
|
221
221
|
```
|
|
@@ -258,7 +258,7 @@ npm run test:bench-ci # self-bench (matches what bench.yml runs)
|
|
|
258
258
|
- [ ] Extension added to `CODE_EXTS` and `detectLanguage()` in `sync-v2.js`
|
|
259
259
|
- [ ] No changes to merger logic (unless explicitly fixing a merger bug)
|
|
260
260
|
- [ ] No network calls added
|
|
261
|
-
- [ ] `npm test` passes (
|
|
261
|
+
- [ ] `npm test` passes (62/62)
|
|
262
262
|
- [ ] `node test/correctness.js` passes (31/31)
|
|
263
263
|
|
|
264
264
|
---
|
package/README.md
CHANGED
|
@@ -138,7 +138,7 @@ Reads `tsconfig.json` / `jsconfig.json` for `paths` config. `@/components/Button
|
|
|
138
138
|
|
|
139
139
|
## Domain detection
|
|
140
140
|
|
|
141
|
-
|
|
141
|
+
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.).
|
|
142
142
|
|
|
143
143
|
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.
|
|
144
144
|
|
|
@@ -227,27 +227,10 @@ Other agents are smart but blind. Carto sees the architecture:
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
-
## V1 → V2 migration
|
|
231
|
-
|
|
232
|
-
Run `carto sync` — it auto-migrates your existing `graph-cache.json` and `map.json` into SQLite and renames them to `.bak`. No manual steps.
|
|
233
|
-
|
|
234
|
-
What changed under the hood:
|
|
235
|
-
|
|
236
|
-
| | V1 | V2 |
|
|
237
|
-
|---|---|---|
|
|
238
|
-
| Storage | JSON blobs | SQLite (WAL mode, indexed queries) |
|
|
239
|
-
| Parsing | Babel-only | tree-sitter for all languages, Babel only for deep route/model extraction |
|
|
240
|
-
| File limit | 300 cap | Unlimited |
|
|
241
|
-
| Languages | JS/TS/Python/Go/R | + Rust/Java/C++/C#/Ruby |
|
|
242
|
-
| Domain detection | Hardcoded keywords | Leiden+CPM graph clustering |
|
|
243
|
-
| Watcher | Per-file chokidar | Single recursive directory watch (<20 file descriptors) |
|
|
244
|
-
| MCP startup | Re-indexes on start | Opens SQLite instantly (<10ms) |
|
|
245
|
-
| Path aliases | Not resolved | `@/`, `~/` resolved via tsconfig |
|
|
246
|
-
|
|
247
|
-
---
|
|
248
|
-
|
|
249
230
|
## MCP config (if auto-wire missed your IDE)
|
|
250
231
|
|
|
232
|
+
`carto init` auto-wires into Kiro, Cursor, and Claude Desktop. If it missed yours, add manually:
|
|
233
|
+
|
|
251
234
|
**Kiro** — `~/.kiro/settings/mcp.json`
|
|
252
235
|
```json
|
|
253
236
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
@@ -263,6 +246,16 @@ What changed under the hood:
|
|
|
263
246
|
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
264
247
|
```
|
|
265
248
|
|
|
249
|
+
**VS Code Copilot** — `.vscode/mcp.json` in your project root, or Command Palette → `MCP: Add Server`
|
|
250
|
+
```json
|
|
251
|
+
{ "servers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Windsurf** — `~/.windsurf/mcp.json`
|
|
255
|
+
```json
|
|
256
|
+
{ "mcpServers": { "carto": { "command": "carto", "args": ["serve"], "cwd": "/your/project" } } }
|
|
257
|
+
```
|
|
258
|
+
|
|
266
259
|
---
|
|
267
260
|
|
|
268
261
|
## What Carto never does
|
package/index.js
CHANGED
|
@@ -4,17 +4,27 @@
|
|
|
4
4
|
* carto-md — public module API
|
|
5
5
|
*
|
|
6
6
|
* Usage:
|
|
7
|
-
* const {
|
|
8
|
-
* const carto = new
|
|
7
|
+
* const { StoreAdapter } = require('carto-md');
|
|
8
|
+
* const carto = new StoreAdapter();
|
|
9
9
|
* await carto.index('/path/to/project');
|
|
10
10
|
*
|
|
11
|
-
* // Get everything
|
|
11
|
+
* // Get everything an editor agent needs for a file
|
|
12
12
|
* const ctx = carto.getContextForFile('src/auth/auth.service.ts');
|
|
13
13
|
*
|
|
14
|
-
* //
|
|
15
|
-
* carto.
|
|
14
|
+
* // Read structural facts
|
|
15
|
+
* const routes = carto.getRoutes();
|
|
16
|
+
* const radius = carto.getBlastRadius('src/auth/auth.service.ts');
|
|
17
|
+
* carto.close();
|
|
18
|
+
*
|
|
19
|
+
* Back-compat: `Carto` is exported as an alias for `StoreAdapter`.
|
|
20
|
+
* Existing programs that did `const { Carto } = require('carto-md')`
|
|
21
|
+
* continue to work without changes. The alias (and the `terminate()`
|
|
22
|
+
* method on StoreAdapter) will be removed in 3.0.0.
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
|
-
const {
|
|
25
|
+
const { StoreAdapter } = require('./src/store/store-adapter');
|
|
19
26
|
|
|
20
|
-
module.exports = {
|
|
27
|
+
module.exports = {
|
|
28
|
+
StoreAdapter,
|
|
29
|
+
Carto: StoreAdapter, // deprecated alias, removed in 3.0.0
|
|
30
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "carto-md",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
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"
|
package/src/acp/session.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const {
|
|
6
|
+
const { StoreAdapter } = require('../store/store-adapter');
|
|
7
7
|
|
|
8
8
|
class Session {
|
|
9
9
|
constructor(id, workingDir) {
|
|
@@ -23,20 +23,16 @@ class Session {
|
|
|
23
23
|
if (this._indexed) return null;
|
|
24
24
|
|
|
25
25
|
const dbPath = path.join(this.workingDir, '.carto', 'carto.db');
|
|
26
|
-
|
|
26
|
+
const wasIndexed = fs.existsSync(dbPath);
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
// DB exists — just load the index (fast path, <10ms)
|
|
30
|
-
await this.carto.index(this.workingDir);
|
|
31
|
-
this._indexed = true;
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// No DB — run full indexing
|
|
28
|
+
this.carto = new StoreAdapter();
|
|
36
29
|
const start = Date.now();
|
|
37
|
-
await this.carto.index(this.workingDir, {
|
|
30
|
+
await this.carto.index(this.workingDir, { writeOutputs: false });
|
|
38
31
|
this._indexed = true;
|
|
39
32
|
|
|
33
|
+
if (wasIndexed) return null;
|
|
34
|
+
|
|
35
|
+
// Fresh index — surface status message
|
|
40
36
|
const meta = this.carto.getMeta();
|
|
41
37
|
const duration = ((Date.now() - start) / 1000).toFixed(1);
|
|
42
38
|
const domains = this.carto.getDomainsList();
|
|
@@ -64,6 +60,8 @@ class SessionManager {
|
|
|
64
60
|
}
|
|
65
61
|
|
|
66
62
|
delete(id) {
|
|
63
|
+
const s = this._sessions.get(id);
|
|
64
|
+
if (s && s.carto) s.carto.close();
|
|
67
65
|
this._sessions.delete(id);
|
|
68
66
|
}
|
|
69
67
|
}
|
package/src/cli/impact.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const { Carto } = require('../../index.js');
|
|
3
|
+
const { StoreAdapter } = require('../store/store-adapter');
|
|
5
4
|
const { checkForUpdate } = require('./update-check');
|
|
6
5
|
|
|
7
6
|
async function run(projectRoot, fileArg) {
|
|
@@ -11,9 +10,9 @@ async function run(projectRoot, fileArg) {
|
|
|
11
10
|
process.exit(1);
|
|
12
11
|
}
|
|
13
12
|
|
|
14
|
-
const carto = new
|
|
13
|
+
const carto = new StoreAdapter();
|
|
15
14
|
try {
|
|
16
|
-
await carto.index(projectRoot
|
|
15
|
+
await carto.index(projectRoot);
|
|
17
16
|
} catch (err) {
|
|
18
17
|
console.error(`[CARTO] Error loading index: ${err.message}`);
|
|
19
18
|
process.exit(1);
|
|
@@ -22,6 +21,7 @@ async function run(projectRoot, fileArg) {
|
|
|
22
21
|
const br = carto.getBlastRadius(fileArg);
|
|
23
22
|
if (!br) {
|
|
24
23
|
console.error(`[CARTO] File not found in project graph: ${fileArg}`);
|
|
24
|
+
carto.close();
|
|
25
25
|
process.exit(1);
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -55,7 +55,7 @@ async function run(projectRoot, fileArg) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
console.log('');
|
|
58
|
-
carto.
|
|
58
|
+
carto.close();
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
module.exports = { run };
|
package/src/cli/init.js
CHANGED
|
@@ -54,8 +54,6 @@ async function run(projectRoot) {
|
|
|
54
54
|
installGitHook(projectRoot);
|
|
55
55
|
|
|
56
56
|
// Run first sync — V2 SQLite-backed indexer.
|
|
57
|
-
// (Previously: V1 runFullSync with empty file lists from resolveConfig — produced
|
|
58
|
-
// a 23ms no-op that left .carto/carto.db missing and AGENTS.md unpopulated.)
|
|
59
57
|
await runSyncV2({
|
|
60
58
|
projectRoot,
|
|
61
59
|
output: path.resolve(projectRoot, config.output || 'AGENTS.md')
|
|
@@ -67,22 +65,6 @@ async function run(projectRoot) {
|
|
|
67
65
|
console.log('[CARTO] AGENTS.md generated. Carto will sync on every git commit.');
|
|
68
66
|
}
|
|
69
67
|
|
|
70
|
-
/**
|
|
71
|
-
* Resolves config paths to absolute paths.
|
|
72
|
-
* V2: no watch file lists — sync-v2 discovers files itself.
|
|
73
|
-
*/
|
|
74
|
-
function resolveConfig(projectRoot, config) {
|
|
75
|
-
return {
|
|
76
|
-
watch: {
|
|
77
|
-
routeFiles: (config.watch && config.watch.routeFiles || []).map(f => path.resolve(projectRoot, f)),
|
|
78
|
-
modelFiles: (config.watch && config.watch.modelFiles || []).map(f => path.resolve(projectRoot, f)),
|
|
79
|
-
frontendFiles: (config.watch && config.watch.frontendFiles || []).map(f => path.resolve(projectRoot, f))
|
|
80
|
-
},
|
|
81
|
-
output: path.resolve(projectRoot, config.output || 'AGENTS.md'),
|
|
82
|
-
projectRoot
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
68
|
function installGitHook(projectRoot) {
|
|
87
69
|
const gitDir = path.join(projectRoot, '.git');
|
|
88
70
|
if (!fs.existsSync(gitDir)) return;
|
|
@@ -163,4 +145,4 @@ function wireIDEs(projectRoot) {
|
|
|
163
145
|
}
|
|
164
146
|
}
|
|
165
147
|
|
|
166
|
-
module.exports = { run
|
|
148
|
+
module.exports = { run };
|
package/src/cli/serve.js
CHANGED
|
@@ -4,22 +4,21 @@ const { checkForUpdate } = require('./update-check');
|
|
|
4
4
|
|
|
5
5
|
function run(projectRoot) {
|
|
6
6
|
checkForUpdate(); // fire and forget
|
|
7
|
-
// Prefer V2 SQLite-backed server
|
|
8
7
|
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
9
|
-
if (fs.existsSync(dbPath)) {
|
|
10
|
-
console.error('[CARTO] MCP server starting (V2 SQLite)...');
|
|
11
|
-
require('../mcp/server-v2');
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
if (!fs.existsSync(dbPath)) {
|
|
10
|
+
// Could be pre-2.0.4 install with only map.json, or never initialized.
|
|
11
|
+
const mapPath = path.join(projectRoot, '.carto', 'map.json');
|
|
12
|
+
if (fs.existsSync(mapPath)) {
|
|
13
|
+
console.error('[CARTO] Legacy index found (map.json) but no SQLite DB. Run `carto init` to upgrade your index.');
|
|
14
|
+
} else {
|
|
15
|
+
console.error('[CARTO] No index found. Run `carto init` first.');
|
|
16
|
+
}
|
|
19
17
|
process.exit(1);
|
|
20
18
|
}
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
|
|
20
|
+
console.error('[CARTO] MCP server starting...');
|
|
21
|
+
require('../mcp/server-v2');
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
module.exports = { run };
|
package/src/cli/sync.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { runSyncV2 } = require('../store/sync-v2');
|
|
4
|
-
const { runFullSync } = require('../sync');
|
|
5
|
-
const { resolveConfig } = require('./init');
|
|
6
4
|
const { checkForUpdate } = require('./update-check');
|
|
7
5
|
|
|
8
6
|
async function run(projectRoot) {
|
|
@@ -22,20 +20,10 @@ async function run(projectRoot) {
|
|
|
22
20
|
process.exit(1);
|
|
23
21
|
}
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
const v2Config = {
|
|
23
|
+
await runSyncV2({
|
|
27
24
|
projectRoot,
|
|
28
25
|
output: path.resolve(projectRoot, config.output || 'AGENTS.md')
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
await runSyncV2(v2Config);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
// Fallback to V1 if V2 fails (e.g., better-sqlite3 not available)
|
|
35
|
-
console.warn(`[CARTO] V2 sync failed (${err.message}), falling back to V1`);
|
|
36
|
-
const resolved = resolveConfig(projectRoot, config);
|
|
37
|
-
await runFullSync(resolved);
|
|
38
|
-
}
|
|
26
|
+
});
|
|
39
27
|
|
|
40
28
|
console.log('[CARTO] Sync complete.');
|
|
41
29
|
}
|
|
@@ -6,6 +6,34 @@ const fs = require('fs');
|
|
|
6
6
|
|
|
7
7
|
const SCHEMA_VERSION = '1';
|
|
8
8
|
|
|
9
|
+
/**
|
|
10
|
+
* normalizePath(p) — Canonicalize a relative path for storage and query.
|
|
11
|
+
*
|
|
12
|
+
* Converts backslashes to forward slashes (Windows → POSIX), strips leading
|
|
13
|
+
* './', and rejects absolute paths (callers must `path.relative(projectRoot, p)`
|
|
14
|
+
* before passing in).
|
|
15
|
+
*
|
|
16
|
+
* Single-source-of-truth for "what does a row in `files.path` look like?".
|
|
17
|
+
* Apply this at every boundary that writes or queries the column, and every
|
|
18
|
+
* downstream join (imports.from_file_id → files.path) stays consistent.
|
|
19
|
+
*
|
|
20
|
+
* Cross-platform invariant: same code, same DB, same query results on macOS,
|
|
21
|
+
* Linux, and Windows. Closes Bug 2 (carto impact path normalization) and the
|
|
22
|
+
* Windows-only "0 dependents" failures in the Store adapter test suite.
|
|
23
|
+
*/
|
|
24
|
+
function normalizePath(p) {
|
|
25
|
+
if (typeof p !== 'string' || p.length === 0) return p;
|
|
26
|
+
let out = p;
|
|
27
|
+
// Backslash → forward slash (Windows → POSIX). path.sep is '\\' on Windows.
|
|
28
|
+
if (path.sep !== '/') out = out.split(path.sep).join('/');
|
|
29
|
+
// Some inputs may already use mixed separators (e.g., `src\utils/foo.js`).
|
|
30
|
+
// Normalize aggressively.
|
|
31
|
+
out = out.replace(/\\/g, '/');
|
|
32
|
+
// Strip leading './'
|
|
33
|
+
if (out.startsWith('./')) out = out.slice(2);
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
9
37
|
class SQLiteStore {
|
|
10
38
|
constructor(projectRoot) {
|
|
11
39
|
this._projectRoot = projectRoot;
|
|
@@ -213,7 +241,7 @@ class SQLiteStore {
|
|
|
213
241
|
// ─── File operations ───────────────────────────────────────────────────
|
|
214
242
|
|
|
215
243
|
getFileByPath(relPath) {
|
|
216
|
-
return this._db.prepare('SELECT * FROM files WHERE path = ?').get(relPath);
|
|
244
|
+
return this._db.prepare('SELECT * FROM files WHERE path = ?').get(normalizePath(relPath));
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
getFileById(id) {
|
|
@@ -230,7 +258,8 @@ class SQLiteStore {
|
|
|
230
258
|
}
|
|
231
259
|
|
|
232
260
|
upsertFile(relPath, { language, hash, mtime, size }) {
|
|
233
|
-
const
|
|
261
|
+
const norm = normalizePath(relPath);
|
|
262
|
+
const existing = this.getFileByPath(norm);
|
|
234
263
|
if (existing) {
|
|
235
264
|
this._db.prepare(
|
|
236
265
|
'UPDATE files SET language=?, hash=?, mtime=?, size=?, last_indexed_at=? WHERE id=?'
|
|
@@ -239,18 +268,18 @@ class SQLiteStore {
|
|
|
239
268
|
}
|
|
240
269
|
const info = this._db.prepare(
|
|
241
270
|
'INSERT INTO files (path, language, hash, mtime, size, last_indexed_at) VALUES (?,?,?,?,?,?)'
|
|
242
|
-
).run(
|
|
271
|
+
).run(norm, language, hash, mtime, size, Date.now());
|
|
243
272
|
return info.lastInsertRowid;
|
|
244
273
|
}
|
|
245
274
|
|
|
246
275
|
updateFileMtime(relPath, mtime, size) {
|
|
247
276
|
this._db.prepare(
|
|
248
277
|
'UPDATE files SET mtime=?, size=? WHERE path=?'
|
|
249
|
-
).run(mtime, size, relPath);
|
|
278
|
+
).run(mtime, size, normalizePath(relPath));
|
|
250
279
|
}
|
|
251
280
|
|
|
252
281
|
removeFile(relPath) {
|
|
253
|
-
this._db.prepare('DELETE FROM files WHERE path = ?').run(relPath);
|
|
282
|
+
this._db.prepare('DELETE FROM files WHERE path = ?').run(normalizePath(relPath));
|
|
254
283
|
}
|
|
255
284
|
|
|
256
285
|
removeStaleFiles(currentPaths) {
|
|
@@ -744,4 +773,4 @@ class SQLiteStore {
|
|
|
744
773
|
}
|
|
745
774
|
}
|
|
746
775
|
|
|
747
|
-
module.exports = { SQLiteStore };
|
|
776
|
+
module.exports = { SQLiteStore, normalizePath };
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { SQLiteStore } = require('./sqlite-store');
|
|
6
|
+
const { runSyncV2 } = require('./sync-v2');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* StoreAdapter — V1-compatible API wrapping SQLiteStore.
|
|
10
|
+
* Drop-in replacement for the V1 Carto class in acp/session.js.
|
|
11
|
+
* tools.js calls the same methods with the same shapes.
|
|
12
|
+
*/
|
|
13
|
+
class StoreAdapter {
|
|
14
|
+
constructor() {
|
|
15
|
+
this._store = null;
|
|
16
|
+
this._projectRoot = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* index(projectRoot, opts?)
|
|
21
|
+
* Opens the SQLite DB; runs runSyncV2 if DB is missing.
|
|
22
|
+
* opts.force — re-extract all files
|
|
23
|
+
* opts.writeOutputs — if false, skip AGENTS.md + context file generation (default: false)
|
|
24
|
+
*/
|
|
25
|
+
async index(projectRoot, opts = {}) {
|
|
26
|
+
this._projectRoot = projectRoot;
|
|
27
|
+
const dbPath = path.join(projectRoot, '.carto', 'carto.db');
|
|
28
|
+
const dbExists = fs.existsSync(dbPath);
|
|
29
|
+
|
|
30
|
+
if (!dbExists || opts.force) {
|
|
31
|
+
await runSyncV2({
|
|
32
|
+
projectRoot,
|
|
33
|
+
output: opts.writeOutputs ? path.resolve(projectRoot, 'AGENTS.md') : null,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this._store = new SQLiteStore(projectRoot);
|
|
38
|
+
this._store.open();
|
|
39
|
+
return this.getMeta();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ─── V1-shaped query API ────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
getMeta() {
|
|
45
|
+
const s = this._store.getStructure();
|
|
46
|
+
return s.meta || { totalFiles: 0, totalRoutes: 0, totalImportEdges: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getRoutes() { return this._store.getRoutes(); }
|
|
50
|
+
searchRoutes(q) { return this._store.searchRoutes(q); }
|
|
51
|
+
getModels(domain) { return this._store.getModels(domain); }
|
|
52
|
+
getDomainsList() { return this._store.getDomainsList(); }
|
|
53
|
+
getDomain(name) { return this._store.getDomain(name); }
|
|
54
|
+
getCrossDomainDeps() { return this._store.getCrossDomainDeps(); }
|
|
55
|
+
getHighImpactFiles(n) { return this._store.getHighImpactFiles(n); }
|
|
56
|
+
|
|
57
|
+
getStructure() {
|
|
58
|
+
return this._store.getStructure();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getNeighbors(file, hops = 1) {
|
|
62
|
+
return this._store.getNeighbors(file, hops);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* getBlastRadius(file) — V1 rich shape:
|
|
67
|
+
* { file, risk, directlyAffected, potentiallyAffected,
|
|
68
|
+
* routesImpacted, domainsImpacted, dependentFiles }
|
|
69
|
+
*/
|
|
70
|
+
getBlastRadius(file) {
|
|
71
|
+
const radius = this._store.getBlastRadius(file, 5);
|
|
72
|
+
if (!radius) return null;
|
|
73
|
+
|
|
74
|
+
const directDeps = radius.filter(r => r.hop_distance === 1).map(r => r.file);
|
|
75
|
+
const allDepFiles = radius.map(r => r.file);
|
|
76
|
+
|
|
77
|
+
// Routes impacted
|
|
78
|
+
const allRoutes = this._store.getRoutes();
|
|
79
|
+
const affectedSet = new Set([file, ...allDepFiles]);
|
|
80
|
+
const routesImpacted = allRoutes
|
|
81
|
+
.filter(r => affectedSet.has(r.file))
|
|
82
|
+
.map(r => {
|
|
83
|
+
const isAuth = /auth|login|session/i.test(r.path);
|
|
84
|
+
const isPay = /billing|payment|checkout/i.test(r.path);
|
|
85
|
+
const riskLevel = isAuth || isPay ? 'HIGH'
|
|
86
|
+
: directDeps.length >= 3 ? 'HIGH'
|
|
87
|
+
: directDeps.length >= 2 ? 'MEDIUM' : 'LOW';
|
|
88
|
+
return { method: r.method, path: r.path, risk: riskLevel };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Domains impacted
|
|
92
|
+
const domainsImpacted = new Set();
|
|
93
|
+
const fileDomain = this._store.getDomainForFile(file);
|
|
94
|
+
if (fileDomain) domainsImpacted.add(fileDomain);
|
|
95
|
+
for (const f of allDepFiles) {
|
|
96
|
+
const d = this._store.getDomainForFile(f);
|
|
97
|
+
if (d) domainsImpacted.add(d);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const depCount = directDeps.length;
|
|
101
|
+
const risk = depCount >= 5 ? 'HIGH'
|
|
102
|
+
: depCount >= 3 ? 'HIGH'
|
|
103
|
+
: depCount >= 2 ? 'MEDIUM'
|
|
104
|
+
: depCount >= 1 ? 'LOW' : 'SAFE';
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
file,
|
|
108
|
+
risk,
|
|
109
|
+
directlyAffected: { files: directDeps.length, domains: domainsImpacted.size },
|
|
110
|
+
potentiallyAffected: { files: allDepFiles.length, domains: domainsImpacted.size },
|
|
111
|
+
routesImpacted,
|
|
112
|
+
domainsImpacted: [...domainsImpacted],
|
|
113
|
+
dependentFiles: allDepFiles,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* getContextForFile(file) — composed context matching V1 shape.
|
|
119
|
+
*/
|
|
120
|
+
getContextForFile(file) {
|
|
121
|
+
const fileRow = this._store.getFileByPath(file);
|
|
122
|
+
if (!fileRow) return null;
|
|
123
|
+
|
|
124
|
+
const domain = this._store.getDomainForFile(file) || 'CORE';
|
|
125
|
+
const routes = this._store.getRoutes().filter(r => r.file === file).map(r => `${r.method} ${r.path}`);
|
|
126
|
+
const models = this._store.getModels().filter(m => m.file === file).map(m => m.name);
|
|
127
|
+
const blastRadius = this.getBlastRadius(file);
|
|
128
|
+
const neighbors = this.getNeighbors(file, 2);
|
|
129
|
+
const crossDomain = this._store.getCrossDomainDeps().filter(d => d.from === file || d.to === file);
|
|
130
|
+
|
|
131
|
+
let domainContext = null;
|
|
132
|
+
if (domain && this._projectRoot) {
|
|
133
|
+
const ctxPath = path.join(this._projectRoot, '.carto', 'context', `${domain}.md`);
|
|
134
|
+
try { domainContext = fs.readFileSync(ctxPath, 'utf-8'); } catch {}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
file,
|
|
139
|
+
domain,
|
|
140
|
+
routes,
|
|
141
|
+
models,
|
|
142
|
+
functions: [],
|
|
143
|
+
envVars: [],
|
|
144
|
+
blastRadius,
|
|
145
|
+
neighbors,
|
|
146
|
+
crossDomainDeps: crossDomain,
|
|
147
|
+
domainContext,
|
|
148
|
+
meta: {
|
|
149
|
+
importCount: 0,
|
|
150
|
+
dependentCount: blastRadius ? blastRadius.directlyAffected.files : 0,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
close() {
|
|
156
|
+
if (this._store) { this._store.close(); this._store = null; }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* terminate() — alias for close().
|
|
161
|
+
* Kept so the V1-style `Carto` export from index.js (which is now an alias
|
|
162
|
+
* for StoreAdapter) remains fully API-compatible. The V1 Carto class
|
|
163
|
+
* exposed terminate(); legacy programmatic users may still call it.
|
|
164
|
+
* Safe to remove in 3.0.0 along with the Carto alias.
|
|
165
|
+
*/
|
|
166
|
+
terminate() { return this.close(); }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = { StoreAdapter };
|
package/src/store/sync-v2.js
CHANGED
|
@@ -24,7 +24,10 @@ const IGNORE_DIRS = new Set([
|
|
|
24
24
|
'dist', '.next', '.turbo', 'build', 'coverage', '.carto',
|
|
25
25
|
'out', '.cache', 'generated', '__generated__',
|
|
26
26
|
'storybook-static', 'public', 'static',
|
|
27
|
-
'tmp-bench', 'vendor', 'third_party', '.yarn'
|
|
27
|
+
'tmp-bench', 'vendor', 'third_party', '.yarn',
|
|
28
|
+
// Test directories — ported from V1 detector/files.js
|
|
29
|
+
'test', 'tests', '__tests__', 'e2e', 'playwright',
|
|
30
|
+
'cypress', 'fixtures', 'mocks', '__mocks__'
|
|
28
31
|
]);
|
|
29
32
|
|
|
30
33
|
const CODE_EXTS = new Set([
|
|
@@ -34,6 +37,36 @@ const CODE_EXTS = new Set([
|
|
|
34
37
|
'.swift', '.kt'
|
|
35
38
|
]);
|
|
36
39
|
|
|
40
|
+
const JS_LIKE_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* isTestFile(relPath) → true if the file is a test/spec/stories file
|
|
44
|
+
* Ported from V1 detector/files.js exclusion patterns.
|
|
45
|
+
* R: test_*, test-*, *_test.r (case-insensitive)
|
|
46
|
+
* Python: test_*.py, *_test.py
|
|
47
|
+
* JS/TS: *.test.*, *.spec.*, *.stories.*
|
|
48
|
+
* Path-based directory exclusions (test/, tests/, __tests__/, etc.)
|
|
49
|
+
* are handled by IGNORE_DIRS during the walk — see above.
|
|
50
|
+
*/
|
|
51
|
+
function isTestFile(relPath) {
|
|
52
|
+
const base = path.basename(relPath);
|
|
53
|
+
const lbase = base.toLowerCase();
|
|
54
|
+
const ext = path.extname(base).toLowerCase();
|
|
55
|
+
|
|
56
|
+
if (ext === '.r') {
|
|
57
|
+
return lbase.startsWith('test_') || lbase.startsWith('test-') || lbase.endsWith('_test.r');
|
|
58
|
+
}
|
|
59
|
+
if (ext === '.py') {
|
|
60
|
+
return lbase.startsWith('test_') || lbase.endsWith('_test.py');
|
|
61
|
+
}
|
|
62
|
+
if (JS_LIKE_EXTS.has(ext)) {
|
|
63
|
+
if (lbase.includes('.test.') || lbase.includes('.spec.') || lbase.includes('.stories.')) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
37
70
|
/**
|
|
38
71
|
* discoverFiles(projectRoot)
|
|
39
72
|
* Recursively walks the project tree. No file cap. Respects ignore dirs + .cartoignore.
|
|
@@ -55,7 +88,7 @@ function discoverFiles(projectRoot) {
|
|
|
55
88
|
walk(full);
|
|
56
89
|
} else if (entry.isFile()) {
|
|
57
90
|
const ext = path.extname(entry.name).toLowerCase();
|
|
58
|
-
if (CODE_EXTS.has(ext) && !isIgnored(rel)) {
|
|
91
|
+
if (CODE_EXTS.has(ext) && !isIgnored(rel) && !isTestFile(rel)) {
|
|
59
92
|
results.push(rel);
|
|
60
93
|
}
|
|
61
94
|
}
|
|
@@ -419,8 +452,8 @@ async function runSyncV2(config) {
|
|
|
419
452
|
}
|
|
420
453
|
}
|
|
421
454
|
|
|
422
|
-
// 8. Generate outputs (only if files changed)
|
|
423
|
-
if (toProcess.length > 0) {
|
|
455
|
+
// 8. Generate outputs (only if files changed AND output not suppressed)
|
|
456
|
+
if (toProcess.length > 0 && config.output !== null && config.output !== false) {
|
|
424
457
|
await generateOutputs(store, config, projectRoot, store.getImportGraph());
|
|
425
458
|
}
|
|
426
459
|
|