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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal unified-diff parser.
|
|
5
|
+
*
|
|
6
|
+
* Zero deps, zero allocations beyond the result rows. Handles the subset
|
|
7
|
+
* of unified diff that GitHub PRs, `git diff`, and `git format-patch`
|
|
8
|
+
* actually emit:
|
|
9
|
+
*
|
|
10
|
+
* - Per-file headers: `--- a/path` / `+++ b/path`
|
|
11
|
+
* - File-mode markers: `new file mode`, `deleted file mode`, `rename from`,
|
|
12
|
+
* `rename to`. We surface `kind` ∈ { 'modify' | 'add' | 'delete' | 'rename' }.
|
|
13
|
+
* - Hunk headers: `@@ -from,len +to,len @@ optional context`
|
|
14
|
+
* - Body lines: `+added`, `-removed`, ` context`, `\\ No newline at EOF`
|
|
15
|
+
* - `Binary files ... differ` lines — we skip the whole file.
|
|
16
|
+
*
|
|
17
|
+
* Out of scope:
|
|
18
|
+
* - Combined diffs (`@@@ ... @@@`) — git rarely emits these except for
|
|
19
|
+
* octopus merges. We tolerate (skip) the file.
|
|
20
|
+
* - SVN / Mercurial extension lines.
|
|
21
|
+
*
|
|
22
|
+
* Output shape — one entry per file:
|
|
23
|
+
* {
|
|
24
|
+
* path: string, // post-image path (`+++ b/...`)
|
|
25
|
+
* oldPath: string | null, // pre-image path (different on rename)
|
|
26
|
+
* kind: 'modify' | 'add' | 'delete' | 'rename',
|
|
27
|
+
* added: [{lineNo, content}], // line numbers in the new file
|
|
28
|
+
* removed: [{lineNo, content}], // line numbers in the old file
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* Malformed input must never throw — return whatever we parsed.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
function stripDiffPath(raw) {
|
|
35
|
+
if (!raw || typeof raw !== 'string') return null;
|
|
36
|
+
// Strip trailing tab/timestamp markers that some unified diffs append.
|
|
37
|
+
let p = raw.split('\t')[0].trim();
|
|
38
|
+
// Drop git's a/ or b/ prefix if present.
|
|
39
|
+
if (p.startsWith('a/') || p.startsWith('b/')) p = p.slice(2);
|
|
40
|
+
// /dev/null sentinel for new/deleted files.
|
|
41
|
+
if (p === '/dev/null') return null;
|
|
42
|
+
return p;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* parseDiff(diffText) → [{ path, oldPath, kind, added, removed }]
|
|
47
|
+
*
|
|
48
|
+
* Top-level entry. Defensive against truncation, malformed hunks, or
|
|
49
|
+
* input that isn't a diff at all (returns []).
|
|
50
|
+
*/
|
|
51
|
+
function parseDiff(diffText) {
|
|
52
|
+
if (typeof diffText !== 'string' || diffText.length === 0) return [];
|
|
53
|
+
const lines = diffText.split(/\r?\n/);
|
|
54
|
+
const files = [];
|
|
55
|
+
let cur = null;
|
|
56
|
+
let oldLine = 0;
|
|
57
|
+
let newLine = 0;
|
|
58
|
+
let inHunk = false;
|
|
59
|
+
// `pendingMode` tracks file-mode hints between the `diff --git` header
|
|
60
|
+
// and the `---/+++` lines that finalize the entry.
|
|
61
|
+
let pendingKind = null;
|
|
62
|
+
let pendingOldPath = null;
|
|
63
|
+
let pendingNewPath = null;
|
|
64
|
+
|
|
65
|
+
function flush() {
|
|
66
|
+
// Materialise rename-only entries that never produced a `+++ ` line.
|
|
67
|
+
if (!cur && pendingKind === 'rename' && pendingOldPath && pendingNewPath) {
|
|
68
|
+
cur = {
|
|
69
|
+
path: pendingNewPath,
|
|
70
|
+
oldPath: pendingOldPath !== pendingNewPath ? pendingOldPath : null,
|
|
71
|
+
kind: 'rename',
|
|
72
|
+
added: [],
|
|
73
|
+
removed: [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (cur) files.push(cur);
|
|
77
|
+
cur = null;
|
|
78
|
+
inHunk = false;
|
|
79
|
+
pendingKind = null;
|
|
80
|
+
pendingOldPath = null;
|
|
81
|
+
pendingNewPath = null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
|
|
87
|
+
// `diff --git a/foo b/bar` — start of a new file entry. Pre-resolve
|
|
88
|
+
// the rename paths from the header so we have them even if the
|
|
89
|
+
// rename has zero hunks.
|
|
90
|
+
if (line.startsWith('diff --git ')) {
|
|
91
|
+
flush();
|
|
92
|
+
pendingKind = 'modify';
|
|
93
|
+
pendingOldPath = null;
|
|
94
|
+
pendingNewPath = null;
|
|
95
|
+
const m = line.match(/^diff --git a\/(.+?) b\/(.+)$/);
|
|
96
|
+
if (m) {
|
|
97
|
+
pendingOldPath = m[1];
|
|
98
|
+
pendingNewPath = m[2];
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (line.startsWith('new file mode')) { pendingKind = 'add'; continue; }
|
|
104
|
+
if (line.startsWith('deleted file mode')) { pendingKind = 'delete'; continue; }
|
|
105
|
+
if (line.startsWith('rename from ')) {
|
|
106
|
+
pendingKind = 'rename';
|
|
107
|
+
pendingOldPath = line.slice('rename from '.length).trim();
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (line.startsWith('rename to ')) {
|
|
111
|
+
pendingKind = 'rename';
|
|
112
|
+
pendingNewPath = line.slice('rename to '.length).trim();
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Skip binary diffs — we can't validate them anyway.
|
|
117
|
+
if (line.startsWith('Binary files ') && line.endsWith(' differ')) {
|
|
118
|
+
flush();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (line.startsWith('--- ')) {
|
|
123
|
+
const oldP = stripDiffPath(line.slice(4));
|
|
124
|
+
if (oldP !== null) pendingOldPath = oldP;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (line.startsWith('+++ ')) {
|
|
129
|
+
// Finalize the file header. Open a new `cur` entry.
|
|
130
|
+
const newP = stripDiffPath(line.slice(4));
|
|
131
|
+
if (newP !== null) pendingNewPath = newP;
|
|
132
|
+
const path = pendingNewPath || pendingOldPath;
|
|
133
|
+
if (!path) continue;
|
|
134
|
+
cur = {
|
|
135
|
+
path,
|
|
136
|
+
oldPath: pendingOldPath !== pendingNewPath ? pendingOldPath : null,
|
|
137
|
+
kind: pendingKind || 'modify',
|
|
138
|
+
added: [],
|
|
139
|
+
removed: [],
|
|
140
|
+
};
|
|
141
|
+
// /dev/null on the old side means add; on the new side means delete.
|
|
142
|
+
if (cur.kind === 'modify') {
|
|
143
|
+
if (pendingOldPath === null && pendingNewPath !== null) cur.kind = 'add';
|
|
144
|
+
else if (pendingNewPath === null && pendingOldPath !== null) cur.kind = 'delete';
|
|
145
|
+
}
|
|
146
|
+
pendingKind = null;
|
|
147
|
+
pendingOldPath = null;
|
|
148
|
+
pendingNewPath = null;
|
|
149
|
+
inHunk = false;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!cur) continue;
|
|
154
|
+
|
|
155
|
+
// Hunk header: @@ -oldStart,oldLen +newStart,newLen @@ (lengths optional)
|
|
156
|
+
if (line.startsWith('@@')) {
|
|
157
|
+
const m = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
158
|
+
if (m) {
|
|
159
|
+
oldLine = parseInt(m[1], 10);
|
|
160
|
+
newLine = parseInt(m[2], 10);
|
|
161
|
+
inHunk = true;
|
|
162
|
+
} else {
|
|
163
|
+
inHunk = false;
|
|
164
|
+
}
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!inHunk) continue;
|
|
169
|
+
|
|
170
|
+
// Body lines.
|
|
171
|
+
if (line.length === 0) {
|
|
172
|
+
// A blank body line is "context" with empty content. Both old and
|
|
173
|
+
// new advance.
|
|
174
|
+
oldLine++;
|
|
175
|
+
newLine++;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const tag = line[0];
|
|
179
|
+
const content = line.slice(1);
|
|
180
|
+
if (tag === '+') {
|
|
181
|
+
cur.added.push({ lineNo: newLine, content });
|
|
182
|
+
newLine++;
|
|
183
|
+
} else if (tag === '-') {
|
|
184
|
+
cur.removed.push({ lineNo: oldLine, content });
|
|
185
|
+
oldLine++;
|
|
186
|
+
} else if (tag === ' ') {
|
|
187
|
+
oldLine++;
|
|
188
|
+
newLine++;
|
|
189
|
+
} else if (tag === '\\') {
|
|
190
|
+
// "" — no line advance.
|
|
191
|
+
continue;
|
|
192
|
+
} else {
|
|
193
|
+
// Unknown body line — be tolerant and stop hunk parsing.
|
|
194
|
+
inHunk = false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
flush();
|
|
199
|
+
return files;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* extractAddedImports(file) → string[]
|
|
204
|
+
*
|
|
205
|
+
* Heuristic: scan added lines for import-style statements common across
|
|
206
|
+
* JS/TS, Python, Go, Rust, Ruby, Java. Returns the bare module / path
|
|
207
|
+
* specifier (the thing inside the quotes / after `from`/`import`/`use`).
|
|
208
|
+
*
|
|
209
|
+
* Used by validate.js to detect new cross-domain edges. Exported for
|
|
210
|
+
* test coverage. Best-effort — we'd rather miss than spuriously flag.
|
|
211
|
+
*/
|
|
212
|
+
function extractAddedImports(file) {
|
|
213
|
+
if (!file || !Array.isArray(file.added)) return [];
|
|
214
|
+
const out = [];
|
|
215
|
+
for (const { content } of file.added) {
|
|
216
|
+
if (!content) continue;
|
|
217
|
+
const trimmed = content.trim();
|
|
218
|
+
if (!trimmed || trimmed.startsWith('//') || trimmed.startsWith('#')) continue;
|
|
219
|
+
// ES module: import X from 'mod' / import 'mod' / import { x } from "mod"
|
|
220
|
+
let m = trimmed.match(/^import\s+(?:[^'"]*?from\s+)?['"]([^'"]+)['"]/);
|
|
221
|
+
if (m) { out.push(m[1]); continue; }
|
|
222
|
+
// CommonJS: const x = require('mod')
|
|
223
|
+
m = trimmed.match(/require\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
224
|
+
if (m) { out.push(m[1]); continue; }
|
|
225
|
+
// Dynamic import: import('mod')
|
|
226
|
+
m = trimmed.match(/^import\(\s*['"]([^'"]+)['"]\s*\)/);
|
|
227
|
+
if (m) { out.push(m[1]); continue; }
|
|
228
|
+
// Python: from mod import x / import mod
|
|
229
|
+
m = trimmed.match(/^from\s+([\w.]+)\s+import\b/);
|
|
230
|
+
if (m) { out.push(m[1]); continue; }
|
|
231
|
+
m = trimmed.match(/^import\s+([\w.]+)\s*$/);
|
|
232
|
+
if (m) { out.push(m[1]); continue; }
|
|
233
|
+
// Go: import "mod" / multi-line import block
|
|
234
|
+
m = trimmed.match(/^import\s+['"]([^'"]+)['"]/);
|
|
235
|
+
if (m) { out.push(m[1]); continue; }
|
|
236
|
+
// Rust: use crate::a::b::c;
|
|
237
|
+
m = trimmed.match(/^use\s+([\w:]+)/);
|
|
238
|
+
if (m) { out.push(m[1]); continue; }
|
|
239
|
+
// Java: import a.b.C;
|
|
240
|
+
m = trimmed.match(/^import\s+([\w.]+);/);
|
|
241
|
+
if (m) { out.push(m[1]); continue; }
|
|
242
|
+
}
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
module.exports = { parseDiff, extractAddedImports, stripDiffPath };
|