depgraph-core 1.0.2 → 1.0.3
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 +181 -105
- package/depgraph-output.json +1273 -233
- package/depgraph.js +349 -66
- package/docs/README.md +56 -0
- package/docs/architecture.md +89 -0
- package/docs/data-types.md +218 -0
- package/docs/language-registry.md +205 -0
- package/docs/stage-collector.md +109 -0
- package/docs/stage-graph.md +157 -0
- package/docs/stage-impact.md +154 -0
- package/docs/stage-metrics.md +113 -0
- package/docs/stage-output.md +144 -0
- package/docs/stage-parser.md +144 -0
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Data Types Reference
|
|
2
|
+
|
|
3
|
+
> **File**: `src/types.ts`
|
|
4
|
+
> All shared TypeScript interfaces used across every stage of the pipeline.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Data Flow Summary
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
RawEntity ┐
|
|
12
|
+
RawImport ├──► ParsedFile ──► DepNode / DepEdge ──► DepGraph
|
|
13
|
+
┘ │
|
|
14
|
+
▼
|
|
15
|
+
AffectedNode ──► ImpactReport
|
|
16
|
+
│
|
|
17
|
+
▼
|
|
18
|
+
OutputJSON (written to disk)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Stage 1–2 Types (Parser Output)
|
|
24
|
+
|
|
25
|
+
### `RawEntity`
|
|
26
|
+
|
|
27
|
+
A code construct extracted directly from source — before any cross-file resolution.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
interface RawEntity {
|
|
31
|
+
name: string; // e.g. "getUserById"
|
|
32
|
+
type: string; // "function" | "class" | "component" | "hook" | "api" | "interface" | "type"
|
|
33
|
+
line: number; // 1-indexed line number in the source file
|
|
34
|
+
complexity: string; // "low" | "medium" | "high"
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Entity types by language:**
|
|
39
|
+
|
|
40
|
+
| Type | Languages | Example |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `function` | JS/TS, Python | `function doSomething()` |
|
|
43
|
+
| `class` | JS/TS, Python | `class UserService` |
|
|
44
|
+
| `component` | JS/TS (React) | `const Button = () =>` |
|
|
45
|
+
| `hook` | JS/TS (React) | `const useAuth = () =>` |
|
|
46
|
+
| `interface` | TypeScript | `interface User` |
|
|
47
|
+
| `type` | TypeScript | `type UserId = string` |
|
|
48
|
+
| `api` | JS/TS (Express) | `app.get('/users', ...)` |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
### `RawImport`
|
|
53
|
+
|
|
54
|
+
An import statement extracted from a source file.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface RawImport {
|
|
58
|
+
source: string; // the import path, e.g. "./userService" or "express"
|
|
59
|
+
names: string[]; // the names imported, e.g. ["getUserById", "createUser"]
|
|
60
|
+
isLocal: boolean; // true if source starts with "." (relative path)
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**Key distinction**: Only imports where `isLocal === true` become graph edges. External package imports (`"express"`, `"lodash"`, etc.) are ignored during graph construction.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
### `ParsedFile`
|
|
69
|
+
|
|
70
|
+
The complete output for one source file after parsing.
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
interface ParsedFile {
|
|
74
|
+
filePath: string; // absolute or relative path on disk
|
|
75
|
+
lang: string; // parser's lang id, e.g. "js" or "py"
|
|
76
|
+
lines: number; // total line count
|
|
77
|
+
entities: RawEntity[]; // all found code constructs
|
|
78
|
+
imports: RawImport[]; // all import statements
|
|
79
|
+
exports: string[]; // names of exported entities
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Stage 3–4 Types (Graph)
|
|
86
|
+
|
|
87
|
+
### `DepNode`
|
|
88
|
+
|
|
89
|
+
A fully resolved node in the dependency graph. Built from a `RawEntity`, enriched with graph metrics.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
interface DepNode {
|
|
93
|
+
id: string; // composite ID: "entityName__fileBase"
|
|
94
|
+
name: string; // entity name, e.g. "getUserById"
|
|
95
|
+
type: string; // same as RawEntity.type
|
|
96
|
+
file: string; // source file path
|
|
97
|
+
line: number; // source line number
|
|
98
|
+
lang: string; // language id
|
|
99
|
+
complexity: string; // "low" | "medium" | "high"
|
|
100
|
+
inDegree: number; // how many other nodes import this one
|
|
101
|
+
outDegree: number; // how many nodes this one imports
|
|
102
|
+
centralityScore: number; // inDegree * 2 + outDegree
|
|
103
|
+
connections: string[]; // IDs of directly connected nodes (both directions)
|
|
104
|
+
|
|
105
|
+
// Optional — only present for specific entity types:
|
|
106
|
+
extends?: string; // parent class/interface name
|
|
107
|
+
method?: string; // HTTP method for API routes, e.g. "GET"
|
|
108
|
+
route?: string; // URL path for API routes, e.g. "/api/users"
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Node ID format**: `getUserById__userService`
|
|
113
|
+
(entity name + `__` + file basename without extension, with non-alphanumeric chars replaced by `_`)
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### `DepEdge`
|
|
118
|
+
|
|
119
|
+
A directed link from one node to another, representing an import relationship.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface DepEdge {
|
|
123
|
+
from: string; // source node ID (the importer)
|
|
124
|
+
to: string; // target node ID (the imported entity)
|
|
125
|
+
type: string; // currently always "imports"
|
|
126
|
+
description: string; // human-readable, e.g. "UserRouter imports getUserById from userService"
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Direction convention**: `from → to` means "`from` depends on `to`".
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
### `DepGraph`
|
|
135
|
+
|
|
136
|
+
The complete graph.
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
interface DepGraph {
|
|
140
|
+
nodes: Map<string, DepNode>; // keyed by node ID
|
|
141
|
+
edges: DepEdge[];
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The `nodes` Map is converted to an Array when writing to JSON output.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Stage 5 Types (Impact)
|
|
150
|
+
|
|
151
|
+
### `AffectedNode`
|
|
152
|
+
|
|
153
|
+
A downstream node discovered during BFS traversal from the impact target.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
interface AffectedNode {
|
|
157
|
+
nodeId: string; // graph node ID
|
|
158
|
+
name: string; // entity name
|
|
159
|
+
file: string; // source file path
|
|
160
|
+
depth: number; // BFS depth from target (1 = direct dependent)
|
|
161
|
+
impact: string; // "critical" | "high" | "medium" | "low"
|
|
162
|
+
reason: string; // why this node is affected
|
|
163
|
+
changeRequired: string; // what action needs to be taken
|
|
164
|
+
breakingChange: boolean; // true if depth <= 2
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### `ImpactReport`
|
|
171
|
+
|
|
172
|
+
The full simulation result returned by `simulateImpact`.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
interface ImpactReport {
|
|
176
|
+
targetNode: string; // the node ID of the changed entity
|
|
177
|
+
changeDescription: string; // description from --impact flag
|
|
178
|
+
riskScore: number; // 0–100
|
|
179
|
+
riskLevel: string; // "LOW" | "MEDIUM" | "HIGH" | "CRITICAL"
|
|
180
|
+
affectedNodes: AffectedNode[]; // all downstream nodes
|
|
181
|
+
breakingChanges: AffectedNode[]; // subset where breakingChange === true
|
|
182
|
+
testingPlan: string[]; // recommended test actions
|
|
183
|
+
recommendations: string[]; // code review / deployment guidance
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Risk score formula**: `C×30 + H×15 + M×7 + L×2 + targetInDegree×3` (capped at 100)
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Output Type
|
|
192
|
+
|
|
193
|
+
### `OutputJSON`
|
|
194
|
+
|
|
195
|
+
The root schema of the generated `depgraph-output.json` file.
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
interface OutputJSON {
|
|
199
|
+
meta: {
|
|
200
|
+
version: string; // tool version, e.g. "1.0.0"
|
|
201
|
+
timestamp: string; // ISO 8601, e.g. "2024-01-15T09:30:00.000Z"
|
|
202
|
+
totalFiles: number;
|
|
203
|
+
totalLines: number;
|
|
204
|
+
};
|
|
205
|
+
summary: {
|
|
206
|
+
totalNodes: number;
|
|
207
|
+
totalEdges: number;
|
|
208
|
+
entryPoints: string[]; // node IDs with inDegree=0 & outDegree>0
|
|
209
|
+
leafNodes: string[]; // node IDs with outDegree=0 & inDegree>0
|
|
210
|
+
isolatedNodes: string[]; // node IDs with both degrees = 0 (dead code)
|
|
211
|
+
criticalNodes: string[]; // node IDs with centralityScore > 20
|
|
212
|
+
};
|
|
213
|
+
nodes: DepNode[];
|
|
214
|
+
edges: DepEdge[];
|
|
215
|
+
files: ParsedFile[];
|
|
216
|
+
impact?: ImpactReport; // only present when --impact was used
|
|
217
|
+
}
|
|
218
|
+
```
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Language Registry
|
|
2
|
+
|
|
3
|
+
> **File**: `src/languages/registry.ts`
|
|
4
|
+
> **Exports**: `LanguageParser` (interface), `registerParser`, `getLanguageParser`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
The language registry is a **simple plugin system**. It solves one problem: the parser stage needs to know *how* to parse a `.py` file differently from a `.ts` file, without hardcoding language logic into the stage itself.
|
|
11
|
+
|
|
12
|
+
Any file that imports `registerParser` and calls it at module load time becomes a **language plugin**. The parser stage just calls `getLanguageParser(ext)` and gets back the right handler — it doesn't know or care which language it's dealing with.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The `LanguageParser` Interface
|
|
17
|
+
|
|
18
|
+
This is the contract every language plugin must fulfil:
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
export interface LanguageParser {
|
|
22
|
+
/** A short unique identifier for the language, e.g. "js", "py". */
|
|
23
|
+
lang: string;
|
|
24
|
+
|
|
25
|
+
/** File extensions this parser handles, e.g. [".js", ".ts", ".tsx"]. */
|
|
26
|
+
extensions: string[];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract named code entities (functions, classes, routes, etc.)
|
|
30
|
+
* from clean source code (single-line comments already stripped).
|
|
31
|
+
*/
|
|
32
|
+
extractEntities: (code: string, filePath: string) => RawEntity[];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract import statements from clean source code.
|
|
36
|
+
*/
|
|
37
|
+
extractImports: (code: string) => RawImport[];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract the names of exported entities from clean source code.
|
|
41
|
+
*/
|
|
42
|
+
extractExports: (code: string) => string[];
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## How Registration Works
|
|
49
|
+
|
|
50
|
+
The registry stores parsers in a private module-level array:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const parsers: LanguageParser[] = [];
|
|
54
|
+
|
|
55
|
+
export function registerParser(parser: LanguageParser): void {
|
|
56
|
+
parsers.push(parser);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getLanguageParser(ext: string): LanguageParser | null {
|
|
60
|
+
return parsers.find(p => p.extensions.includes(ext)) ?? null;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's the entire registry — an array and two functions.
|
|
65
|
+
|
|
66
|
+
### Lookup
|
|
67
|
+
|
|
68
|
+
`getLanguageParser(".ts")` returns the first registered parser whose `extensions` array includes `".ts"`. If none match, it returns `null` and the file is skipped by the parser stage.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## How Plugins Self-Register
|
|
73
|
+
|
|
74
|
+
Each language file (e.g. `src/languages/javascript.ts`) defines a parser object and calls `registerParser` at the **bottom of the file**, at module scope:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const JavaScriptParser: LanguageParser = {
|
|
78
|
+
lang: 'js',
|
|
79
|
+
extensions: ['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'],
|
|
80
|
+
extractEntities,
|
|
81
|
+
extractImports,
|
|
82
|
+
extractExports,
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
registerParser(JavaScriptParser);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
This code runs **once**, when the module is first imported.
|
|
89
|
+
|
|
90
|
+
### Triggering registration
|
|
91
|
+
|
|
92
|
+
Plugins must be imported somewhere in the application for their side-effect (`registerParser(...)`) to run. In `src/main.ts`:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import './languages/javascript';
|
|
96
|
+
import './languages/python';
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
These are **side-effect imports** (no named export is consumed). They exist purely to execute the registration call.
|
|
100
|
+
|
|
101
|
+
> If you add a new language parser file but forget to import it in `main.ts`, it will never be registered and its extensions will never be recognised.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Currently Registered Languages
|
|
106
|
+
|
|
107
|
+
| File | `lang` | Extensions |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| `javascript.ts` | `js` | `.js` `.jsx` `.mjs` `.cjs` `.ts` `.tsx` |
|
|
110
|
+
| `python.ts` | `py` | `.py` |
|
|
111
|
+
|
|
112
|
+
The following language files exist as stubs (imported but with minimal or no implementation yet):
|
|
113
|
+
|
|
114
|
+
- `java.ts`
|
|
115
|
+
- `csharp.ts`
|
|
116
|
+
- `go.ts`
|
|
117
|
+
- `kotlin.ts`
|
|
118
|
+
- `php.ts`
|
|
119
|
+
- `ruby.ts`
|
|
120
|
+
- `swift.ts`
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## How to Add a New Language
|
|
125
|
+
|
|
126
|
+
Follow these steps to add full support for a new language.
|
|
127
|
+
|
|
128
|
+
### 1. Create the parser file
|
|
129
|
+
|
|
130
|
+
Create `src/languages/mylang.ts`:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { RawEntity, RawImport } from '../types';
|
|
134
|
+
import { LanguageParser, registerParser } from './registry';
|
|
135
|
+
|
|
136
|
+
function extractEntities(code: string, filePath: string): RawEntity[] {
|
|
137
|
+
const entities: RawEntity[] = [];
|
|
138
|
+
// Use regex or any other technique to find functions, classes, etc.
|
|
139
|
+
// Populate and return the entities array.
|
|
140
|
+
return entities;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractImports(code: string): RawImport[] {
|
|
144
|
+
const imports: RawImport[] = [];
|
|
145
|
+
// Parse import/require/use statements.
|
|
146
|
+
// Set isLocal: true for relative imports ('./something').
|
|
147
|
+
return imports;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function extractExports(code: string): string[] {
|
|
151
|
+
// Return an array of exported entity names.
|
|
152
|
+
return [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const MyLangParser: LanguageParser = {
|
|
156
|
+
lang: 'mylang',
|
|
157
|
+
extensions: ['.ml', '.mls'],
|
|
158
|
+
extractEntities,
|
|
159
|
+
extractImports,
|
|
160
|
+
extractExports,
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
registerParser(MyLangParser);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 2. Import it in `main.ts`
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import './languages/mylang';
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Add this **before** the pipeline starts (with the other language imports at the top of the file).
|
|
173
|
+
|
|
174
|
+
### 3. Ensure the extension is in `SUPPORTED_EXTS`
|
|
175
|
+
|
|
176
|
+
In `src/constants.ts`, add your extension to the set:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
export const SUPPORTED_EXTS = new Set([
|
|
180
|
+
// ... existing extensions
|
|
181
|
+
'.ml', '.mls',
|
|
182
|
+
]);
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Without this, the collector will never hand files with that extension to the parser in the first place.
|
|
186
|
+
|
|
187
|
+
### 4. Test it
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
node depgraph.js ./path/to/mylang-project --verbose
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Look for your files appearing in the verbose output and entities showing up in the summary.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
## Rules for Writing a Good Parser
|
|
198
|
+
|
|
199
|
+
| Rule | Why |
|
|
200
|
+
|---|---|
|
|
201
|
+
| Only use `code` (the cleaned version) — never read files | The stage already strips comments and passes clean code |
|
|
202
|
+
| Reset `regex.lastIndex = 0` before each `exec` loop | Global regexes maintain state; forgetting this causes skipped matches |
|
|
203
|
+
| Guard against duplicates (`entities.some(e => e.name === name)`) | The graph stage skips duplicates, but clean data is better upstream |
|
|
204
|
+
| Mark relative imports with `isLocal: true` | Only local imports become graph edges; external packages are ignored |
|
|
205
|
+
| Keep extraction logic regex-based or simple string parsing | The project has zero AST dependencies on purpose — keep it that way |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Stage 1 — Collector
|
|
2
|
+
|
|
3
|
+
> **File**: `src/stages/collector.ts`
|
|
4
|
+
> **Exports**: `collectFiles(dir: string): string[]`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What It Does
|
|
9
|
+
|
|
10
|
+
The collector is the very first thing that runs. Its job is simple: **walk a directory tree and return a flat list of file paths** that the rest of the pipeline should process.
|
|
11
|
+
|
|
12
|
+
It does **not** read file contents — it only looks at names, extensions, and file sizes.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
### 1. Recursive directory walk
|
|
19
|
+
|
|
20
|
+
`collectFiles` calls an internal `walk()` helper which:
|
|
21
|
+
|
|
22
|
+
1. Reads the entries of the current directory with `fs.readdirSync`.
|
|
23
|
+
2. For each entry:
|
|
24
|
+
- If it's a **directory**, check it against `IGNORE_DIRS`. If not ignored, recurse into it.
|
|
25
|
+
- If it's a **file**, check its extension against `SUPPORTED_EXTS` and its size against `MAX_FILE_SIZE`.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
if (stat.isDirectory()) {
|
|
29
|
+
if (!IGNORE_DIRS.has(entry)) walk(fullPath);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ext = path.extname(entry);
|
|
34
|
+
if (SUPPORTED_EXTS.has(ext) && stat.size < MAX_FILE_SIZE) {
|
|
35
|
+
results.push(fullPath);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Ignored directories
|
|
40
|
+
|
|
41
|
+
Defined in `src/constants.ts`:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
export const IGNORE_DIRS = new Set([
|
|
45
|
+
'node_modules', '.git', 'dist', 'build',
|
|
46
|
+
'.next', '__pycache__', 'vendor', 'venv',
|
|
47
|
+
'target', 'out', 'coverage', '.cache'
|
|
48
|
+
]);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
These are skipped entirely — the walker never descends into them.
|
|
52
|
+
|
|
53
|
+
### 3. Supported extensions
|
|
54
|
+
|
|
55
|
+
Also from constants:
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
export const SUPPORTED_EXTS = new Set([
|
|
59
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
60
|
+
'.py', '.go', '.java', '.cs', '.rb',
|
|
61
|
+
'.php', '.swift', '.kt', '.vue', '.svelte'
|
|
62
|
+
]);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
> **Note**: Collecting a file with `.java` doesn't mean it will be *parsed* successfully. The collector just gathers eligible paths. If no language parser is registered for that extension, the parser stage silently drops it.
|
|
66
|
+
|
|
67
|
+
### 4. Size guard
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
export const MAX_FILE_SIZE = 300_000; // 300 KB
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Files over 300 KB are skipped. This prevents the scanner from choking on minified bundles, lock files accidentally using a supported extension, or generated code.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Error Handling
|
|
78
|
+
|
|
79
|
+
The collector is fault-tolerant at the entry level:
|
|
80
|
+
- If a directory can't be read (permissions, broken symlink), it logs a warning and continues.
|
|
81
|
+
- If `fs.statSync` fails on an entry, it skips that entry and continues.
|
|
82
|
+
|
|
83
|
+
It **never throws** — it always returns whatever it managed to collect.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Output
|
|
88
|
+
|
|
89
|
+
A flat `string[]` of absolute file paths, for example:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
[
|
|
93
|
+
"/project/src/main.ts",
|
|
94
|
+
"/project/src/utils/auth.ts",
|
|
95
|
+
"/project/src/routes/users.ts"
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Configuration Quick Reference
|
|
102
|
+
|
|
103
|
+
| Constant | Default | Description |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| `IGNORE_DIRS` | See above | Directory names to skip entirely |
|
|
106
|
+
| `SUPPORTED_EXTS` | See above | File extensions that are eligible |
|
|
107
|
+
| `MAX_FILE_SIZE` | `300_000` (300 KB) | Maximum file size in bytes |
|
|
108
|
+
|
|
109
|
+
All constants live in [`src/constants.ts`](../src/constants.ts).
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Stage 3 — Graph Builder
|
|
2
|
+
|
|
3
|
+
> **File**: `src/stages/graph.ts`
|
|
4
|
+
> **Exports**: `buildGraph(parsedFiles: ParsedFile[]): DepGraph`
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## What It Does
|
|
9
|
+
|
|
10
|
+
The graph builder takes the flat list of `ParsedFile` objects and turns it into a **directed dependency graph**: a collection of **nodes** (entities) connected by **edges** (import relationships).
|
|
11
|
+
|
|
12
|
+
After this stage, you know exactly which entity depends on which other entity, across the entire codebase.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## The Three-Step Build Process
|
|
17
|
+
|
|
18
|
+
### Step 1 — Create a node for every entity
|
|
19
|
+
|
|
20
|
+
Every `RawEntity` found in every file becomes a `DepNode` in the graph:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
for (const file of parsedFiles) {
|
|
24
|
+
const fileBase = path.basename(file.filePath, path.extname(file.filePath));
|
|
25
|
+
|
|
26
|
+
for (const entity of file.entities) {
|
|
27
|
+
const id = makeId(entity.name, fileBase); // e.g. "getUserById__userService"
|
|
28
|
+
|
|
29
|
+
if (nodes.has(id)) continue; // skip duplicates
|
|
30
|
+
|
|
31
|
+
nodes.set(id, {
|
|
32
|
+
id,
|
|
33
|
+
name: entity.name,
|
|
34
|
+
type: entity.type,
|
|
35
|
+
file: file.filePath,
|
|
36
|
+
line: entity.line,
|
|
37
|
+
lang: file.lang,
|
|
38
|
+
complexity: entity.complexity,
|
|
39
|
+
inDegree: 0,
|
|
40
|
+
outDegree: 0,
|
|
41
|
+
centralityScore: 0,
|
|
42
|
+
connections: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Node IDs** are composite strings: `entityName__fileBasename`.
|
|
49
|
+
For example, the function `getUserById` in `src/services/userService.ts` gets ID `getUserById__userService`.
|
|
50
|
+
|
|
51
|
+
This scheme prevents collisions when two files have a function with the same name.
|
|
52
|
+
|
|
53
|
+
### Step 2 — Build a file path lookup map
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const fileMap = new Map<string, ParsedFile>();
|
|
57
|
+
for (const file of parsedFiles) {
|
|
58
|
+
fileMap.set(file.filePath, file);
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
This allows O(1) lookup from a resolved file path → its `ParsedFile` data.
|
|
63
|
+
|
|
64
|
+
### Step 3 — Connect nodes through imports
|
|
65
|
+
|
|
66
|
+
This is where the real graph-building happens. For every file:
|
|
67
|
+
|
|
68
|
+
1. Iterate over its `imports`.
|
|
69
|
+
2. Skip **external** imports (packages like `express`, `lodash`) — only local imports matter.
|
|
70
|
+
3. **Resolve** the import path to an actual file on disk (see path resolution below).
|
|
71
|
+
4. For each imported name, find the matching entity in the target file.
|
|
72
|
+
5. Create an edge from **every entity in the importing file** to the **matching target entity**.
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
edges.push({
|
|
76
|
+
from: fromId,
|
|
77
|
+
to: toId,
|
|
78
|
+
type: 'imports',
|
|
79
|
+
description: `${fromEntity.name} imports ${importedName} from ${path.basename(resolvedPath)}`,
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Duplicate edges are guarded against before insertion.
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
## Path Resolution
|
|
88
|
+
|
|
89
|
+
Import paths like `"./userService"` don't include an extension. The `resolvePath` helper tries these candidates in priority order:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
./userService
|
|
93
|
+
./userService.ts
|
|
94
|
+
./userService.tsx
|
|
95
|
+
./userService.js
|
|
96
|
+
./userService.jsx
|
|
97
|
+
./userService/index.ts
|
|
98
|
+
./userService/index.js
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It finds the first candidate that matches a known file in `parsedFiles` and returns that path. If nothing matches, the import is silently skipped.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Output Shape — `DepGraph`
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
interface DepGraph {
|
|
109
|
+
nodes: Map<string, DepNode>;
|
|
110
|
+
edges: DepEdge[];
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
At this point, all `inDegree`, `outDegree`, and `centralityScore` fields on each node are still `0`. The **Metrics stage** fills these in (see [stage-metrics.md](./stage-metrics.md)).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Edge Direction Convention
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
FROM → TO
|
|
122
|
+
(importer) (imported)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
If `routes/users.ts` imports `getUserById` from `services/userService.ts`, the edge is:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
GET /api/users__users → getUserById__userService
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
This forward-direction convention means: **following edges = following dependencies**.
|
|
132
|
+
For impact analysis (who depends on me?), edges are traversed **in reverse** (see [stage-impact.md](./stage-impact.md)).
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## The `makeId` Helper
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
function makeId(name: string, fileBase: string): string {
|
|
140
|
+
const cleanName = name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
141
|
+
const cleanBase = fileBase.replace(/[^a-zA-Z0-9]/g, '_');
|
|
142
|
+
return `${cleanName}__${cleanBase}`;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Non-alphanumeric characters (e.g. spaces, slashes in route paths like `GET /api/users`) are replaced with underscores. This keeps IDs safe for use as JSON keys and stable across runs.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Common Gotchas
|
|
151
|
+
|
|
152
|
+
| Situation | Behaviour |
|
|
153
|
+
|---|---|
|
|
154
|
+
| Two files export the same function name | First one wins; second is skipped (duplicate `id` guard) |
|
|
155
|
+
| Import resolves to a file with no entities | The import is dropped — nothing to link to |
|
|
156
|
+
| Circular imports (A imports B, B imports A) | Both edges are created; no infinite loop — the graph is built in a single linear pass |
|
|
157
|
+
| External package imports | Skipped via the `isLocal` flag on `RawImport` |
|