@chappibunny/repolens 0.6.4 โ†’ 0.8.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  All notable changes to RepoLens will be documented in this file.
4
4
 
5
+ ## 0.8.0
6
+
7
+ ### โœจ New Features
8
+ - **GraphQL Schema Detection** (`src/analyzers/graphql-analyzer.js`): Detects `.graphql`/`.gql` schema files, inline SDL via gql tagged templates, 11 library patterns (Apollo, Yoga, Mercurius, Nexus, Pothos, type-graphql, Relay, urql, etc.), and resolver patterns. Parses queries, mutations, subscriptions, object types, enums, inputs, interfaces, unions, scalars, and directives.
9
+ - **TypeScript Type Graph Analysis** (`src/analyzers/typescript-analyzer.js`): Parses interfaces (with extends), type aliases (with reference extraction), classes (extends + implements), enums, and generic constraints. Builds relationship graph with deduplication, filtering to project-declared types only.
10
+ - **Dependency Graph with Cycle Detection** (`src/analyzers/dependency-graph.js`): Parses ES imports, dynamic imports, CommonJS require, and re-exports. Builds directed adjacency graph with cycle detection via iterative DFS (3-color marking). Tracks hub modules, orphan files, and external dependencies.
11
+ - **Architecture Drift Detection** (`src/analyzers/drift-detector.js`): Compares current architecture against stored baseline snapshots. Detects changes across 8 categories (modules, APIs, pages, dependencies, frameworks, circular deps, GraphQL types, file count). Categorizes drifts by severity: ๐Ÿ”ด critical, ๐ŸŸก warning, ๐ŸŸข info. Baselines auto-saved to `.repolens/architecture-baseline.json`.
12
+ - **Extended Analysis Renderers** (`src/renderers/renderAnalysis.js`): Markdown renderers for all 4 new document types with tables, Unicode relationship trees, and severity-grouped reports.
13
+ - **4 New Document Types**: `graphql_schema`, `type_graph`, `dependency_graph`, `architecture_drift` โ€” bringing total to 15 audience-aware documents.
14
+
15
+ ### ๐Ÿงช Tests
16
+ - Added 31 new tests for extended analysis features (121 tests across 12 files)
17
+
18
+ ## 0.7.0
19
+
20
+ ### โœจ New Features
21
+ - **Interactive Init Wizard**: `repolens init --interactive` โ€” step-by-step configuration wizard with scan presets (Next.js, Express, generic), publisher selection, AI provider setup, and branch filtering
22
+ - **Watch Mode**: `repolens watch` โ€” watches source directories for changes and regenerates Markdown docs with 500ms debounce (no API calls)
23
+ - **Enhanced Error Messages**: Centralized error catalog with actionable guidance โ€” every error now shows what went wrong, why, and how to fix it
24
+ - **Performance Monitoring**: Scan, render, and publish timing summary printed after every `publish` run
25
+ - **Coverage Scoring Improvements**: New section completeness metric (12 document types tracked), updated health score weights, and `metrics.json` snapshots saved to `.repolens/`
26
+
5
27
  ## 0.6.4
6
28
 
7
29
  ### ๐Ÿ”ง Maintenance
package/README.md CHANGED
@@ -1,7 +1,3 @@
1
- <p align="center">
2
- <img src="Avatar.png" alt="RepoLens" width="120" />
3
- </p>
4
-
5
1
  ```
6
2
  โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•— โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—
7
3
  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•โ–ˆโ–ˆโ–ˆโ–ˆโ•— โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•”โ•โ•โ•โ•โ•
@@ -9,7 +5,7 @@
9
5
  โ–ˆโ–ˆโ•”โ•โ•โ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•”โ•โ•โ•โ• โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•”โ•โ•โ• โ–ˆโ–ˆโ•‘โ•šโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘โ•šโ•โ•โ•โ•โ–ˆโ–ˆโ•‘
10
6
  โ–ˆโ–ˆโ•‘ โ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•”โ•โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•—โ–ˆโ–ˆโ•‘ โ•šโ–ˆโ–ˆโ–ˆโ–ˆโ•‘โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ•‘
11
7
  โ•šโ•โ• โ•šโ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•โ• โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•โ•šโ•โ• โ•šโ•โ•โ•โ•โ•šโ•โ•โ•โ•โ•โ•โ•
12
- ๐Ÿ” Repository Intelligence CLI ๐Ÿ“Š
8
+ Repository Intelligence CLI
13
9
  ```
14
10
 
15
11
  [![Tests](https://img.shields.io/badge/tests-90%20passing-brightgreen)](https://github.com/CHAPIBUNNY/repolens/actions)
@@ -20,7 +16,7 @@
20
16
 
21
17
  AI-assisted documentation intelligence system that generates architecture docs for engineers AND readable system docs for stakeholders
22
18
 
23
- **Current Status**: v0.6.4 โ€” User Feedback & Team Features
19
+ **Current Status**: v0.8.0 โ€” Extended Analysis
24
20
 
25
21
  RepoLens automatically generates and maintains living architecture documentation by analyzing your repository structure, extracting meaningful insights from your package.json, and creating visual dependency graphs. Run it once, or let it auto-update on every push.
26
22
 
@@ -132,7 +128,15 @@ RepoLens automatically detects:
132
128
  โœ… **Branch-Aware** - Prevent doc conflicts across branches
133
129
  โœ… **GitHub Actions** - Autonomous operation on every push
134
130
  โœ… **Team Notifications** - Discord integration with rich embeds (NEW in v0.6.0)
135
- โœ… **Health Score Tracking** - Monitor documentation quality over time (NEW in v0.6.0)
131
+ โœ… **Health Score Tracking** - Monitor documentation quality over time
132
+ โœ… **Watch Mode** - Auto-regenerate docs on file changes (NEW in v0.7.0)
133
+ โœ… **Interactive Setup** - Step-by-step configuration wizard (NEW in v0.7.0)
134
+ โœ… **Performance Metrics** - Timing breakdown for scan/render/publish (NEW in v0.7.0)
135
+ โœ… **Actionable Errors** - Enhanced error messages with fix guidance (NEW in v0.7.0)
136
+ โœ… **GraphQL Schema Detection** - Discover types, queries, mutations, and resolvers (NEW in v0.8.0)
137
+ โœ… **TypeScript Type Graph** - Map interfaces, classes, and type relationships (NEW in v0.8.0)
138
+ โœ… **Dependency Graph** - Import analysis with circular dependency detection (NEW in v0.8.0)
139
+ โœ… **Architecture Drift** - Track structural changes against a baseline (NEW in v0.8.0)
136
140
 
137
141
  ---
138
142
 
@@ -224,7 +228,7 @@ npm link
224
228
  Install from a specific version:
225
229
 
226
230
  ```bash
227
- npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v0.6.4/chappibunny-repolens-0.6.4.tgz
231
+ npm install https://github.com/CHAPIBUNNY/repolens/releases/download/v0.8.0/chappibunny-repolens-0.8.0.tgz
228
232
  ```
229
233
  </details>
230
234
 
@@ -1077,7 +1081,7 @@ Simulates the full user installation experience:
1077
1081
  npm pack
1078
1082
 
1079
1083
  # Install globally from tarball
1080
- npm install -g chappibunny-repolens-0.6.4.tgz
1084
+ npm install -g chappibunny-repolens-0.8.0.tgz
1081
1085
 
1082
1086
  # Verify
1083
1087
  repolens --version
@@ -1091,18 +1095,23 @@ repolens/
1091
1095
  โ”‚ โ””โ”€โ”€ repolens.js # CLI executable wrapper
1092
1096
  โ”œโ”€โ”€ src/
1093
1097
  โ”‚ โ”œโ”€โ”€ cli.js # Command orchestration + banner
1094
- โ”‚ โ”œโ”€โ”€ init.js # Scaffolding command
1098
+ โ”‚ โ”œโ”€โ”€ init.js # Scaffolding command (+ interactive wizard)
1095
1099
  โ”‚ โ”œโ”€โ”€ doctor.js # Validation command
1096
1100
  โ”‚ โ”œโ”€โ”€ migrate.js # Workflow migration (legacy โ†’ current)
1101
+ โ”‚ โ”œโ”€โ”€ watch.js # Watch mode for local development
1097
1102
  โ”‚ โ”œโ”€โ”€ core/
1098
1103
  โ”‚ โ”‚ โ”œโ”€โ”€ config.js # Config loading + validation
1099
1104
  โ”‚ โ”‚ โ”œโ”€โ”€ config-schema.js # Schema version tracking
1100
1105
  โ”‚ โ”‚ โ”œโ”€โ”€ diff.js # Git diff operations
1101
1106
  โ”‚ โ”‚ โ””โ”€โ”€ scan.js # Repository scanning + metadata extraction
1102
1107
  โ”‚ โ”œโ”€โ”€ analyzers/
1103
- โ”‚ โ”‚ โ”œโ”€โ”€ domain-inference.js # Business domain mapping
1104
- โ”‚ โ”‚ โ”œโ”€โ”€ context-builder.js # Structured AI context assembly
1105
- โ”‚ โ”‚ โ””โ”€โ”€ flow-inference.js # Data flow detection
1108
+ โ”‚ โ”‚ โ”œโ”€โ”€ domain-inference.js # Business domain mapping
1109
+ โ”‚ โ”‚ โ”œโ”€โ”€ context-builder.js # Structured AI context assembly
1110
+ โ”‚ โ”‚ โ”œโ”€โ”€ flow-inference.js # Data flow detection
1111
+ โ”‚ โ”‚ โ”œโ”€โ”€ graphql-analyzer.js # GraphQL schema detection
1112
+ โ”‚ โ”‚ โ”œโ”€โ”€ typescript-analyzer.js # TypeScript type graph analysis
1113
+ โ”‚ โ”‚ โ”œโ”€โ”€ dependency-graph.js # Import graph with cycle detection
1114
+ โ”‚ โ”‚ โ””โ”€โ”€ drift-detector.js # Architecture drift detection
1106
1115
  โ”‚ โ”œโ”€โ”€ ai/
1107
1116
  โ”‚ โ”‚ โ”œโ”€โ”€ provider.js # Provider-agnostic AI generation
1108
1117
  โ”‚ โ”‚ โ”œโ”€โ”€ prompts.js # Strict prompt templates
@@ -1112,9 +1121,10 @@ repolens/
1112
1121
  โ”‚ โ”‚ โ”œโ”€โ”€ generate-doc-set.js # Document generation orchestration
1113
1122
  โ”‚ โ”‚ โ””โ”€โ”€ write-doc-set.js # Write docs to disk
1114
1123
  โ”‚ โ”œโ”€โ”€ renderers/
1115
- โ”‚ โ”‚ โ”œโ”€โ”€ render.js # System overview, catalog, API, routes
1116
- โ”‚ โ”‚ โ”œโ”€โ”€ renderDiff.js # Architecture diff rendering
1117
- โ”‚ โ”‚ โ””โ”€โ”€ renderMap.js # Unicode dependency diagrams
1124
+ โ”‚ โ”‚ โ”œโ”€โ”€ render.js # System overview, catalog, API, routes
1125
+ โ”‚ โ”‚ โ”œโ”€โ”€ renderDiff.js # Architecture diff rendering
1126
+ โ”‚ โ”‚ โ”œโ”€โ”€ renderMap.js # Unicode dependency diagrams
1127
+ โ”‚ โ”‚ โ””โ”€โ”€ renderAnalysis.js # Extended analysis renderers
1118
1128
  โ”‚ โ”œโ”€โ”€ publishers/
1119
1129
  โ”‚ โ”‚ โ”œโ”€โ”€ index.js # Publisher orchestration + branch filtering
1120
1130
  โ”‚ โ”‚ โ”œโ”€โ”€ publish.js # Publishing pipeline
@@ -1133,9 +1143,10 @@ repolens/
1133
1143
  โ”‚ โ”œโ”€โ”€ metrics.js # Documentation coverage & health scoring
1134
1144
  โ”‚ โ”œโ”€โ”€ rate-limit.js # Token bucket rate limiter for APIs
1135
1145
  โ”‚ โ”œโ”€โ”€ secrets.js # Secret detection & sanitization
1136
- โ”‚ โ”œโ”€โ”€ telemetry.js # Opt-in error tracking (Sentry)
1146
+ โ”‚ โ”œโ”€โ”€ telemetry.js # Opt-in error tracking + performance timers
1147
+ โ”‚ โ”œโ”€โ”€ errors.js # Enhanced error messages with guidance
1137
1148
  โ”‚ โ””โ”€โ”€ update-check.js # Version update notifications
1138
- โ”œโ”€โ”€ tests/ # Vitest test suite (90 tests across 11 files)
1149
+ โ”œโ”€โ”€ tests/ # Vitest test suite (121 tests across 12 files)
1139
1150
  โ”œโ”€โ”€ .repolens.yml # Dogfooding config
1140
1151
  โ”œโ”€โ”€ package.json
1141
1152
  โ”œโ”€โ”€ CHANGELOG.md
@@ -1152,13 +1163,13 @@ RepoLens uses automated GitHub Actions releases.
1152
1163
  ### Creating a Release
1153
1164
 
1154
1165
  ```bash
1155
- # Patch version (0.6.4 โ†’ 0.6.5) - Bug fixes
1166
+ # Patch version (0.8.0 โ†’ 0.8.1) - Bug fixes
1156
1167
  npm run release:patch
1157
1168
 
1158
- # Minor version (0.6.4 โ†’ 0.7.0) - New features
1169
+ # Minor version (0.8.0 โ†’ 0.9.0) - New features
1159
1170
  npm run release:minor
1160
1171
 
1161
- # Major version (0.6.4 โ†’ 1.0.0) - Breaking changes
1172
+ # Major version (0.8.0 โ†’ 1.0.0) - Breaking changes
1162
1173
  npm run release:major
1163
1174
 
1164
1175
  # Push the tag to trigger workflow
@@ -1190,11 +1201,11 @@ RepoLens is currently in early access. v1.0 will open for community contribution
1190
1201
 
1191
1202
  ## ๐Ÿ—บ๏ธ Roadmap to v1.0
1192
1203
 
1193
- **Current Status:** v0.6.4 โ€” User Feedback & Team Features
1204
+ **Current Status:** v0.8.0 โ€” Extended Analysis
1194
1205
 
1195
1206
  ### Completed โœ…
1196
1207
 
1197
- - [x] CLI commands: `init`, `doctor`, `publish`, `migrate`, `feedback`, `version`, `help`
1208
+ - [x] CLI commands: `init`, `doctor`, `publish`, `migrate`, `watch`, `feedback`, `version`, `help`
1198
1209
  - [x] Config schema v1 with validation
1199
1210
  - [x] Auto-discovery of `.repolens.yml`
1200
1211
  - [x] Publishers: Notion + Confluence + Markdown
@@ -1206,7 +1217,7 @@ RepoLens is currently in early access. v1.0 will open for community contribution
1206
1217
  - [x] GitHub Actions automation (publish + release)
1207
1218
  - [x] PR architecture diff comments
1208
1219
  - [x] Performance guardrails (10k warning, 50k limit)
1209
- - [x] Comprehensive test suite (90 tests across 11 files)
1220
+ - [x] Comprehensive test suite (121 tests across 12 files)
1210
1221
  - [x] Security hardening (secret detection, injection prevention, fuzzing)
1211
1222
  - [x] Discord notifications with rich embeds
1212
1223
  - [x] Documentation coverage & health scoring
@@ -1214,14 +1225,19 @@ RepoLens is currently in early access. v1.0 will open for community contribution
1214
1225
  - [x] npm registry publication (`@chappibunny/repolens`)
1215
1226
  - [x] Automated npm releases via GitHub Actions
1216
1227
  - [x] Workflow migration command (`repolens migrate`)
1228
+ - [x] Interactive configuration wizard (`repolens init --interactive`)
1229
+ - [x] Watch mode for local development (`repolens watch`)
1230
+ - [x] Enhanced error messages with actionable guidance
1231
+ - [x] Performance monitoring (scan/render/publish timing)
1232
+ - [x] Improved documentation coverage scoring
1233
+ - [x] GraphQL schema detection (queries, mutations, types, resolvers)
1234
+ - [x] TypeScript type graph analysis (interfaces, classes, relationships)
1235
+ - [x] Dependency graph with circular dependency detection
1236
+ - [x] Architecture drift detection (baseline comparison)
1217
1237
 
1218
1238
  ### Planned for v1.0 ๐ŸŽฏ
1219
1239
 
1220
1240
  - [ ] Plugin system for custom renderers
1221
- - [ ] GraphQL schema detection
1222
- - [ ] TypeScript type graph analysis
1223
- - [ ] Interactive configuration wizard
1224
- - [ ] Watch mode for local development
1225
1241
  - [ ] Additional publishers (GitHub Wiki, Obsidian)
1226
1242
 
1227
1243
  See [ROADMAP.md](./ROADMAP.md) for detailed planning.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chappibunny/repolens",
3
- "version": "0.6.4",
3
+ "version": "0.8.0",
4
4
  "description": "AI-assisted documentation intelligence system for technical and non-technical audiences",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -88,6 +88,38 @@ export const DOCUMENT_PLAN = [
88
88
  audience: "technical",
89
89
  ai: true,
90
90
  description: "Quick start guide for new developers"
91
+ },
92
+ {
93
+ key: "graphql_schema",
94
+ filename: "11-graphql-schema.md",
95
+ title: "GraphQL Schema",
96
+ audience: "technical",
97
+ ai: false,
98
+ description: "GraphQL types, queries, mutations, and resolver map"
99
+ },
100
+ {
101
+ key: "type_graph",
102
+ filename: "12-type-graph.md",
103
+ title: "TypeScript Type Graph",
104
+ audience: "technical",
105
+ ai: false,
106
+ description: "TypeScript interfaces, types, classes, and relationships"
107
+ },
108
+ {
109
+ key: "dependency_graph",
110
+ filename: "13-dependency-graph.md",
111
+ title: "Dependency Graph",
112
+ audience: "technical",
113
+ ai: false,
114
+ description: "Module dependency analysis with cycle detection"
115
+ },
116
+ {
117
+ key: "architecture_drift",
118
+ filename: "14-architecture-drift.md",
119
+ title: "Architecture Drift",
120
+ audience: "mixed",
121
+ ai: false,
122
+ description: "Structural changes compared to baseline snapshot"
91
123
  }
92
124
  ];
93
125
 
@@ -0,0 +1,254 @@
1
+ // Dependency graph analysis with cycle detection
2
+ // Parses import/require statements, builds a directed graph, detects cycles via DFS
3
+
4
+ import fs from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { info, warn } from "../utils/logger.js";
7
+
8
+ const CODE_EXTENSIONS = new Set([".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"]);
9
+
10
+ // Patterns for extracting imports
11
+ const IMPORT_PATTERNS = [
12
+ // ES module: import ... from "..."
13
+ /import\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g,
14
+ // Dynamic import: import("...")
15
+ /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
16
+ // CommonJS: require("...")
17
+ /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
18
+ // Re-export: export ... from "..."
19
+ /export\s+(?:[\w*{},\s]+\s+from\s+)?['"]([^'"]+)['"]/g,
20
+ ];
21
+
22
+ async function readFileSafe(filePath) {
23
+ try {
24
+ return await fs.readFile(filePath, "utf8");
25
+ } catch {
26
+ return "";
27
+ }
28
+ }
29
+
30
+ function isRelativeImport(specifier) {
31
+ return specifier.startsWith("./") || specifier.startsWith("../");
32
+ }
33
+
34
+ function resolveImportPath(importerFile, specifier) {
35
+ // Only resolve relative imports โ€” skip node_modules / bare specifiers
36
+ if (!isRelativeImport(specifier)) return null;
37
+
38
+ const importerDir = path.dirname(importerFile);
39
+ let resolved = path.posix.join(importerDir, specifier);
40
+
41
+ // Normalize path separators
42
+ resolved = resolved.replace(/\\/g, "/");
43
+
44
+ // Strip known extensions for dedup โ€” we normalize to extensionless keys
45
+ resolved = resolved.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, "");
46
+
47
+ // Strip /index suffix
48
+ resolved = resolved.replace(/\/index$/, "");
49
+
50
+ return resolved;
51
+ }
52
+
53
+ function normalizeFileKey(file) {
54
+ // Normalize a source file to a key that can match resolved imports
55
+ let key = file.replace(/\\/g, "/");
56
+ key = key.replace(/\.(js|ts|jsx|tsx|mjs|cjs)$/, "");
57
+ key = key.replace(/\/index$/, "");
58
+ return key;
59
+ }
60
+
61
+ export async function analyzeDependencyGraph(files, repoRoot) {
62
+ const result = {
63
+ nodes: [], // { key, file, imports: string[], importedBy: string[] }
64
+ edges: [], // { from, to }
65
+ cycles: [], // [ [a, b, c, a], ... ]
66
+ externalDeps: [],// sorted unique bare-specifier imports
67
+ stats: null,
68
+ summary: null,
69
+ };
70
+
71
+ const codeFiles = files.filter(f => {
72
+ const ext = path.extname(f);
73
+ return CODE_EXTENSIONS.has(ext);
74
+ });
75
+
76
+ if (codeFiles.length === 0) return result;
77
+
78
+ // Build adjacency: fileKey โ†’ Set<fileKey>
79
+ const adjacency = new Map();
80
+ const fileKeyToFile = new Map();
81
+ const allFileKeys = new Set();
82
+ const externalSet = new Set();
83
+
84
+ // Register all known file keys
85
+ for (const file of codeFiles) {
86
+ const key = normalizeFileKey(file);
87
+ allFileKeys.add(key);
88
+ fileKeyToFile.set(key, file);
89
+ if (!adjacency.has(key)) adjacency.set(key, new Set());
90
+ }
91
+
92
+ // Parse imports and build edges
93
+ for (const file of codeFiles) {
94
+ const content = await readFileSafe(path.join(repoRoot, file));
95
+ if (!content) continue;
96
+
97
+ const fromKey = normalizeFileKey(file);
98
+
99
+ for (const pattern of IMPORT_PATTERNS) {
100
+ // Reset lastIndex since we reuse regex objects
101
+ const regex = new RegExp(pattern.source, pattern.flags);
102
+ let match;
103
+ while ((match = regex.exec(content)) !== null) {
104
+ const specifier = match[1];
105
+
106
+ if (isRelativeImport(specifier)) {
107
+ const resolved = resolveImportPath(file, specifier);
108
+ if (resolved && allFileKeys.has(resolved)) {
109
+ adjacency.get(fromKey).add(resolved);
110
+ }
111
+ } else if (!specifier.startsWith("node:")) {
112
+ // Bare specifier โ†’ external dependency
113
+ // Normalize scoped packages: @scope/pkg/path โ†’ @scope/pkg
114
+ const parts = specifier.startsWith("@") ? specifier.split("/").slice(0, 2) : specifier.split("/").slice(0, 1);
115
+ externalSet.add(parts.join("/"));
116
+ }
117
+ }
118
+ }
119
+ }
120
+
121
+ // Build nodes and edges
122
+ const importedByMap = new Map();
123
+ for (const key of allFileKeys) {
124
+ importedByMap.set(key, []);
125
+ }
126
+
127
+ for (const [fromKey, deps] of adjacency) {
128
+ for (const toKey of deps) {
129
+ result.edges.push({ from: fromKey, to: toKey });
130
+ if (importedByMap.has(toKey)) {
131
+ importedByMap.get(toKey).push(fromKey);
132
+ }
133
+ }
134
+ }
135
+
136
+ for (const key of allFileKeys) {
137
+ result.nodes.push({
138
+ key,
139
+ file: fileKeyToFile.get(key) || key,
140
+ imports: [...(adjacency.get(key) || [])],
141
+ importedBy: importedByMap.get(key) || [],
142
+ });
143
+ }
144
+
145
+ result.externalDeps = [...externalSet].sort();
146
+
147
+ // Detect cycles using iterative DFS (Tarjan-lite / coloring)
148
+ result.cycles = detectCycles(adjacency);
149
+
150
+ // Stats
151
+ const totalImports = result.edges.length;
152
+ const orphans = result.nodes.filter(n => n.imports.length === 0 && n.importedBy.length === 0);
153
+ const hubThreshold = Math.max(5, Math.floor(result.nodes.length * 0.1));
154
+ const hubs = result.nodes
155
+ .filter(n => n.importedBy.length >= hubThreshold)
156
+ .sort((a, b) => b.importedBy.length - a.importedBy.length)
157
+ .slice(0, 10)
158
+ .map(n => ({ key: n.key, importedBy: n.importedBy.length }));
159
+
160
+ result.stats = {
161
+ totalFiles: codeFiles.length,
162
+ totalEdges: totalImports,
163
+ externalDeps: result.externalDeps.length,
164
+ cycles: result.cycles.length,
165
+ orphanFiles: orphans.length,
166
+ hubs,
167
+ };
168
+
169
+ result.summary = buildSummary(result);
170
+
171
+ if (result.cycles.length > 0) {
172
+ warn(`Dependency graph: ${result.cycles.length} circular dependency cycle(s) detected`);
173
+ } else {
174
+ info(`Dependency graph: ${totalImports} edges across ${codeFiles.length} files, no cycles`);
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Detect all cycles in a directed graph using DFS with 3-color marking.
182
+ * Returns an array of cycle paths (each path ends where it started).
183
+ */
184
+ function detectCycles(adjacency) {
185
+ const WHITE = 0, GRAY = 1, BLACK = 2;
186
+ const color = new Map();
187
+ const parent = new Map();
188
+ const cycles = [];
189
+
190
+ for (const node of adjacency.keys()) {
191
+ color.set(node, WHITE);
192
+ }
193
+
194
+ for (const startNode of adjacency.keys()) {
195
+ if (color.get(startNode) !== WHITE) continue;
196
+
197
+ // Iterative DFS
198
+ const stack = [{ node: startNode, neighbors: [...(adjacency.get(startNode) || [])], idx: 0 }];
199
+ color.set(startNode, GRAY);
200
+ parent.set(startNode, null);
201
+
202
+ while (stack.length > 0) {
203
+ const frame = stack[stack.length - 1];
204
+
205
+ if (frame.idx >= frame.neighbors.length) {
206
+ // All neighbors processed โ€” mark black and backtrack
207
+ color.set(frame.node, BLACK);
208
+ stack.pop();
209
+ continue;
210
+ }
211
+
212
+ const neighbor = frame.neighbors[frame.idx++];
213
+
214
+ if (color.get(neighbor) === GRAY) {
215
+ // Back edge โ†’ cycle found. Reconstruct the cycle path.
216
+ const cyclePath = [neighbor];
217
+ for (let i = stack.length - 1; i >= 0; i--) {
218
+ cyclePath.push(stack[i].node);
219
+ if (stack[i].node === neighbor) break;
220
+ }
221
+ cyclePath.reverse();
222
+ // Only keep reasonably short cycles to avoid noise
223
+ if (cyclePath.length <= 20) {
224
+ cycles.push(cyclePath);
225
+ }
226
+ // Cap cycles to prevent runaway in huge graphs
227
+ if (cycles.length >= 50) return cycles;
228
+ } else if (color.get(neighbor) === WHITE) {
229
+ color.set(neighbor, GRAY);
230
+ parent.set(neighbor, frame.node);
231
+ stack.push({ node: neighbor, neighbors: [...(adjacency.get(neighbor) || [])], idx: 0 });
232
+ }
233
+ }
234
+ }
235
+
236
+ return cycles;
237
+ }
238
+
239
+ function buildSummary(result) {
240
+ const parts = [
241
+ `${result.stats.totalFiles} files`,
242
+ `${result.stats.totalEdges} imports`,
243
+ `${result.stats.externalDeps} external packages`,
244
+ ];
245
+ if (result.stats.cycles > 0) {
246
+ parts.push(`โš ๏ธ ${result.stats.cycles} circular dependency cycle(s)`);
247
+ } else {
248
+ parts.push("โœ“ no circular dependencies");
249
+ }
250
+ if (result.stats.orphanFiles > 0) {
251
+ parts.push(`${result.stats.orphanFiles} orphan file(s)`);
252
+ }
253
+ return parts.join(" ยท ");
254
+ }