@optave/codegraph 3.1.3 → 3.1.5
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 +38 -84
- package/package.json +13 -8
- package/src/ast-analysis/engine.js +32 -12
- package/src/ast-analysis/shared.js +6 -5
- package/src/cli/commands/ast.js +22 -0
- package/src/cli/commands/audit.js +45 -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 +26 -0
- package/src/cli/commands/check.js +74 -0
- package/src/cli/commands/children.js +28 -0
- package/src/cli/commands/co-change.js +67 -0
- package/src/cli/commands/communities.js +19 -0
- package/src/cli/commands/complexity.js +46 -0
- package/src/cli/commands/context.js +30 -0
- package/src/cli/commands/cycles.js +32 -0
- package/src/cli/commands/dataflow.js +28 -0
- package/src/cli/commands/deps.js +12 -0
- package/src/cli/commands/diff-impact.js +26 -0
- package/src/cli/commands/embed.js +30 -0
- package/src/cli/commands/export.js +78 -0
- package/src/cli/commands/exports.js +14 -0
- package/src/cli/commands/flow.js +32 -0
- package/src/cli/commands/fn-impact.js +26 -0
- package/src/cli/commands/impact.js +12 -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 +89 -0
- package/src/cli/commands/query.js +45 -0
- package/src/cli/commands/registry.js +100 -0
- package/src/cli/commands/roles.js +30 -0
- package/src/cli/commands/search.js +42 -0
- package/src/cli/commands/sequence.js +28 -0
- package/src/cli/commands/snapshot.js +66 -0
- package/src/cli/commands/stats.js +15 -0
- package/src/cli/commands/structure.js +33 -0
- package/src/cli/commands/triage.js +78 -0
- package/src/cli/commands/watch.js +12 -0
- package/src/cli/commands/where.js +20 -0
- package/src/cli/index.js +124 -0
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +59 -0
- package/src/cli/shared/output.js +1 -0
- package/src/cli.js +11 -1522
- package/src/db/connection.js +130 -7
- package/src/{db.js → db/index.js} +17 -5
- package/src/db/migrations.js +42 -1
- package/src/db/query-builder.js +20 -12
- package/src/db/repository/base.js +201 -0
- package/src/db/repository/graph-read.js +7 -4
- package/src/db/repository/in-memory-repository.js +575 -0
- package/src/db/repository/index.js +5 -1
- package/src/db/repository/nodes.js +60 -6
- package/src/db/repository/sqlite-repository.js +219 -0
- package/src/domain/analysis/context.js +408 -0
- package/src/domain/analysis/dependencies.js +341 -0
- package/src/domain/analysis/exports.js +134 -0
- package/src/domain/analysis/impact.js +466 -0
- package/src/domain/analysis/module-map.js +322 -0
- package/src/domain/analysis/roles.js +45 -0
- package/src/domain/analysis/symbol-lookup.js +238 -0
- package/src/domain/graph/builder/context.js +85 -0
- package/src/domain/graph/builder/helpers.js +218 -0
- package/src/domain/graph/builder/incremental.js +178 -0
- package/src/domain/graph/builder/pipeline.js +130 -0
- package/src/domain/graph/builder/stages/build-edges.js +297 -0
- package/src/domain/graph/builder/stages/build-structure.js +113 -0
- package/src/domain/graph/builder/stages/collect-files.js +44 -0
- package/src/domain/graph/builder/stages/detect-changes.js +413 -0
- package/src/domain/graph/builder/stages/finalize.js +139 -0
- package/src/domain/graph/builder/stages/insert-nodes.js +195 -0
- package/src/domain/graph/builder/stages/parse-files.js +28 -0
- package/src/domain/graph/builder/stages/resolve-imports.js +143 -0
- package/src/domain/graph/builder/stages/run-analyses.js +44 -0
- package/src/domain/graph/builder.js +11 -0
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/domain/graph/cycles.js +82 -0
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +3 -3
- package/src/{watcher.js → domain/graph/watcher.js} +10 -150
- package/src/{parser.js → domain/parser.js} +5 -5
- package/src/domain/queries.js +48 -0
- package/src/domain/search/generator.js +163 -0
- package/src/domain/search/index.js +13 -0
- package/src/domain/search/models.js +218 -0
- package/src/domain/search/search/cli-formatter.js +151 -0
- package/src/domain/search/search/filters.js +46 -0
- package/src/domain/search/search/hybrid.js +121 -0
- package/src/domain/search/search/keyword.js +68 -0
- package/src/domain/search/search/prepare.js +66 -0
- package/src/domain/search/search/semantic.js +145 -0
- package/src/domain/search/stores/fts5.js +27 -0
- package/src/domain/search/stores/sqlite-blob.js +24 -0
- package/src/domain/search/strategies/source.js +14 -0
- package/src/domain/search/strategies/structured.js +43 -0
- package/src/domain/search/strategies/text-utils.js +43 -0
- 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 +39 -2
- package/src/extractors/php.js +3 -1
- package/src/extractors/python.js +14 -3
- package/src/extractors/rust.js +3 -1
- package/src/{ast.js → features/ast.js} +8 -8
- package/src/{audit.js → features/audit.js} +16 -44
- package/src/{batch.js → features/batch.js} +6 -5
- package/src/{boundaries.js → features/boundaries.js} +2 -2
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +11 -12
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +18 -90
- package/src/{complexity.js → features/complexity.js} +13 -13
- package/src/{dataflow.js → features/dataflow.js} +12 -13
- package/src/features/export.js +378 -0
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/features/graph-enrichment.js +327 -0
- package/src/{manifesto.js → features/manifesto.js} +6 -6
- package/src/{owners.js → features/owners.js} +2 -2
- package/src/{sequence.js → features/sequence.js} +16 -52
- package/src/{snapshot.js → features/snapshot.js} +8 -7
- package/src/{structure.js → features/structure.js} +20 -45
- package/src/{triage.js → features/triage.js} +27 -79
- 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 +110 -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.cjs +16 -0
- package/src/index.js +42 -219
- package/src/{native.js → infrastructure/native.js} +3 -1
- 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.js → mcp/tool-registry.js} +6 -675
- 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 +12 -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/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +6 -6
- package/src/presentation/colors.js +44 -0
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/presentation/export.js +444 -0
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +4 -4
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +46 -0
- package/src/presentation/queries-cli/impact.js +198 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +334 -0
- package/src/presentation/queries-cli/overview.js +197 -0
- package/src/presentation/queries-cli/path.js +58 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +144 -0
- package/src/presentation/sequence-renderer.js +43 -0
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +2 -2
- package/src/presentation/table.js +47 -0
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{viewer.js → presentation/viewer.js} +68 -382
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/errors.js +78 -0
- 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/builder.js +0 -1486
- package/src/cycles.js +0 -137
- package/src/embedder.js +0 -1097
- package/src/export.js +0 -681
- package/src/queries-cli.js +0 -866
- package/src/queries.js +0 -2289
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{kinds.js → shared/kinds.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { effectiveOffset, MCP_DEFAULTS } from '../middleware.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'sequence';
|
|
4
|
+
|
|
5
|
+
export async function handler(args, ctx) {
|
|
6
|
+
const { sequenceData, sequenceToMermaid } = await import('../../features/sequence.js');
|
|
7
|
+
const seqResult = sequenceData(args.name, ctx.dbPath, {
|
|
8
|
+
depth: args.depth,
|
|
9
|
+
file: args.file,
|
|
10
|
+
kind: args.kind,
|
|
11
|
+
dataflow: args.dataflow,
|
|
12
|
+
noTests: args.no_tests,
|
|
13
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.execution_flow, ctx.MCP_MAX_LIMIT),
|
|
14
|
+
offset: effectiveOffset(args),
|
|
15
|
+
});
|
|
16
|
+
return args.format === 'json' ? seqResult : { text: sequenceToMermaid(seqResult), ...seqResult };
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { effectiveLimit, effectiveOffset } from '../middleware.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'structure';
|
|
4
|
+
|
|
5
|
+
export async function handler(args, ctx) {
|
|
6
|
+
const { structureData } = await import('../../features/structure.js');
|
|
7
|
+
return structureData(ctx.dbPath, {
|
|
8
|
+
directory: args.directory,
|
|
9
|
+
depth: args.depth,
|
|
10
|
+
sort: args.sort,
|
|
11
|
+
full: args.full,
|
|
12
|
+
limit: effectiveLimit(args, name),
|
|
13
|
+
offset: effectiveOffset(args),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { effectiveOffset, MCP_DEFAULTS } from '../middleware.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'symbol_children';
|
|
4
|
+
|
|
5
|
+
export async function handler(args, ctx) {
|
|
6
|
+
const { childrenData } = await ctx.getQueries();
|
|
7
|
+
return childrenData(args.name, ctx.dbPath, {
|
|
8
|
+
file: args.file,
|
|
9
|
+
kind: args.kind,
|
|
10
|
+
noTests: args.no_tests,
|
|
11
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.context, ctx.MCP_MAX_LIMIT),
|
|
12
|
+
offset: effectiveOffset(args),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { effectiveLimit, effectiveOffset, MCP_DEFAULTS, MCP_MAX_LIMIT } from '../middleware.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'triage';
|
|
4
|
+
|
|
5
|
+
export async function handler(args, ctx) {
|
|
6
|
+
if (args.level === 'file' || args.level === 'directory') {
|
|
7
|
+
const { hotspotsData } = await import('../../features/structure.js');
|
|
8
|
+
const TRIAGE_TO_HOTSPOT = {
|
|
9
|
+
risk: 'fan-in',
|
|
10
|
+
complexity: 'density',
|
|
11
|
+
churn: 'coupling',
|
|
12
|
+
mi: 'fan-in',
|
|
13
|
+
};
|
|
14
|
+
const metric = TRIAGE_TO_HOTSPOT[args.sort] ?? args.sort;
|
|
15
|
+
return hotspotsData(ctx.dbPath, {
|
|
16
|
+
metric,
|
|
17
|
+
level: args.level,
|
|
18
|
+
limit: Math.min(args.limit ?? MCP_DEFAULTS.hotspots, MCP_MAX_LIMIT),
|
|
19
|
+
offset: effectiveOffset(args),
|
|
20
|
+
noTests: args.no_tests,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
const { triageData } = await import('../../features/triage.js');
|
|
24
|
+
return triageData(ctx.dbPath, {
|
|
25
|
+
sort: args.sort,
|
|
26
|
+
minScore: args.min_score,
|
|
27
|
+
role: args.role,
|
|
28
|
+
file: args.file,
|
|
29
|
+
kind: args.kind,
|
|
30
|
+
noTests: args.no_tests,
|
|
31
|
+
weights: args.weights,
|
|
32
|
+
limit: effectiveLimit(args, name),
|
|
33
|
+
offset: effectiveOffset(args),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { effectiveLimit, effectiveOffset } from '../middleware.js';
|
|
2
|
+
|
|
3
|
+
export const name = 'where';
|
|
4
|
+
|
|
5
|
+
export async function handler(args, ctx) {
|
|
6
|
+
const { whereData } = await ctx.getQueries();
|
|
7
|
+
return whereData(args.target, ctx.dbPath, {
|
|
8
|
+
file: args.file_mode,
|
|
9
|
+
noTests: args.no_tests,
|
|
10
|
+
limit: effectiveLimit(args, name),
|
|
11
|
+
offset: effectiveOffset(args),
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kindIcon } from '../domain/queries.js';
|
|
2
|
+
import { auditData } from '../features/audit.js';
|
|
2
3
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
|
-
import { kindIcon } from '../queries.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* CLI formatter for the audit command.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { kindIcon } from '../domain/queries.js';
|
|
2
|
+
import { branchCompareData, branchCompareMermaid } from '../features/branch-compare.js';
|
|
2
3
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
|
-
import { kindIcon } from '../queries.js';
|
|
4
4
|
|
|
5
5
|
// ─── Text Formatting ────────────────────────────────────────────────────
|
|
6
6
|
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import { checkData } from '../check.js';
|
|
1
|
+
import { checkData } from '../features/check.js';
|
|
2
2
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
3
|
+
import { AnalysisError } from '../shared/errors.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* CLI formatter — prints check results and
|
|
6
|
+
* CLI formatter — prints check results and sets exitCode 1 on failure.
|
|
6
7
|
*/
|
|
7
8
|
export function check(customDbPath, opts = {}) {
|
|
8
9
|
const data = checkData(customDbPath, opts);
|
|
9
10
|
|
|
10
11
|
if (data.error) {
|
|
11
|
-
|
|
12
|
-
process.exit(1);
|
|
12
|
+
throw new AnalysisError(data.error);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
if (outputResult(data, null, opts)) {
|
|
16
|
-
if (!data.passed) process.
|
|
16
|
+
if (!data.passed) process.exitCode = 1;
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -77,6 +77,6 @@ export function check(customDbPath, opts = {}) {
|
|
|
77
77
|
console.log(`\n ${s.total} predicates | ${s.passed} passed | ${s.failed} failed\n`);
|
|
78
78
|
|
|
79
79
|
if (!data.passed) {
|
|
80
|
-
process.
|
|
80
|
+
process.exitCode = 1;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared color constants for the graph viewer.
|
|
3
|
+
*
|
|
4
|
+
* These live in a standalone module so both the domain layer (src/viewer.js)
|
|
5
|
+
* and the presentation layer (src/presentation/viewer.js) can import them
|
|
6
|
+
* without creating a cross-layer dependency.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_NODE_COLORS = {
|
|
10
|
+
function: '#4CAF50',
|
|
11
|
+
method: '#66BB6A',
|
|
12
|
+
class: '#2196F3',
|
|
13
|
+
interface: '#42A5F5',
|
|
14
|
+
type: '#7E57C2',
|
|
15
|
+
struct: '#FF7043',
|
|
16
|
+
enum: '#FFA726',
|
|
17
|
+
trait: '#26A69A',
|
|
18
|
+
record: '#EC407A',
|
|
19
|
+
module: '#78909C',
|
|
20
|
+
file: '#90A4AE',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_ROLE_COLORS = {
|
|
24
|
+
entry: '#e8f5e9',
|
|
25
|
+
core: '#e3f2fd',
|
|
26
|
+
utility: '#f5f5f5',
|
|
27
|
+
dead: '#ffebee',
|
|
28
|
+
leaf: '#fffde7',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const COMMUNITY_COLORS = [
|
|
32
|
+
'#4CAF50',
|
|
33
|
+
'#2196F3',
|
|
34
|
+
'#FF9800',
|
|
35
|
+
'#9C27B0',
|
|
36
|
+
'#F44336',
|
|
37
|
+
'#00BCD4',
|
|
38
|
+
'#CDDC39',
|
|
39
|
+
'#E91E63',
|
|
40
|
+
'#3F51B5',
|
|
41
|
+
'#FF5722',
|
|
42
|
+
'#009688',
|
|
43
|
+
'#795548',
|
|
44
|
+
];
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Graph export serializers — pure data → formatted string transforms.
|
|
3
|
+
*
|
|
4
|
+
* Each function receives pre-loaded graph data and returns a formatted string
|
|
5
|
+
* (or structured object for CSV). No DB access — all data must be pre-loaded.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
|
|
10
|
+
// ─── Escape Helpers ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
/** Escape special XML characters. */
|
|
13
|
+
export function escapeXml(s) {
|
|
14
|
+
return String(s)
|
|
15
|
+
.replace(/&/g, '&')
|
|
16
|
+
.replace(/</g, '<')
|
|
17
|
+
.replace(/>/g, '>')
|
|
18
|
+
.replace(/"/g, '"')
|
|
19
|
+
.replace(/'/g, ''');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** RFC 4180 CSV field escaping — quote fields containing commas, quotes, or newlines. */
|
|
23
|
+
export function escapeCsv(s) {
|
|
24
|
+
const str = String(s);
|
|
25
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
26
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
27
|
+
}
|
|
28
|
+
return str;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Escape double quotes for Mermaid labels. */
|
|
32
|
+
export function escapeLabel(label) {
|
|
33
|
+
return label.replace(/"/g, '#quot;');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Map node kind to Mermaid shape wrapper. */
|
|
37
|
+
export function mermaidShape(kind, label) {
|
|
38
|
+
const escaped = escapeLabel(label);
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case 'function':
|
|
41
|
+
case 'method':
|
|
42
|
+
return `(["${escaped}"])`;
|
|
43
|
+
case 'class':
|
|
44
|
+
case 'interface':
|
|
45
|
+
case 'type':
|
|
46
|
+
case 'struct':
|
|
47
|
+
case 'enum':
|
|
48
|
+
case 'trait':
|
|
49
|
+
case 'record':
|
|
50
|
+
return `{{"${escaped}"}}`;
|
|
51
|
+
case 'module':
|
|
52
|
+
return `[["${escaped}"]]`;
|
|
53
|
+
default:
|
|
54
|
+
return `["${escaped}"]`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Map node role to Mermaid style colors. */
|
|
59
|
+
export const ROLE_STYLES = {
|
|
60
|
+
entry: 'fill:#e8f5e9,stroke:#4caf50',
|
|
61
|
+
core: 'fill:#e3f2fd,stroke:#2196f3',
|
|
62
|
+
utility: 'fill:#f5f5f5,stroke:#9e9e9e',
|
|
63
|
+
dead: 'fill:#ffebee,stroke:#f44336',
|
|
64
|
+
leaf: 'fill:#fffde7,stroke:#fdd835',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ─── DOT Serializer ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Render file-level graph data as DOT (Graphviz) format.
|
|
71
|
+
*
|
|
72
|
+
* @param {{ dirs: Array<{ name: string, files: Array<{ path: string, basename: string }>, cohesion: number|null }>, edges: Array<{ source: string, target: string }>, totalEdges: number, limit?: number }} data
|
|
73
|
+
* @returns {string}
|
|
74
|
+
*/
|
|
75
|
+
export function renderFileLevelDOT(data) {
|
|
76
|
+
const lines = [
|
|
77
|
+
'digraph codegraph {',
|
|
78
|
+
' rankdir=LR;',
|
|
79
|
+
' node [shape=box, fontname="monospace", fontsize=10];',
|
|
80
|
+
' edge [color="#666666"];',
|
|
81
|
+
'',
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
let clusterIdx = 0;
|
|
85
|
+
for (const dir of data.dirs) {
|
|
86
|
+
lines.push(` subgraph cluster_${clusterIdx++} {`);
|
|
87
|
+
const cohLabel = dir.cohesion !== null ? ` (cohesion: ${dir.cohesion.toFixed(2)})` : '';
|
|
88
|
+
lines.push(` label="${dir.name}${cohLabel}";`);
|
|
89
|
+
lines.push(` style=dashed;`);
|
|
90
|
+
lines.push(` color="#999999";`);
|
|
91
|
+
for (const f of dir.files) {
|
|
92
|
+
lines.push(` "${f.path}" [label="${f.basename}"];`);
|
|
93
|
+
}
|
|
94
|
+
lines.push(` }`);
|
|
95
|
+
lines.push('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const { source, target } of data.edges) {
|
|
99
|
+
lines.push(` "${source}" -> "${target}";`);
|
|
100
|
+
}
|
|
101
|
+
if (data.limit && data.totalEdges > data.limit) {
|
|
102
|
+
lines.push(` // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
lines.push('}');
|
|
106
|
+
return lines.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Render function-level graph data as DOT (Graphviz) format.
|
|
111
|
+
*
|
|
112
|
+
* @param {{ edges: Array<{ source_name: string, source_file: string, target_name: string, target_file: string }>, totalEdges: number, limit?: number }} data
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
export function renderFunctionLevelDOT(data) {
|
|
116
|
+
const lines = [
|
|
117
|
+
'digraph codegraph {',
|
|
118
|
+
' rankdir=LR;',
|
|
119
|
+
' node [shape=box, fontname="monospace", fontsize=10];',
|
|
120
|
+
' edge [color="#666666"];',
|
|
121
|
+
'',
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const emittedNodes = new Set();
|
|
125
|
+
for (const e of data.edges) {
|
|
126
|
+
const sId = `${e.source_file}:${e.source_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
127
|
+
const tId = `${e.target_file}:${e.target_name}`.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
128
|
+
if (!emittedNodes.has(sId)) {
|
|
129
|
+
lines.push(` ${sId} [label="${e.source_name}\\n${path.basename(e.source_file)}"];`);
|
|
130
|
+
emittedNodes.add(sId);
|
|
131
|
+
}
|
|
132
|
+
if (!emittedNodes.has(tId)) {
|
|
133
|
+
lines.push(` ${tId} [label="${e.target_name}\\n${path.basename(e.target_file)}"];`);
|
|
134
|
+
emittedNodes.add(tId);
|
|
135
|
+
}
|
|
136
|
+
lines.push(` ${sId} -> ${tId};`);
|
|
137
|
+
}
|
|
138
|
+
if (data.limit && data.totalEdges > data.limit) {
|
|
139
|
+
lines.push(` // Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
lines.push('}');
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Mermaid Serializer ──────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Render file-level graph data as Mermaid flowchart format.
|
|
150
|
+
*
|
|
151
|
+
* @param {{ direction: string, dirs: Array<{ name: string, files: string[] }>, edges: Array<{ source: string, target: string, edge_kind: string }>, totalEdges: number, limit?: number }} data
|
|
152
|
+
* @returns {string}
|
|
153
|
+
*/
|
|
154
|
+
export function renderFileLevelMermaid(data) {
|
|
155
|
+
const lines = [`flowchart ${data.direction || 'LR'}`];
|
|
156
|
+
|
|
157
|
+
let nodeCounter = 0;
|
|
158
|
+
const nodeIdMap = new Map();
|
|
159
|
+
function nodeId(key) {
|
|
160
|
+
if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
161
|
+
return nodeIdMap.get(key);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Emit subgraphs
|
|
165
|
+
for (const dir of data.dirs) {
|
|
166
|
+
const sgId = dir.name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
167
|
+
lines.push(` subgraph ${sgId}["${escapeLabel(dir.name)}"]`);
|
|
168
|
+
for (const f of dir.files) {
|
|
169
|
+
const nId = nodeId(f);
|
|
170
|
+
lines.push(` ${nId}["${escapeLabel(path.basename(f))}"]`);
|
|
171
|
+
}
|
|
172
|
+
lines.push(' end');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Deduplicate edges per source-target pair, collecting all distinct kinds
|
|
176
|
+
const edgeMap = new Map();
|
|
177
|
+
for (const { source, target, edge_kind } of data.edges) {
|
|
178
|
+
const key = `${source}|${target}`;
|
|
179
|
+
const label = edge_kind === 'imports-type' ? 'imports' : edge_kind;
|
|
180
|
+
if (!edgeMap.has(key)) edgeMap.set(key, { source, target, labels: new Set() });
|
|
181
|
+
edgeMap.get(key).labels.add(label);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
for (const { source, target, labels } of edgeMap.values()) {
|
|
185
|
+
lines.push(` ${nodeId(source)} -->|${[...labels].join(', ')}| ${nodeId(target)}`);
|
|
186
|
+
}
|
|
187
|
+
if (data.limit && data.totalEdges > data.limit) {
|
|
188
|
+
lines.push(` %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return lines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Render function-level graph data as Mermaid flowchart format.
|
|
196
|
+
*
|
|
197
|
+
* @param {{ direction: string, edges: Array, roles: Map<string, string>, totalEdges: number, limit?: number }} data
|
|
198
|
+
* @returns {string}
|
|
199
|
+
*/
|
|
200
|
+
export function renderFunctionLevelMermaid(data) {
|
|
201
|
+
const lines = [`flowchart ${data.direction || 'LR'}`];
|
|
202
|
+
|
|
203
|
+
let nodeCounter = 0;
|
|
204
|
+
const nodeIdMap = new Map();
|
|
205
|
+
function nodeId(key) {
|
|
206
|
+
if (!nodeIdMap.has(key)) nodeIdMap.set(key, `n${nodeCounter++}`);
|
|
207
|
+
return nodeIdMap.get(key);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Group nodes by file for subgraphs
|
|
211
|
+
const fileNodes = new Map();
|
|
212
|
+
const nodeKinds = new Map();
|
|
213
|
+
for (const e of data.edges) {
|
|
214
|
+
const sKey = `${e.source_file}::${e.source_name}`;
|
|
215
|
+
const tKey = `${e.target_file}::${e.target_name}`;
|
|
216
|
+
nodeId(sKey);
|
|
217
|
+
nodeId(tKey);
|
|
218
|
+
nodeKinds.set(sKey, e.source_kind);
|
|
219
|
+
nodeKinds.set(tKey, e.target_kind);
|
|
220
|
+
|
|
221
|
+
if (!fileNodes.has(e.source_file)) fileNodes.set(e.source_file, new Map());
|
|
222
|
+
fileNodes.get(e.source_file).set(sKey, e.source_name);
|
|
223
|
+
|
|
224
|
+
if (!fileNodes.has(e.target_file)) fileNodes.set(e.target_file, new Map());
|
|
225
|
+
fileNodes.get(e.target_file).set(tKey, e.target_name);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Emit subgraphs grouped by file
|
|
229
|
+
for (const [file, nodes] of [...fileNodes].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
230
|
+
const sgId = file.replace(/[^a-zA-Z0-9]/g, '_');
|
|
231
|
+
lines.push(` subgraph ${sgId}["${escapeLabel(file)}"]`);
|
|
232
|
+
for (const [key, name] of nodes) {
|
|
233
|
+
const kind = nodeKinds.get(key);
|
|
234
|
+
lines.push(` ${nodeId(key)}${mermaidShape(kind, name)}`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(' end');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Emit edges with labels
|
|
240
|
+
for (const e of data.edges) {
|
|
241
|
+
const sId = nodeId(`${e.source_file}::${e.source_name}`);
|
|
242
|
+
const tId = nodeId(`${e.target_file}::${e.target_name}`);
|
|
243
|
+
lines.push(` ${sId} -->|${e.edge_kind}| ${tId}`);
|
|
244
|
+
}
|
|
245
|
+
if (data.limit && data.totalEdges > data.limit) {
|
|
246
|
+
lines.push(` %% Truncated: showing ${data.edges.length} of ${data.totalEdges} edges`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Role styling
|
|
250
|
+
const roleStyles = [];
|
|
251
|
+
for (const [key, nid] of nodeIdMap) {
|
|
252
|
+
const role = data.roles?.get(key);
|
|
253
|
+
if (role && ROLE_STYLES[role]) {
|
|
254
|
+
roleStyles.push(` style ${nid} ${ROLE_STYLES[role]}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
lines.push(...roleStyles);
|
|
258
|
+
|
|
259
|
+
return lines.join('\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─── GraphML Serializer ──────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Render file-level graph data as GraphML (XML) format.
|
|
266
|
+
*
|
|
267
|
+
* @param {{ edges: Array<{ source: string, target: string }> }} data
|
|
268
|
+
* @returns {string}
|
|
269
|
+
*/
|
|
270
|
+
export function renderFileLevelGraphML(data) {
|
|
271
|
+
const lines = [
|
|
272
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
273
|
+
'<graphml xmlns="http://graphml.graphstruct.net/graphml">',
|
|
274
|
+
' <key id="d0" for="node" attr.name="name" attr.type="string"/>',
|
|
275
|
+
' <key id="d1" for="node" attr.name="file" attr.type="string"/>',
|
|
276
|
+
' <key id="d2" for="edge" attr.name="kind" attr.type="string"/>',
|
|
277
|
+
' <graph id="codegraph" edgedefault="directed">',
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
const files = new Set();
|
|
281
|
+
for (const { source, target } of data.edges) {
|
|
282
|
+
files.add(source);
|
|
283
|
+
files.add(target);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const fileIds = new Map();
|
|
287
|
+
let nIdx = 0;
|
|
288
|
+
for (const f of files) {
|
|
289
|
+
const id = `n${nIdx++}`;
|
|
290
|
+
fileIds.set(f, id);
|
|
291
|
+
lines.push(` <node id="${id}">`);
|
|
292
|
+
lines.push(` <data key="d0">${escapeXml(path.basename(f))}</data>`);
|
|
293
|
+
lines.push(` <data key="d1">${escapeXml(f)}</data>`);
|
|
294
|
+
lines.push(' </node>');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let eIdx = 0;
|
|
298
|
+
for (const { source, target } of data.edges) {
|
|
299
|
+
lines.push(
|
|
300
|
+
` <edge id="e${eIdx++}" source="${fileIds.get(source)}" target="${fileIds.get(target)}">`,
|
|
301
|
+
);
|
|
302
|
+
lines.push(' <data key="d2">imports</data>');
|
|
303
|
+
lines.push(' </edge>');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
lines.push(' </graph>');
|
|
307
|
+
lines.push('</graphml>');
|
|
308
|
+
return lines.join('\n');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Render function-level graph data as GraphML (XML) format.
|
|
313
|
+
*
|
|
314
|
+
* @param {{ edges: Array }} data
|
|
315
|
+
* @returns {string}
|
|
316
|
+
*/
|
|
317
|
+
export function renderFunctionLevelGraphML(data) {
|
|
318
|
+
const lines = [
|
|
319
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
320
|
+
'<graphml xmlns="http://graphml.graphstruct.net/graphml">',
|
|
321
|
+
' <key id="d0" for="node" attr.name="name" attr.type="string"/>',
|
|
322
|
+
' <key id="d1" for="node" attr.name="kind" attr.type="string"/>',
|
|
323
|
+
' <key id="d2" for="node" attr.name="file" attr.type="string"/>',
|
|
324
|
+
' <key id="d3" for="node" attr.name="line" attr.type="int"/>',
|
|
325
|
+
' <key id="d4" for="node" attr.name="role" attr.type="string"/>',
|
|
326
|
+
' <key id="d5" for="edge" attr.name="kind" attr.type="string"/>',
|
|
327
|
+
' <key id="d6" for="edge" attr.name="confidence" attr.type="double"/>',
|
|
328
|
+
' <graph id="codegraph" edgedefault="directed">',
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
const emittedNodes = new Set();
|
|
332
|
+
function emitNode(id, name, kind, file, line, role) {
|
|
333
|
+
if (emittedNodes.has(id)) return;
|
|
334
|
+
emittedNodes.add(id);
|
|
335
|
+
lines.push(` <node id="n${id}">`);
|
|
336
|
+
lines.push(` <data key="d0">${escapeXml(name)}</data>`);
|
|
337
|
+
lines.push(` <data key="d1">${escapeXml(kind)}</data>`);
|
|
338
|
+
lines.push(` <data key="d2">${escapeXml(file)}</data>`);
|
|
339
|
+
lines.push(` <data key="d3">${line}</data>`);
|
|
340
|
+
if (role) lines.push(` <data key="d4">${escapeXml(role)}</data>`);
|
|
341
|
+
lines.push(' </node>');
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let eIdx = 0;
|
|
345
|
+
for (const e of data.edges) {
|
|
346
|
+
emitNode(
|
|
347
|
+
e.source_id,
|
|
348
|
+
e.source_name,
|
|
349
|
+
e.source_kind,
|
|
350
|
+
e.source_file,
|
|
351
|
+
e.source_line,
|
|
352
|
+
e.source_role,
|
|
353
|
+
);
|
|
354
|
+
emitNode(
|
|
355
|
+
e.target_id,
|
|
356
|
+
e.target_name,
|
|
357
|
+
e.target_kind,
|
|
358
|
+
e.target_file,
|
|
359
|
+
e.target_line,
|
|
360
|
+
e.target_role,
|
|
361
|
+
);
|
|
362
|
+
lines.push(` <edge id="e${eIdx++}" source="n${e.source_id}" target="n${e.target_id}">`);
|
|
363
|
+
lines.push(` <data key="d5">${escapeXml(e.edge_kind)}</data>`);
|
|
364
|
+
lines.push(` <data key="d6">${e.confidence}</data>`);
|
|
365
|
+
lines.push(' </edge>');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
lines.push(' </graph>');
|
|
369
|
+
lines.push('</graphml>');
|
|
370
|
+
return lines.join('\n');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ─── Neo4j CSV Serializer ────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Render file-level graph data as Neo4j bulk-import CSV.
|
|
377
|
+
*
|
|
378
|
+
* @param {{ edges: Array<{ source: string, target: string, edge_kind: string, confidence: number }> }} data
|
|
379
|
+
* @returns {{ nodes: string, relationships: string }}
|
|
380
|
+
*/
|
|
381
|
+
export function renderFileLevelNeo4jCSV(data) {
|
|
382
|
+
const files = new Map();
|
|
383
|
+
let idx = 0;
|
|
384
|
+
for (const { source, target } of data.edges) {
|
|
385
|
+
if (!files.has(source)) files.set(source, idx++);
|
|
386
|
+
if (!files.has(target)) files.set(target, idx++);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const nodeLines = ['nodeId:ID,name,file:string,:LABEL'];
|
|
390
|
+
for (const [file, id] of files) {
|
|
391
|
+
nodeLines.push(`${id},${escapeCsv(path.basename(file))},${escapeCsv(file)},File`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
395
|
+
for (const e of data.edges) {
|
|
396
|
+
const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
|
|
397
|
+
relLines.push(`${files.get(e.source)},${files.get(e.target)},${edgeType},${e.confidence}`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Render function-level graph data as Neo4j bulk-import CSV.
|
|
405
|
+
*
|
|
406
|
+
* @param {{ edges: Array }} data
|
|
407
|
+
* @returns {{ nodes: string, relationships: string }}
|
|
408
|
+
*/
|
|
409
|
+
export function renderFunctionLevelNeo4jCSV(data) {
|
|
410
|
+
const emitted = new Set();
|
|
411
|
+
const nodeLines = ['nodeId:ID,name,kind,file:string,line:int,role,:LABEL'];
|
|
412
|
+
function emitNode(id, name, kind, file, line, role) {
|
|
413
|
+
if (emitted.has(id)) return;
|
|
414
|
+
emitted.add(id);
|
|
415
|
+
const label = kind.charAt(0).toUpperCase() + kind.slice(1);
|
|
416
|
+
nodeLines.push(
|
|
417
|
+
`${id},${escapeCsv(name)},${escapeCsv(kind)},${escapeCsv(file)},${line},${escapeCsv(role || '')},${label}`,
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const relLines = [':START_ID,:END_ID,:TYPE,confidence:float'];
|
|
422
|
+
for (const e of data.edges) {
|
|
423
|
+
emitNode(
|
|
424
|
+
e.source_id,
|
|
425
|
+
e.source_name,
|
|
426
|
+
e.source_kind,
|
|
427
|
+
e.source_file,
|
|
428
|
+
e.source_line,
|
|
429
|
+
e.source_role,
|
|
430
|
+
);
|
|
431
|
+
emitNode(
|
|
432
|
+
e.target_id,
|
|
433
|
+
e.target_name,
|
|
434
|
+
e.target_kind,
|
|
435
|
+
e.target_file,
|
|
436
|
+
e.target_line,
|
|
437
|
+
e.target_role,
|
|
438
|
+
);
|
|
439
|
+
const edgeType = e.edge_kind.toUpperCase().replace(/-/g, '_');
|
|
440
|
+
relLines.push(`${e.source_id},${e.target_id},${edgeType},${e.confidence}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
return { nodes: nodeLines.join('\n'), relationships: relLines.join('\n') };
|
|
444
|
+
}
|