@optave/codegraph 2.3.0 → 2.3.1-dev.1aeea34
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 +40 -14
- package/package.json +5 -5
- package/src/builder.js +66 -0
- package/src/cli.js +113 -9
- package/src/cochange.js +498 -0
- package/src/config.js +6 -0
- package/src/db.js +40 -0
- package/src/embedder.js +53 -2
- package/src/export.js +158 -13
- package/src/extractors/helpers.js +2 -1
- package/src/extractors/javascript.js +294 -78
- package/src/index.js +13 -0
- package/src/mcp.js +62 -1
- package/src/parser.js +39 -2
- package/src/queries.js +158 -9
- package/src/registry.js +9 -1
- package/src/structure.js +94 -0
package/src/export.js
CHANGED
|
@@ -125,6 +125,42 @@ export function exportDOT(db, opts = {}) {
|
|
|
125
125
|
return lines.join('\n');
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** Escape double quotes for Mermaid labels. */
|
|
129
|
+
function escapeLabel(label) {
|
|
130
|
+
return label.replace(/"/g, '#quot;');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Map node kind to Mermaid shape wrapper. */
|
|
134
|
+
function mermaidShape(kind, label) {
|
|
135
|
+
const escaped = escapeLabel(label);
|
|
136
|
+
switch (kind) {
|
|
137
|
+
case 'function':
|
|
138
|
+
case 'method':
|
|
139
|
+
return `(["${escaped}"])`;
|
|
140
|
+
case 'class':
|
|
141
|
+
case 'interface':
|
|
142
|
+
case 'type':
|
|
143
|
+
case 'struct':
|
|
144
|
+
case 'enum':
|
|
145
|
+
case 'trait':
|
|
146
|
+
case 'record':
|
|
147
|
+
return `{{"${escaped}"}}`;
|
|
148
|
+
case 'module':
|
|
149
|
+
return `[["${escaped}"]]`;
|
|
150
|
+
default:
|
|
151
|
+
return `["${escaped}"]`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Map node role to Mermaid style colors. */
|
|
156
|
+
const ROLE_STYLES = {
|
|
157
|
+
entry: 'fill:#e8f5e9,stroke:#4caf50',
|
|
158
|
+
core: 'fill:#e3f2fd,stroke:#2196f3',
|
|
159
|
+
utility: 'fill:#f5f5f5,stroke:#9e9e9e',
|
|
160
|
+
dead: 'fill:#ffebee,stroke:#f44336',
|
|
161
|
+
leaf: 'fill:#fffde7,stroke:#fdd835',
|
|
162
|
+
};
|
|
163
|
+
|
|
128
164
|
/**
|
|
129
165
|
* Export the dependency graph in Mermaid format.
|
|
130
166
|
*/
|
|
@@ -132,12 +168,20 @@ export function exportMermaid(db, opts = {}) {
|
|
|
132
168
|
const fileLevel = opts.fileLevel !== false;
|
|
133
169
|
const noTests = opts.noTests || false;
|
|
134
170
|
const minConf = opts.minConfidence ?? DEFAULT_MIN_CONFIDENCE;
|
|
135
|
-
const
|
|
171
|
+
const direction = opts.direction || 'LR';
|
|
172
|
+
const lines = [`flowchart ${direction}`];
|
|
173
|
+
|
|
174
|
+
let nodeCounter = 0;
|
|
175
|
+
const nodeIdMap = new Map();
|
|
176
|
+
function nodeId(key) {
|
|
177
|
+
if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
178
|
+
return nodeIdMap.get(key);
|
|
179
|
+
}
|
|
136
180
|
|
|
137
181
|
if (fileLevel) {
|
|
138
182
|
let edges = db
|
|
139
183
|
.prepare(`
|
|
140
|
-
SELECT DISTINCT n1.file AS source, n2.file AS target
|
|
184
|
+
SELECT DISTINCT n1.file AS source, n2.file AS target, e.kind AS edge_kind
|
|
141
185
|
FROM edges e
|
|
142
186
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
143
187
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
@@ -147,32 +191,133 @@ export function exportMermaid(db, opts = {}) {
|
|
|
147
191
|
.all(minConf);
|
|
148
192
|
if (noTests) edges = edges.filter((e) => !isTestFile(e.source) && !isTestFile(e.target));
|
|
149
193
|
|
|
194
|
+
// Collect all files referenced in edges
|
|
195
|
+
const allFiles = new Set();
|
|
150
196
|
for (const { source, target } of edges) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
allFiles.add(source);
|
|
198
|
+
allFiles.add(target);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Build directory groupings — try DB directory nodes first, fall back to path.dirname()
|
|
202
|
+
const dirs = new Map();
|
|
203
|
+
const hasDirectoryNodes =
|
|
204
|
+
db.prepare("SELECT COUNT(*) as c FROM nodes WHERE kind = 'directory'").get().c > 0;
|
|
205
|
+
|
|
206
|
+
if (hasDirectoryNodes) {
|
|
207
|
+
const dbDirs = db.prepare("SELECT id, name FROM nodes WHERE kind = 'directory'").all();
|
|
208
|
+
for (const d of dbDirs) {
|
|
209
|
+
const containedFiles = db
|
|
210
|
+
.prepare(`
|
|
211
|
+
SELECT n.name FROM edges e
|
|
212
|
+
JOIN nodes n ON e.target_id = n.id
|
|
213
|
+
WHERE e.source_id = ? AND e.kind = 'contains' AND n.kind = 'file'
|
|
214
|
+
`)
|
|
215
|
+
.all(d.id)
|
|
216
|
+
.map((r) => r.name)
|
|
217
|
+
.filter((f) => allFiles.has(f));
|
|
218
|
+
if (containedFiles.length > 0) dirs.set(d.name, containedFiles);
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
for (const file of allFiles) {
|
|
222
|
+
const dir = path.dirname(file) || '.';
|
|
223
|
+
if (!dirs.has(dir)) dirs.set(dir, []);
|
|
224
|
+
dirs.get(dir).push(file);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Emit subgraphs
|
|
229
|
+
for (const [dir, files] of [...dirs].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
230
|
+
const sgId = dir.replace(/[^a-zA-Z0-9]/g, '_');
|
|
231
|
+
lines.push(` subgraph ${sgId}["${escapeLabel(dir)}"]`);
|
|
232
|
+
for (const f of files) {
|
|
233
|
+
const nId = nodeId(f);
|
|
234
|
+
lines.push(` ${nId}["${escapeLabel(path.basename(f))}"]`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(' end');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Deduplicate edges per source-target pair, collecting all distinct kinds
|
|
240
|
+
const edgeMap = new Map();
|
|
241
|
+
for (const { source, target, edge_kind } of edges) {
|
|
242
|
+
const key = `${source}|${target}`;
|
|
243
|
+
const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
|
|
244
|
+
if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
|
|
245
|
+
edgeMap.get(key).labels.add(label);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (const { source, target, labels } of edgeMap.values()) {
|
|
249
|
+
lines.push(` ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
|
|
154
250
|
}
|
|
155
251
|
} else {
|
|
156
252
|
let edges = db
|
|
157
253
|
.prepare(`
|
|
158
|
-
SELECT n1.name AS source_name, n1.file AS source_file,
|
|
159
|
-
n2.name AS target_name, n2.file AS target_file
|
|
254
|
+
SELECT n1.name AS source_name, n1.kind AS source_kind, n1.file AS source_file,
|
|
255
|
+
n2.name AS target_name, n2.kind AS target_kind, n2.file AS target_file,
|
|
256
|
+
e.kind AS edge_kind
|
|
160
257
|
FROM edges e
|
|
161
258
|
JOIN nodes n1 ON e.source_id = n1.id
|
|
162
259
|
JOIN nodes n2 ON e.target_id = n2.id
|
|
163
|
-
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
164
|
-
|
|
165
|
-
|
|
260
|
+
WHERE n1.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
261
|
+
AND n2.kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')
|
|
262
|
+
AND e.kind = 'calls'
|
|
263
|
+
AND e.confidence >= ?
|
|
166
264
|
`)
|
|
167
265
|
.all(minConf);
|
|
168
266
|
if (noTests)
|
|
169
267
|
edges = edges.filter((e) => !isTestFile(e.source_file) && !isTestFile(e.target_file));
|
|
170
268
|
|
|
269
|
+
// Group nodes by file for subgraphs
|
|
270
|
+
const fileNodes = new Map();
|
|
271
|
+
const nodeKinds = new Map();
|
|
272
|
+
for (const e of edges) {
|
|
273
|
+
const sKey = `${e.source_file}::${e.source_name}`;
|
|
274
|
+
const tKey = `${e.target_file}::${e.target_name}`;
|
|
275
|
+
nodeId(sKey);
|
|
276
|
+
nodeId(tKey);
|
|
277
|
+
nodeKinds.set(sKey, e.source_kind);
|
|
278
|
+
nodeKinds.set(tKey, e.target_kind);
|
|
279
|
+
|
|
280
|
+
if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
|
|
281
|
+
fileNodes.get(e.source_file).set(sKey, e.source_name);
|
|
282
|
+
|
|
283
|
+
if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
|
|
284
|
+
fileNodes.get(e.target_file).set(tKey, e.target_name);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Emit subgraphs grouped by file
|
|
288
|
+
for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
289
|
+
const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
|
|
290
|
+
lines.push(` subgraph ${sgId}["${escapeLabel(file)}"]`);
|
|
291
|
+
for (const [key, name] of nodes) {
|
|
292
|
+
const kind = nodeKinds.get(key);
|
|
293
|
+
lines.push(` ${nodeId(key)}${mermaidShape(kind, name)}`);
|
|
294
|
+
}
|
|
295
|
+
lines.push(' end');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Emit edges with labels
|
|
171
299
|
for (const e of edges) {
|
|
172
|
-
const sId = `${e.source_file}
|
|
173
|
-
const tId = `${e.target_file}
|
|
174
|
-
lines.push(` ${sId}
|
|
300
|
+
const sId = nodeId(`${e.source_file}::${e.source_name}`);
|
|
301
|
+
const tId = nodeId(`${e.target_file}::${e.target_name}`);
|
|
302
|
+
lines.push(` ${sId} -->|${e.edge_kind}| ${tId}`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Role styling — query roles for all referenced nodes
|
|
306
|
+
const allKeys = [...nodeIdMap.keys()];
|
|
307
|
+
const roleStyles = [];
|
|
308
|
+
for (const key of allKeys) {
|
|
309
|
+
const colonIdx = key.indexOf('::');
|
|
310
|
+
if (colonIdx === -1) continue;
|
|
311
|
+
const file = key.slice(0, colonIdx);
|
|
312
|
+
const name = key.slice(colonIdx + 2);
|
|
313
|
+
const row = db
|
|
314
|
+
.prepare('SELECT role FROM nodes WHERE file = ? AND name = ? AND role IS NOT NULL LIMIT 1')
|
|
315
|
+
.get(file, name);
|
|
316
|
+
if (row?.role && ROLE_STYLES[row.role]) {
|
|
317
|
+
roleStyles.push(` style ${nodeIdMap.get(key)} ${ROLE_STYLES[row.role]}`);
|
|
318
|
+
}
|
|
175
319
|
}
|
|
320
|
+
lines.push(...roleStyles);
|
|
176
321
|
}
|
|
177
322
|
|
|
178
323
|
return lines.join('\n');
|
|
@@ -4,7 +4,8 @@ export function nodeEndLine(node) {
|
|
|
4
4
|
|
|
5
5
|
export function findChild(node, type) {
|
|
6
6
|
for (let i = 0; i < node.childCount; i++) {
|
|
7
|
-
|
|
7
|
+
const child = node.child(i);
|
|
8
|
+
if (child.type === type) return child;
|
|
8
9
|
}
|
|
9
10
|
return null;
|
|
10
11
|
}
|