@optave/codegraph 3.1.3 → 3.1.4
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 +17 -19
- package/package.json +10 -7
- package/src/analysis/context.js +408 -0
- package/src/analysis/dependencies.js +341 -0
- package/src/analysis/exports.js +130 -0
- package/src/analysis/impact.js +463 -0
- package/src/analysis/module-map.js +322 -0
- package/src/analysis/roles.js +45 -0
- package/src/analysis/symbol-lookup.js +232 -0
- package/src/ast-analysis/shared.js +5 -4
- package/src/batch.js +2 -1
- package/src/builder/context.js +85 -0
- package/src/builder/helpers.js +218 -0
- package/src/builder/incremental.js +178 -0
- package/src/builder/pipeline.js +130 -0
- package/src/builder/stages/build-edges.js +297 -0
- package/src/builder/stages/build-structure.js +113 -0
- package/src/builder/stages/collect-files.js +44 -0
- package/src/builder/stages/detect-changes.js +413 -0
- package/src/builder/stages/finalize.js +139 -0
- package/src/builder/stages/insert-nodes.js +195 -0
- package/src/builder/stages/parse-files.js +28 -0
- package/src/builder/stages/resolve-imports.js +143 -0
- package/src/builder/stages/run-analyses.js +44 -0
- package/src/builder.js +10 -1485
- package/src/cfg.js +1 -2
- package/src/cli/commands/ast.js +26 -0
- package/src/cli/commands/audit.js +46 -0
- package/src/cli/commands/batch.js +68 -0
- package/src/cli/commands/branch-compare.js +21 -0
- package/src/cli/commands/build.js +26 -0
- package/src/cli/commands/cfg.js +30 -0
- package/src/cli/commands/check.js +79 -0
- package/src/cli/commands/children.js +31 -0
- package/src/cli/commands/co-change.js +65 -0
- package/src/cli/commands/communities.js +23 -0
- package/src/cli/commands/complexity.js +45 -0
- package/src/cli/commands/context.js +34 -0
- package/src/cli/commands/cycles.js +28 -0
- package/src/cli/commands/dataflow.js +32 -0
- package/src/cli/commands/deps.js +16 -0
- package/src/cli/commands/diff-impact.js +30 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +75 -0
- package/src/cli/commands/exports.js +18 -0
- package/src/cli/commands/flow.js +36 -0
- package/src/cli/commands/fn-impact.js +30 -0
- package/src/cli/commands/impact.js +16 -0
- package/src/cli/commands/info.js +76 -0
- package/src/cli/commands/map.js +19 -0
- package/src/cli/commands/mcp.js +18 -0
- package/src/cli/commands/models.js +19 -0
- package/src/cli/commands/owners.js +25 -0
- package/src/cli/commands/path.js +36 -0
- package/src/cli/commands/plot.js +80 -0
- package/src/cli/commands/query.js +49 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +34 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +32 -0
- package/src/cli/commands/snapshot.js +61 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +32 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +24 -0
- package/src/cli/index.js +118 -0
- package/src/cli/shared/options.js +39 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/commands/check.js +5 -5
- package/src/commands/manifesto.js +3 -3
- package/src/commands/structure.js +1 -1
- package/src/communities.js +15 -87
- package/src/cycles.js +30 -85
- package/src/dataflow.js +1 -2
- package/src/db/connection.js +4 -4
- package/src/db/migrations.js +41 -0
- package/src/db/query-builder.js +6 -5
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +5 -2
- package/src/db/repository/in-memory-repository.js +584 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +63 -4
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/db.js +5 -0
- package/src/embeddings/generator.js +163 -0
- package/src/embeddings/index.js +13 -0
- package/src/embeddings/models.js +218 -0
- package/src/embeddings/search/cli-formatter.js +151 -0
- package/src/embeddings/search/filters.js +46 -0
- package/src/embeddings/search/hybrid.js +121 -0
- package/src/embeddings/search/keyword.js +68 -0
- package/src/embeddings/search/prepare.js +66 -0
- package/src/embeddings/search/semantic.js +145 -0
- package/src/embeddings/stores/fts5.js +27 -0
- package/src/embeddings/stores/sqlite-blob.js +24 -0
- package/src/embeddings/strategies/source.js +14 -0
- package/src/embeddings/strategies/structured.js +43 -0
- package/src/embeddings/strategies/text-utils.js +43 -0
- package/src/errors.js +78 -0
- package/src/export.js +217 -520
- package/src/extractors/csharp.js +10 -2
- package/src/extractors/go.js +3 -1
- package/src/extractors/helpers.js +71 -0
- package/src/extractors/java.js +9 -2
- package/src/extractors/javascript.js +38 -1
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/graph/algorithms/bfs.js +49 -0
- package/src/graph/algorithms/centrality.js +16 -0
- package/src/graph/algorithms/index.js +5 -0
- package/src/graph/algorithms/louvain.js +26 -0
- package/src/graph/algorithms/shortest-path.js +41 -0
- package/src/graph/algorithms/tarjan.js +49 -0
- package/src/graph/builders/dependency.js +91 -0
- package/src/graph/builders/index.js +3 -0
- package/src/graph/builders/structure.js +40 -0
- package/src/graph/builders/temporal.js +33 -0
- package/src/graph/classifiers/index.js +2 -0
- package/src/graph/classifiers/risk.js +85 -0
- package/src/graph/classifiers/roles.js +64 -0
- package/src/graph/index.js +13 -0
- package/src/graph/model.js +230 -0
- package/src/index.js +33 -210
- package/src/infrastructure/result-formatter.js +2 -21
- package/src/mcp/index.js +2 -0
- package/src/mcp/middleware.js +26 -0
- package/src/mcp/server.js +128 -0
- package/src/mcp/tool-registry.js +801 -0
- package/src/mcp/tools/ast-query.js +14 -0
- package/src/mcp/tools/audit.js +21 -0
- package/src/mcp/tools/batch-query.js +11 -0
- package/src/mcp/tools/branch-compare.js +10 -0
- package/src/mcp/tools/cfg.js +21 -0
- package/src/mcp/tools/check.js +43 -0
- package/src/mcp/tools/co-changes.js +20 -0
- package/src/mcp/tools/code-owners.js +12 -0
- package/src/mcp/tools/communities.js +15 -0
- package/src/mcp/tools/complexity.js +18 -0
- package/src/mcp/tools/context.js +17 -0
- package/src/mcp/tools/dataflow.js +26 -0
- package/src/mcp/tools/diff-impact.js +24 -0
- package/src/mcp/tools/execution-flow.js +26 -0
- package/src/mcp/tools/export-graph.js +57 -0
- package/src/mcp/tools/file-deps.js +12 -0
- package/src/mcp/tools/file-exports.js +13 -0
- package/src/mcp/tools/find-cycles.js +15 -0
- package/src/mcp/tools/fn-impact.js +15 -0
- package/src/mcp/tools/impact-analysis.js +12 -0
- package/src/mcp/tools/index.js +71 -0
- package/src/mcp/tools/list-functions.js +14 -0
- package/src/mcp/tools/list-repos.js +11 -0
- package/src/mcp/tools/module-map.js +6 -0
- package/src/mcp/tools/node-roles.js +14 -0
- package/src/mcp/tools/path.js +12 -0
- package/src/mcp/tools/query.js +30 -0
- package/src/mcp/tools/semantic-search.js +65 -0
- package/src/mcp/tools/sequence.js +17 -0
- package/src/mcp/tools/structure.js +15 -0
- package/src/mcp/tools/symbol-children.js +14 -0
- package/src/mcp/tools/triage.js +35 -0
- package/src/mcp/tools/where.js +13 -0
- package/src/mcp.js +2 -1470
- package/src/native.js +3 -1
- package/src/presentation/colors.js +44 -0
- package/src/presentation/export.js +444 -0
- package/src/presentation/result-formatter.js +21 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/presentation/table.js +47 -0
- package/src/presentation/viewer.js +634 -0
- package/src/queries.js +35 -2276
- package/src/resolve.js +1 -1
- package/src/sequence.js +2 -38
- package/src/shared/file-utils.js +153 -0
- package/src/shared/generators.js +125 -0
- package/src/shared/hierarchy.js +27 -0
- package/src/shared/normalize.js +59 -0
- package/src/snapshot.js +6 -5
- package/src/structure.js +15 -40
- package/src/triage.js +20 -72
- package/src/viewer.js +35 -656
- package/src/watcher.js +8 -148
- package/src/embedder.js +0 -1097
package/src/resolve.js
CHANGED
|
@@ -178,4 +178,4 @@ export function resolveImportsBatch(inputs, rootDir, aliases, knownFiles) {
|
|
|
178
178
|
|
|
179
179
|
// ── Exported for testing ────────────────────────────────────────────
|
|
180
180
|
|
|
181
|
-
export {
|
|
181
|
+
export { computeConfidenceJS, resolveImportPathJS };
|
package/src/sequence.js
CHANGED
|
@@ -285,41 +285,5 @@ export function sequenceData(name, dbPath, opts = {}) {
|
|
|
285
285
|
}
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Escape special Mermaid characters in labels.
|
|
292
|
-
*/
|
|
293
|
-
function escapeMermaid(str) {
|
|
294
|
-
return str
|
|
295
|
-
.replace(/</g, '<')
|
|
296
|
-
.replace(/>/g, '>')
|
|
297
|
-
.replace(/:/g, '#colon;')
|
|
298
|
-
.replace(/"/g, '#quot;');
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Convert sequenceData result to Mermaid sequenceDiagram syntax.
|
|
303
|
-
* @param {{ participants, messages, truncated }} seqResult
|
|
304
|
-
* @returns {string}
|
|
305
|
-
*/
|
|
306
|
-
export function sequenceToMermaid(seqResult) {
|
|
307
|
-
const lines = ['sequenceDiagram'];
|
|
308
|
-
|
|
309
|
-
for (const p of seqResult.participants) {
|
|
310
|
-
lines.push(` participant ${p.id} as ${escapeMermaid(p.label)}`);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
for (const msg of seqResult.messages) {
|
|
314
|
-
const arrow = msg.type === 'return' ? '-->>' : '->>';
|
|
315
|
-
lines.push(` ${msg.from}${arrow}${msg.to}: ${escapeMermaid(msg.label)}`);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
if (seqResult.truncated && seqResult.participants.length > 0) {
|
|
319
|
-
lines.push(
|
|
320
|
-
` note right of ${seqResult.participants[0].id}: Truncated at depth ${seqResult.depth}`,
|
|
321
|
-
);
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return lines.join('\n');
|
|
325
|
-
}
|
|
288
|
+
// Re-export Mermaid renderer from presentation layer
|
|
289
|
+
export { sequenceToMermaid } from './presentation/sequence-renderer.js';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { debug } from '../logger.js';
|
|
4
|
+
import { LANGUAGE_REGISTRY } from '../parser.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
|
|
8
|
+
* Returns null if the resolved path escapes repoRoot.
|
|
9
|
+
*/
|
|
10
|
+
export function safePath(repoRoot, file) {
|
|
11
|
+
const resolved = path.resolve(repoRoot, file);
|
|
12
|
+
if (!resolved.startsWith(repoRoot + path.sep) && resolved !== repoRoot) return null;
|
|
13
|
+
return resolved;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function readSourceRange(repoRoot, file, startLine, endLine) {
|
|
17
|
+
try {
|
|
18
|
+
const absPath = safePath(repoRoot, file);
|
|
19
|
+
if (!absPath) return null;
|
|
20
|
+
const content = fs.readFileSync(absPath, 'utf-8');
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
const start = Math.max(0, (startLine || 1) - 1);
|
|
23
|
+
const end = Math.min(lines.length, endLine || startLine + 50);
|
|
24
|
+
return lines.slice(start, end).join('\n');
|
|
25
|
+
} catch (e) {
|
|
26
|
+
debug(`readSourceRange failed for ${file}: ${e.message}`);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function extractSummary(fileLines, line) {
|
|
32
|
+
if (!fileLines || !line || line <= 1) return null;
|
|
33
|
+
const idx = line - 2; // line above the definition (0-indexed)
|
|
34
|
+
// Scan up to 10 lines above for JSDoc or comment
|
|
35
|
+
let jsdocEnd = -1;
|
|
36
|
+
for (let i = idx; i >= Math.max(0, idx - 10); i--) {
|
|
37
|
+
const trimmed = fileLines[i].trim();
|
|
38
|
+
if (trimmed.endsWith('*/')) {
|
|
39
|
+
jsdocEnd = i;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('#')) {
|
|
43
|
+
// Single-line comment immediately above
|
|
44
|
+
const text = trimmed
|
|
45
|
+
.replace(/^\/\/\s*/, '')
|
|
46
|
+
.replace(/^#\s*/, '')
|
|
47
|
+
.trim();
|
|
48
|
+
return text.length > 100 ? `${text.slice(0, 100)}...` : text;
|
|
49
|
+
}
|
|
50
|
+
if (trimmed !== '' && !trimmed.startsWith('*') && !trimmed.startsWith('/*')) break;
|
|
51
|
+
}
|
|
52
|
+
if (jsdocEnd >= 0) {
|
|
53
|
+
// Find opening /**
|
|
54
|
+
for (let i = jsdocEnd; i >= Math.max(0, jsdocEnd - 20); i--) {
|
|
55
|
+
if (fileLines[i].trim().startsWith('/**')) {
|
|
56
|
+
// Extract first non-tag, non-empty line
|
|
57
|
+
for (let j = i + 1; j <= jsdocEnd; j++) {
|
|
58
|
+
const docLine = fileLines[j]
|
|
59
|
+
.trim()
|
|
60
|
+
.replace(/^\*\s?/, '')
|
|
61
|
+
.trim();
|
|
62
|
+
if (docLine && !docLine.startsWith('@') && docLine !== '/' && docLine !== '*/') {
|
|
63
|
+
return docLine.length > 100 ? `${docLine.slice(0, 100)}...` : docLine;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function extractSignature(fileLines, line) {
|
|
74
|
+
if (!fileLines || !line) return null;
|
|
75
|
+
const idx = line - 1;
|
|
76
|
+
// Gather up to 5 lines to handle multi-line params
|
|
77
|
+
const chunk = fileLines.slice(idx, Math.min(fileLines.length, idx + 5)).join('\n');
|
|
78
|
+
|
|
79
|
+
// JS/TS: function name(params) or (params) => or async function
|
|
80
|
+
let m = chunk.match(
|
|
81
|
+
/(?:export\s+)?(?:async\s+)?function\s*\*?\s*\w*\s*\(([^)]*)\)\s*(?::\s*([^\n{]+))?/,
|
|
82
|
+
);
|
|
83
|
+
if (m) {
|
|
84
|
+
return {
|
|
85
|
+
params: m[1].trim() || null,
|
|
86
|
+
returnType: m[2] ? m[2].trim().replace(/\s*\{$/, '') : null,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
// Arrow: const name = (params) => or (params):ReturnType =>
|
|
90
|
+
m = chunk.match(/=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*([^=>\n{]+))?\s*=>/);
|
|
91
|
+
if (m) {
|
|
92
|
+
return {
|
|
93
|
+
params: m[1].trim() || null,
|
|
94
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
// Python: def name(params) -> return:
|
|
98
|
+
m = chunk.match(/def\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^:\n]+))?/);
|
|
99
|
+
if (m) {
|
|
100
|
+
return {
|
|
101
|
+
params: m[1].trim() || null,
|
|
102
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
// Go: func (recv) name(params) (returns)
|
|
106
|
+
m = chunk.match(/func\s+(?:\([^)]*\)\s+)?\w+\s*\(([^)]*)\)\s*(?:\(([^)]+)\)|(\w[^\n{]*))?/);
|
|
107
|
+
if (m) {
|
|
108
|
+
return {
|
|
109
|
+
params: m[1].trim() || null,
|
|
110
|
+
returnType: (m[2] || m[3] || '').trim() || null,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Rust: fn name(params) -> ReturnType
|
|
114
|
+
m = chunk.match(/fn\s+\w+\s*\(([^)]*)\)\s*(?:->\s*([^\n{]+))?/);
|
|
115
|
+
if (m) {
|
|
116
|
+
return {
|
|
117
|
+
params: m[1].trim() || null,
|
|
118
|
+
returnType: m[2] ? m[2].trim() : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function createFileLinesReader(repoRoot) {
|
|
125
|
+
const cache = new Map();
|
|
126
|
+
return function getFileLines(file) {
|
|
127
|
+
if (cache.has(file)) return cache.get(file);
|
|
128
|
+
try {
|
|
129
|
+
const absPath = safePath(repoRoot, file);
|
|
130
|
+
if (!absPath) {
|
|
131
|
+
cache.set(file, null);
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const lines = fs.readFileSync(absPath, 'utf-8').split('\n');
|
|
135
|
+
cache.set(file, lines);
|
|
136
|
+
return lines;
|
|
137
|
+
} catch (e) {
|
|
138
|
+
debug(`getFileLines failed for ${file}: ${e.message}`);
|
|
139
|
+
cache.set(file, null);
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function isFileLikeTarget(target) {
|
|
146
|
+
if (target.includes('/') || target.includes('\\')) return true;
|
|
147
|
+
const ext = path.extname(target).toLowerCase();
|
|
148
|
+
if (!ext) return false;
|
|
149
|
+
for (const entry of LANGUAGE_REGISTRY) {
|
|
150
|
+
if (entry.extensions.includes(ext)) return true;
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { iterateFunctionNodes, openReadonlyOrFail } from '../db.js';
|
|
2
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
3
|
+
import { ALL_SYMBOL_KINDS } from '../kinds.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generator: stream functions one-by-one using .iterate() for memory efficiency.
|
|
7
|
+
* @param {string} [customDbPath]
|
|
8
|
+
* @param {object} [opts]
|
|
9
|
+
* @param {boolean} [opts.noTests]
|
|
10
|
+
* @param {string} [opts.file]
|
|
11
|
+
* @param {string} [opts.pattern]
|
|
12
|
+
* @yields {{ name: string, kind: string, file: string, line: number, role: string|null }}
|
|
13
|
+
*/
|
|
14
|
+
export function* iterListFunctions(customDbPath, opts = {}) {
|
|
15
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
16
|
+
try {
|
|
17
|
+
const noTests = opts.noTests || false;
|
|
18
|
+
|
|
19
|
+
for (const row of iterateFunctionNodes(db, { file: opts.file, pattern: opts.pattern })) {
|
|
20
|
+
if (noTests && isTestFile(row.file)) continue;
|
|
21
|
+
yield {
|
|
22
|
+
name: row.name,
|
|
23
|
+
kind: row.kind,
|
|
24
|
+
file: row.file,
|
|
25
|
+
line: row.line,
|
|
26
|
+
endLine: row.end_line ?? null,
|
|
27
|
+
role: row.role ?? null,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
} finally {
|
|
31
|
+
db.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generator: stream role-classified symbols one-by-one.
|
|
37
|
+
* @param {string} [customDbPath]
|
|
38
|
+
* @param {object} [opts]
|
|
39
|
+
* @param {boolean} [opts.noTests]
|
|
40
|
+
* @param {string} [opts.role]
|
|
41
|
+
* @param {string} [opts.file]
|
|
42
|
+
* @yields {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string }}
|
|
43
|
+
*/
|
|
44
|
+
export function* iterRoles(customDbPath, opts = {}) {
|
|
45
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
46
|
+
try {
|
|
47
|
+
const noTests = opts.noTests || false;
|
|
48
|
+
const conditions = ['role IS NOT NULL'];
|
|
49
|
+
const params = [];
|
|
50
|
+
|
|
51
|
+
if (opts.role) {
|
|
52
|
+
conditions.push('role = ?');
|
|
53
|
+
params.push(opts.role);
|
|
54
|
+
}
|
|
55
|
+
if (opts.file) {
|
|
56
|
+
conditions.push('file LIKE ?');
|
|
57
|
+
params.push(`%${opts.file}%`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const stmt = db.prepare(
|
|
61
|
+
`SELECT name, kind, file, line, end_line, role FROM nodes WHERE ${conditions.join(' AND ')} ORDER BY role, file, line`,
|
|
62
|
+
);
|
|
63
|
+
for (const row of stmt.iterate(...params)) {
|
|
64
|
+
if (noTests && isTestFile(row.file)) continue;
|
|
65
|
+
yield {
|
|
66
|
+
name: row.name,
|
|
67
|
+
kind: row.kind,
|
|
68
|
+
file: row.file,
|
|
69
|
+
line: row.line,
|
|
70
|
+
endLine: row.end_line ?? null,
|
|
71
|
+
role: row.role ?? null,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
db.close();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generator: stream symbol lookup results one-by-one.
|
|
81
|
+
* @param {string} target - Symbol name to search for (partial match)
|
|
82
|
+
* @param {string} [customDbPath]
|
|
83
|
+
* @param {object} [opts]
|
|
84
|
+
* @param {boolean} [opts.noTests]
|
|
85
|
+
* @yields {{ name: string, kind: string, file: string, line: number, role: string|null, exported: boolean, uses: object[] }}
|
|
86
|
+
*/
|
|
87
|
+
export function* iterWhere(target, customDbPath, opts = {}) {
|
|
88
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
89
|
+
try {
|
|
90
|
+
const noTests = opts.noTests || false;
|
|
91
|
+
const placeholders = ALL_SYMBOL_KINDS.map(() => '?').join(', ');
|
|
92
|
+
const stmt = db.prepare(
|
|
93
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN (${placeholders}) ORDER BY file, line`,
|
|
94
|
+
);
|
|
95
|
+
const crossFileCallersStmt = db.prepare(
|
|
96
|
+
`SELECT COUNT(*) as cnt FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
97
|
+
WHERE e.target_id = ? AND e.kind = 'calls' AND n.file != ?`,
|
|
98
|
+
);
|
|
99
|
+
const usesStmt = db.prepare(
|
|
100
|
+
`SELECT n.name, n.file, n.line FROM edges e JOIN nodes n ON e.source_id = n.id
|
|
101
|
+
WHERE e.target_id = ? AND e.kind = 'calls'`,
|
|
102
|
+
);
|
|
103
|
+
for (const node of stmt.iterate(`%${target}%`, ...ALL_SYMBOL_KINDS)) {
|
|
104
|
+
if (noTests && isTestFile(node.file)) continue;
|
|
105
|
+
|
|
106
|
+
const crossFileCallers = crossFileCallersStmt.get(node.id, node.file);
|
|
107
|
+
const exported = crossFileCallers.cnt > 0;
|
|
108
|
+
|
|
109
|
+
let uses = usesStmt.all(node.id);
|
|
110
|
+
if (noTests) uses = uses.filter((u) => !isTestFile(u.file));
|
|
111
|
+
|
|
112
|
+
yield {
|
|
113
|
+
name: node.name,
|
|
114
|
+
kind: node.kind,
|
|
115
|
+
file: node.file,
|
|
116
|
+
line: node.line,
|
|
117
|
+
role: node.role || null,
|
|
118
|
+
exported,
|
|
119
|
+
uses: uses.map((u) => ({ name: u.name, file: u.file, line: u.line })),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
} finally {
|
|
123
|
+
db.close();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { getClassHierarchy } from '../db.js';
|
|
2
|
+
|
|
3
|
+
export function resolveMethodViaHierarchy(db, methodName) {
|
|
4
|
+
const methods = db
|
|
5
|
+
.prepare(`SELECT * FROM nodes WHERE kind = 'method' AND name LIKE ?`)
|
|
6
|
+
.all(`%.${methodName}`);
|
|
7
|
+
|
|
8
|
+
const results = [...methods];
|
|
9
|
+
for (const m of methods) {
|
|
10
|
+
const className = m.name.split('.')[0];
|
|
11
|
+
const classNode = db
|
|
12
|
+
.prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'class' AND file = ?`)
|
|
13
|
+
.get(className, m.file);
|
|
14
|
+
if (!classNode) continue;
|
|
15
|
+
|
|
16
|
+
const ancestors = getClassHierarchy(db, classNode.id);
|
|
17
|
+
for (const ancestorId of ancestors) {
|
|
18
|
+
const ancestor = db.prepare('SELECT name FROM nodes WHERE id = ?').get(ancestorId);
|
|
19
|
+
if (!ancestor) continue;
|
|
20
|
+
const parentMethods = db
|
|
21
|
+
.prepare(`SELECT * FROM nodes WHERE name = ? AND kind = 'method'`)
|
|
22
|
+
.all(`${ancestor.name}.${methodName}`);
|
|
23
|
+
results.push(...parentMethods);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return results;
|
|
27
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export function getFileHash(db, file) {
|
|
2
|
+
const row = db.prepare('SELECT hash FROM file_hashes WHERE file = ?').get(file);
|
|
3
|
+
return row ? row.hash : null;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function kindIcon(kind) {
|
|
7
|
+
switch (kind) {
|
|
8
|
+
case 'function':
|
|
9
|
+
return 'f';
|
|
10
|
+
case 'class':
|
|
11
|
+
return '*';
|
|
12
|
+
case 'method':
|
|
13
|
+
return 'o';
|
|
14
|
+
case 'file':
|
|
15
|
+
return '#';
|
|
16
|
+
case 'interface':
|
|
17
|
+
return 'I';
|
|
18
|
+
case 'type':
|
|
19
|
+
return 'T';
|
|
20
|
+
case 'parameter':
|
|
21
|
+
return 'p';
|
|
22
|
+
case 'property':
|
|
23
|
+
return '.';
|
|
24
|
+
case 'constant':
|
|
25
|
+
return 'C';
|
|
26
|
+
default:
|
|
27
|
+
return '-';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a raw DB/query row into the stable 7-field symbol shape.
|
|
33
|
+
* @param {object} row - Raw row (from SELECT * or explicit columns)
|
|
34
|
+
* @param {object} [db] - Open DB handle; when null, fileHash will be null
|
|
35
|
+
* @param {Map} [hashCache] - Optional per-file cache to avoid repeated getFileHash calls
|
|
36
|
+
* @returns {{ name: string, kind: string, file: string, line: number, endLine: number|null, role: string|null, fileHash: string|null }}
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeSymbol(row, db, hashCache) {
|
|
39
|
+
let fileHash = null;
|
|
40
|
+
if (db) {
|
|
41
|
+
if (hashCache) {
|
|
42
|
+
if (!hashCache.has(row.file)) {
|
|
43
|
+
hashCache.set(row.file, getFileHash(db, row.file));
|
|
44
|
+
}
|
|
45
|
+
fileHash = hashCache.get(row.file);
|
|
46
|
+
} else {
|
|
47
|
+
fileHash = getFileHash(db, row.file);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
name: row.name,
|
|
52
|
+
kind: row.kind,
|
|
53
|
+
file: row.file,
|
|
54
|
+
line: row.line,
|
|
55
|
+
endLine: row.end_line ?? row.endLine ?? null,
|
|
56
|
+
role: row.role ?? null,
|
|
57
|
+
fileHash,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/snapshot.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import Database from 'better-sqlite3';
|
|
4
4
|
import { findDbPath } from './db.js';
|
|
5
|
+
import { ConfigError, DbError } from './errors.js';
|
|
5
6
|
import { debug } from './logger.js';
|
|
6
7
|
|
|
7
8
|
const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
@@ -12,7 +13,7 @@ const NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
|
12
13
|
*/
|
|
13
14
|
export function validateSnapshotName(name) {
|
|
14
15
|
if (!name || !NAME_RE.test(name)) {
|
|
15
|
-
throw new
|
|
16
|
+
throw new ConfigError(
|
|
16
17
|
`Invalid snapshot name "${name}". Use only letters, digits, hyphens, and underscores.`,
|
|
17
18
|
);
|
|
18
19
|
}
|
|
@@ -39,7 +40,7 @@ export function snapshotSave(name, options = {}) {
|
|
|
39
40
|
validateSnapshotName(name);
|
|
40
41
|
const dbPath = options.dbPath || findDbPath();
|
|
41
42
|
if (!fs.existsSync(dbPath)) {
|
|
42
|
-
throw new
|
|
43
|
+
throw new DbError(`Database not found: ${dbPath}`, { file: dbPath });
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
const dir = snapshotsDir(dbPath);
|
|
@@ -47,7 +48,7 @@ export function snapshotSave(name, options = {}) {
|
|
|
47
48
|
|
|
48
49
|
if (fs.existsSync(dest)) {
|
|
49
50
|
if (!options.force) {
|
|
50
|
-
throw new
|
|
51
|
+
throw new ConfigError(`Snapshot "${name}" already exists. Use --force to overwrite.`);
|
|
51
52
|
}
|
|
52
53
|
fs.unlinkSync(dest);
|
|
53
54
|
debug(`Deleted existing snapshot: ${dest}`);
|
|
@@ -82,7 +83,7 @@ export function snapshotRestore(name, options = {}) {
|
|
|
82
83
|
const src = path.join(dir, `${name}.db`);
|
|
83
84
|
|
|
84
85
|
if (!fs.existsSync(src)) {
|
|
85
|
-
throw new
|
|
86
|
+
throw new DbError(`Snapshot "${name}" not found at ${src}`, { file: src });
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
// Remove WAL/SHM sidecar files for a clean restore
|
|
@@ -141,7 +142,7 @@ export function snapshotDelete(name, options = {}) {
|
|
|
141
142
|
const target = path.join(dir, `${name}.db`);
|
|
142
143
|
|
|
143
144
|
if (!fs.existsSync(target)) {
|
|
144
|
-
throw new
|
|
145
|
+
throw new DbError(`Snapshot "${name}" not found at ${target}`, { file: target });
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
fs.unlinkSync(target);
|
package/src/structure.js
CHANGED
|
@@ -312,13 +312,10 @@ export function buildStructure(db, fileSymbols, _rootDir, lineCountMap, director
|
|
|
312
312
|
|
|
313
313
|
// ─── Node role classification ─────────────────────────────────────────
|
|
314
314
|
|
|
315
|
-
export
|
|
315
|
+
// Re-export from classifier for backward compatibility
|
|
316
|
+
export { FRAMEWORK_ENTRY_PREFIXES } from './graph/classifiers/roles.js';
|
|
316
317
|
|
|
317
|
-
|
|
318
|
-
if (sorted.length === 0) return 0;
|
|
319
|
-
const mid = Math.floor(sorted.length / 2);
|
|
320
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
321
|
-
}
|
|
318
|
+
import { classifyRoles } from './graph/classifiers/roles.js';
|
|
322
319
|
|
|
323
320
|
export function classifyNodeRoles(db) {
|
|
324
321
|
const rows = db
|
|
@@ -354,44 +351,22 @@ export function classifyNodeRoles(db) {
|
|
|
354
351
|
.map((r) => r.target_id),
|
|
355
352
|
);
|
|
356
353
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
.
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
.
|
|
364
|
-
|
|
354
|
+
// Delegate classification to the pure-logic classifier
|
|
355
|
+
const classifierInput = rows.map((r) => ({
|
|
356
|
+
id: String(r.id),
|
|
357
|
+
name: r.name,
|
|
358
|
+
fanIn: r.fan_in,
|
|
359
|
+
fanOut: r.fan_out,
|
|
360
|
+
isExported: exportedIds.has(r.id),
|
|
361
|
+
}));
|
|
365
362
|
|
|
366
|
-
const
|
|
367
|
-
const medFanOut = median(nonZeroFanOut);
|
|
363
|
+
const roleMap = classifyRoles(classifierInput);
|
|
368
364
|
|
|
369
|
-
|
|
365
|
+
// Build summary and updates
|
|
370
366
|
const summary = { entry: 0, core: 0, utility: 0, adapter: 0, dead: 0, leaf: 0 };
|
|
371
|
-
|
|
367
|
+
const updates = [];
|
|
372
368
|
for (const row of rows) {
|
|
373
|
-
const
|
|
374
|
-
const highOut = row.fan_out >= medFanOut && row.fan_out > 0;
|
|
375
|
-
const isExported = exportedIds.has(row.id);
|
|
376
|
-
|
|
377
|
-
let role;
|
|
378
|
-
const isFrameworkEntry = FRAMEWORK_ENTRY_PREFIXES.some((p) => row.name.startsWith(p));
|
|
379
|
-
if (isFrameworkEntry) {
|
|
380
|
-
role = 'entry';
|
|
381
|
-
} else if (row.fan_in === 0 && !isExported) {
|
|
382
|
-
role = 'dead';
|
|
383
|
-
} else if (row.fan_in === 0 && isExported) {
|
|
384
|
-
role = 'entry';
|
|
385
|
-
} else if (highIn && !highOut) {
|
|
386
|
-
role = 'core';
|
|
387
|
-
} else if (highIn && highOut) {
|
|
388
|
-
role = 'utility';
|
|
389
|
-
} else if (!highIn && highOut) {
|
|
390
|
-
role = 'adapter';
|
|
391
|
-
} else {
|
|
392
|
-
role = 'leaf';
|
|
393
|
-
}
|
|
394
|
-
|
|
369
|
+
const role = roleMap.get(String(row.id)) || 'leaf';
|
|
395
370
|
updates.push({ id: row.id, role });
|
|
396
371
|
summary[role]++;
|
|
397
372
|
}
|
package/src/triage.js
CHANGED
|
@@ -1,40 +1,9 @@
|
|
|
1
1
|
import { findNodesForTriage, openReadonlyOrFail } from './db.js';
|
|
2
|
+
import { DEFAULT_WEIGHTS, scoreRisk } from './graph/classifiers/risk.js';
|
|
2
3
|
import { isTestFile } from './infrastructure/test-filter.js';
|
|
3
4
|
import { warn } from './logger.js';
|
|
4
5
|
import { paginateResult } from './paginate.js';
|
|
5
6
|
|
|
6
|
-
// ─── Constants ────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
const DEFAULT_WEIGHTS = {
|
|
9
|
-
fanIn: 0.25,
|
|
10
|
-
complexity: 0.3,
|
|
11
|
-
churn: 0.2,
|
|
12
|
-
role: 0.15,
|
|
13
|
-
mi: 0.1,
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const ROLE_WEIGHTS = {
|
|
17
|
-
core: 1.0,
|
|
18
|
-
utility: 0.9,
|
|
19
|
-
entry: 0.8,
|
|
20
|
-
adapter: 0.5,
|
|
21
|
-
leaf: 0.2,
|
|
22
|
-
dead: 0.1,
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const DEFAULT_ROLE_WEIGHT = 0.5;
|
|
26
|
-
|
|
27
|
-
// ─── Helpers ──────────────────────────────────────────────────────────
|
|
28
|
-
|
|
29
|
-
/** Min-max normalize an array of numbers. All-equal → all zeros. */
|
|
30
|
-
function minMaxNormalize(values) {
|
|
31
|
-
const min = Math.min(...values);
|
|
32
|
-
const max = Math.max(...values);
|
|
33
|
-
if (max === min) return values.map(() => 0);
|
|
34
|
-
const range = max - min;
|
|
35
|
-
return values.map((v) => (v - min) / range);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
7
|
// ─── Data Function ────────────────────────────────────────────────────
|
|
39
8
|
|
|
40
9
|
/**
|
|
@@ -81,48 +50,27 @@ export function triageData(customDbPath, opts = {}) {
|
|
|
81
50
|
};
|
|
82
51
|
}
|
|
83
52
|
|
|
84
|
-
//
|
|
85
|
-
const
|
|
86
|
-
const cognitives = filtered.map((r) => r.cognitive);
|
|
87
|
-
const churns = filtered.map((r) => r.churn);
|
|
88
|
-
const mis = filtered.map((r) => r.mi);
|
|
89
|
-
|
|
90
|
-
// Min-max normalize
|
|
91
|
-
const normFanIns = minMaxNormalize(fanIns);
|
|
92
|
-
const normCognitives = minMaxNormalize(cognitives);
|
|
93
|
-
const normChurns = minMaxNormalize(churns);
|
|
94
|
-
// MI: higher is better, so invert: 1 - norm(mi)
|
|
95
|
-
const normMIsRaw = minMaxNormalize(mis);
|
|
96
|
-
const normMIs = normMIsRaw.map((v) => round4(1 - v));
|
|
53
|
+
// Delegate scoring to classifier
|
|
54
|
+
const riskMetrics = scoreRisk(filtered, weights);
|
|
97
55
|
|
|
98
56
|
// Compute risk scores
|
|
99
|
-
const items = filtered.map((r, i) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
churn: r.churn,
|
|
117
|
-
maintainabilityIndex: r.mi,
|
|
118
|
-
normFanIn: round4(normFanIns[i]),
|
|
119
|
-
normComplexity: round4(normCognitives[i]),
|
|
120
|
-
normChurn: round4(normChurns[i]),
|
|
121
|
-
normMI: round4(normMIs[i]),
|
|
122
|
-
roleWeight,
|
|
123
|
-
riskScore: round4(riskScore),
|
|
124
|
-
};
|
|
125
|
-
});
|
|
57
|
+
const items = filtered.map((r, i) => ({
|
|
58
|
+
name: r.name,
|
|
59
|
+
kind: r.kind,
|
|
60
|
+
file: r.file,
|
|
61
|
+
line: r.line,
|
|
62
|
+
role: r.role || null,
|
|
63
|
+
fanIn: r.fan_in,
|
|
64
|
+
cognitive: r.cognitive,
|
|
65
|
+
churn: r.churn,
|
|
66
|
+
maintainabilityIndex: r.mi,
|
|
67
|
+
normFanIn: riskMetrics[i].normFanIn,
|
|
68
|
+
normComplexity: riskMetrics[i].normComplexity,
|
|
69
|
+
normChurn: riskMetrics[i].normChurn,
|
|
70
|
+
normMI: riskMetrics[i].normMI,
|
|
71
|
+
roleWeight: riskMetrics[i].roleWeight,
|
|
72
|
+
riskScore: riskMetrics[i].riskScore,
|
|
73
|
+
}));
|
|
126
74
|
|
|
127
75
|
// Apply minScore filter
|
|
128
76
|
const scored = minScore != null ? items.filter((it) => it.riskScore >= minScore) : items;
|