@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,197 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { kindIcon, moduleMapData, rolesData, statsData } from '../../domain/queries.js';
|
|
3
|
+
import { outputResult } from '../../infrastructure/result-formatter.js';
|
|
4
|
+
|
|
5
|
+
export async function stats(customDbPath, opts = {}) {
|
|
6
|
+
const data = statsData(customDbPath, { noTests: opts.noTests });
|
|
7
|
+
|
|
8
|
+
// Community detection summary (async import for lazy-loading)
|
|
9
|
+
try {
|
|
10
|
+
const { communitySummaryForStats } = await import('../../features/communities.js');
|
|
11
|
+
data.communities = communitySummaryForStats(customDbPath, { noTests: opts.noTests });
|
|
12
|
+
} catch {
|
|
13
|
+
/* graphology may not be available */
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (outputResult(data, null, opts)) return;
|
|
17
|
+
|
|
18
|
+
// Human-readable output
|
|
19
|
+
console.log('\n# Codegraph Stats\n');
|
|
20
|
+
|
|
21
|
+
// Nodes
|
|
22
|
+
console.log(`Nodes: ${data.nodes.total} total`);
|
|
23
|
+
const kindEntries = Object.entries(data.nodes.byKind).sort((a, b) => b[1] - a[1]);
|
|
24
|
+
const kindParts = kindEntries.map(([k, v]) => `${k} ${v}`);
|
|
25
|
+
for (let i = 0; i < kindParts.length; i += 3) {
|
|
26
|
+
const row = kindParts
|
|
27
|
+
.slice(i, i + 3)
|
|
28
|
+
.map((p) => p.padEnd(18))
|
|
29
|
+
.join('');
|
|
30
|
+
console.log(` ${row}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Edges
|
|
34
|
+
console.log(`\nEdges: ${data.edges.total} total`);
|
|
35
|
+
const edgeEntries = Object.entries(data.edges.byKind).sort((a, b) => b[1] - a[1]);
|
|
36
|
+
const edgeParts = edgeEntries.map(([k, v]) => `${k} ${v}`);
|
|
37
|
+
for (let i = 0; i < edgeParts.length; i += 3) {
|
|
38
|
+
const row = edgeParts
|
|
39
|
+
.slice(i, i + 3)
|
|
40
|
+
.map((p) => p.padEnd(18))
|
|
41
|
+
.join('');
|
|
42
|
+
console.log(` ${row}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Files
|
|
46
|
+
console.log(`\nFiles: ${data.files.total} (${data.files.languages} languages)`);
|
|
47
|
+
const langEntries = Object.entries(data.files.byLanguage).sort((a, b) => b[1] - a[1]);
|
|
48
|
+
const langParts = langEntries.map(([k, v]) => `${k} ${v}`);
|
|
49
|
+
for (let i = 0; i < langParts.length; i += 3) {
|
|
50
|
+
const row = langParts
|
|
51
|
+
.slice(i, i + 3)
|
|
52
|
+
.map((p) => p.padEnd(18))
|
|
53
|
+
.join('');
|
|
54
|
+
console.log(` ${row}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Cycles
|
|
58
|
+
console.log(
|
|
59
|
+
`\nCycles: ${data.cycles.fileLevel} file-level, ${data.cycles.functionLevel} function-level`,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Hotspots
|
|
63
|
+
if (data.hotspots.length > 0) {
|
|
64
|
+
console.log(`\nTop ${data.hotspots.length} coupling hotspots:`);
|
|
65
|
+
for (let i = 0; i < data.hotspots.length; i++) {
|
|
66
|
+
const h = data.hotspots[i];
|
|
67
|
+
console.log(
|
|
68
|
+
` ${String(i + 1).padStart(2)}. ${h.file.padEnd(35)} fan-in: ${String(h.fanIn).padStart(3)} fan-out: ${String(h.fanOut).padStart(3)}`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Embeddings
|
|
74
|
+
if (data.embeddings) {
|
|
75
|
+
const e = data.embeddings;
|
|
76
|
+
console.log(
|
|
77
|
+
`\nEmbeddings: ${e.count} vectors (${e.model || 'unknown'}, ${e.dim || '?'}d) built ${e.builtAt || 'unknown'}`,
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
console.log('\nEmbeddings: not built');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Quality
|
|
84
|
+
if (data.quality) {
|
|
85
|
+
const q = data.quality;
|
|
86
|
+
const cc = q.callerCoverage;
|
|
87
|
+
const cf = q.callConfidence;
|
|
88
|
+
console.log(`\nGraph Quality: ${q.score}/100`);
|
|
89
|
+
console.log(
|
|
90
|
+
` Caller coverage: ${(cc.ratio * 100).toFixed(1)}% (${cc.covered}/${cc.total} functions have >=1 caller)`,
|
|
91
|
+
);
|
|
92
|
+
console.log(
|
|
93
|
+
` Call confidence: ${(cf.ratio * 100).toFixed(1)}% (${cf.highConf}/${cf.total} call edges are high-confidence)`,
|
|
94
|
+
);
|
|
95
|
+
if (q.falsePositiveWarnings.length > 0) {
|
|
96
|
+
console.log(' False-positive warnings:');
|
|
97
|
+
for (const fp of q.falsePositiveWarnings) {
|
|
98
|
+
console.log(` ! ${fp.name} (${fp.callerCount} callers) -- ${fp.file}:${fp.line}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Roles
|
|
104
|
+
if (data.roles && Object.keys(data.roles).length > 0) {
|
|
105
|
+
const total = Object.values(data.roles).reduce((a, b) => a + b, 0);
|
|
106
|
+
console.log(`\nRoles: ${total} classified symbols`);
|
|
107
|
+
const roleParts = Object.entries(data.roles)
|
|
108
|
+
.sort((a, b) => b[1] - a[1])
|
|
109
|
+
.map(([k, v]) => `${k} ${v}`);
|
|
110
|
+
for (let i = 0; i < roleParts.length; i += 3) {
|
|
111
|
+
const row = roleParts
|
|
112
|
+
.slice(i, i + 3)
|
|
113
|
+
.map((p) => p.padEnd(18))
|
|
114
|
+
.join('');
|
|
115
|
+
console.log(` ${row}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Complexity
|
|
120
|
+
if (data.complexity) {
|
|
121
|
+
const cx = data.complexity;
|
|
122
|
+
const miPart = cx.avgMI != null ? ` | avg MI: ${cx.avgMI} | min MI: ${cx.minMI}` : '';
|
|
123
|
+
console.log(
|
|
124
|
+
`\nComplexity: ${cx.analyzed} functions | avg cognitive: ${cx.avgCognitive} | avg cyclomatic: ${cx.avgCyclomatic} | max cognitive: ${cx.maxCognitive}${miPart}`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Communities
|
|
129
|
+
if (data.communities) {
|
|
130
|
+
const cm = data.communities;
|
|
131
|
+
console.log(
|
|
132
|
+
`\nCommunities: ${cm.communityCount} detected | modularity: ${cm.modularity} | drift: ${cm.driftScore}%`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function moduleMap(customDbPath, limit = 20, opts = {}) {
|
|
140
|
+
const data = moduleMapData(customDbPath, limit, { noTests: opts.noTests });
|
|
141
|
+
if (outputResult(data, 'topNodes', opts)) return;
|
|
142
|
+
|
|
143
|
+
console.log(`\nModule map (top ${limit} most-connected nodes):\n`);
|
|
144
|
+
const dirs = new Map();
|
|
145
|
+
for (const n of data.topNodes) {
|
|
146
|
+
if (!dirs.has(n.dir)) dirs.set(n.dir, []);
|
|
147
|
+
dirs.get(n.dir).push(n);
|
|
148
|
+
}
|
|
149
|
+
for (const [dir, files] of [...dirs].sort()) {
|
|
150
|
+
console.log(` [${dir}/]`);
|
|
151
|
+
for (const f of files) {
|
|
152
|
+
const coupling = f.inEdges + f.outEdges;
|
|
153
|
+
const bar = '#'.repeat(Math.min(coupling, 40));
|
|
154
|
+
console.log(
|
|
155
|
+
` ${path.basename(f.file).padEnd(35)} <-${String(f.inEdges).padStart(3)} ->${String(f.outEdges).padStart(3)} =${String(coupling).padStart(3)} ${bar}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
console.log(
|
|
160
|
+
`\n Total: ${data.stats.totalFiles} files, ${data.stats.totalNodes} symbols, ${data.stats.totalEdges} edges\n`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function roles(customDbPath, opts = {}) {
|
|
165
|
+
const data = rolesData(customDbPath, opts);
|
|
166
|
+
if (outputResult(data, 'symbols', opts)) return;
|
|
167
|
+
|
|
168
|
+
if (data.count === 0) {
|
|
169
|
+
console.log('No classified symbols found. Run "codegraph build" first.');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const total = data.count;
|
|
174
|
+
console.log(`\nNode roles (${total} symbols):\n`);
|
|
175
|
+
|
|
176
|
+
const summaryParts = Object.entries(data.summary)
|
|
177
|
+
.sort((a, b) => b[1] - a[1])
|
|
178
|
+
.map(([role, count]) => `${role}: ${count}`);
|
|
179
|
+
console.log(` ${summaryParts.join(' ')}\n`);
|
|
180
|
+
|
|
181
|
+
const byRole = {};
|
|
182
|
+
for (const s of data.symbols) {
|
|
183
|
+
if (!byRole[s.role]) byRole[s.role] = [];
|
|
184
|
+
byRole[s.role].push(s);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const [role, symbols] of Object.entries(byRole)) {
|
|
188
|
+
console.log(`## ${role} (${symbols.length})`);
|
|
189
|
+
for (const s of symbols.slice(0, 30)) {
|
|
190
|
+
console.log(` ${kindIcon(s.kind)} ${s.name} ${s.file}:${s.line}`);
|
|
191
|
+
}
|
|
192
|
+
if (symbols.length > 30) {
|
|
193
|
+
console.log(` ... and ${symbols.length - 30} more`);
|
|
194
|
+
}
|
|
195
|
+
console.log();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { kindIcon, pathData } from '../../domain/queries.js';
|
|
2
|
+
import { outputResult } from '../../infrastructure/result-formatter.js';
|
|
3
|
+
|
|
4
|
+
export function symbolPath(from, to, customDbPath, opts = {}) {
|
|
5
|
+
const data = pathData(from, to, customDbPath, opts);
|
|
6
|
+
if (outputResult(data, null, opts)) return;
|
|
7
|
+
|
|
8
|
+
if (data.error) {
|
|
9
|
+
console.log(data.error);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (!data.found) {
|
|
14
|
+
const dir = data.reverse ? 'reverse ' : '';
|
|
15
|
+
console.log(`No ${dir}path from "${from}" to "${to}" within ${data.maxDepth} hops.`);
|
|
16
|
+
if (data.fromCandidates.length > 1) {
|
|
17
|
+
console.log(
|
|
18
|
+
`\n "${from}" matched ${data.fromCandidates.length} symbols — using top match: ${data.fromCandidates[0].name} (${data.fromCandidates[0].file}:${data.fromCandidates[0].line})`,
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (data.toCandidates.length > 1) {
|
|
22
|
+
console.log(
|
|
23
|
+
` "${to}" matched ${data.toCandidates.length} symbols — using top match: ${data.toCandidates[0].name} (${data.toCandidates[0].file}:${data.toCandidates[0].line})`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (data.hops === 0) {
|
|
30
|
+
console.log(`\n"${from}" and "${to}" resolve to the same symbol (0 hops):`);
|
|
31
|
+
const n = data.path[0];
|
|
32
|
+
console.log(` ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dir = data.reverse ? ' (reverse)' : '';
|
|
37
|
+
console.log(
|
|
38
|
+
`\nPath from ${from} to ${to} (${data.hops} ${data.hops === 1 ? 'hop' : 'hops'})${dir}:\n`,
|
|
39
|
+
);
|
|
40
|
+
for (let i = 0; i < data.path.length; i++) {
|
|
41
|
+
const n = data.path[i];
|
|
42
|
+
const indent = ' '.repeat(i + 1);
|
|
43
|
+
if (i === 0) {
|
|
44
|
+
console.log(`${indent}${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`);
|
|
45
|
+
} else {
|
|
46
|
+
console.log(
|
|
47
|
+
`${indent}--[${n.edgeKind}]--> ${kindIcon(n.kind)} ${n.name} (${n.kind}) -- ${n.file}:${n.line}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (data.alternateCount > 0) {
|
|
53
|
+
console.log(
|
|
54
|
+
`\n (${data.alternateCount} alternate shortest ${data.alternateCount === 1 ? 'path' : 'paths'} at same depth)`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
console.log();
|
|
58
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queries-cli.js — barrel re-export for backward compatibility.
|
|
3
|
+
*
|
|
4
|
+
* The actual implementations live in queries-cli/ split by concern:
|
|
5
|
+
* path.js — symbolPath
|
|
6
|
+
* overview.js — stats, moduleMap, roles
|
|
7
|
+
* inspect.js — where, queryName, context, children, explain
|
|
8
|
+
* impact.js — fileDeps, fnDeps, impactAnalysis, fnImpact, diffImpact
|
|
9
|
+
* exports.js — fileExports
|
|
10
|
+
*/
|
|
11
|
+
export {
|
|
12
|
+
children,
|
|
13
|
+
context,
|
|
14
|
+
diffImpact,
|
|
15
|
+
explain,
|
|
16
|
+
fileDeps,
|
|
17
|
+
fileExports,
|
|
18
|
+
fnDeps,
|
|
19
|
+
fnImpact,
|
|
20
|
+
impactAnalysis,
|
|
21
|
+
moduleMap,
|
|
22
|
+
queryName,
|
|
23
|
+
roles,
|
|
24
|
+
stats,
|
|
25
|
+
symbolPath,
|
|
26
|
+
where,
|
|
27
|
+
} from './queries-cli/index.js';
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { printNdjson } from '../shared/paginate.js';
|
|
2
|
+
import { formatTable, truncEnd } from './table.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Flatten a nested object into dot-notation keys.
|
|
6
|
+
* Arrays are JSON-stringified; nested objects are recursed.
|
|
7
|
+
*
|
|
8
|
+
* Note: this assumes input objects do not contain literal dot-notation keys
|
|
9
|
+
* (e.g. `{ "a.b": 1 }`). If they do, flattened keys will silently collide
|
|
10
|
+
* with nested paths (e.g. `{ a: { b: 2 } }` also produces `"a.b"`).
|
|
11
|
+
*/
|
|
12
|
+
function flattenObject(obj, prefix = '') {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
15
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
16
|
+
if (
|
|
17
|
+
value !== null &&
|
|
18
|
+
typeof value === 'object' &&
|
|
19
|
+
!Array.isArray(value) &&
|
|
20
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
21
|
+
) {
|
|
22
|
+
Object.assign(result, flattenObject(value, fullKey));
|
|
23
|
+
} else if (Array.isArray(value)) {
|
|
24
|
+
result[fullKey] = JSON.stringify(value);
|
|
25
|
+
} else {
|
|
26
|
+
result[fullKey] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Flatten items array and derive column names.
|
|
34
|
+
* Shared by printCsv and printAutoTable.
|
|
35
|
+
* @returns {{ flatItems: object[], columns: string[] } | null}
|
|
36
|
+
*/
|
|
37
|
+
function prepareFlatItems(data, field) {
|
|
38
|
+
const items = field ? data[field] : data;
|
|
39
|
+
if (!Array.isArray(items)) return null;
|
|
40
|
+
|
|
41
|
+
const flatItems = items.map((item) =>
|
|
42
|
+
typeof item === 'object' && item !== null && !Array.isArray(item)
|
|
43
|
+
? flattenObject(item)
|
|
44
|
+
: { value: item },
|
|
45
|
+
);
|
|
46
|
+
const columns = (() => {
|
|
47
|
+
const keys = new Set();
|
|
48
|
+
for (const item of flatItems) for (const key of Object.keys(item)) keys.add(key);
|
|
49
|
+
return [...keys];
|
|
50
|
+
})();
|
|
51
|
+
if (columns.length === 0) columns.push('value');
|
|
52
|
+
|
|
53
|
+
return { flatItems, columns };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Escape a value for CSV output (LF line endings). */
|
|
57
|
+
function escapeCsv(val) {
|
|
58
|
+
const str = val == null ? '' : String(val);
|
|
59
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
60
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
61
|
+
}
|
|
62
|
+
return str;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Print data as CSV to stdout.
|
|
67
|
+
* @param {object} data - Result object from a *Data() function
|
|
68
|
+
* @param {string} field - Array field name (e.g. 'results')
|
|
69
|
+
*/
|
|
70
|
+
function printCsv(data, field) {
|
|
71
|
+
const prepared = prepareFlatItems(data, field);
|
|
72
|
+
if (!prepared) return false;
|
|
73
|
+
const { flatItems, columns } = prepared;
|
|
74
|
+
|
|
75
|
+
console.log(columns.map(escapeCsv).join(','));
|
|
76
|
+
for (const row of flatItems) {
|
|
77
|
+
console.log(columns.map((col) => escapeCsv(row[col])).join(','));
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const MAX_COL_WIDTH = 40;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print data as an aligned table to stdout.
|
|
86
|
+
* @param {object} data - Result object from a *Data() function
|
|
87
|
+
* @param {string} field - Array field name (e.g. 'results')
|
|
88
|
+
*/
|
|
89
|
+
function printAutoTable(data, field) {
|
|
90
|
+
const prepared = prepareFlatItems(data, field);
|
|
91
|
+
if (!prepared) return false;
|
|
92
|
+
const { flatItems, columns } = prepared;
|
|
93
|
+
|
|
94
|
+
const colDefs = columns.map((col) => {
|
|
95
|
+
const maxLen = flatItems.reduce(
|
|
96
|
+
(max, item) => Math.max(max, String(item[col] ?? '').length),
|
|
97
|
+
col.length,
|
|
98
|
+
);
|
|
99
|
+
const isNumeric =
|
|
100
|
+
flatItems.length > 0 &&
|
|
101
|
+
flatItems.every((item) => {
|
|
102
|
+
const v = item[col];
|
|
103
|
+
return v == null || v === '' || (typeof v !== 'boolean' && Number.isFinite(Number(v)));
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
header: col,
|
|
107
|
+
width: Math.min(maxLen, MAX_COL_WIDTH),
|
|
108
|
+
align: isNumeric ? 'right' : 'left',
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const rows = flatItems.map((item) =>
|
|
113
|
+
columns.map((col) => truncEnd(String(item[col] ?? ''), MAX_COL_WIDTH)),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
console.log(formatTable({ columns: colDefs, rows }));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Shared JSON / NDJSON / table / CSV output dispatch for CLI wrappers.
|
|
122
|
+
*
|
|
123
|
+
* @param {object} data - Result object from a *Data() function
|
|
124
|
+
* @param {string} field - Array field name for NDJSON streaming (e.g. 'results')
|
|
125
|
+
* @param {object} opts - CLI options ({ json?, ndjson?, table?, csv? })
|
|
126
|
+
* @returns {boolean} true if output was handled (caller should return early)
|
|
127
|
+
*/
|
|
128
|
+
export function outputResult(data, field, opts) {
|
|
129
|
+
if (opts.ndjson) {
|
|
130
|
+
printNdjson(data, field);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (opts.json) {
|
|
134
|
+
console.log(JSON.stringify(data, null, 2));
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
if (opts.csv) {
|
|
138
|
+
return printCsv(data, field) !== false;
|
|
139
|
+
}
|
|
140
|
+
if (opts.table) {
|
|
141
|
+
return printAutoTable(data, field) !== false;
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid sequence diagram renderer — pure data → string transform.
|
|
3
|
+
*
|
|
4
|
+
* Converts sequenceData() output into Mermaid sequenceDiagram syntax.
|
|
5
|
+
* No DB access, no I/O — just data in, formatted string out.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Escape special Mermaid characters in labels.
|
|
10
|
+
*/
|
|
11
|
+
function escapeMermaid(str) {
|
|
12
|
+
return str
|
|
13
|
+
.replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>')
|
|
15
|
+
.replace(/:/g, '#colon;')
|
|
16
|
+
.replace(/"/g, '#quot;');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert sequenceData result to Mermaid sequenceDiagram syntax.
|
|
21
|
+
* @param {{ participants, messages, truncated, depth }} seqResult
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
export function sequenceToMermaid(seqResult) {
|
|
25
|
+
const lines = ['sequenceDiagram'];
|
|
26
|
+
|
|
27
|
+
for (const p of seqResult.participants) {
|
|
28
|
+
lines.push(` participant ${p.id} as ${escapeMermaid(p.label)}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const msg of seqResult.messages) {
|
|
32
|
+
const arrow = msg.type === 'return' ? '-->>' : '->>';
|
|
33
|
+
lines.push(` ${msg.from}${arrow}${msg.to}: ${escapeMermaid(msg.label)}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (seqResult.truncated && seqResult.participants.length > 0) {
|
|
37
|
+
lines.push(
|
|
38
|
+
` note right of ${seqResult.participants[0].id}: Truncated at depth ${seqResult.depth}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return lines.join('\n');
|
|
43
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { kindIcon } from '../domain/queries.js';
|
|
2
|
+
import { sequenceData, sequenceToMermaid } from '../features/sequence.js';
|
|
1
3
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
-
import { kindIcon } from '../queries.js';
|
|
3
|
-
import { sequenceData, sequenceToMermaid } from '../sequence.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* CLI entry point — format sequence data as mermaid, JSON, or ndjson.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
|
|
2
|
+
import { hotspotsData, moduleBoundariesData, structureData } from '../features/structure.js';
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export { hotspotsData, moduleBoundariesData, structureData };
|
|
5
5
|
|
|
6
6
|
export function formatStructure(data) {
|
|
7
7
|
if (data.count === 0) return 'No directory structure found. Run "codegraph build" first.';
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared table formatting utilities for CLI output.
|
|
3
|
+
*
|
|
4
|
+
* Pure data → formatted string transforms. No I/O — callers handle printing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Format a table with aligned columns.
|
|
9
|
+
*
|
|
10
|
+
* @param {object} opts
|
|
11
|
+
* @param {Array<{ header: string, width: number, align?: 'left'|'right' }>} opts.columns
|
|
12
|
+
* @param {string[][]} opts.rows - Each row is an array of string cell values
|
|
13
|
+
* @param {number} [opts.indent=2] - Leading spaces per line
|
|
14
|
+
* @returns {string} Formatted table string (header + separator + data rows)
|
|
15
|
+
*/
|
|
16
|
+
export function formatTable({ columns, rows, indent = 2 }) {
|
|
17
|
+
const prefix = ' '.repeat(indent);
|
|
18
|
+
const header = columns
|
|
19
|
+
.map((c) => (c.align === 'right' ? c.header.padStart(c.width) : c.header.padEnd(c.width)))
|
|
20
|
+
.join(' ');
|
|
21
|
+
const separator = columns.map((c) => '\u2500'.repeat(c.width)).join(' ');
|
|
22
|
+
const lines = [`${prefix}${header}`, `${prefix}${separator}`];
|
|
23
|
+
for (const row of rows) {
|
|
24
|
+
const cells = columns.map((c, i) => {
|
|
25
|
+
const val = row[i] ?? '';
|
|
26
|
+
return c.align === 'right' ? val.padStart(c.width) : val.padEnd(c.width);
|
|
27
|
+
});
|
|
28
|
+
lines.push(`${prefix}${cells.join(' ')}`);
|
|
29
|
+
}
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Truncate a string from the end, appending '\u2026' if truncated.
|
|
35
|
+
*/
|
|
36
|
+
export function truncEnd(str, maxLen) {
|
|
37
|
+
if (str.length <= maxLen) return str;
|
|
38
|
+
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Truncate a string from the start, prepending '\u2026' if truncated.
|
|
43
|
+
*/
|
|
44
|
+
export function truncStart(str, maxLen) {
|
|
45
|
+
if (str.length <= maxLen) return str;
|
|
46
|
+
return `\u2026${str.slice(-(maxLen - 1))}`;
|
|
47
|
+
}
|