@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/README.md
CHANGED
|
@@ -31,13 +31,11 @@
|
|
|
31
31
|
|
|
32
32
|
## The Problem
|
|
33
33
|
|
|
34
|
-
AI agents
|
|
34
|
+
AI agents face an impossible trade-off. They either spend thousands of tokens reading files to understand a codebase's structure — blowing up their context window until quality degrades — or they assume how things work, and the assumptions are often wrong. Either way, things break. The larger the codebase, the worse it gets.
|
|
35
35
|
|
|
36
|
-
An agent
|
|
36
|
+
An agent modifies a function without knowing 9 files import it. It misreads what a helper does and builds logic on top of that misunderstanding. It leaves dead code behind after a refactor. The PR gets opened, and your reviewer — human or automated — flags the same structural issues again and again: _"this breaks 14 callers,"_ _"that function already exists,"_ _"this export is now dead."_ If the reviewer catches it, that's multiple rounds of back-and-forth. If they don't, it can ship to production. Multiply that by every PR, every developer, every repo.
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
The information exists — it's in the code itself. But without a structured map, agents hallucinate, developers guess, and architecture degrades one unreviewed change at a time.
|
|
38
|
+
The information to prevent these issues exists — it's in the code itself. But without a structured map, agents lack the context to get it right consistently, reviewers waste cycles on preventable issues, and architecture degrades one unreviewed change at a time.
|
|
41
39
|
|
|
42
40
|
## What Codegraph Does
|
|
43
41
|
|
|
@@ -50,7 +48,7 @@ It parses your code with [tree-sitter](https://tree-sitter.github.io/) (native R
|
|
|
50
48
|
- **CI gates** — `check` and `manifesto` commands enforce quality thresholds with exit codes
|
|
51
49
|
- **Programmatic API** — embed codegraph in your own tools via `npm install`
|
|
52
50
|
|
|
53
|
-
Instead of an agent
|
|
51
|
+
Instead of an agent editing code without structural context and letting reviewers catch the fallout, it knows _"this function has 14 callers across 9 files"_ before it touches anything. Dead exports, circular dependencies, and boundary violations surface during development — not during review. The result: PRs that need fewer review rounds.
|
|
54
52
|
|
|
55
53
|
**Free. Open source. Fully local.** Zero network calls, zero telemetry. Your code stays on your machine. When you want deeper intelligence, bring your own LLM provider — your code only goes where you choose to send it.
|
|
56
54
|
|
|
@@ -62,18 +60,18 @@ cd your-project
|
|
|
62
60
|
codegraph build
|
|
63
61
|
```
|
|
64
62
|
|
|
65
|
-
No config files, no Docker, no JVM, no API keys, no accounts. Point your agent at the MCP server and it has
|
|
63
|
+
No config files, no Docker, no JVM, no API keys, no accounts. Point your agent at the MCP server and it has structural awareness of your codebase.
|
|
66
64
|
|
|
67
65
|
### Why it matters
|
|
68
66
|
|
|
69
67
|
| | Without codegraph | With codegraph |
|
|
70
68
|
|---|---|---|
|
|
71
|
-
| **
|
|
72
|
-
| **AI agents** | Modify `parseConfig()` without knowing 9 files import it | `fn-impact parseConfig` shows every caller before the edit |
|
|
73
|
-
| **AI agents** |
|
|
69
|
+
| **Code review** | Reviewers flag broken callers, dead code, and boundary violations round after round | Structural issues are caught during development — PRs pass review with fewer rounds |
|
|
70
|
+
| **AI agents** | Modify `parseConfig()` without knowing 9 files import it — reviewer catches it | `fn-impact parseConfig` shows every caller before the edit — agent fixes it proactively |
|
|
71
|
+
| **AI agents** | Leave dead exports and duplicate helpers behind after refactors | Dead code, cycles, and duplicates surface in real time via hooks and MCP queries |
|
|
72
|
+
| **AI agents** | Produce code that works but doesn't fit the codebase structure | `context <name> -T` returns source, deps, callers, and tests — the agent writes code that fits |
|
|
74
73
|
| **CI pipelines** | Catch test failures but miss structural degradation | `check --staged` fails the build when blast radius or complexity thresholds are exceeded |
|
|
75
74
|
| **Developers** | Inherit a codebase and grep for hours to understand what calls what | `context handleAuth -T` gives the same structured view agents use |
|
|
76
|
-
| **Developers** | Rename a function, break 14 call sites silently | `diff-impact --staged` catches breakage before you commit |
|
|
77
75
|
| **Architects** | Draw boundary rules that erode within weeks | `manifesto` and `boundaries` enforce architecture rules on every commit |
|
|
78
76
|
|
|
79
77
|
### Feature comparison
|
|
@@ -117,9 +115,9 @@ No config files, no Docker, no JVM, no API keys, no accounts. Point your agent a
|
|
|
117
115
|
| | Differentiator | In practice |
|
|
118
116
|
|---|---|---|
|
|
119
117
|
| **🤖** | **AI-first architecture** | 30-tool [MCP server](https://modelcontextprotocol.io/) — agents query the graph directly instead of scraping the filesystem. One call replaces 20+ grep/find/cat invocations |
|
|
120
|
-
| **🏷️** | **Role classification** | Every symbol auto-tagged as `entry`/`core`/`utility`/`adapter`/`dead`/`leaf` — agents
|
|
118
|
+
| **🏷️** | **Role classification** | Every symbol auto-tagged as `entry`/`core`/`utility`/`adapter`/`dead`/`leaf` — agents understand a symbol's architectural role without reading surrounding code |
|
|
121
119
|
| **🔬** | **Function-level, not just files** | Traces `handleAuth()` → `validateToken()` → `decryptJWT()` and shows 14 callers across 9 files break if `decryptJWT` changes |
|
|
122
|
-
| **⚡** | **Always-fresh graph** | Three-tier change detection: journal (O(changed)) → mtime+size (O(n) stats) → hash (O(changed) reads). Sub-second rebuilds — agents
|
|
120
|
+
| **⚡** | **Always-fresh graph** | Three-tier change detection: journal (O(changed)) → mtime+size (O(n) stats) → hash (O(changed) reads). Sub-second rebuilds — agents work with current data |
|
|
123
121
|
| **💥** | **Git diff impact** | `codegraph diff-impact` shows changed functions, their callers, and full blast radius — enriched with historically coupled files from git co-change analysis. Ships with a GitHub Actions workflow |
|
|
124
122
|
| **🌐** | **Multi-language, one graph** | JS/TS + Python + Go + Rust + Java + C# + PHP + Ruby + HCL in a single graph — agents don't need per-language tools |
|
|
125
123
|
| **🧠** | **Hybrid search** | BM25 keyword + semantic embeddings fused via RRF — `hybrid` (default), `semantic`, or `keyword` mode; multi-query via `"auth; token; JWT"` |
|
|
@@ -562,14 +560,14 @@ Self-measured on every release via CI ([build benchmarks](generated/benchmarks/B
|
|
|
562
560
|
|
|
563
561
|
| Metric | Latest |
|
|
564
562
|
|---|---|
|
|
565
|
-
| Build speed (native) | **5.
|
|
566
|
-
| Build speed (WASM) | **
|
|
563
|
+
| Build speed (native) | **5.1 ms/file** |
|
|
564
|
+
| Build speed (WASM) | **14.6 ms/file** |
|
|
567
565
|
| Query time | **4ms** |
|
|
568
566
|
| No-op rebuild (native) | **6ms** |
|
|
569
|
-
| 1-file rebuild (native) | **
|
|
570
|
-
| Query: fn-deps | **0.
|
|
567
|
+
| 1-file rebuild (native) | **282ms** |
|
|
568
|
+
| Query: fn-deps | **0.9ms** |
|
|
571
569
|
| Query: path | **0.8ms** |
|
|
572
|
-
| ~50,000 files (est.) | **~
|
|
570
|
+
| ~50,000 files (est.) | **~255.0s build** |
|
|
573
571
|
|
|
574
572
|
Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files.
|
|
575
573
|
|
|
@@ -823,7 +821,7 @@ See **[ROADMAP.md](docs/roadmap/ROADMAP.md)** for the full development roadmap a
|
|
|
823
821
|
1. ~~**Rust Core**~~ — **Complete** (v1.3.0) — native tree-sitter parsing via napi-rs, parallel multi-core parsing, incremental re-parsing, import resolution & cycle detection in Rust
|
|
824
822
|
2. ~~**Foundation Hardening**~~ — **Complete** (v1.4.0) — parser registry, 12-tool MCP server with multi-repo support, test coverage 62%→75%, `apiKeyCommand` secret resolution, global repo registry
|
|
825
823
|
3. ~~**Deep Analysis**~~ — **Complete** (v3.0.0) — dataflow analysis (flows_to, returns, mutates), intraprocedural CFG for all 11 languages, stored AST nodes, expanded node/edge types (parameter, property, constant, contains, parameter_of, receiver), GraphML/GraphSON/Neo4j CSV export, interactive HTML viewer, CLI consolidation, stable JSON schema
|
|
826
|
-
4. **Architectural Refactoring** —
|
|
824
|
+
4. **Architectural Refactoring** — **In Progress** (v3.1.4) — unified AST analysis, composable MCP, domain errors, builder pipeline, embedder subsystem, graph model, qualified names, presentation layer, InMemoryRepository (11/14 tasks complete)
|
|
827
825
|
5. **Natural Language Queries** — `codegraph ask` command, conversational sessions
|
|
828
826
|
6. **Expanded Language Support** — 8 new languages (12 → 20)
|
|
829
827
|
7. **GitHub Integration & CI** — reusable GitHub Action, PR review, SARIF output
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@optave/codegraph",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.4",
|
|
4
4
|
"description": "Local code graph CLI — parse codebases with tree-sitter, build dependency graphs, query them",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
".": {
|
|
9
9
|
"import": "./src/index.js"
|
|
10
10
|
},
|
|
11
|
+
"./cli": {
|
|
12
|
+
"import": "./src/cli.js"
|
|
13
|
+
},
|
|
11
14
|
"./package.json": "./package.json"
|
|
12
15
|
},
|
|
13
16
|
"bin": {
|
|
@@ -71,12 +74,12 @@
|
|
|
71
74
|
},
|
|
72
75
|
"optionalDependencies": {
|
|
73
76
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
74
|
-
"@optave/codegraph-darwin-arm64": "3.1.
|
|
75
|
-
"@optave/codegraph-darwin-x64": "3.1.
|
|
76
|
-
"@optave/codegraph-linux-arm64-gnu": "3.1.
|
|
77
|
-
"@optave/codegraph-linux-x64-gnu": "3.1.
|
|
78
|
-
"@optave/codegraph-linux-x64-musl": "3.1.
|
|
79
|
-
"@optave/codegraph-win32-x64-msvc": "3.1.
|
|
77
|
+
"@optave/codegraph-darwin-arm64": "3.1.4",
|
|
78
|
+
"@optave/codegraph-darwin-x64": "3.1.4",
|
|
79
|
+
"@optave/codegraph-linux-arm64-gnu": "3.1.4",
|
|
80
|
+
"@optave/codegraph-linux-x64-gnu": "3.1.4",
|
|
81
|
+
"@optave/codegraph-linux-x64-musl": "3.1.4",
|
|
82
|
+
"@optave/codegraph-win32-x64-msvc": "3.1.4"
|
|
80
83
|
},
|
|
81
84
|
"devDependencies": {
|
|
82
85
|
"@biomejs/biome": "^2.4.4",
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import {
|
|
3
|
+
findCallees,
|
|
4
|
+
findCallers,
|
|
5
|
+
findCrossFileCallTargets,
|
|
6
|
+
findDbPath,
|
|
7
|
+
findFileNodes,
|
|
8
|
+
findImportSources,
|
|
9
|
+
findImportTargets,
|
|
10
|
+
findIntraFileCallEdges,
|
|
11
|
+
findNodeChildren,
|
|
12
|
+
findNodesByFile,
|
|
13
|
+
getComplexityForNode,
|
|
14
|
+
openReadonlyOrFail,
|
|
15
|
+
} from '../db.js';
|
|
16
|
+
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
17
|
+
import { paginateResult } from '../paginate.js';
|
|
18
|
+
import {
|
|
19
|
+
createFileLinesReader,
|
|
20
|
+
extractSignature,
|
|
21
|
+
extractSummary,
|
|
22
|
+
isFileLikeTarget,
|
|
23
|
+
readSourceRange,
|
|
24
|
+
} from '../shared/file-utils.js';
|
|
25
|
+
import { resolveMethodViaHierarchy } from '../shared/hierarchy.js';
|
|
26
|
+
import { normalizeSymbol } from '../shared/normalize.js';
|
|
27
|
+
import { findMatchingNodes } from './symbol-lookup.js';
|
|
28
|
+
|
|
29
|
+
function explainFileImpl(db, target, getFileLines) {
|
|
30
|
+
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
31
|
+
if (fileNodes.length === 0) return [];
|
|
32
|
+
|
|
33
|
+
return fileNodes.map((fn) => {
|
|
34
|
+
const symbols = findNodesByFile(db, fn.file);
|
|
35
|
+
|
|
36
|
+
// IDs of symbols that have incoming calls from other files (public)
|
|
37
|
+
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
38
|
+
|
|
39
|
+
const fileLines = getFileLines(fn.file);
|
|
40
|
+
const mapSymbol = (s) => ({
|
|
41
|
+
name: s.name,
|
|
42
|
+
kind: s.kind,
|
|
43
|
+
line: s.line,
|
|
44
|
+
role: s.role || null,
|
|
45
|
+
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
46
|
+
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
|
|
50
|
+
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
51
|
+
|
|
52
|
+
// Imports / importedBy
|
|
53
|
+
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
54
|
+
|
|
55
|
+
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
56
|
+
|
|
57
|
+
// Intra-file data flow
|
|
58
|
+
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
59
|
+
|
|
60
|
+
const dataFlowMap = new Map();
|
|
61
|
+
for (const edge of intraEdges) {
|
|
62
|
+
if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
|
|
63
|
+
dataFlowMap.get(edge.caller_name).push(edge.callee_name);
|
|
64
|
+
}
|
|
65
|
+
const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
|
|
66
|
+
caller,
|
|
67
|
+
callees,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
// Line count: prefer node_metrics (actual), fall back to MAX(end_line)
|
|
71
|
+
const metric = db
|
|
72
|
+
.prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
|
|
73
|
+
.get(fn.id);
|
|
74
|
+
let lineCount = metric?.line_count || null;
|
|
75
|
+
if (!lineCount) {
|
|
76
|
+
const maxLine = db
|
|
77
|
+
.prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
|
|
78
|
+
.get(fn.file);
|
|
79
|
+
lineCount = maxLine?.max_end || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
file: fn.file,
|
|
84
|
+
lineCount,
|
|
85
|
+
symbolCount: symbols.length,
|
|
86
|
+
publicApi,
|
|
87
|
+
internal,
|
|
88
|
+
imports,
|
|
89
|
+
importedBy,
|
|
90
|
+
dataFlow,
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
96
|
+
let nodes = db
|
|
97
|
+
.prepare(
|
|
98
|
+
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
|
|
99
|
+
)
|
|
100
|
+
.all(`%${target}%`);
|
|
101
|
+
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
102
|
+
if (nodes.length === 0) return [];
|
|
103
|
+
|
|
104
|
+
const hc = new Map();
|
|
105
|
+
return nodes.slice(0, 10).map((node) => {
|
|
106
|
+
const fileLines = getFileLines(node.file);
|
|
107
|
+
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
108
|
+
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
109
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
110
|
+
|
|
111
|
+
const callees = findCallees(db, node.id).map((c) => ({
|
|
112
|
+
name: c.name,
|
|
113
|
+
kind: c.kind,
|
|
114
|
+
file: c.file,
|
|
115
|
+
line: c.line,
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
let callers = findCallers(db, node.id).map((c) => ({
|
|
119
|
+
name: c.name,
|
|
120
|
+
kind: c.kind,
|
|
121
|
+
file: c.file,
|
|
122
|
+
line: c.line,
|
|
123
|
+
}));
|
|
124
|
+
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
125
|
+
|
|
126
|
+
const testCallerRows = findCallers(db, node.id);
|
|
127
|
+
const seenFiles = new Set();
|
|
128
|
+
const relatedTests = testCallerRows
|
|
129
|
+
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
130
|
+
.map((r) => ({ file: r.file }));
|
|
131
|
+
|
|
132
|
+
// Complexity metrics
|
|
133
|
+
let complexityMetrics = null;
|
|
134
|
+
try {
|
|
135
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
136
|
+
if (cRow) {
|
|
137
|
+
complexityMetrics = {
|
|
138
|
+
cognitive: cRow.cognitive,
|
|
139
|
+
cyclomatic: cRow.cyclomatic,
|
|
140
|
+
maxNesting: cRow.max_nesting,
|
|
141
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
142
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
/* table may not exist */
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
...normalizeSymbol(node, db, hc),
|
|
151
|
+
lineCount,
|
|
152
|
+
summary,
|
|
153
|
+
signature,
|
|
154
|
+
complexity: complexityMetrics,
|
|
155
|
+
callees,
|
|
156
|
+
callers,
|
|
157
|
+
relatedTests,
|
|
158
|
+
};
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Exported functions ──────────────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
export function contextData(name, customDbPath, opts = {}) {
|
|
165
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
166
|
+
try {
|
|
167
|
+
const depth = opts.depth || 0;
|
|
168
|
+
const noSource = opts.noSource || false;
|
|
169
|
+
const noTests = opts.noTests || false;
|
|
170
|
+
const includeTests = opts.includeTests || false;
|
|
171
|
+
|
|
172
|
+
const dbPath = findDbPath(customDbPath);
|
|
173
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
174
|
+
|
|
175
|
+
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
176
|
+
if (nodes.length === 0) {
|
|
177
|
+
return { name, results: [] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
181
|
+
|
|
182
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
183
|
+
|
|
184
|
+
const results = nodes.map((node) => {
|
|
185
|
+
const fileLines = getFileLines(node.file);
|
|
186
|
+
|
|
187
|
+
// Source
|
|
188
|
+
const source = noSource
|
|
189
|
+
? null
|
|
190
|
+
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
191
|
+
|
|
192
|
+
// Signature
|
|
193
|
+
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
194
|
+
|
|
195
|
+
// Callees
|
|
196
|
+
const calleeRows = findCallees(db, node.id);
|
|
197
|
+
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
198
|
+
|
|
199
|
+
const callees = filteredCallees.map((c) => {
|
|
200
|
+
const cLines = getFileLines(c.file);
|
|
201
|
+
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
202
|
+
let calleeSource = null;
|
|
203
|
+
if (depth >= 1) {
|
|
204
|
+
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
name: c.name,
|
|
208
|
+
kind: c.kind,
|
|
209
|
+
file: c.file,
|
|
210
|
+
line: c.line,
|
|
211
|
+
endLine: c.end_line || null,
|
|
212
|
+
summary,
|
|
213
|
+
source: calleeSource,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
218
|
+
if (depth > 1) {
|
|
219
|
+
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
220
|
+
visited.add(node.id);
|
|
221
|
+
let frontier = filteredCallees.map((c) => c.id);
|
|
222
|
+
const maxDepth = Math.min(depth, 5);
|
|
223
|
+
for (let d = 2; d <= maxDepth; d++) {
|
|
224
|
+
const nextFrontier = [];
|
|
225
|
+
for (const fid of frontier) {
|
|
226
|
+
const deeper = findCallees(db, fid);
|
|
227
|
+
for (const c of deeper) {
|
|
228
|
+
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
229
|
+
visited.add(c.id);
|
|
230
|
+
nextFrontier.push(c.id);
|
|
231
|
+
const cLines = getFileLines(c.file);
|
|
232
|
+
callees.push({
|
|
233
|
+
name: c.name,
|
|
234
|
+
kind: c.kind,
|
|
235
|
+
file: c.file,
|
|
236
|
+
line: c.line,
|
|
237
|
+
endLine: c.end_line || null,
|
|
238
|
+
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
239
|
+
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
frontier = nextFrontier;
|
|
245
|
+
if (frontier.length === 0) break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Callers
|
|
250
|
+
let callerRows = findCallers(db, node.id);
|
|
251
|
+
|
|
252
|
+
// Method hierarchy resolution
|
|
253
|
+
if (node.kind === 'method' && node.name.includes('.')) {
|
|
254
|
+
const methodName = node.name.split('.').pop();
|
|
255
|
+
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
256
|
+
for (const rm of relatedMethods) {
|
|
257
|
+
if (rm.id === node.id) continue;
|
|
258
|
+
const extraCallers = findCallers(db, rm.id);
|
|
259
|
+
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
263
|
+
|
|
264
|
+
const callers = callerRows.map((c) => ({
|
|
265
|
+
name: c.name,
|
|
266
|
+
kind: c.kind,
|
|
267
|
+
file: c.file,
|
|
268
|
+
line: c.line,
|
|
269
|
+
viaHierarchy: c.viaHierarchy || undefined,
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
// Related tests: callers that live in test files
|
|
273
|
+
const testCallerRows = findCallers(db, node.id);
|
|
274
|
+
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
275
|
+
|
|
276
|
+
const testsByFile = new Map();
|
|
277
|
+
for (const tc of testCallers) {
|
|
278
|
+
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
279
|
+
testsByFile.get(tc.file).push(tc);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const relatedTests = [];
|
|
283
|
+
for (const [file] of testsByFile) {
|
|
284
|
+
const tLines = getFileLines(file);
|
|
285
|
+
const testNames = [];
|
|
286
|
+
if (tLines) {
|
|
287
|
+
for (const tl of tLines) {
|
|
288
|
+
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
289
|
+
if (tm) testNames.push(tm[1]);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
293
|
+
relatedTests.push({
|
|
294
|
+
file,
|
|
295
|
+
testCount: testNames.length,
|
|
296
|
+
testNames,
|
|
297
|
+
source: testSource,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Complexity metrics
|
|
302
|
+
let complexityMetrics = null;
|
|
303
|
+
try {
|
|
304
|
+
const cRow = getComplexityForNode(db, node.id);
|
|
305
|
+
if (cRow) {
|
|
306
|
+
complexityMetrics = {
|
|
307
|
+
cognitive: cRow.cognitive,
|
|
308
|
+
cyclomatic: cRow.cyclomatic,
|
|
309
|
+
maxNesting: cRow.max_nesting,
|
|
310
|
+
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
311
|
+
halsteadVolume: cRow.halstead_volume || 0,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
} catch {
|
|
315
|
+
/* table may not exist */
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Children (parameters, properties, constants)
|
|
319
|
+
let nodeChildren = [];
|
|
320
|
+
try {
|
|
321
|
+
nodeChildren = findNodeChildren(db, node.id).map((c) => ({
|
|
322
|
+
name: c.name,
|
|
323
|
+
kind: c.kind,
|
|
324
|
+
line: c.line,
|
|
325
|
+
endLine: c.end_line || null,
|
|
326
|
+
}));
|
|
327
|
+
} catch {
|
|
328
|
+
/* parent_id column may not exist */
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
name: node.name,
|
|
333
|
+
kind: node.kind,
|
|
334
|
+
file: node.file,
|
|
335
|
+
line: node.line,
|
|
336
|
+
role: node.role || null,
|
|
337
|
+
endLine: node.end_line || null,
|
|
338
|
+
source,
|
|
339
|
+
signature,
|
|
340
|
+
complexity: complexityMetrics,
|
|
341
|
+
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
342
|
+
callees,
|
|
343
|
+
callers,
|
|
344
|
+
relatedTests,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const base = { name, results };
|
|
349
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
350
|
+
} finally {
|
|
351
|
+
db.close();
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function explainData(target, customDbPath, opts = {}) {
|
|
356
|
+
const db = openReadonlyOrFail(customDbPath);
|
|
357
|
+
try {
|
|
358
|
+
const noTests = opts.noTests || false;
|
|
359
|
+
const depth = opts.depth || 0;
|
|
360
|
+
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
361
|
+
|
|
362
|
+
const dbPath = findDbPath(customDbPath);
|
|
363
|
+
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
364
|
+
|
|
365
|
+
const getFileLines = createFileLinesReader(repoRoot);
|
|
366
|
+
|
|
367
|
+
const results =
|
|
368
|
+
kind === 'file'
|
|
369
|
+
? explainFileImpl(db, target, getFileLines)
|
|
370
|
+
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
371
|
+
|
|
372
|
+
// Recursive dependency explanation for function targets
|
|
373
|
+
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
374
|
+
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
375
|
+
|
|
376
|
+
function explainCallees(parentResults, currentDepth) {
|
|
377
|
+
if (currentDepth <= 0) return;
|
|
378
|
+
for (const r of parentResults) {
|
|
379
|
+
const newCallees = [];
|
|
380
|
+
for (const callee of r.callees) {
|
|
381
|
+
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
382
|
+
if (visited.has(key)) continue;
|
|
383
|
+
visited.add(key);
|
|
384
|
+
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
385
|
+
const exact = calleeResults.find(
|
|
386
|
+
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
387
|
+
);
|
|
388
|
+
if (exact) {
|
|
389
|
+
exact._depth = (r._depth || 0) + 1;
|
|
390
|
+
newCallees.push(exact);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (newCallees.length > 0) {
|
|
394
|
+
r.depDetails = newCallees;
|
|
395
|
+
explainCallees(newCallees, currentDepth - 1);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
explainCallees(results, depth);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const base = { target, kind, results };
|
|
404
|
+
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
405
|
+
} finally {
|
|
406
|
+
db.close();
|
|
407
|
+
}
|
|
408
|
+
}
|