depwire-cli 0.3.1 → 0.5.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/README.md +10 -3
- package/dist/{chunk-LOX5NEND.js → chunk-Q733EWFA.js} +2448 -203
- package/dist/index.js +6 -6
- package/dist/mcpb-entry.js +2 -2
- package/dist/parser/grammars/tree-sitter-go.wasm +0 -0
- package/dist/parser/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/parser/grammars/tree-sitter-python.wasm +0 -0
- package/dist/parser/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/parser/grammars/tree-sitter-typescript.wasm +0 -0
- package/package.json +4 -8
|
@@ -54,9 +54,50 @@ function fileExists(filePath) {
|
|
|
54
54
|
// src/parser/detect.ts
|
|
55
55
|
import { extname as extname3 } from "path";
|
|
56
56
|
|
|
57
|
-
// src/parser/
|
|
58
|
-
import Parser from "tree-sitter";
|
|
59
|
-
import
|
|
57
|
+
// src/parser/wasm-init.ts
|
|
58
|
+
import { Parser, Language } from "web-tree-sitter";
|
|
59
|
+
import path from "path";
|
|
60
|
+
import { fileURLToPath } from "url";
|
|
61
|
+
import { existsSync as existsSync2 } from "fs";
|
|
62
|
+
var initialized = false;
|
|
63
|
+
var languages = /* @__PURE__ */ new Map();
|
|
64
|
+
async function initParser() {
|
|
65
|
+
if (initialized) return;
|
|
66
|
+
await Parser.init();
|
|
67
|
+
const __dirname3 = path.dirname(fileURLToPath(import.meta.url));
|
|
68
|
+
let grammarsDir = path.join(__dirname3, "parser", "grammars");
|
|
69
|
+
if (!existsSync2(grammarsDir)) {
|
|
70
|
+
grammarsDir = path.join(path.dirname(__dirname3), "parser", "grammars");
|
|
71
|
+
}
|
|
72
|
+
if (!existsSync2(grammarsDir)) {
|
|
73
|
+
grammarsDir = path.join(__dirname3, "grammars");
|
|
74
|
+
}
|
|
75
|
+
const grammarFiles = {
|
|
76
|
+
"typescript": "tree-sitter-typescript.wasm",
|
|
77
|
+
"tsx": "tree-sitter-tsx.wasm",
|
|
78
|
+
"javascript": "tree-sitter-javascript.wasm",
|
|
79
|
+
"python": "tree-sitter-python.wasm",
|
|
80
|
+
"go": "tree-sitter-go.wasm"
|
|
81
|
+
};
|
|
82
|
+
for (const [name, file] of Object.entries(grammarFiles)) {
|
|
83
|
+
const wasmPath = path.join(grammarsDir, file);
|
|
84
|
+
const lang = await Language.load(wasmPath);
|
|
85
|
+
languages.set(name, lang);
|
|
86
|
+
}
|
|
87
|
+
initialized = true;
|
|
88
|
+
}
|
|
89
|
+
function getParser(language) {
|
|
90
|
+
if (!initialized) {
|
|
91
|
+
throw new Error("Parser not initialized. Call initParser() first.");
|
|
92
|
+
}
|
|
93
|
+
const lang = languages.get(language);
|
|
94
|
+
if (!lang) {
|
|
95
|
+
throw new Error(`Language '${language}' not loaded.`);
|
|
96
|
+
}
|
|
97
|
+
const parser = new Parser();
|
|
98
|
+
parser.setLanguage(lang);
|
|
99
|
+
return parser;
|
|
100
|
+
}
|
|
60
101
|
|
|
61
102
|
// src/parser/resolver.ts
|
|
62
103
|
import { join as join2, dirname, resolve, relative as relative2 } from "path";
|
|
@@ -152,13 +193,10 @@ function resolveImportPath(importPath, fromFile, projectRoot) {
|
|
|
152
193
|
}
|
|
153
194
|
|
|
154
195
|
// src/parser/typescript.ts
|
|
155
|
-
var tsParser = new Parser();
|
|
156
|
-
tsParser.setLanguage(TypeScript.typescript);
|
|
157
|
-
var tsxParser = new Parser();
|
|
158
|
-
tsxParser.setLanguage(TypeScript.tsx);
|
|
159
196
|
function parseTypeScriptFile(filePath, sourceCode, projectRoot) {
|
|
160
|
-
const
|
|
161
|
-
const
|
|
197
|
+
const languageType = filePath.endsWith(".tsx") ? "tsx" : "typescript";
|
|
198
|
+
const parser = getParser(languageType);
|
|
199
|
+
const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
|
|
162
200
|
const context = {
|
|
163
201
|
filePath,
|
|
164
202
|
projectRoot,
|
|
@@ -613,14 +651,11 @@ var typescriptParser = {
|
|
|
613
651
|
};
|
|
614
652
|
|
|
615
653
|
// src/parser/python.ts
|
|
616
|
-
import Parser2 from "tree-sitter";
|
|
617
|
-
import Python from "tree-sitter-python";
|
|
618
654
|
import { dirname as dirname2, join as join3 } from "path";
|
|
619
|
-
import { existsSync as
|
|
620
|
-
var pyParser = new Parser2();
|
|
621
|
-
pyParser.setLanguage(Python);
|
|
655
|
+
import { existsSync as existsSync3 } from "fs";
|
|
622
656
|
function parsePythonFile(filePath, sourceCode, projectRoot) {
|
|
623
|
-
const
|
|
657
|
+
const parser = getParser("python");
|
|
658
|
+
const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
|
|
624
659
|
const context = {
|
|
625
660
|
filePath,
|
|
626
661
|
projectRoot,
|
|
@@ -907,13 +942,13 @@ function resolveImportPath2(moduleName, currentFile, projectRoot) {
|
|
|
907
942
|
join3(targetDir, modulePath2, "__init__.py")
|
|
908
943
|
];
|
|
909
944
|
for (const candidate of candidates2) {
|
|
910
|
-
if (
|
|
945
|
+
if (existsSync3(candidate)) {
|
|
911
946
|
return candidate.substring(projectRoot.length + 1);
|
|
912
947
|
}
|
|
913
948
|
}
|
|
914
949
|
} else {
|
|
915
950
|
const initPath = join3(targetDir, "__init__.py");
|
|
916
|
-
if (
|
|
951
|
+
if (existsSync3(initPath)) {
|
|
917
952
|
return initPath.substring(projectRoot.length + 1);
|
|
918
953
|
}
|
|
919
954
|
}
|
|
@@ -925,7 +960,7 @@ function resolveImportPath2(moduleName, currentFile, projectRoot) {
|
|
|
925
960
|
join3(projectRoot, modulePath, "__init__.py")
|
|
926
961
|
];
|
|
927
962
|
for (const candidate of candidates) {
|
|
928
|
-
if (
|
|
963
|
+
if (existsSync3(candidate)) {
|
|
929
964
|
return candidate.substring(projectRoot.length + 1);
|
|
930
965
|
}
|
|
931
966
|
}
|
|
@@ -982,14 +1017,11 @@ var pythonParser = {
|
|
|
982
1017
|
};
|
|
983
1018
|
|
|
984
1019
|
// src/parser/javascript.ts
|
|
985
|
-
import
|
|
986
|
-
import JavaScript from "tree-sitter-javascript";
|
|
987
|
-
import { existsSync as existsSync3 } from "fs";
|
|
1020
|
+
import { existsSync as existsSync4 } from "fs";
|
|
988
1021
|
import { join as join4, dirname as dirname3, extname as extname2 } from "path";
|
|
989
|
-
var jsParser = new Parser3();
|
|
990
|
-
jsParser.setLanguage(JavaScript);
|
|
991
1022
|
function parseJavaScriptFile(filePath, sourceCode, projectRoot) {
|
|
992
|
-
const
|
|
1023
|
+
const parser = getParser("javascript");
|
|
1024
|
+
const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
|
|
993
1025
|
const context = {
|
|
994
1026
|
filePath,
|
|
995
1027
|
projectRoot,
|
|
@@ -1402,20 +1434,20 @@ function resolveJavaScriptImport(importPath, currentFile, projectRoot) {
|
|
|
1402
1434
|
const indexFiles = ["index.js", "index.jsx", "index.mjs"];
|
|
1403
1435
|
if (extname2(importPath)) {
|
|
1404
1436
|
const fullPath = targetPath;
|
|
1405
|
-
if (
|
|
1437
|
+
if (existsSync4(fullPath)) {
|
|
1406
1438
|
return fullPath.substring(projectRoot.length + 1);
|
|
1407
1439
|
}
|
|
1408
1440
|
return null;
|
|
1409
1441
|
}
|
|
1410
1442
|
for (const ext of extensions) {
|
|
1411
1443
|
const candidate = `${targetPath}${ext}`;
|
|
1412
|
-
if (
|
|
1444
|
+
if (existsSync4(candidate)) {
|
|
1413
1445
|
return candidate.substring(projectRoot.length + 1);
|
|
1414
1446
|
}
|
|
1415
1447
|
}
|
|
1416
1448
|
for (const indexFile of indexFiles) {
|
|
1417
1449
|
const candidate = join4(targetPath, indexFile);
|
|
1418
|
-
if (
|
|
1450
|
+
if (existsSync4(candidate)) {
|
|
1419
1451
|
return candidate.substring(projectRoot.length + 1);
|
|
1420
1452
|
}
|
|
1421
1453
|
}
|
|
@@ -1486,13 +1518,10 @@ var javascriptParser = {
|
|
|
1486
1518
|
};
|
|
1487
1519
|
|
|
1488
1520
|
// src/parser/go.ts
|
|
1489
|
-
import
|
|
1490
|
-
import Go from "tree-sitter-go";
|
|
1491
|
-
import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
1521
|
+
import { existsSync as existsSync5, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
1492
1522
|
import { join as join5, dirname as dirname4 } from "path";
|
|
1493
|
-
var parser = new Parser4();
|
|
1494
|
-
parser.setLanguage(Go);
|
|
1495
1523
|
function parseGoFile(filePath, sourceCode, projectRoot) {
|
|
1524
|
+
const parser = getParser("go");
|
|
1496
1525
|
const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
|
|
1497
1526
|
const moduleName = readGoModuleName(projectRoot);
|
|
1498
1527
|
const context = {
|
|
@@ -1773,7 +1802,7 @@ function readGoModuleName(projectRoot) {
|
|
|
1773
1802
|
let currentDir = projectRoot;
|
|
1774
1803
|
for (let i = 0; i < 5; i++) {
|
|
1775
1804
|
const goModPath = join5(currentDir, "go.mod");
|
|
1776
|
-
if (
|
|
1805
|
+
if (existsSync5(goModPath)) {
|
|
1777
1806
|
try {
|
|
1778
1807
|
const content = readFileSync2(goModPath, "utf-8");
|
|
1779
1808
|
const lines = content.split("\n");
|
|
@@ -1804,13 +1833,13 @@ function resolveGoImport(importPath, projectRoot, moduleName) {
|
|
|
1804
1833
|
}
|
|
1805
1834
|
const segments = importPath.split("/");
|
|
1806
1835
|
const packageDir = join5(projectRoot, ...segments);
|
|
1807
|
-
if (
|
|
1836
|
+
if (existsSync5(packageDir)) {
|
|
1808
1837
|
return findGoFilesInDir(packageDir, projectRoot);
|
|
1809
1838
|
}
|
|
1810
1839
|
return [];
|
|
1811
1840
|
}
|
|
1812
1841
|
function findGoFilesInDir(dir, projectRoot) {
|
|
1813
|
-
if (!
|
|
1842
|
+
if (!existsSync5(dir)) return [];
|
|
1814
1843
|
try {
|
|
1815
1844
|
const files = readdirSync2(dir);
|
|
1816
1845
|
const goFiles = files.filter((f) => f.endsWith(".go") && !f.endsWith("_test.go"));
|
|
@@ -1938,7 +1967,8 @@ function shouldParseFile(fullPath) {
|
|
|
1938
1967
|
return false;
|
|
1939
1968
|
}
|
|
1940
1969
|
}
|
|
1941
|
-
function parseProject(projectRoot, options) {
|
|
1970
|
+
async function parseProject(projectRoot, options) {
|
|
1971
|
+
await initParser();
|
|
1942
1972
|
const files = scanDirectory(projectRoot);
|
|
1943
1973
|
const parsedFiles = [];
|
|
1944
1974
|
let skippedFiles = 0;
|
|
@@ -1965,14 +1995,14 @@ function parseProject(projectRoot, options) {
|
|
|
1965
1995
|
if (options?.verbose) {
|
|
1966
1996
|
console.error(`[Parser] Parsing: ${file}`);
|
|
1967
1997
|
}
|
|
1968
|
-
const
|
|
1969
|
-
if (!
|
|
1998
|
+
const parser = getParserForFile(file);
|
|
1999
|
+
if (!parser) {
|
|
1970
2000
|
console.error(`No parser found for file: ${file}`);
|
|
1971
2001
|
skippedFiles++;
|
|
1972
2002
|
continue;
|
|
1973
2003
|
}
|
|
1974
2004
|
const sourceCode = readFileSync3(fullPath, "utf-8");
|
|
1975
|
-
const parsed =
|
|
2005
|
+
const parsed = parser.parseFile(file, sourceCode, projectRoot);
|
|
1976
2006
|
parsedFiles.push(parsed);
|
|
1977
2007
|
} catch (err) {
|
|
1978
2008
|
errorFiles++;
|
|
@@ -2397,10 +2427,10 @@ function watchProject(projectRoot, callbacks) {
|
|
|
2397
2427
|
// src/viz/server.ts
|
|
2398
2428
|
import express from "express";
|
|
2399
2429
|
import open from "open";
|
|
2400
|
-
import { fileURLToPath } from "url";
|
|
2430
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2401
2431
|
import { dirname as dirname5, join as join7 } from "path";
|
|
2402
2432
|
import { WebSocketServer } from "ws";
|
|
2403
|
-
var __filename =
|
|
2433
|
+
var __filename = fileURLToPath2(import.meta.url);
|
|
2404
2434
|
var __dirname2 = dirname5(__filename);
|
|
2405
2435
|
var activeServer = null;
|
|
2406
2436
|
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
@@ -2473,7 +2503,7 @@ Depwire visualization running at ${url2}`);
|
|
|
2473
2503
|
onFileChanged: async (filePath) => {
|
|
2474
2504
|
console.error(`File changed: ${filePath} \u2014 re-parsing project...`);
|
|
2475
2505
|
try {
|
|
2476
|
-
const parsedFiles = parseProject(projectRoot, options);
|
|
2506
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
2477
2507
|
const newGraph = buildGraph(parsedFiles);
|
|
2478
2508
|
graph.clear();
|
|
2479
2509
|
newGraph.forEachNode((node, attrs) => {
|
|
@@ -2492,7 +2522,7 @@ Depwire visualization running at ${url2}`);
|
|
|
2492
2522
|
onFileAdded: async (filePath) => {
|
|
2493
2523
|
console.error(`File added: ${filePath} \u2014 re-parsing project...`);
|
|
2494
2524
|
try {
|
|
2495
|
-
const parsedFiles = parseProject(projectRoot, options);
|
|
2525
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
2496
2526
|
const newGraph = buildGraph(parsedFiles);
|
|
2497
2527
|
graph.clear();
|
|
2498
2528
|
newGraph.forEachNode((node, attrs) => {
|
|
@@ -2508,10 +2538,10 @@ Depwire visualization running at ${url2}`);
|
|
|
2508
2538
|
console.error(`Failed to update graph for ${filePath}:`, error);
|
|
2509
2539
|
}
|
|
2510
2540
|
},
|
|
2511
|
-
onFileDeleted: (filePath) => {
|
|
2541
|
+
onFileDeleted: async (filePath) => {
|
|
2512
2542
|
console.error(`File deleted: ${filePath} \u2014 re-parsing project...`);
|
|
2513
2543
|
try {
|
|
2514
|
-
const parsedFiles = parseProject(projectRoot, options);
|
|
2544
|
+
const parsedFiles = await parseProject(projectRoot, options);
|
|
2515
2545
|
const newGraph = buildGraph(parsedFiles);
|
|
2516
2546
|
graph.clear();
|
|
2517
2547
|
newGraph.forEachNode((node, attrs) => {
|
|
@@ -2609,8 +2639,8 @@ async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
|
2609
2639
|
}
|
|
2610
2640
|
|
|
2611
2641
|
// src/docs/generator.ts
|
|
2612
|
-
import { writeFileSync as writeFileSync2, mkdirSync, existsSync as
|
|
2613
|
-
import { join as
|
|
2642
|
+
import { writeFileSync as writeFileSync2, mkdirSync, existsSync as existsSync8 } from "fs";
|
|
2643
|
+
import { join as join11 } from "path";
|
|
2614
2644
|
|
|
2615
2645
|
// src/docs/architecture.ts
|
|
2616
2646
|
import { dirname as dirname6 } from "path";
|
|
@@ -2725,7 +2755,7 @@ function generateProjectSummary(graph, parseTime) {
|
|
|
2725
2755
|
const fileCount = getFileCount(graph);
|
|
2726
2756
|
const symbolCount = graph.order;
|
|
2727
2757
|
const edgeCount = graph.size;
|
|
2728
|
-
const
|
|
2758
|
+
const languages2 = getLanguageStats(graph);
|
|
2729
2759
|
let output = "";
|
|
2730
2760
|
output += `- **Total Files:** ${formatNumber(fileCount)}
|
|
2731
2761
|
`;
|
|
@@ -2735,10 +2765,10 @@ function generateProjectSummary(graph, parseTime) {
|
|
|
2735
2765
|
`;
|
|
2736
2766
|
output += `- **Parse Time:** ${parseTime.toFixed(1)}s
|
|
2737
2767
|
`;
|
|
2738
|
-
if (Object.keys(
|
|
2768
|
+
if (Object.keys(languages2).length > 1) {
|
|
2739
2769
|
output += "\n**Languages:**\n\n";
|
|
2740
2770
|
const totalFiles = fileCount;
|
|
2741
|
-
for (const [lang, count] of Object.entries(
|
|
2771
|
+
for (const [lang, count] of Object.entries(languages2).sort((a, b) => b[1] - a[1])) {
|
|
2742
2772
|
output += `- ${lang}: ${count} files (${formatPercent(count, totalFiles)})
|
|
2743
2773
|
`;
|
|
2744
2774
|
}
|
|
@@ -3657,20 +3687,20 @@ function findLongestPaths(graph, limit) {
|
|
|
3657
3687
|
}
|
|
3658
3688
|
const allPaths = [];
|
|
3659
3689
|
const visited = /* @__PURE__ */ new Set();
|
|
3660
|
-
function dfs(file,
|
|
3690
|
+
function dfs(file, path2) {
|
|
3661
3691
|
visited.add(file);
|
|
3662
|
-
|
|
3692
|
+
path2.push(file);
|
|
3663
3693
|
const neighbors = fileGraph.get(file);
|
|
3664
3694
|
if (!neighbors || neighbors.size === 0) {
|
|
3665
|
-
allPaths.push([...
|
|
3695
|
+
allPaths.push([...path2]);
|
|
3666
3696
|
} else {
|
|
3667
3697
|
for (const neighbor of neighbors) {
|
|
3668
3698
|
if (!visited.has(neighbor)) {
|
|
3669
|
-
dfs(neighbor,
|
|
3699
|
+
dfs(neighbor, path2);
|
|
3670
3700
|
}
|
|
3671
3701
|
}
|
|
3672
3702
|
}
|
|
3673
|
-
|
|
3703
|
+
path2.pop();
|
|
3674
3704
|
visited.delete(file);
|
|
3675
3705
|
}
|
|
3676
3706
|
for (const root of roots.slice(0, 10)) {
|
|
@@ -3841,8 +3871,8 @@ function getLanguageStats2(graph) {
|
|
|
3841
3871
|
}
|
|
3842
3872
|
function generateQuickOrientation(graph) {
|
|
3843
3873
|
const fileCount = getFileCount4(graph);
|
|
3844
|
-
const
|
|
3845
|
-
const primaryLang = Object.entries(
|
|
3874
|
+
const languages2 = getLanguageStats2(graph);
|
|
3875
|
+
const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
|
|
3846
3876
|
const dirs = /* @__PURE__ */ new Set();
|
|
3847
3877
|
graph.forEachNode((node, attrs) => {
|
|
3848
3878
|
const dir = dirname7(attrs.filePath);
|
|
@@ -4173,138 +4203,2353 @@ function generateDepwireUsage(projectRoot) {
|
|
|
4173
4203
|
return output;
|
|
4174
4204
|
}
|
|
4175
4205
|
|
|
4176
|
-
// src/docs/
|
|
4177
|
-
import {
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
const
|
|
4181
|
-
|
|
4182
|
-
|
|
4206
|
+
// src/docs/files.ts
|
|
4207
|
+
import { dirname as dirname8, basename as basename3 } from "path";
|
|
4208
|
+
function generateFiles(graph, projectRoot, version) {
|
|
4209
|
+
let output = "";
|
|
4210
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4211
|
+
const fileCount = getFileCount5(graph);
|
|
4212
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
4213
|
+
output += header("File Catalog");
|
|
4214
|
+
output += "Complete catalog of every file in the project with key metrics.\n\n";
|
|
4215
|
+
output += header("File Summary", 2);
|
|
4216
|
+
output += generateFileSummaryTable(graph);
|
|
4217
|
+
output += header("Directory Breakdown", 2);
|
|
4218
|
+
output += generateDirectoryBreakdown(graph);
|
|
4219
|
+
output += header("File Size Distribution", 2);
|
|
4220
|
+
output += generateFileSizeDistribution(graph);
|
|
4221
|
+
output += header("Orphan Files", 2);
|
|
4222
|
+
output += generateOrphanFiles(graph);
|
|
4223
|
+
output += header("Hub Files", 2);
|
|
4224
|
+
output += generateHubFiles2(graph);
|
|
4225
|
+
return output;
|
|
4226
|
+
}
|
|
4227
|
+
function getFileCount5(graph) {
|
|
4228
|
+
const files = /* @__PURE__ */ new Set();
|
|
4229
|
+
graph.forEachNode((node, attrs) => {
|
|
4230
|
+
files.add(attrs.filePath);
|
|
4231
|
+
});
|
|
4232
|
+
return files.size;
|
|
4233
|
+
}
|
|
4234
|
+
function getFileStats2(graph) {
|
|
4235
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
4236
|
+
graph.forEachNode((node, attrs) => {
|
|
4237
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
4238
|
+
fileMap.set(attrs.filePath, {
|
|
4239
|
+
filePath: attrs.filePath,
|
|
4240
|
+
language: getLanguageFromPath(attrs.filePath),
|
|
4241
|
+
symbolCount: 0,
|
|
4242
|
+
importCount: 0,
|
|
4243
|
+
exportedSymbolCount: 0,
|
|
4244
|
+
incomingConnections: 0,
|
|
4245
|
+
outgoingConnections: 0,
|
|
4246
|
+
totalConnections: 0,
|
|
4247
|
+
maxLine: 0
|
|
4248
|
+
});
|
|
4249
|
+
}
|
|
4250
|
+
const stats = fileMap.get(attrs.filePath);
|
|
4251
|
+
stats.symbolCount++;
|
|
4252
|
+
if (attrs.exported && attrs.name !== "default") {
|
|
4253
|
+
stats.exportedSymbolCount++;
|
|
4254
|
+
}
|
|
4255
|
+
if (attrs.kind === "import") {
|
|
4256
|
+
stats.importCount++;
|
|
4257
|
+
}
|
|
4258
|
+
if (attrs.endLine > stats.maxLine) {
|
|
4259
|
+
stats.maxLine = attrs.endLine;
|
|
4260
|
+
}
|
|
4261
|
+
});
|
|
4262
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4263
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
4264
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
4265
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
4266
|
+
const sourceStats = fileMap.get(sourceAttrs.filePath);
|
|
4267
|
+
const targetStats = fileMap.get(targetAttrs.filePath);
|
|
4268
|
+
if (sourceStats) {
|
|
4269
|
+
sourceStats.outgoingConnections++;
|
|
4270
|
+
}
|
|
4271
|
+
if (targetStats) {
|
|
4272
|
+
targetStats.incomingConnections++;
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
});
|
|
4276
|
+
fileMap.forEach((stats) => {
|
|
4277
|
+
stats.totalConnections = stats.incomingConnections + stats.outgoingConnections;
|
|
4278
|
+
});
|
|
4279
|
+
return Array.from(fileMap.values());
|
|
4280
|
+
}
|
|
4281
|
+
function getLanguageFromPath(filePath) {
|
|
4282
|
+
const ext = filePath.toLowerCase();
|
|
4283
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
|
|
4284
|
+
if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
|
|
4285
|
+
if (ext.endsWith(".py")) return "Python";
|
|
4286
|
+
if (ext.endsWith(".go")) return "Go";
|
|
4287
|
+
return "Other";
|
|
4288
|
+
}
|
|
4289
|
+
function generateFileSummaryTable(graph) {
|
|
4290
|
+
const fileStats = getFileStats2(graph);
|
|
4291
|
+
if (fileStats.length === 0) {
|
|
4292
|
+
return "No files detected.\n\n";
|
|
4183
4293
|
}
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4294
|
+
fileStats.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
4295
|
+
const headers = ["File", "Language", "Symbols", "Imports", "Exports", "Connections", "Lines"];
|
|
4296
|
+
const rows = fileStats.map((f) => [
|
|
4297
|
+
`\`${f.filePath}\``,
|
|
4298
|
+
f.language,
|
|
4299
|
+
formatNumber(f.symbolCount),
|
|
4300
|
+
formatNumber(f.importCount),
|
|
4301
|
+
formatNumber(f.exportedSymbolCount),
|
|
4302
|
+
formatNumber(f.totalConnections),
|
|
4303
|
+
formatNumber(f.maxLine)
|
|
4304
|
+
]);
|
|
4305
|
+
return table(headers, rows);
|
|
4306
|
+
}
|
|
4307
|
+
function generateDirectoryBreakdown(graph) {
|
|
4308
|
+
const fileStats = getFileStats2(graph);
|
|
4309
|
+
const dirMap = /* @__PURE__ */ new Map();
|
|
4310
|
+
for (const file of fileStats) {
|
|
4311
|
+
const dir = dirname8(file.filePath);
|
|
4312
|
+
const topDir = dir === "." ? "." : dir.split("/")[0];
|
|
4313
|
+
if (!dirMap.has(topDir)) {
|
|
4314
|
+
dirMap.set(topDir, {
|
|
4315
|
+
fileCount: 0,
|
|
4316
|
+
symbolCount: 0,
|
|
4317
|
+
mostConnectedFile: "",
|
|
4318
|
+
maxConnections: 0
|
|
4319
|
+
});
|
|
4320
|
+
}
|
|
4321
|
+
const dirStats = dirMap.get(topDir);
|
|
4322
|
+
dirStats.fileCount++;
|
|
4323
|
+
dirStats.symbolCount += file.symbolCount;
|
|
4324
|
+
if (file.totalConnections > dirStats.maxConnections) {
|
|
4325
|
+
dirStats.maxConnections = file.totalConnections;
|
|
4326
|
+
dirStats.mostConnectedFile = basename3(file.filePath);
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
if (dirMap.size === 0) {
|
|
4330
|
+
return "No directories detected.\n\n";
|
|
4331
|
+
}
|
|
4332
|
+
let output = "";
|
|
4333
|
+
const sortedDirs = Array.from(dirMap.entries()).sort((a, b) => b[1].fileCount - a[1].fileCount);
|
|
4334
|
+
for (const [dir, stats] of sortedDirs) {
|
|
4335
|
+
output += `**${dir === "." ? "Root" : dir}/**
|
|
4336
|
+
|
|
4337
|
+
`;
|
|
4338
|
+
output += `- **Files:** ${formatNumber(stats.fileCount)}
|
|
4339
|
+
`;
|
|
4340
|
+
output += `- **Symbols:** ${formatNumber(stats.symbolCount)}
|
|
4341
|
+
`;
|
|
4342
|
+
output += `- **Most Connected:** \`${stats.mostConnectedFile}\` (${formatNumber(stats.maxConnections)} connections)
|
|
4343
|
+
|
|
4344
|
+
`;
|
|
4190
4345
|
}
|
|
4346
|
+
return output;
|
|
4191
4347
|
}
|
|
4192
|
-
function
|
|
4193
|
-
const
|
|
4194
|
-
|
|
4348
|
+
function generateFileSizeDistribution(graph) {
|
|
4349
|
+
const fileStats = getFileStats2(graph);
|
|
4350
|
+
if (fileStats.length === 0) {
|
|
4351
|
+
return "No files detected.\n\n";
|
|
4352
|
+
}
|
|
4353
|
+
const bySymbols = [...fileStats].sort((a, b) => b.symbolCount - a.symbolCount);
|
|
4354
|
+
let output = "";
|
|
4355
|
+
output += "**Largest Files (by symbol count):**\n\n";
|
|
4356
|
+
const largest = bySymbols.slice(0, 10);
|
|
4357
|
+
const headers1 = ["File", "Symbols", "Lines"];
|
|
4358
|
+
const rows1 = largest.map((f) => [
|
|
4359
|
+
`\`${f.filePath}\``,
|
|
4360
|
+
formatNumber(f.symbolCount),
|
|
4361
|
+
formatNumber(f.maxLine)
|
|
4362
|
+
]);
|
|
4363
|
+
output += table(headers1, rows1);
|
|
4364
|
+
if (bySymbols.length > 10) {
|
|
4365
|
+
output += "**Smallest Files (by symbol count):**\n\n";
|
|
4366
|
+
const smallest = bySymbols.slice(-10).reverse();
|
|
4367
|
+
const headers2 = ["File", "Symbols", "Lines"];
|
|
4368
|
+
const rows2 = smallest.map((f) => [
|
|
4369
|
+
`\`${f.filePath}\``,
|
|
4370
|
+
formatNumber(f.symbolCount),
|
|
4371
|
+
formatNumber(f.maxLine)
|
|
4372
|
+
]);
|
|
4373
|
+
output += table(headers2, rows2);
|
|
4374
|
+
}
|
|
4375
|
+
const avgSymbols = Math.round(fileStats.reduce((sum, f) => sum + f.symbolCount, 0) / fileStats.length);
|
|
4376
|
+
const avgLines = Math.round(fileStats.reduce((sum, f) => sum + f.maxLine, 0) / fileStats.length);
|
|
4377
|
+
output += `**Average File Size:**
|
|
4378
|
+
|
|
4379
|
+
`;
|
|
4380
|
+
output += `- Symbols per file: ${formatNumber(avgSymbols)}
|
|
4381
|
+
`;
|
|
4382
|
+
output += `- Lines per file: ${formatNumber(avgLines)}
|
|
4383
|
+
|
|
4384
|
+
`;
|
|
4385
|
+
return output;
|
|
4195
4386
|
}
|
|
4196
|
-
function
|
|
4197
|
-
const
|
|
4198
|
-
const
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
documents[docType] = {
|
|
4202
|
-
generated_at: now,
|
|
4203
|
-
file: fileName
|
|
4204
|
-
};
|
|
4387
|
+
function generateOrphanFiles(graph) {
|
|
4388
|
+
const fileStats = getFileStats2(graph);
|
|
4389
|
+
const orphans = fileStats.filter((f) => f.totalConnections === 0);
|
|
4390
|
+
if (orphans.length === 0) {
|
|
4391
|
+
return "\u2705 No orphan files detected. All files are connected.\n\n";
|
|
4205
4392
|
}
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
edge_count: edgeCount,
|
|
4213
|
-
documents
|
|
4214
|
-
};
|
|
4393
|
+
let output = `Found ${orphans.length} file${orphans.length === 1 ? "" : "s"} with zero connections:
|
|
4394
|
+
|
|
4395
|
+
`;
|
|
4396
|
+
output += unorderedList(orphans.map((f) => `\`${f.filePath}\` (${f.symbolCount} symbols)`));
|
|
4397
|
+
output += "These files may be entry points, standalone scripts, or dead code.\n\n";
|
|
4398
|
+
return output;
|
|
4215
4399
|
}
|
|
4216
|
-
function
|
|
4217
|
-
const
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4400
|
+
function generateHubFiles2(graph) {
|
|
4401
|
+
const fileStats = getFileStats2(graph);
|
|
4402
|
+
const hubs = fileStats.filter((f) => f.totalConnections > 0).sort((a, b) => b.totalConnections - a.totalConnections).slice(0, 10);
|
|
4403
|
+
if (hubs.length === 0) {
|
|
4404
|
+
return "No hub files detected.\n\n";
|
|
4405
|
+
}
|
|
4406
|
+
let output = "Files with the most connections (changing these breaks the most things):\n\n";
|
|
4407
|
+
const headers = ["File", "Total Connections", "Incoming", "Outgoing", "Symbols"];
|
|
4408
|
+
const rows = hubs.map((f) => [
|
|
4409
|
+
`\`${f.filePath}\``,
|
|
4410
|
+
formatNumber(f.totalConnections),
|
|
4411
|
+
formatNumber(f.incomingConnections),
|
|
4412
|
+
formatNumber(f.outgoingConnections),
|
|
4413
|
+
formatNumber(f.symbolCount)
|
|
4414
|
+
]);
|
|
4415
|
+
output += table(headers, rows);
|
|
4416
|
+
return output;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
// src/docs/api-surface.ts
|
|
4420
|
+
function generateApiSurface(graph, projectRoot, version) {
|
|
4421
|
+
let output = "";
|
|
4422
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4423
|
+
const fileCount = getFileCount6(graph);
|
|
4424
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
4425
|
+
output += header("API Surface");
|
|
4426
|
+
output += "Every exported symbol in the project \u2014 the public API.\n\n";
|
|
4427
|
+
output += header("Exports by File", 2);
|
|
4428
|
+
output += generateExportsByFile(graph);
|
|
4429
|
+
output += header("Exports by Kind", 2);
|
|
4430
|
+
output += generateExportsByKind(graph);
|
|
4431
|
+
output += header("Most-Used Exports", 2);
|
|
4432
|
+
output += generateMostUsedExports(graph);
|
|
4433
|
+
output += header("Unused Exports", 2);
|
|
4434
|
+
output += generateUnusedExports(graph);
|
|
4435
|
+
output += header("Re-exports / Barrel Files", 2);
|
|
4436
|
+
output += generateReExports(graph);
|
|
4437
|
+
return output;
|
|
4438
|
+
}
|
|
4439
|
+
function getFileCount6(graph) {
|
|
4440
|
+
const files = /* @__PURE__ */ new Set();
|
|
4441
|
+
graph.forEachNode((node, attrs) => {
|
|
4442
|
+
files.add(attrs.filePath);
|
|
4443
|
+
});
|
|
4444
|
+
return files.size;
|
|
4445
|
+
}
|
|
4446
|
+
function getExportedSymbols(graph) {
|
|
4447
|
+
const exports = [];
|
|
4448
|
+
graph.forEachNode((node, attrs) => {
|
|
4449
|
+
if (attrs.exported && attrs.name !== "__file__") {
|
|
4450
|
+
const dependentCount = graph.inDegree(node);
|
|
4451
|
+
exports.push({
|
|
4452
|
+
name: attrs.name,
|
|
4453
|
+
kind: attrs.kind,
|
|
4454
|
+
filePath: attrs.filePath,
|
|
4455
|
+
line: attrs.startLine,
|
|
4456
|
+
dependentCount
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
});
|
|
4460
|
+
return exports;
|
|
4461
|
+
}
|
|
4462
|
+
function generateExportsByFile(graph) {
|
|
4463
|
+
const exports = getExportedSymbols(graph);
|
|
4464
|
+
if (exports.length === 0) {
|
|
4465
|
+
return "No exported symbols detected.\n\n";
|
|
4466
|
+
}
|
|
4467
|
+
const fileExports = /* @__PURE__ */ new Map();
|
|
4468
|
+
for (const exp of exports) {
|
|
4469
|
+
if (!fileExports.has(exp.filePath)) {
|
|
4470
|
+
fileExports.set(exp.filePath, []);
|
|
4221
4471
|
}
|
|
4472
|
+
fileExports.get(exp.filePath).push(exp);
|
|
4222
4473
|
}
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4474
|
+
const sortedFiles = Array.from(fileExports.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
4475
|
+
let output = "";
|
|
4476
|
+
for (const [filePath, fileExports2] of sortedFiles) {
|
|
4477
|
+
output += header(filePath, 3);
|
|
4478
|
+
const sorted = fileExports2.sort((a, b) => b.dependentCount - a.dependentCount);
|
|
4479
|
+
const items = sorted.map((exp) => {
|
|
4480
|
+
const depInfo = exp.dependentCount > 0 ? ` \u2014 ${formatNumber(exp.dependentCount)} dependents` : "";
|
|
4481
|
+
return `${code(exp.name)} (${exp.kind}, line ${exp.line})${depInfo}`;
|
|
4482
|
+
});
|
|
4483
|
+
output += unorderedList(items);
|
|
4484
|
+
}
|
|
4485
|
+
return output;
|
|
4486
|
+
}
|
|
4487
|
+
function generateExportsByKind(graph) {
|
|
4488
|
+
const exports = getExportedSymbols(graph);
|
|
4489
|
+
if (exports.length === 0) {
|
|
4490
|
+
return "No exported symbols detected.\n\n";
|
|
4491
|
+
}
|
|
4492
|
+
const kindGroups = /* @__PURE__ */ new Map();
|
|
4493
|
+
for (const exp of exports) {
|
|
4494
|
+
if (!kindGroups.has(exp.kind)) {
|
|
4495
|
+
kindGroups.set(exp.kind, []);
|
|
4496
|
+
}
|
|
4497
|
+
kindGroups.get(exp.kind).push(exp);
|
|
4498
|
+
}
|
|
4499
|
+
let output = "";
|
|
4500
|
+
const sortedKinds = Array.from(kindGroups.entries()).sort((a, b) => b[1].length - a[1].length);
|
|
4501
|
+
for (const [kind, kindExports] of sortedKinds) {
|
|
4502
|
+
if (kind === "import" || kind === "export") continue;
|
|
4503
|
+
output += `**${capitalizeKind(kind)}s (${kindExports.length}):**
|
|
4504
|
+
|
|
4505
|
+
`;
|
|
4506
|
+
const sorted = kindExports.sort((a, b) => b.dependentCount - a.dependentCount).slice(0, 20);
|
|
4507
|
+
const items = sorted.map((exp) => {
|
|
4508
|
+
return `${code(exp.name)} \u2014 ${code(exp.filePath)}:${exp.line}`;
|
|
4509
|
+
});
|
|
4510
|
+
output += unorderedList(items);
|
|
4511
|
+
}
|
|
4512
|
+
return output;
|
|
4513
|
+
}
|
|
4514
|
+
function capitalizeKind(kind) {
|
|
4515
|
+
const map = {
|
|
4516
|
+
function: "Function",
|
|
4517
|
+
class: "Class",
|
|
4518
|
+
variable: "Variable",
|
|
4519
|
+
constant: "Constant",
|
|
4520
|
+
type_alias: "Type",
|
|
4521
|
+
interface: "Interface",
|
|
4522
|
+
enum: "Enum",
|
|
4523
|
+
import: "Import",
|
|
4524
|
+
export: "Export",
|
|
4525
|
+
method: "Method",
|
|
4526
|
+
property: "Property",
|
|
4527
|
+
decorator: "Decorator",
|
|
4528
|
+
module: "Module"
|
|
4529
|
+
};
|
|
4530
|
+
return map[kind] || kind;
|
|
4228
4531
|
}
|
|
4532
|
+
function generateMostUsedExports(graph) {
|
|
4533
|
+
const exports = getExportedSymbols(graph);
|
|
4534
|
+
if (exports.length === 0) {
|
|
4535
|
+
return "No exported symbols detected.\n\n";
|
|
4536
|
+
}
|
|
4537
|
+
const sorted = exports.filter((exp) => exp.dependentCount > 0).sort((a, b) => b.dependentCount - a.dependentCount).slice(0, 20);
|
|
4538
|
+
if (sorted.length === 0) {
|
|
4539
|
+
return "No exports with dependents detected.\n\n";
|
|
4540
|
+
}
|
|
4541
|
+
let output = "Top 20 exports by dependent count \u2014 these are the most critical symbols:\n\n";
|
|
4542
|
+
const items = sorted.map((exp) => {
|
|
4543
|
+
return `${code(exp.name)} (${exp.kind}) \u2014 ${formatNumber(exp.dependentCount)} dependents \u2014 ${code(exp.filePath)}:${exp.line}`;
|
|
4544
|
+
});
|
|
4545
|
+
output += unorderedList(items);
|
|
4546
|
+
return output;
|
|
4547
|
+
}
|
|
4548
|
+
function generateUnusedExports(graph) {
|
|
4549
|
+
const exports = getExportedSymbols(graph);
|
|
4550
|
+
if (exports.length === 0) {
|
|
4551
|
+
return "No exported symbols detected.\n\n";
|
|
4552
|
+
}
|
|
4553
|
+
const unused = exports.filter((exp) => exp.dependentCount === 0 && exp.kind !== "export");
|
|
4554
|
+
if (unused.length === 0) {
|
|
4555
|
+
return "\u2705 No unused exports detected. All exports are used.\n\n";
|
|
4556
|
+
}
|
|
4557
|
+
let output = `Found ${unused.length} exported symbol${unused.length === 1 ? "" : "s"} with zero dependents:
|
|
4229
4558
|
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
const
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
try {
|
|
4236
|
-
if (!existsSync6(options.outputDir)) {
|
|
4237
|
-
mkdirSync(options.outputDir, { recursive: true });
|
|
4238
|
-
if (options.verbose) {
|
|
4239
|
-
console.log(`Created output directory: ${options.outputDir}`);
|
|
4240
|
-
}
|
|
4559
|
+
`;
|
|
4560
|
+
const fileGroups = /* @__PURE__ */ new Map();
|
|
4561
|
+
for (const exp of unused) {
|
|
4562
|
+
if (!fileGroups.has(exp.filePath)) {
|
|
4563
|
+
fileGroups.set(exp.filePath, []);
|
|
4241
4564
|
}
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4565
|
+
fileGroups.get(exp.filePath).push(exp);
|
|
4566
|
+
}
|
|
4567
|
+
for (const [filePath, fileExports] of fileGroups.entries()) {
|
|
4568
|
+
output += `**${filePath}:**
|
|
4569
|
+
|
|
4570
|
+
`;
|
|
4571
|
+
const items = fileExports.map((exp) => `${code(exp.name)} (${exp.kind}, line ${exp.line})`);
|
|
4572
|
+
output += unorderedList(items);
|
|
4573
|
+
}
|
|
4574
|
+
output += "These symbols may be part of the intended public API but are not currently used, or they may be dead code.\n\n";
|
|
4575
|
+
return output;
|
|
4576
|
+
}
|
|
4577
|
+
function generateReExports(graph) {
|
|
4578
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
4579
|
+
graph.forEachNode((node, attrs) => {
|
|
4580
|
+
if (!fileStats.has(attrs.filePath)) {
|
|
4581
|
+
fileStats.set(attrs.filePath, {
|
|
4582
|
+
exportCount: 0,
|
|
4583
|
+
reExportCount: 0,
|
|
4584
|
+
reExportSources: /* @__PURE__ */ new Set()
|
|
4585
|
+
});
|
|
4245
4586
|
}
|
|
4246
|
-
|
|
4247
|
-
|
|
4587
|
+
const stats = fileStats.get(attrs.filePath);
|
|
4588
|
+
if (attrs.exported) {
|
|
4589
|
+
stats.exportCount++;
|
|
4248
4590
|
}
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
metadata = loadMetadata(options.outputDir);
|
|
4591
|
+
if (attrs.kind === "export") {
|
|
4592
|
+
stats.reExportCount++;
|
|
4252
4593
|
}
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
const
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
const filePath = join10(options.outputDir, "ARCHITECTURE.md");
|
|
4262
|
-
writeFileSync2(filePath, content, "utf-8");
|
|
4263
|
-
generated.push("ARCHITECTURE.md");
|
|
4264
|
-
} catch (err) {
|
|
4265
|
-
errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
|
|
4266
|
-
}
|
|
4267
|
-
}
|
|
4268
|
-
if (docsToGenerate.includes("conventions")) {
|
|
4269
|
-
try {
|
|
4270
|
-
if (options.verbose) console.log("Generating CONVENTIONS.md...");
|
|
4271
|
-
const content = generateConventions(graph, projectRoot, version);
|
|
4272
|
-
const filePath = join10(options.outputDir, "CONVENTIONS.md");
|
|
4273
|
-
writeFileSync2(filePath, content, "utf-8");
|
|
4274
|
-
generated.push("CONVENTIONS.md");
|
|
4275
|
-
} catch (err) {
|
|
4276
|
-
errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
|
|
4277
|
-
}
|
|
4278
|
-
}
|
|
4279
|
-
if (docsToGenerate.includes("dependencies")) {
|
|
4280
|
-
try {
|
|
4281
|
-
if (options.verbose) console.log("Generating DEPENDENCIES.md...");
|
|
4282
|
-
const content = generateDependencies(graph, projectRoot, version);
|
|
4283
|
-
const filePath = join10(options.outputDir, "DEPENDENCIES.md");
|
|
4284
|
-
writeFileSync2(filePath, content, "utf-8");
|
|
4285
|
-
generated.push("DEPENDENCIES.md");
|
|
4286
|
-
} catch (err) {
|
|
4287
|
-
errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
|
|
4288
|
-
}
|
|
4289
|
-
}
|
|
4290
|
-
if (docsToGenerate.includes("onboarding")) {
|
|
4291
|
-
try {
|
|
4292
|
-
if (options.verbose) console.log("Generating ONBOARDING.md...");
|
|
4293
|
-
const content = generateOnboarding(graph, projectRoot, version);
|
|
4294
|
-
const filePath = join10(options.outputDir, "ONBOARDING.md");
|
|
4295
|
-
writeFileSync2(filePath, content, "utf-8");
|
|
4296
|
-
generated.push("ONBOARDING.md");
|
|
4297
|
-
} catch (err) {
|
|
4298
|
-
errors.push(`Failed to generate ONBOARDING.md: ${err}`);
|
|
4299
|
-
}
|
|
4594
|
+
});
|
|
4595
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4596
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
4597
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
4598
|
+
if (sourceAttrs.kind === "export" && sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
4599
|
+
const stats = fileStats.get(sourceAttrs.filePath);
|
|
4600
|
+
if (stats) {
|
|
4601
|
+
stats.reExportSources.add(targetAttrs.filePath);
|
|
4300
4602
|
}
|
|
4301
|
-
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4603
|
+
}
|
|
4604
|
+
});
|
|
4605
|
+
const barrels = [];
|
|
4606
|
+
for (const [filePath, stats] of fileStats.entries()) {
|
|
4607
|
+
if (stats.reExportCount > 0 && stats.reExportCount >= stats.exportCount * 0.5) {
|
|
4608
|
+
barrels.push({
|
|
4609
|
+
filePath,
|
|
4610
|
+
exportCount: stats.exportCount,
|
|
4611
|
+
reExportCount: stats.reExportCount,
|
|
4612
|
+
sources: Array.from(stats.reExportSources)
|
|
4613
|
+
});
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
if (barrels.length === 0) {
|
|
4617
|
+
return "No barrel files detected.\n\n";
|
|
4618
|
+
}
|
|
4619
|
+
let output = `Found ${barrels.length} barrel file${barrels.length === 1 ? "" : "s"} (files that primarily re-export from other files):
|
|
4620
|
+
|
|
4621
|
+
`;
|
|
4622
|
+
for (const barrel of barrels) {
|
|
4623
|
+
output += header(barrel.filePath, 3);
|
|
4624
|
+
output += `- **Total exports:** ${formatNumber(barrel.exportCount)}
|
|
4625
|
+
`;
|
|
4626
|
+
output += `- **Re-exports:** ${formatNumber(barrel.reExportCount)}
|
|
4627
|
+
`;
|
|
4628
|
+
if (barrel.sources.length > 0) {
|
|
4629
|
+
output += `- **Sources:**
|
|
4630
|
+
|
|
4631
|
+
`;
|
|
4632
|
+
output += unorderedList(barrel.sources.map((s) => code(s)));
|
|
4633
|
+
} else {
|
|
4634
|
+
output += "\n";
|
|
4635
|
+
}
|
|
4636
|
+
}
|
|
4637
|
+
return output;
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
// src/docs/errors.ts
|
|
4641
|
+
function generateErrors(graph, projectRoot, version) {
|
|
4642
|
+
let output = "";
|
|
4643
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4644
|
+
const fileCount = getFileCount7(graph);
|
|
4645
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
4646
|
+
output += header("Error Handling Analysis");
|
|
4647
|
+
output += "Analysis of error handling patterns and error-prone areas in the codebase.\n\n";
|
|
4648
|
+
output += header("Error-Related Symbols", 2);
|
|
4649
|
+
output += generateErrorRelatedSymbols(graph);
|
|
4650
|
+
output += header("Custom Error Classes", 2);
|
|
4651
|
+
output += generateCustomErrorClasses(graph);
|
|
4652
|
+
output += header("Error-Prone Files", 2);
|
|
4653
|
+
output += generateErrorProneFiles(graph);
|
|
4654
|
+
output += header("Detected Patterns", 2);
|
|
4655
|
+
output += generateErrorHandlingPatterns(graph);
|
|
4656
|
+
output += header("Recommendations", 2);
|
|
4657
|
+
output += generateRecommendations(graph);
|
|
4658
|
+
return output;
|
|
4659
|
+
}
|
|
4660
|
+
function getFileCount7(graph) {
|
|
4661
|
+
const files = /* @__PURE__ */ new Set();
|
|
4662
|
+
graph.forEachNode((node, attrs) => {
|
|
4663
|
+
files.add(attrs.filePath);
|
|
4664
|
+
});
|
|
4665
|
+
return files.size;
|
|
4666
|
+
}
|
|
4667
|
+
function getErrorRelatedSymbols(graph) {
|
|
4668
|
+
const errorKeywords = [
|
|
4669
|
+
"error",
|
|
4670
|
+
"err",
|
|
4671
|
+
"exception",
|
|
4672
|
+
"throw",
|
|
4673
|
+
"fail",
|
|
4674
|
+
"invalid",
|
|
4675
|
+
"not_found",
|
|
4676
|
+
"notfound",
|
|
4677
|
+
"unauthorized",
|
|
4678
|
+
"forbidden",
|
|
4679
|
+
"timeout",
|
|
4680
|
+
"retry",
|
|
4681
|
+
"catch",
|
|
4682
|
+
"try"
|
|
4683
|
+
];
|
|
4684
|
+
const symbols = [];
|
|
4685
|
+
graph.forEachNode((node, attrs) => {
|
|
4686
|
+
if (attrs.name === "__file__") return;
|
|
4687
|
+
const nameLower = attrs.name.toLowerCase();
|
|
4688
|
+
for (const keyword of errorKeywords) {
|
|
4689
|
+
if (nameLower.includes(keyword)) {
|
|
4690
|
+
let category = "error_handling";
|
|
4691
|
+
if (nameLower.includes("retry") || nameLower.includes("timeout")) {
|
|
4692
|
+
category = "retry_timeout";
|
|
4693
|
+
} else if (nameLower.includes("invalid") || nameLower.includes("validate")) {
|
|
4694
|
+
category = "validation";
|
|
4695
|
+
} else if (nameLower.includes("unauthorized") || nameLower.includes("forbidden")) {
|
|
4696
|
+
category = "auth_error";
|
|
4697
|
+
} else if (nameLower.includes("notfound") || nameLower.includes("not_found")) {
|
|
4698
|
+
category = "not_found";
|
|
4699
|
+
}
|
|
4700
|
+
symbols.push({
|
|
4701
|
+
name: attrs.name,
|
|
4702
|
+
kind: attrs.kind,
|
|
4703
|
+
filePath: attrs.filePath,
|
|
4704
|
+
line: attrs.startLine,
|
|
4705
|
+
category
|
|
4706
|
+
});
|
|
4707
|
+
break;
|
|
4708
|
+
}
|
|
4709
|
+
}
|
|
4710
|
+
});
|
|
4711
|
+
return symbols;
|
|
4712
|
+
}
|
|
4713
|
+
function generateErrorRelatedSymbols(graph) {
|
|
4714
|
+
const symbols = getErrorRelatedSymbols(graph);
|
|
4715
|
+
if (symbols.length === 0) {
|
|
4716
|
+
return "No error-related symbols detected.\n\n";
|
|
4717
|
+
}
|
|
4718
|
+
let output = `Found ${symbols.length} error-related symbol${symbols.length === 1 ? "" : "s"}:
|
|
4719
|
+
|
|
4720
|
+
`;
|
|
4721
|
+
const categories = /* @__PURE__ */ new Map();
|
|
4722
|
+
for (const sym of symbols) {
|
|
4723
|
+
if (!categories.has(sym.category)) {
|
|
4724
|
+
categories.set(sym.category, []);
|
|
4725
|
+
}
|
|
4726
|
+
categories.get(sym.category).push(sym);
|
|
4727
|
+
}
|
|
4728
|
+
for (const [category, syms] of categories.entries()) {
|
|
4729
|
+
output += `**${formatCategory(category)} (${syms.length}):**
|
|
4730
|
+
|
|
4731
|
+
`;
|
|
4732
|
+
const items = syms.slice(0, 10).map((s) => {
|
|
4733
|
+
return `${code(s.name)} (${s.kind}) \u2014 ${code(s.filePath)}:${s.line}`;
|
|
4734
|
+
});
|
|
4735
|
+
output += unorderedList(items);
|
|
4736
|
+
if (syms.length > 10) {
|
|
4737
|
+
output += `... and ${syms.length - 10} more.
|
|
4738
|
+
|
|
4739
|
+
`;
|
|
4740
|
+
}
|
|
4741
|
+
}
|
|
4742
|
+
return output;
|
|
4743
|
+
}
|
|
4744
|
+
function formatCategory(category) {
|
|
4745
|
+
const map = {
|
|
4746
|
+
"error_handling": "Error Handling",
|
|
4747
|
+
"retry_timeout": "Retry / Timeout",
|
|
4748
|
+
"validation": "Validation",
|
|
4749
|
+
"auth_error": "Authentication Errors",
|
|
4750
|
+
"not_found": "Not Found Errors"
|
|
4751
|
+
};
|
|
4752
|
+
return map[category] || category;
|
|
4753
|
+
}
|
|
4754
|
+
function generateCustomErrorClasses(graph) {
|
|
4755
|
+
const errorClasses = [];
|
|
4756
|
+
graph.forEachNode((node, attrs) => {
|
|
4757
|
+
if (attrs.kind === "class") {
|
|
4758
|
+
const nameLower = attrs.name.toLowerCase();
|
|
4759
|
+
if (nameLower.includes("error") || nameLower.includes("exception")) {
|
|
4760
|
+
errorClasses.push({
|
|
4761
|
+
name: attrs.name,
|
|
4762
|
+
filePath: attrs.filePath,
|
|
4763
|
+
line: attrs.startLine
|
|
4764
|
+
});
|
|
4765
|
+
}
|
|
4766
|
+
}
|
|
4767
|
+
});
|
|
4768
|
+
if (errorClasses.length === 0) {
|
|
4769
|
+
return "No custom error classes detected.\n\n";
|
|
4770
|
+
}
|
|
4771
|
+
let output = `Found ${errorClasses.length} custom error class${errorClasses.length === 1 ? "" : "es"}:
|
|
4772
|
+
|
|
4773
|
+
`;
|
|
4774
|
+
const items = errorClasses.map((c) => {
|
|
4775
|
+
return `${code(c.name)} \u2014 ${code(c.filePath)}:${c.line}`;
|
|
4776
|
+
});
|
|
4777
|
+
output += unorderedList(items);
|
|
4778
|
+
return output;
|
|
4779
|
+
}
|
|
4780
|
+
function generateErrorProneFiles(graph) {
|
|
4781
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
4782
|
+
graph.forEachNode((node, attrs) => {
|
|
4783
|
+
if (!fileStats.has(attrs.filePath)) {
|
|
4784
|
+
fileStats.set(attrs.filePath, {
|
|
4785
|
+
connectionCount: 0,
|
|
4786
|
+
errorSymbolCount: 0,
|
|
4787
|
+
symbolCount: 0
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
fileStats.get(attrs.filePath).symbolCount++;
|
|
4791
|
+
});
|
|
4792
|
+
const errorSymbols = getErrorRelatedSymbols(graph);
|
|
4793
|
+
for (const sym of errorSymbols) {
|
|
4794
|
+
const stats = fileStats.get(sym.filePath);
|
|
4795
|
+
if (stats) {
|
|
4796
|
+
stats.errorSymbolCount++;
|
|
4797
|
+
}
|
|
4798
|
+
}
|
|
4799
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4800
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
4801
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
4802
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
4803
|
+
const sourceStats = fileStats.get(sourceAttrs.filePath);
|
|
4804
|
+
const targetStats = fileStats.get(targetAttrs.filePath);
|
|
4805
|
+
if (sourceStats) sourceStats.connectionCount++;
|
|
4806
|
+
if (targetStats) targetStats.connectionCount++;
|
|
4807
|
+
}
|
|
4808
|
+
});
|
|
4809
|
+
const errorProneFiles = [];
|
|
4810
|
+
for (const [filePath, stats] of fileStats.entries()) {
|
|
4811
|
+
if (stats.connectionCount > 5) {
|
|
4812
|
+
const riskScore = stats.connectionCount * (1 + stats.errorSymbolCount * 0.5);
|
|
4813
|
+
errorProneFiles.push({
|
|
4814
|
+
filePath,
|
|
4815
|
+
connectionCount: stats.connectionCount,
|
|
4816
|
+
errorSymbolCount: stats.errorSymbolCount,
|
|
4817
|
+
riskScore
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
}
|
|
4821
|
+
errorProneFiles.sort((a, b) => b.riskScore - a.riskScore);
|
|
4822
|
+
if (errorProneFiles.length === 0) {
|
|
4823
|
+
return "No high-risk files detected.\n\n";
|
|
4824
|
+
}
|
|
4825
|
+
let output = "Files with high complexity and error-related code (riskiest to modify):\n\n";
|
|
4826
|
+
const headers = ["File", "Connections", "Error Symbols", "Risk Score"];
|
|
4827
|
+
const rows = errorProneFiles.slice(0, 15).map((f) => [
|
|
4828
|
+
`\`${f.filePath}\``,
|
|
4829
|
+
formatNumber(f.connectionCount),
|
|
4830
|
+
formatNumber(f.errorSymbolCount),
|
|
4831
|
+
f.riskScore.toFixed(1)
|
|
4832
|
+
]);
|
|
4833
|
+
output += table(headers, rows);
|
|
4834
|
+
return output;
|
|
4835
|
+
}
|
|
4836
|
+
function generateErrorHandlingPatterns(graph) {
|
|
4837
|
+
const patterns = {
|
|
4838
|
+
custom_errors: 0,
|
|
4839
|
+
retry: 0,
|
|
4840
|
+
timeout: 0,
|
|
4841
|
+
validation: 0,
|
|
4842
|
+
guard: 0
|
|
4843
|
+
};
|
|
4844
|
+
graph.forEachNode((node, attrs) => {
|
|
4845
|
+
const nameLower = attrs.name.toLowerCase();
|
|
4846
|
+
if (attrs.kind === "class" && (nameLower.includes("error") || nameLower.includes("exception"))) {
|
|
4847
|
+
patterns.custom_errors++;
|
|
4848
|
+
}
|
|
4849
|
+
if (nameLower.includes("retry") || nameLower.includes("attempt")) {
|
|
4850
|
+
patterns.retry++;
|
|
4851
|
+
}
|
|
4852
|
+
if (nameLower.includes("timeout")) {
|
|
4853
|
+
patterns.timeout++;
|
|
4854
|
+
}
|
|
4855
|
+
if (nameLower.includes("validate") || nameLower.includes("validator") || nameLower.includes("check")) {
|
|
4856
|
+
patterns.validation++;
|
|
4857
|
+
}
|
|
4858
|
+
if (nameLower.includes("guard") || nameLower.startsWith("is") || nameLower.startsWith("has")) {
|
|
4859
|
+
patterns.guard++;
|
|
4860
|
+
}
|
|
4861
|
+
});
|
|
4862
|
+
const detectedPatterns = Object.entries(patterns).filter(([, count]) => count > 0);
|
|
4863
|
+
if (detectedPatterns.length === 0) {
|
|
4864
|
+
return "No error handling patterns detected.\n\n";
|
|
4865
|
+
}
|
|
4866
|
+
let output = "";
|
|
4867
|
+
for (const [pattern, count] of detectedPatterns) {
|
|
4868
|
+
const description = getPatternDescription2(pattern);
|
|
4869
|
+
output += `- **${formatPatternName(pattern)}:** ${count} occurrences \u2014 ${description}
|
|
4870
|
+
`;
|
|
4871
|
+
}
|
|
4872
|
+
output += "\n";
|
|
4873
|
+
return output;
|
|
4874
|
+
}
|
|
4875
|
+
function formatPatternName(pattern) {
|
|
4876
|
+
const map = {
|
|
4877
|
+
custom_errors: "Custom Error Hierarchy",
|
|
4878
|
+
retry: "Retry Pattern",
|
|
4879
|
+
timeout: "Timeout Handling",
|
|
4880
|
+
validation: "Input Validation",
|
|
4881
|
+
guard: "Guard Clauses"
|
|
4882
|
+
};
|
|
4883
|
+
return map[pattern] || pattern;
|
|
4884
|
+
}
|
|
4885
|
+
function getPatternDescription2(pattern) {
|
|
4886
|
+
const map = {
|
|
4887
|
+
custom_errors: "Custom error classes for domain-specific exceptions",
|
|
4888
|
+
retry: "Retry logic for transient failures",
|
|
4889
|
+
timeout: "Timeout handling for long-running operations",
|
|
4890
|
+
validation: "Input validation to prevent errors",
|
|
4891
|
+
guard: "Guard clauses to check preconditions"
|
|
4892
|
+
};
|
|
4893
|
+
return map[pattern] || "";
|
|
4894
|
+
}
|
|
4895
|
+
function generateRecommendations(graph) {
|
|
4896
|
+
const recommendations = [];
|
|
4897
|
+
const fileStats = /* @__PURE__ */ new Map();
|
|
4898
|
+
graph.forEachNode((node, attrs) => {
|
|
4899
|
+
if (!fileStats.has(attrs.filePath)) {
|
|
4900
|
+
fileStats.set(attrs.filePath, {
|
|
4901
|
+
connectionCount: 0,
|
|
4902
|
+
errorSymbolCount: 0
|
|
4903
|
+
});
|
|
4904
|
+
}
|
|
4905
|
+
});
|
|
4906
|
+
const errorSymbols = getErrorRelatedSymbols(graph);
|
|
4907
|
+
for (const sym of errorSymbols) {
|
|
4908
|
+
const stats = fileStats.get(sym.filePath);
|
|
4909
|
+
if (stats) {
|
|
4910
|
+
stats.errorSymbolCount++;
|
|
4911
|
+
}
|
|
4912
|
+
}
|
|
4913
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4914
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
4915
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
4916
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
4917
|
+
const sourceStats = fileStats.get(sourceAttrs.filePath);
|
|
4918
|
+
const targetStats = fileStats.get(targetAttrs.filePath);
|
|
4919
|
+
if (sourceStats) sourceStats.connectionCount++;
|
|
4920
|
+
if (targetStats) targetStats.connectionCount++;
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4923
|
+
const needsErrorHandling = [];
|
|
4924
|
+
for (const [filePath, stats] of fileStats.entries()) {
|
|
4925
|
+
if (stats.connectionCount > 10 && stats.errorSymbolCount === 0) {
|
|
4926
|
+
needsErrorHandling.push(filePath);
|
|
4927
|
+
}
|
|
4928
|
+
}
|
|
4929
|
+
if (needsErrorHandling.length > 0) {
|
|
4930
|
+
recommendations.push(`**Add error handling to high-connection files:** ${needsErrorHandling.slice(0, 5).map((f) => code(f)).join(", ")}`);
|
|
4931
|
+
}
|
|
4932
|
+
const errorClasses = [];
|
|
4933
|
+
graph.forEachNode((node, attrs) => {
|
|
4934
|
+
if (attrs.kind === "class") {
|
|
4935
|
+
const nameLower = attrs.name.toLowerCase();
|
|
4936
|
+
if (nameLower.includes("error") || nameLower.includes("exception")) {
|
|
4937
|
+
const dependents = graph.inDegree(node);
|
|
4938
|
+
if (dependents === 0) {
|
|
4939
|
+
errorClasses.push(attrs.name);
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
}
|
|
4943
|
+
});
|
|
4944
|
+
if (errorClasses.length > 0) {
|
|
4945
|
+
recommendations.push(`**Unused error classes detected:** ${errorClasses.slice(0, 5).map((c) => code(c)).join(", ")} \u2014 Consider removing or documenting why they exist`);
|
|
4946
|
+
}
|
|
4947
|
+
if (recommendations.length === 0) {
|
|
4948
|
+
return "\u2705 No specific recommendations. Error handling appears well-distributed.\n\n";
|
|
4949
|
+
}
|
|
4950
|
+
return unorderedList(recommendations);
|
|
4951
|
+
}
|
|
4952
|
+
|
|
4953
|
+
// src/docs/tests.ts
|
|
4954
|
+
import { basename as basename4, dirname as dirname9 } from "path";
|
|
4955
|
+
function generateTests(graph, projectRoot, version) {
|
|
4956
|
+
let output = "";
|
|
4957
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
4958
|
+
const fileCount = getFileCount8(graph);
|
|
4959
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
4960
|
+
output += header("Test Analysis");
|
|
4961
|
+
output += "Test file inventory and coverage mapping.\n\n";
|
|
4962
|
+
output += header("Test File Inventory", 2);
|
|
4963
|
+
output += generateTestFileInventory(graph);
|
|
4964
|
+
output += header("Test-to-Source Mapping", 2);
|
|
4965
|
+
output += generateTestToSourceMapping(graph);
|
|
4966
|
+
output += header("Untested Files", 2);
|
|
4967
|
+
output += generateUntestedFiles(graph);
|
|
4968
|
+
output += header("Test Coverage Map", 2);
|
|
4969
|
+
output += generateTestCoverageMap(graph);
|
|
4970
|
+
output += header("Test Statistics", 2);
|
|
4971
|
+
output += generateTestStatistics(graph);
|
|
4972
|
+
return output;
|
|
4973
|
+
}
|
|
4974
|
+
function getFileCount8(graph) {
|
|
4975
|
+
const files = /* @__PURE__ */ new Set();
|
|
4976
|
+
graph.forEachNode((node, attrs) => {
|
|
4977
|
+
files.add(attrs.filePath);
|
|
4978
|
+
});
|
|
4979
|
+
return files.size;
|
|
4980
|
+
}
|
|
4981
|
+
function isTestFile(filePath) {
|
|
4982
|
+
const fileName = basename4(filePath).toLowerCase();
|
|
4983
|
+
const dirPath = dirname9(filePath).toLowerCase();
|
|
4984
|
+
if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
|
|
4985
|
+
return true;
|
|
4986
|
+
}
|
|
4987
|
+
if (fileName.includes(".test.") || fileName.includes(".spec.") || fileName.includes("_test.") || fileName.includes("_spec.")) {
|
|
4988
|
+
return true;
|
|
4989
|
+
}
|
|
4990
|
+
return false;
|
|
4991
|
+
}
|
|
4992
|
+
function getTestFiles(graph) {
|
|
4993
|
+
const testFiles = /* @__PURE__ */ new Map();
|
|
4994
|
+
graph.forEachNode((node, attrs) => {
|
|
4995
|
+
if (isTestFile(attrs.filePath)) {
|
|
4996
|
+
if (!testFiles.has(attrs.filePath)) {
|
|
4997
|
+
testFiles.set(attrs.filePath, {
|
|
4998
|
+
filePath: attrs.filePath,
|
|
4999
|
+
language: getLanguageFromPath2(attrs.filePath),
|
|
5000
|
+
symbolCount: 0,
|
|
5001
|
+
functionCount: 0
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
const info = testFiles.get(attrs.filePath);
|
|
5005
|
+
info.symbolCount++;
|
|
5006
|
+
if (attrs.kind === "function" || attrs.kind === "method") {
|
|
5007
|
+
info.functionCount++;
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
});
|
|
5011
|
+
return Array.from(testFiles.values());
|
|
5012
|
+
}
|
|
5013
|
+
function getLanguageFromPath2(filePath) {
|
|
5014
|
+
const ext = filePath.toLowerCase();
|
|
5015
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
|
|
5016
|
+
if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
|
|
5017
|
+
if (ext.endsWith(".py")) return "Python";
|
|
5018
|
+
if (ext.endsWith(".go")) return "Go";
|
|
5019
|
+
return "Other";
|
|
5020
|
+
}
|
|
5021
|
+
function generateTestFileInventory(graph) {
|
|
5022
|
+
const testFiles = getTestFiles(graph);
|
|
5023
|
+
if (testFiles.length === 0) {
|
|
5024
|
+
return "No test files detected.\n\n";
|
|
5025
|
+
}
|
|
5026
|
+
let output = `Found ${testFiles.length} test file${testFiles.length === 1 ? "" : "s"}:
|
|
5027
|
+
|
|
5028
|
+
`;
|
|
5029
|
+
testFiles.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
5030
|
+
const headers = ["Test File", "Language", "Symbols", "Functions"];
|
|
5031
|
+
const rows = testFiles.map((t) => [
|
|
5032
|
+
`\`${t.filePath}\``,
|
|
5033
|
+
t.language,
|
|
5034
|
+
formatNumber(t.symbolCount),
|
|
5035
|
+
formatNumber(t.functionCount)
|
|
5036
|
+
]);
|
|
5037
|
+
output += table(headers, rows);
|
|
5038
|
+
return output;
|
|
5039
|
+
}
|
|
5040
|
+
function matchTestToSource(testFile) {
|
|
5041
|
+
const testFileName = basename4(testFile);
|
|
5042
|
+
const testDir = dirname9(testFile);
|
|
5043
|
+
let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
|
|
5044
|
+
const possiblePaths = [];
|
|
5045
|
+
possiblePaths.push(testDir + "/" + sourceFileName);
|
|
5046
|
+
if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
|
|
5047
|
+
const parentDir = dirname9(testDir);
|
|
5048
|
+
possiblePaths.push(parentDir + "/" + sourceFileName);
|
|
5049
|
+
}
|
|
5050
|
+
if (testDir.includes("test")) {
|
|
5051
|
+
const srcDir = testDir.replace(/test[s]?/g, "src");
|
|
5052
|
+
possiblePaths.push(srcDir + "/" + sourceFileName);
|
|
5053
|
+
}
|
|
5054
|
+
for (const path2 of possiblePaths) {
|
|
5055
|
+
if (!isTestFile(path2)) {
|
|
5056
|
+
return path2;
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
return null;
|
|
5060
|
+
}
|
|
5061
|
+
function generateTestToSourceMapping(graph) {
|
|
5062
|
+
const testFiles = getTestFiles(graph);
|
|
5063
|
+
if (testFiles.length === 0) {
|
|
5064
|
+
return "No test files detected.\n\n";
|
|
5065
|
+
}
|
|
5066
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
5067
|
+
graph.forEachNode((node, attrs) => {
|
|
5068
|
+
allFiles.add(attrs.filePath);
|
|
5069
|
+
});
|
|
5070
|
+
let output = "";
|
|
5071
|
+
let mappedCount = 0;
|
|
5072
|
+
const mappings = [];
|
|
5073
|
+
for (const testFile of testFiles) {
|
|
5074
|
+
const sourceFile = matchTestToSource(testFile.filePath);
|
|
5075
|
+
const exists = sourceFile && allFiles.has(sourceFile);
|
|
5076
|
+
mappings.push({
|
|
5077
|
+
test: testFile.filePath,
|
|
5078
|
+
source: exists ? sourceFile : null
|
|
5079
|
+
});
|
|
5080
|
+
if (exists) {
|
|
5081
|
+
mappedCount++;
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
output += `Matched ${mappedCount} of ${testFiles.length} test files to source files:
|
|
5085
|
+
|
|
5086
|
+
`;
|
|
5087
|
+
for (const mapping of mappings) {
|
|
5088
|
+
if (mapping.source) {
|
|
5089
|
+
output += `- ${code(mapping.source)} \u2190 ${code(mapping.test)}
|
|
5090
|
+
`;
|
|
5091
|
+
}
|
|
5092
|
+
}
|
|
5093
|
+
output += "\n";
|
|
5094
|
+
const unmapped = mappings.filter((m) => !m.source);
|
|
5095
|
+
if (unmapped.length > 0) {
|
|
5096
|
+
output += `**Unmapped test files (${unmapped.length}):**
|
|
5097
|
+
|
|
5098
|
+
`;
|
|
5099
|
+
output += unorderedList(unmapped.map((m) => code(m.test)));
|
|
5100
|
+
}
|
|
5101
|
+
return output;
|
|
5102
|
+
}
|
|
5103
|
+
function generateUntestedFiles(graph) {
|
|
5104
|
+
const testFiles = getTestFiles(graph);
|
|
5105
|
+
const sourceFiles = [];
|
|
5106
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
5107
|
+
graph.forEachNode((node, attrs) => {
|
|
5108
|
+
allFiles.add(attrs.filePath);
|
|
5109
|
+
});
|
|
5110
|
+
for (const file of allFiles) {
|
|
5111
|
+
if (!isTestFile(file)) {
|
|
5112
|
+
sourceFiles.push(file);
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
if (sourceFiles.length === 0) {
|
|
5116
|
+
return "No source files detected.\n\n";
|
|
5117
|
+
}
|
|
5118
|
+
const testedFiles = /* @__PURE__ */ new Set();
|
|
5119
|
+
for (const testFile of testFiles) {
|
|
5120
|
+
const sourceFile = matchTestToSource(testFile.filePath);
|
|
5121
|
+
if (sourceFile && allFiles.has(sourceFile)) {
|
|
5122
|
+
testedFiles.add(sourceFile);
|
|
5123
|
+
}
|
|
5124
|
+
}
|
|
5125
|
+
const untested = sourceFiles.filter((f) => !testedFiles.has(f));
|
|
5126
|
+
if (untested.length === 0) {
|
|
5127
|
+
return "\u2705 All source files have matching test files.\n\n";
|
|
5128
|
+
}
|
|
5129
|
+
const fileConnections = /* @__PURE__ */ new Map();
|
|
5130
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5131
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
5132
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
5133
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
5134
|
+
fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
|
|
5135
|
+
fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
|
|
5136
|
+
}
|
|
5137
|
+
});
|
|
5138
|
+
const untestedWithConnections = untested.map((f) => ({
|
|
5139
|
+
filePath: f,
|
|
5140
|
+
connections: fileConnections.get(f) || 0
|
|
5141
|
+
})).sort((a, b) => b.connections - a.connections);
|
|
5142
|
+
let output = `\u26A0\uFE0F Found ${untested.length} source file${untested.length === 1 ? "" : "s"} without matching test files:
|
|
5143
|
+
|
|
5144
|
+
`;
|
|
5145
|
+
const headers = ["File", "Connections", "Priority"];
|
|
5146
|
+
const rows = untestedWithConnections.slice(0, 20).map((f) => {
|
|
5147
|
+
const priority = f.connections > 10 ? "\u{1F534} High" : f.connections > 5 ? "\u{1F7E1} Medium" : "\u{1F7E2} Low";
|
|
5148
|
+
return [
|
|
5149
|
+
`\`${f.filePath}\``,
|
|
5150
|
+
formatNumber(f.connections),
|
|
5151
|
+
priority
|
|
5152
|
+
];
|
|
5153
|
+
});
|
|
5154
|
+
output += table(headers, rows);
|
|
5155
|
+
if (untested.length > 20) {
|
|
5156
|
+
output += `... and ${untested.length - 20} more.
|
|
5157
|
+
|
|
5158
|
+
`;
|
|
5159
|
+
}
|
|
5160
|
+
return output;
|
|
5161
|
+
}
|
|
5162
|
+
function generateTestCoverageMap(graph) {
|
|
5163
|
+
const testFiles = getTestFiles(graph);
|
|
5164
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
5165
|
+
const sourceFiles = [];
|
|
5166
|
+
graph.forEachNode((node, attrs) => {
|
|
5167
|
+
allFiles.add(attrs.filePath);
|
|
5168
|
+
});
|
|
5169
|
+
for (const file of allFiles) {
|
|
5170
|
+
if (!isTestFile(file)) {
|
|
5171
|
+
sourceFiles.push(file);
|
|
5172
|
+
}
|
|
5173
|
+
}
|
|
5174
|
+
if (sourceFiles.length === 0) {
|
|
5175
|
+
return "No source files detected.\n\n";
|
|
5176
|
+
}
|
|
5177
|
+
const mappings = [];
|
|
5178
|
+
const testedFiles = /* @__PURE__ */ new Map();
|
|
5179
|
+
for (const testFile of testFiles) {
|
|
5180
|
+
const sourceFile = matchTestToSource(testFile.filePath);
|
|
5181
|
+
if (sourceFile && allFiles.has(sourceFile)) {
|
|
5182
|
+
testedFiles.set(sourceFile, testFile.filePath);
|
|
5183
|
+
}
|
|
5184
|
+
}
|
|
5185
|
+
const fileSymbols = /* @__PURE__ */ new Map();
|
|
5186
|
+
graph.forEachNode((node, attrs) => {
|
|
5187
|
+
fileSymbols.set(attrs.filePath, (fileSymbols.get(attrs.filePath) || 0) + 1);
|
|
5188
|
+
});
|
|
5189
|
+
for (const sourceFile of sourceFiles) {
|
|
5190
|
+
const testFile = testedFiles.get(sourceFile);
|
|
5191
|
+
mappings.push({
|
|
5192
|
+
sourceFile,
|
|
5193
|
+
hasTest: !!testFile,
|
|
5194
|
+
testFile: testFile || null,
|
|
5195
|
+
symbolCount: fileSymbols.get(sourceFile) || 0
|
|
5196
|
+
});
|
|
5197
|
+
}
|
|
5198
|
+
mappings.sort((a, b) => a.sourceFile.localeCompare(b.sourceFile));
|
|
5199
|
+
const headers = ["Source File", "Has Test?", "Test File", "Symbols"];
|
|
5200
|
+
const rows = mappings.slice(0, 30).map((m) => [
|
|
5201
|
+
`\`${m.sourceFile}\``,
|
|
5202
|
+
m.hasTest ? "\u2705" : "\u274C",
|
|
5203
|
+
m.testFile ? `\`${basename4(m.testFile)}\`` : "-",
|
|
5204
|
+
formatNumber(m.symbolCount)
|
|
5205
|
+
]);
|
|
5206
|
+
let output = table(headers, rows);
|
|
5207
|
+
if (mappings.length > 30) {
|
|
5208
|
+
output += `... and ${mappings.length - 30} more files.
|
|
5209
|
+
|
|
5210
|
+
`;
|
|
5211
|
+
}
|
|
5212
|
+
return output;
|
|
5213
|
+
}
|
|
5214
|
+
function generateTestStatistics(graph) {
|
|
5215
|
+
const testFiles = getTestFiles(graph);
|
|
5216
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
5217
|
+
const sourceFiles = [];
|
|
5218
|
+
graph.forEachNode((node, attrs) => {
|
|
5219
|
+
allFiles.add(attrs.filePath);
|
|
5220
|
+
});
|
|
5221
|
+
for (const file of allFiles) {
|
|
5222
|
+
if (!isTestFile(file)) {
|
|
5223
|
+
sourceFiles.push(file);
|
|
5224
|
+
}
|
|
5225
|
+
}
|
|
5226
|
+
const testedFiles = /* @__PURE__ */ new Set();
|
|
5227
|
+
for (const testFile of testFiles) {
|
|
5228
|
+
const sourceFile = matchTestToSource(testFile.filePath);
|
|
5229
|
+
if (sourceFile && allFiles.has(sourceFile)) {
|
|
5230
|
+
testedFiles.add(sourceFile);
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
let output = "";
|
|
5234
|
+
output += `- **Total test files:** ${formatNumber(testFiles.length)}
|
|
5235
|
+
`;
|
|
5236
|
+
output += `- **Total source files:** ${formatNumber(sourceFiles.length)}
|
|
5237
|
+
`;
|
|
5238
|
+
output += `- **Source files with tests:** ${formatNumber(testedFiles.size)} (${formatPercent(testedFiles.size, sourceFiles.length)})
|
|
5239
|
+
`;
|
|
5240
|
+
output += `- **Source files without tests:** ${formatNumber(sourceFiles.length - testedFiles.size)} (${formatPercent(sourceFiles.length - testedFiles.size, sourceFiles.length)})
|
|
5241
|
+
`;
|
|
5242
|
+
const dirTestCoverage = /* @__PURE__ */ new Map();
|
|
5243
|
+
for (const sourceFile of sourceFiles) {
|
|
5244
|
+
const dir = dirname9(sourceFile).split("/")[0];
|
|
5245
|
+
if (!dirTestCoverage.has(dir)) {
|
|
5246
|
+
dirTestCoverage.set(dir, { total: 0, tested: 0 });
|
|
5247
|
+
}
|
|
5248
|
+
dirTestCoverage.get(dir).total++;
|
|
5249
|
+
if (testedFiles.has(sourceFile)) {
|
|
5250
|
+
dirTestCoverage.get(dir).tested++;
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
if (dirTestCoverage.size > 1) {
|
|
5254
|
+
output += "\n**Coverage by directory:**\n\n";
|
|
5255
|
+
const sortedDirs = Array.from(dirTestCoverage.entries()).sort((a, b) => b[1].total - a[1].total);
|
|
5256
|
+
for (const [dir, coverage] of sortedDirs) {
|
|
5257
|
+
const percent = formatPercent(coverage.tested, coverage.total);
|
|
5258
|
+
output += `- **${dir}/**: ${coverage.tested}/${coverage.total} files (${percent})
|
|
5259
|
+
`;
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
output += "\n";
|
|
5263
|
+
return output;
|
|
5264
|
+
}
|
|
5265
|
+
|
|
5266
|
+
// src/docs/history.ts
|
|
5267
|
+
import { dirname as dirname10 } from "path";
|
|
5268
|
+
import { execSync } from "child_process";
|
|
5269
|
+
function generateHistory(graph, projectRoot, version) {
|
|
5270
|
+
let output = "";
|
|
5271
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5272
|
+
const fileCount = getFileCount9(graph);
|
|
5273
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
5274
|
+
output += header("Development History");
|
|
5275
|
+
output += "Git history combined with graph analysis showing feature evolution.\n\n";
|
|
5276
|
+
const hasGit = isGitAvailable(projectRoot);
|
|
5277
|
+
if (!hasGit) {
|
|
5278
|
+
output += "\u26A0\uFE0F **Git history not available.** This project is not a git repository or git is not installed.\n\n";
|
|
5279
|
+
output += "Showing graph-based analysis only:\n\n";
|
|
5280
|
+
}
|
|
5281
|
+
if (hasGit) {
|
|
5282
|
+
output += header("Development Timeline", 2);
|
|
5283
|
+
output += generateDevelopmentTimeline(projectRoot);
|
|
5284
|
+
}
|
|
5285
|
+
if (hasGit) {
|
|
5286
|
+
output += header("File Change Frequency (Churn)", 2);
|
|
5287
|
+
output += generateFileChurn(projectRoot, graph);
|
|
5288
|
+
}
|
|
5289
|
+
if (hasGit) {
|
|
5290
|
+
output += header("Feature Timeline", 2);
|
|
5291
|
+
output += generateFeatureTimeline(projectRoot);
|
|
5292
|
+
}
|
|
5293
|
+
if (hasGit) {
|
|
5294
|
+
output += header("File Age Analysis", 2);
|
|
5295
|
+
output += generateFileAgeAnalysis(projectRoot, graph);
|
|
5296
|
+
}
|
|
5297
|
+
if (hasGit) {
|
|
5298
|
+
output += header("Contributors", 2);
|
|
5299
|
+
output += generateContributors(projectRoot);
|
|
5300
|
+
}
|
|
5301
|
+
output += header("Feature Clusters (Graph-Based)", 2);
|
|
5302
|
+
output += generateFeatureClusters(graph);
|
|
5303
|
+
return output;
|
|
5304
|
+
}
|
|
5305
|
+
function getFileCount9(graph) {
|
|
5306
|
+
const files = /* @__PURE__ */ new Set();
|
|
5307
|
+
graph.forEachNode((node, attrs) => {
|
|
5308
|
+
files.add(attrs.filePath);
|
|
5309
|
+
});
|
|
5310
|
+
return files.size;
|
|
5311
|
+
}
|
|
5312
|
+
function isGitAvailable(projectRoot) {
|
|
5313
|
+
try {
|
|
5314
|
+
execSync("git rev-parse --git-dir", {
|
|
5315
|
+
cwd: projectRoot,
|
|
5316
|
+
encoding: "utf-8",
|
|
5317
|
+
timeout: 5e3,
|
|
5318
|
+
stdio: "pipe"
|
|
5319
|
+
});
|
|
5320
|
+
return true;
|
|
5321
|
+
} catch {
|
|
5322
|
+
return false;
|
|
5323
|
+
}
|
|
5324
|
+
}
|
|
5325
|
+
function executeGitCommand(projectRoot, command) {
|
|
5326
|
+
try {
|
|
5327
|
+
return execSync(command, {
|
|
5328
|
+
cwd: projectRoot,
|
|
5329
|
+
encoding: "utf-8",
|
|
5330
|
+
timeout: 1e4,
|
|
5331
|
+
stdio: "pipe"
|
|
5332
|
+
}).trim();
|
|
5333
|
+
} catch {
|
|
5334
|
+
return "";
|
|
5335
|
+
}
|
|
5336
|
+
}
|
|
5337
|
+
function generateDevelopmentTimeline(projectRoot) {
|
|
5338
|
+
const log = executeGitCommand(projectRoot, 'git log --format="%ai" --all --no-merges');
|
|
5339
|
+
if (!log) {
|
|
5340
|
+
return "Unable to retrieve git log.\n\n";
|
|
5341
|
+
}
|
|
5342
|
+
const dates = log.split("\n").filter((d) => d.length > 0);
|
|
5343
|
+
if (dates.length === 0) {
|
|
5344
|
+
return "No commits found.\n\n";
|
|
5345
|
+
}
|
|
5346
|
+
const firstCommit = new Date(dates[dates.length - 1]);
|
|
5347
|
+
const lastCommit = new Date(dates[0]);
|
|
5348
|
+
const ageInDays = Math.floor((lastCommit.getTime() - firstCommit.getTime()) / (1e3 * 60 * 60 * 24));
|
|
5349
|
+
const ageInMonths = Math.floor(ageInDays / 30);
|
|
5350
|
+
let output = "";
|
|
5351
|
+
output += `- **First commit:** ${firstCommit.toISOString().split("T")[0]}
|
|
5352
|
+
`;
|
|
5353
|
+
output += `- **Last commit:** ${lastCommit.toISOString().split("T")[0]}
|
|
5354
|
+
`;
|
|
5355
|
+
output += `- **Project age:** ${ageInMonths} months (${ageInDays} days)
|
|
5356
|
+
`;
|
|
5357
|
+
output += `- **Total commits:** ${formatNumber(dates.length)}
|
|
5358
|
+
`;
|
|
5359
|
+
const commitsPerMonth = ageInMonths > 0 ? (dates.length / ageInMonths).toFixed(1) : dates.length.toString();
|
|
5360
|
+
output += `- **Average activity:** ${commitsPerMonth} commits/month
|
|
5361
|
+
`;
|
|
5362
|
+
output += "\n";
|
|
5363
|
+
return output;
|
|
5364
|
+
}
|
|
5365
|
+
function generateFileChurn(projectRoot, graph) {
|
|
5366
|
+
const churnOutput = executeGitCommand(
|
|
5367
|
+
projectRoot,
|
|
5368
|
+
'git log --all --name-only --format="" | sort | uniq -c | sort -rn | head -20'
|
|
5369
|
+
);
|
|
5370
|
+
if (!churnOutput) {
|
|
5371
|
+
return "Unable to retrieve file churn data.\n\n";
|
|
5372
|
+
}
|
|
5373
|
+
const lines = churnOutput.split("\n").filter((l) => l.trim().length > 0);
|
|
5374
|
+
if (lines.length === 0) {
|
|
5375
|
+
return "No file churn data available.\n\n";
|
|
5376
|
+
}
|
|
5377
|
+
const churnData = [];
|
|
5378
|
+
for (const line of lines) {
|
|
5379
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
5380
|
+
if (match) {
|
|
5381
|
+
const changes = parseInt(match[1], 10);
|
|
5382
|
+
const file = match[2].trim();
|
|
5383
|
+
if (file && file.length > 0 && !file.startsWith(".")) {
|
|
5384
|
+
churnData.push({ file, changes });
|
|
5385
|
+
}
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
if (churnData.length === 0) {
|
|
5389
|
+
return "No valid file churn data.\n\n";
|
|
5390
|
+
}
|
|
5391
|
+
const fileConnections = /* @__PURE__ */ new Map();
|
|
5392
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5393
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
5394
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
5395
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
5396
|
+
fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
|
|
5397
|
+
fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
|
|
5398
|
+
}
|
|
5399
|
+
});
|
|
5400
|
+
let output = "Top 20 most-changed files:\n\n";
|
|
5401
|
+
const headers = ["File", "Changes", "Connections", "Risk"];
|
|
5402
|
+
const rows = churnData.slice(0, 20).map((item) => {
|
|
5403
|
+
const connections = fileConnections.get(item.file) || 0;
|
|
5404
|
+
let risk = "\u{1F7E2} Low";
|
|
5405
|
+
if (item.changes > 50 && connections > 10) {
|
|
5406
|
+
risk = "\u{1F534} High";
|
|
5407
|
+
} else if (item.changes > 20 && connections > 5) {
|
|
5408
|
+
risk = "\u{1F7E1} Medium";
|
|
5409
|
+
} else if (item.changes > 50 || connections > 10) {
|
|
5410
|
+
risk = "\u{1F7E1} Medium";
|
|
5411
|
+
}
|
|
5412
|
+
return [
|
|
5413
|
+
`\`${item.file}\``,
|
|
5414
|
+
formatNumber(item.changes),
|
|
5415
|
+
formatNumber(connections),
|
|
5416
|
+
risk
|
|
5417
|
+
];
|
|
5418
|
+
});
|
|
5419
|
+
output += table(headers, rows);
|
|
5420
|
+
output += "**Risk levels:**\n\n";
|
|
5421
|
+
output += "- \u{1F534} High churn + high connections = risky hotspot (break often, affect many)\n";
|
|
5422
|
+
output += "- \u{1F7E1} High churn + low connections = actively developed but isolated\n";
|
|
5423
|
+
output += "- \u{1F7E2} Low churn + high connections = stable foundation\n\n";
|
|
5424
|
+
return output;
|
|
5425
|
+
}
|
|
5426
|
+
function generateFeatureTimeline(projectRoot) {
|
|
5427
|
+
const log = executeGitCommand(projectRoot, "git log --oneline --all --no-merges");
|
|
5428
|
+
if (!log) {
|
|
5429
|
+
return "Unable to retrieve commit log.\n\n";
|
|
5430
|
+
}
|
|
5431
|
+
const commits = log.split("\n").filter((c) => c.length > 0);
|
|
5432
|
+
if (commits.length === 0) {
|
|
5433
|
+
return "No commits found.\n\n";
|
|
5434
|
+
}
|
|
5435
|
+
const categories = {
|
|
5436
|
+
features: 0,
|
|
5437
|
+
fixes: 0,
|
|
5438
|
+
refactors: 0,
|
|
5439
|
+
other: 0
|
|
5440
|
+
};
|
|
5441
|
+
const featureKeywords = ["feat", "add", "new", "implement", "create"];
|
|
5442
|
+
const fixKeywords = ["fix", "bug", "patch", "resolve"];
|
|
5443
|
+
const refactorKeywords = ["refactor", "cleanup", "restructure", "improve"];
|
|
5444
|
+
for (const commit of commits) {
|
|
5445
|
+
const messageLower = commit.toLowerCase();
|
|
5446
|
+
if (featureKeywords.some((kw) => messageLower.includes(kw))) {
|
|
5447
|
+
categories.features++;
|
|
5448
|
+
} else if (fixKeywords.some((kw) => messageLower.includes(kw))) {
|
|
5449
|
+
categories.fixes++;
|
|
5450
|
+
} else if (refactorKeywords.some((kw) => messageLower.includes(kw))) {
|
|
5451
|
+
categories.refactors++;
|
|
5452
|
+
} else {
|
|
5453
|
+
categories.other++;
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
let output = "Commit breakdown by type:\n\n";
|
|
5457
|
+
output += `- **Features:** ${formatNumber(categories.features)} commits (${(categories.features / commits.length * 100).toFixed(1)}%)
|
|
5458
|
+
`;
|
|
5459
|
+
output += `- **Bug fixes:** ${formatNumber(categories.fixes)} commits (${(categories.fixes / commits.length * 100).toFixed(1)}%)
|
|
5460
|
+
`;
|
|
5461
|
+
output += `- **Refactors:** ${formatNumber(categories.refactors)} commits (${(categories.refactors / commits.length * 100).toFixed(1)}%)
|
|
5462
|
+
`;
|
|
5463
|
+
output += `- **Other:** ${formatNumber(categories.other)} commits (${(categories.other / commits.length * 100).toFixed(1)}%)
|
|
5464
|
+
`;
|
|
5465
|
+
output += "\n";
|
|
5466
|
+
return output;
|
|
5467
|
+
}
|
|
5468
|
+
function generateFileAgeAnalysis(projectRoot, graph) {
|
|
5469
|
+
const files = /* @__PURE__ */ new Set();
|
|
5470
|
+
graph.forEachNode((node, attrs) => {
|
|
5471
|
+
files.add(attrs.filePath);
|
|
5472
|
+
});
|
|
5473
|
+
if (files.size === 0) {
|
|
5474
|
+
return "No files to analyze.\n\n";
|
|
5475
|
+
}
|
|
5476
|
+
const fileAges = [];
|
|
5477
|
+
const sampleFiles = Array.from(files).slice(0, 20);
|
|
5478
|
+
for (const file of sampleFiles) {
|
|
5479
|
+
const dateStr = executeGitCommand(
|
|
5480
|
+
projectRoot,
|
|
5481
|
+
`git log --format="%ai" --diff-filter=A -- "${file}" | tail -1`
|
|
5482
|
+
);
|
|
5483
|
+
if (dateStr) {
|
|
5484
|
+
fileAges.push({
|
|
5485
|
+
file,
|
|
5486
|
+
date: new Date(dateStr)
|
|
5487
|
+
});
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
if (fileAges.length === 0) {
|
|
5491
|
+
return "Unable to determine file ages.\n\n";
|
|
5492
|
+
}
|
|
5493
|
+
fileAges.sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
5494
|
+
let output = "";
|
|
5495
|
+
output += "**Oldest files (foundation):**\n\n";
|
|
5496
|
+
const oldest = fileAges.slice(0, 5);
|
|
5497
|
+
output += unorderedList(oldest.map((f) => {
|
|
5498
|
+
return `${code(f.file)} \u2014 added ${f.date.toISOString().split("T")[0]}`;
|
|
5499
|
+
}));
|
|
5500
|
+
output += "**Newest files (recent features):**\n\n";
|
|
5501
|
+
const newest = fileAges.slice(-5).reverse();
|
|
5502
|
+
output += unorderedList(newest.map((f) => {
|
|
5503
|
+
return `${code(f.file)} \u2014 added ${f.date.toISOString().split("T")[0]}`;
|
|
5504
|
+
}));
|
|
5505
|
+
return output;
|
|
5506
|
+
}
|
|
5507
|
+
function generateContributors(projectRoot) {
|
|
5508
|
+
const contributors = executeGitCommand(projectRoot, "git shortlog -sn --all");
|
|
5509
|
+
if (!contributors) {
|
|
5510
|
+
return "Unable to retrieve contributor data.\n\n";
|
|
5511
|
+
}
|
|
5512
|
+
const lines = contributors.split("\n").filter((l) => l.trim().length > 0);
|
|
5513
|
+
if (lines.length === 0) {
|
|
5514
|
+
return "No contributors found.\n\n";
|
|
5515
|
+
}
|
|
5516
|
+
let output = `Found ${lines.length} contributor${lines.length === 1 ? "" : "s"}:
|
|
5517
|
+
|
|
5518
|
+
`;
|
|
5519
|
+
const headers = ["Contributor", "Commits", "Percentage"];
|
|
5520
|
+
const contributorData = [];
|
|
5521
|
+
let totalCommits = 0;
|
|
5522
|
+
for (const line of lines) {
|
|
5523
|
+
const match = line.trim().match(/^(\d+)\s+(.+)$/);
|
|
5524
|
+
if (match) {
|
|
5525
|
+
const commits = parseInt(match[1], 10);
|
|
5526
|
+
const name = match[2].trim();
|
|
5527
|
+
contributorData.push({ name, commits });
|
|
5528
|
+
totalCommits += commits;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
const rows = contributorData.slice(0, 10).map((c) => [
|
|
5532
|
+
c.name,
|
|
5533
|
+
formatNumber(c.commits),
|
|
5534
|
+
`${(c.commits / totalCommits * 100).toFixed(1)}%`
|
|
5535
|
+
]);
|
|
5536
|
+
output += table(headers, rows);
|
|
5537
|
+
if (contributorData.length > 10) {
|
|
5538
|
+
output += `... and ${contributorData.length - 10} more contributors.
|
|
5539
|
+
|
|
5540
|
+
`;
|
|
5541
|
+
}
|
|
5542
|
+
return output;
|
|
5543
|
+
}
|
|
5544
|
+
function generateFeatureClusters(graph) {
|
|
5545
|
+
const dirFiles = /* @__PURE__ */ new Map();
|
|
5546
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
5547
|
+
graph.forEachNode((node, attrs) => {
|
|
5548
|
+
const dir = dirname10(attrs.filePath);
|
|
5549
|
+
if (!dirFiles.has(dir)) {
|
|
5550
|
+
dirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
5551
|
+
}
|
|
5552
|
+
dirFiles.get(dir).add(attrs.filePath);
|
|
5553
|
+
});
|
|
5554
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5555
|
+
const sourceFile = graph.getNodeAttributes(source).filePath;
|
|
5556
|
+
const targetFile = graph.getNodeAttributes(target).filePath;
|
|
5557
|
+
if (sourceFile !== targetFile) {
|
|
5558
|
+
if (!fileEdges.has(sourceFile)) {
|
|
5559
|
+
fileEdges.set(sourceFile, /* @__PURE__ */ new Set());
|
|
5560
|
+
}
|
|
5561
|
+
fileEdges.get(sourceFile).add(targetFile);
|
|
5562
|
+
}
|
|
5563
|
+
});
|
|
5564
|
+
const clusters = [];
|
|
5565
|
+
for (const [dir, files] of dirFiles.entries()) {
|
|
5566
|
+
if (dir === "." || files.size < 2) continue;
|
|
5567
|
+
const fileArray = Array.from(files);
|
|
5568
|
+
let internalEdgeCount = 0;
|
|
5569
|
+
for (const file of fileArray) {
|
|
5570
|
+
const targets = fileEdges.get(file);
|
|
5571
|
+
if (targets) {
|
|
5572
|
+
for (const target of targets) {
|
|
5573
|
+
if (files.has(target)) {
|
|
5574
|
+
internalEdgeCount++;
|
|
5575
|
+
}
|
|
5576
|
+
}
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
if (internalEdgeCount >= 2) {
|
|
5580
|
+
const clusterName = inferClusterName2(fileArray, dir);
|
|
5581
|
+
clusters.push({
|
|
5582
|
+
name: clusterName,
|
|
5583
|
+
files: fileArray,
|
|
5584
|
+
internalEdges: internalEdgeCount
|
|
5585
|
+
});
|
|
5586
|
+
}
|
|
5587
|
+
}
|
|
5588
|
+
if (clusters.length === 0) {
|
|
5589
|
+
return "No distinct feature clusters detected.\n\n";
|
|
5590
|
+
}
|
|
5591
|
+
clusters.sort((a, b) => b.internalEdges - a.internalEdges);
|
|
5592
|
+
let output = `Detected ${clusters.length} feature cluster${clusters.length === 1 ? "" : "s"} (tightly-connected file groups):
|
|
5593
|
+
|
|
5594
|
+
`;
|
|
5595
|
+
for (const cluster of clusters.slice(0, 10)) {
|
|
5596
|
+
output += `**${cluster.name}** (${cluster.files.length} files, ${cluster.internalEdges} internal connections):
|
|
5597
|
+
|
|
5598
|
+
`;
|
|
5599
|
+
const items = cluster.files.slice(0, 5).map((f) => code(f));
|
|
5600
|
+
output += unorderedList(items);
|
|
5601
|
+
if (cluster.files.length > 5) {
|
|
5602
|
+
output += `... and ${cluster.files.length - 5} more files.
|
|
5603
|
+
|
|
5604
|
+
`;
|
|
5605
|
+
}
|
|
5606
|
+
}
|
|
5607
|
+
return output;
|
|
5608
|
+
}
|
|
5609
|
+
function inferClusterName2(files, dir) {
|
|
5610
|
+
const words = /* @__PURE__ */ new Map();
|
|
5611
|
+
for (const file of files) {
|
|
5612
|
+
const fileName = file.toLowerCase();
|
|
5613
|
+
const parts = fileName.split(/[\/\-\_\.]/).filter((p) => p.length > 3);
|
|
5614
|
+
for (const part of parts) {
|
|
5615
|
+
words.set(part, (words.get(part) || 0) + 1);
|
|
5616
|
+
}
|
|
5617
|
+
}
|
|
5618
|
+
const sortedWords = Array.from(words.entries()).sort((a, b) => b[1] - a[1]);
|
|
5619
|
+
if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
|
|
5620
|
+
return capitalizeFirst3(sortedWords[0][0]);
|
|
5621
|
+
}
|
|
5622
|
+
const dirName = dir.split("/").pop() || "Core";
|
|
5623
|
+
return capitalizeFirst3(dirName);
|
|
5624
|
+
}
|
|
5625
|
+
function capitalizeFirst3(str) {
|
|
5626
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
// src/docs/current.ts
|
|
5630
|
+
import { dirname as dirname11 } from "path";
|
|
5631
|
+
function generateCurrent(graph, projectRoot, version) {
|
|
5632
|
+
let output = "";
|
|
5633
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5634
|
+
const fileCount = getFileCount10(graph);
|
|
5635
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
5636
|
+
output += header("Complete Codebase Snapshot");
|
|
5637
|
+
output += "> **Note:** This is a complete snapshot of the entire codebase. For a high-level overview, see ARCHITECTURE.md.\n\n";
|
|
5638
|
+
output += header("Project Overview", 2);
|
|
5639
|
+
output += generateProjectOverview(graph);
|
|
5640
|
+
output += header("Complete File Index", 2);
|
|
5641
|
+
output += generateCompleteFileIndex(graph);
|
|
5642
|
+
output += header("Complete Symbol Index", 2);
|
|
5643
|
+
output += generateCompleteSymbolIndex(graph);
|
|
5644
|
+
output += header("Complete Edge List", 2);
|
|
5645
|
+
output += generateCompleteEdgeList(graph);
|
|
5646
|
+
output += header("Connection Matrix", 2);
|
|
5647
|
+
output += generateConnectionMatrix(graph);
|
|
5648
|
+
return output;
|
|
5649
|
+
}
|
|
5650
|
+
function getFileCount10(graph) {
|
|
5651
|
+
const files = /* @__PURE__ */ new Set();
|
|
5652
|
+
graph.forEachNode((node, attrs) => {
|
|
5653
|
+
files.add(attrs.filePath);
|
|
5654
|
+
});
|
|
5655
|
+
return files.size;
|
|
5656
|
+
}
|
|
5657
|
+
function getLanguageStats3(graph) {
|
|
5658
|
+
const stats = {};
|
|
5659
|
+
const files = /* @__PURE__ */ new Set();
|
|
5660
|
+
graph.forEachNode((node, attrs) => {
|
|
5661
|
+
if (!files.has(attrs.filePath)) {
|
|
5662
|
+
files.add(attrs.filePath);
|
|
5663
|
+
const ext = attrs.filePath.toLowerCase();
|
|
5664
|
+
let lang;
|
|
5665
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) {
|
|
5666
|
+
lang = "TypeScript";
|
|
5667
|
+
} else if (ext.endsWith(".py")) {
|
|
5668
|
+
lang = "Python";
|
|
5669
|
+
} else if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) {
|
|
5670
|
+
lang = "JavaScript";
|
|
5671
|
+
} else if (ext.endsWith(".go")) {
|
|
5672
|
+
lang = "Go";
|
|
5673
|
+
} else {
|
|
5674
|
+
lang = "Other";
|
|
5675
|
+
}
|
|
5676
|
+
stats[lang] = (stats[lang] || 0) + 1;
|
|
5677
|
+
}
|
|
5678
|
+
});
|
|
5679
|
+
return stats;
|
|
5680
|
+
}
|
|
5681
|
+
function generateProjectOverview(graph) {
|
|
5682
|
+
const fileCount = getFileCount10(graph);
|
|
5683
|
+
const symbolCount = graph.order;
|
|
5684
|
+
const edgeCount = graph.size;
|
|
5685
|
+
const languages2 = getLanguageStats3(graph);
|
|
5686
|
+
let output = "";
|
|
5687
|
+
output += `- **Total files:** ${formatNumber(fileCount)}
|
|
5688
|
+
`;
|
|
5689
|
+
output += `- **Total symbols:** ${formatNumber(symbolCount)}
|
|
5690
|
+
`;
|
|
5691
|
+
output += `- **Total edges:** ${formatNumber(edgeCount)}
|
|
5692
|
+
`;
|
|
5693
|
+
if (Object.keys(languages2).length > 0) {
|
|
5694
|
+
output += "\n**Language breakdown:**\n\n";
|
|
5695
|
+
for (const [lang, count] of Object.entries(languages2).sort((a, b) => b[1] - a[1])) {
|
|
5696
|
+
output += `- ${lang}: ${count} files
|
|
5697
|
+
`;
|
|
5698
|
+
}
|
|
5699
|
+
}
|
|
5700
|
+
output += "\n";
|
|
5701
|
+
return output;
|
|
5702
|
+
}
|
|
5703
|
+
function getFileInfo(graph) {
|
|
5704
|
+
const fileMap = /* @__PURE__ */ new Map();
|
|
5705
|
+
graph.forEachNode((node, attrs) => {
|
|
5706
|
+
if (!fileMap.has(attrs.filePath)) {
|
|
5707
|
+
fileMap.set(attrs.filePath, {
|
|
5708
|
+
filePath: attrs.filePath,
|
|
5709
|
+
language: getLanguageFromPath3(attrs.filePath),
|
|
5710
|
+
symbols: [],
|
|
5711
|
+
importsFrom: [],
|
|
5712
|
+
importedBy: [],
|
|
5713
|
+
incomingEdges: 0,
|
|
5714
|
+
outgoingEdges: 0
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
5717
|
+
const info = fileMap.get(attrs.filePath);
|
|
5718
|
+
if (attrs.name !== "__file__") {
|
|
5719
|
+
info.symbols.push({
|
|
5720
|
+
name: attrs.name,
|
|
5721
|
+
kind: attrs.kind,
|
|
5722
|
+
line: attrs.startLine
|
|
5723
|
+
});
|
|
5724
|
+
}
|
|
5725
|
+
});
|
|
5726
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
5727
|
+
const fileEdgesReverse = /* @__PURE__ */ new Map();
|
|
5728
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5729
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
5730
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
5731
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
5732
|
+
if (!fileEdges.has(sourceAttrs.filePath)) {
|
|
5733
|
+
fileEdges.set(sourceAttrs.filePath, /* @__PURE__ */ new Set());
|
|
5734
|
+
}
|
|
5735
|
+
fileEdges.get(sourceAttrs.filePath).add(targetAttrs.filePath);
|
|
5736
|
+
if (!fileEdgesReverse.has(targetAttrs.filePath)) {
|
|
5737
|
+
fileEdgesReverse.set(targetAttrs.filePath, /* @__PURE__ */ new Set());
|
|
5738
|
+
}
|
|
5739
|
+
fileEdgesReverse.get(targetAttrs.filePath).add(sourceAttrs.filePath);
|
|
5740
|
+
}
|
|
5741
|
+
});
|
|
5742
|
+
for (const [filePath, info] of fileMap.entries()) {
|
|
5743
|
+
const importsFrom = fileEdges.get(filePath);
|
|
5744
|
+
const importedBy = fileEdgesReverse.get(filePath);
|
|
5745
|
+
info.importsFrom = importsFrom ? Array.from(importsFrom) : [];
|
|
5746
|
+
info.importedBy = importedBy ? Array.from(importedBy) : [];
|
|
5747
|
+
info.outgoingEdges = info.importsFrom.length;
|
|
5748
|
+
info.incomingEdges = info.importedBy.length;
|
|
5749
|
+
}
|
|
5750
|
+
return Array.from(fileMap.values());
|
|
5751
|
+
}
|
|
5752
|
+
function getLanguageFromPath3(filePath) {
|
|
5753
|
+
const ext = filePath.toLowerCase();
|
|
5754
|
+
if (ext.endsWith(".ts") || ext.endsWith(".tsx")) return "TypeScript";
|
|
5755
|
+
if (ext.endsWith(".js") || ext.endsWith(".jsx") || ext.endsWith(".mjs") || ext.endsWith(".cjs")) return "JavaScript";
|
|
5756
|
+
if (ext.endsWith(".py")) return "Python";
|
|
5757
|
+
if (ext.endsWith(".go")) return "Go";
|
|
5758
|
+
return "Other";
|
|
5759
|
+
}
|
|
5760
|
+
function generateCompleteFileIndex(graph) {
|
|
5761
|
+
const fileInfos = getFileInfo(graph);
|
|
5762
|
+
if (fileInfos.length === 0) {
|
|
5763
|
+
return "No files detected.\n\n";
|
|
5764
|
+
}
|
|
5765
|
+
fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
5766
|
+
const dirGroups = /* @__PURE__ */ new Map();
|
|
5767
|
+
for (const info of fileInfos) {
|
|
5768
|
+
const dir = dirname11(info.filePath);
|
|
5769
|
+
const topDir = dir === "." ? "root" : dir.split("/")[0];
|
|
5770
|
+
if (!dirGroups.has(topDir)) {
|
|
5771
|
+
dirGroups.set(topDir, []);
|
|
5772
|
+
}
|
|
5773
|
+
dirGroups.get(topDir).push(info);
|
|
5774
|
+
}
|
|
5775
|
+
let output = "";
|
|
5776
|
+
for (const [dir, files] of Array.from(dirGroups.entries()).sort()) {
|
|
5777
|
+
output += header(dir === "root" ? "Root Directory" : `${dir}/`, 3);
|
|
5778
|
+
for (const file of files) {
|
|
5779
|
+
output += header(file.filePath, 4);
|
|
5780
|
+
output += `- **Language:** ${file.language}
|
|
5781
|
+
`;
|
|
5782
|
+
output += `- **Symbols (${file.symbols.length}):** `;
|
|
5783
|
+
if (file.symbols.length === 0) {
|
|
5784
|
+
output += "None\n";
|
|
5785
|
+
} else if (file.symbols.length <= 10) {
|
|
5786
|
+
output += file.symbols.map((s) => s.name).join(", ") + "\n";
|
|
5787
|
+
} else {
|
|
5788
|
+
output += file.symbols.slice(0, 10).map((s) => s.name).join(", ");
|
|
5789
|
+
output += `, ... and ${file.symbols.length - 10} more
|
|
5790
|
+
`;
|
|
5791
|
+
}
|
|
5792
|
+
if (file.importsFrom.length > 0) {
|
|
5793
|
+
output += `- **Imports from (${file.importsFrom.length}):** `;
|
|
5794
|
+
if (file.importsFrom.length <= 5) {
|
|
5795
|
+
output += file.importsFrom.map((f) => code(f)).join(", ") + "\n";
|
|
5796
|
+
} else {
|
|
5797
|
+
output += file.importsFrom.slice(0, 5).map((f) => code(f)).join(", ");
|
|
5798
|
+
output += `, ... and ${file.importsFrom.length - 5} more
|
|
5799
|
+
`;
|
|
5800
|
+
}
|
|
5801
|
+
}
|
|
5802
|
+
if (file.importedBy.length > 0) {
|
|
5803
|
+
output += `- **Imported by (${file.importedBy.length}):** `;
|
|
5804
|
+
if (file.importedBy.length <= 5) {
|
|
5805
|
+
output += file.importedBy.map((f) => code(f)).join(", ") + "\n";
|
|
5806
|
+
} else {
|
|
5807
|
+
output += file.importedBy.slice(0, 5).map((f) => code(f)).join(", ");
|
|
5808
|
+
output += `, ... and ${file.importedBy.length - 5} more
|
|
5809
|
+
`;
|
|
5810
|
+
}
|
|
5811
|
+
}
|
|
5812
|
+
output += `- **Connections:** ${file.incomingEdges} inbound, ${file.outgoingEdges} outbound
|
|
5813
|
+
|
|
5814
|
+
`;
|
|
5815
|
+
}
|
|
5816
|
+
}
|
|
5817
|
+
return output;
|
|
5818
|
+
}
|
|
5819
|
+
function generateCompleteSymbolIndex(graph) {
|
|
5820
|
+
const symbolsByKind = /* @__PURE__ */ new Map();
|
|
5821
|
+
graph.forEachNode((node, attrs) => {
|
|
5822
|
+
if (attrs.name === "__file__") return;
|
|
5823
|
+
if (!symbolsByKind.has(attrs.kind)) {
|
|
5824
|
+
symbolsByKind.set(attrs.kind, []);
|
|
5825
|
+
}
|
|
5826
|
+
symbolsByKind.get(attrs.kind).push({
|
|
5827
|
+
name: attrs.name,
|
|
5828
|
+
filePath: attrs.filePath,
|
|
5829
|
+
line: attrs.startLine
|
|
5830
|
+
});
|
|
5831
|
+
});
|
|
5832
|
+
if (symbolsByKind.size === 0) {
|
|
5833
|
+
return "No symbols detected.\n\n";
|
|
5834
|
+
}
|
|
5835
|
+
let output = "";
|
|
5836
|
+
const sortedKinds = Array.from(symbolsByKind.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
5837
|
+
for (const [kind, symbols] of sortedKinds) {
|
|
5838
|
+
output += header(`${capitalizeKind2(kind)}s (${symbols.length})`, 3);
|
|
5839
|
+
const sorted = symbols.sort((a, b) => a.name.localeCompare(b.name));
|
|
5840
|
+
const limit = 100;
|
|
5841
|
+
const items = sorted.slice(0, limit).map((s) => {
|
|
5842
|
+
return `${code(s.name)} \u2014 ${code(s.filePath)}:${s.line}`;
|
|
5843
|
+
});
|
|
5844
|
+
output += unorderedList(items);
|
|
5845
|
+
if (symbols.length > limit) {
|
|
5846
|
+
output += `... and ${symbols.length - limit} more.
|
|
5847
|
+
|
|
5848
|
+
`;
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
return output;
|
|
5852
|
+
}
|
|
5853
|
+
function capitalizeKind2(kind) {
|
|
5854
|
+
const map = {
|
|
5855
|
+
function: "Function",
|
|
5856
|
+
class: "Class",
|
|
5857
|
+
variable: "Variable",
|
|
5858
|
+
constant: "Constant",
|
|
5859
|
+
type_alias: "Type",
|
|
5860
|
+
interface: "Interface",
|
|
5861
|
+
enum: "Enum",
|
|
5862
|
+
import: "Import",
|
|
5863
|
+
export: "Export",
|
|
5864
|
+
method: "Method",
|
|
5865
|
+
property: "Property",
|
|
5866
|
+
decorator: "Decorator",
|
|
5867
|
+
module: "Module"
|
|
5868
|
+
};
|
|
5869
|
+
return map[kind] || kind;
|
|
5870
|
+
}
|
|
5871
|
+
function generateCompleteEdgeList(graph) {
|
|
5872
|
+
const fileEdges = /* @__PURE__ */ new Map();
|
|
5873
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5874
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
5875
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
5876
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
5877
|
+
if (!fileEdges.has(sourceAttrs.filePath)) {
|
|
5878
|
+
fileEdges.set(sourceAttrs.filePath, []);
|
|
5879
|
+
}
|
|
5880
|
+
const edgeDesc = `${sourceAttrs.filePath} \u2192 ${targetAttrs.filePath}`;
|
|
5881
|
+
if (!fileEdges.get(sourceAttrs.filePath).includes(edgeDesc)) {
|
|
5882
|
+
fileEdges.get(sourceAttrs.filePath).push(edgeDesc);
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
});
|
|
5886
|
+
if (fileEdges.size === 0) {
|
|
5887
|
+
return "No cross-file edges detected.\n\n";
|
|
5888
|
+
}
|
|
5889
|
+
let output = `Total cross-file edges: ${graph.size}
|
|
5890
|
+
|
|
5891
|
+
`;
|
|
5892
|
+
const sortedEdges = Array.from(fileEdges.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
5893
|
+
const limit = 50;
|
|
5894
|
+
for (const [sourceFile, edges] of sortedEdges.slice(0, limit)) {
|
|
5895
|
+
output += header(sourceFile, 3);
|
|
5896
|
+
output += unorderedList(edges.map((e) => e.replace(`${sourceFile} \u2192 `, "")));
|
|
5897
|
+
}
|
|
5898
|
+
if (sortedEdges.length > limit) {
|
|
5899
|
+
output += `... and ${sortedEdges.length - limit} more source files with edges.
|
|
5900
|
+
|
|
5901
|
+
`;
|
|
5902
|
+
}
|
|
5903
|
+
return output;
|
|
5904
|
+
}
|
|
5905
|
+
function generateConnectionMatrix(graph) {
|
|
5906
|
+
const dirEdges = /* @__PURE__ */ new Map();
|
|
5907
|
+
const allDirs = /* @__PURE__ */ new Set();
|
|
5908
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
5909
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
5910
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
5911
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
5912
|
+
const sourceDir = getTopLevelDir2(sourceAttrs.filePath);
|
|
5913
|
+
const targetDir = getTopLevelDir2(targetAttrs.filePath);
|
|
5914
|
+
if (sourceDir && targetDir) {
|
|
5915
|
+
allDirs.add(sourceDir);
|
|
5916
|
+
allDirs.add(targetDir);
|
|
5917
|
+
if (!dirEdges.has(sourceDir)) {
|
|
5918
|
+
dirEdges.set(sourceDir, /* @__PURE__ */ new Map());
|
|
5919
|
+
}
|
|
5920
|
+
const targetMap = dirEdges.get(sourceDir);
|
|
5921
|
+
targetMap.set(targetDir, (targetMap.get(targetDir) || 0) + 1);
|
|
5922
|
+
}
|
|
5923
|
+
}
|
|
5924
|
+
});
|
|
5925
|
+
if (allDirs.size === 0) {
|
|
5926
|
+
return "No directory structure detected.\n\n";
|
|
5927
|
+
}
|
|
5928
|
+
const sortedDirs = Array.from(allDirs).sort();
|
|
5929
|
+
let output = "Compact matrix showing which directories depend on which:\n\n";
|
|
5930
|
+
output += codeBlock(buildMatrixString(sortedDirs, dirEdges), "");
|
|
5931
|
+
return output;
|
|
5932
|
+
}
|
|
5933
|
+
function buildMatrixString(dirs, edges) {
|
|
5934
|
+
if (dirs.length === 0) return "No directories";
|
|
5935
|
+
let result = " ";
|
|
5936
|
+
for (const dir of dirs) {
|
|
5937
|
+
result += dir.padEnd(10, " ").substring(0, 10);
|
|
5938
|
+
}
|
|
5939
|
+
result += "\n";
|
|
5940
|
+
for (const sourceDir of dirs) {
|
|
5941
|
+
result += sourceDir.padEnd(10, " ").substring(0, 10) + " ";
|
|
5942
|
+
for (const targetDir of dirs) {
|
|
5943
|
+
if (sourceDir === targetDir) {
|
|
5944
|
+
result += "- ";
|
|
5945
|
+
} else {
|
|
5946
|
+
const count = edges.get(sourceDir)?.get(targetDir) || 0;
|
|
5947
|
+
if (count > 0) {
|
|
5948
|
+
result += "\u2192 ";
|
|
5949
|
+
} else {
|
|
5950
|
+
const reverseCount = edges.get(targetDir)?.get(sourceDir) || 0;
|
|
5951
|
+
if (reverseCount > 0) {
|
|
5952
|
+
result += "\u2190 ";
|
|
5953
|
+
} else {
|
|
5954
|
+
result += " ";
|
|
5955
|
+
}
|
|
5956
|
+
}
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
result += "\n";
|
|
5960
|
+
}
|
|
5961
|
+
return result;
|
|
5962
|
+
}
|
|
5963
|
+
function getTopLevelDir2(filePath) {
|
|
5964
|
+
const parts = filePath.split("/");
|
|
5965
|
+
if (parts.length < 2) {
|
|
5966
|
+
return null;
|
|
5967
|
+
}
|
|
5968
|
+
if (parts[0] === "src" && parts.length >= 2) {
|
|
5969
|
+
return parts.length >= 3 ? `${parts[0]}/${parts[1]}` : parts[0];
|
|
5970
|
+
}
|
|
5971
|
+
const firstDir = parts[0];
|
|
5972
|
+
if (firstDir.includes("test") || firstDir.includes("__tests__") || firstDir === "node_modules" || firstDir === "dist" || firstDir === "build") {
|
|
5973
|
+
return null;
|
|
5974
|
+
}
|
|
5975
|
+
return parts[0];
|
|
5976
|
+
}
|
|
5977
|
+
|
|
5978
|
+
// src/docs/status.ts
|
|
5979
|
+
import { readFileSync as readFileSync4, existsSync as existsSync6 } from "fs";
|
|
5980
|
+
import { join as join9 } from "path";
|
|
5981
|
+
function generateStatus(graph, projectRoot, version) {
|
|
5982
|
+
let output = "";
|
|
5983
|
+
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
5984
|
+
const fileCount = getFileCount11(graph);
|
|
5985
|
+
output += timestamp(version, now, fileCount, graph.order);
|
|
5986
|
+
output += header("Project Status");
|
|
5987
|
+
output += "TODO/FIXME/HACK inventory showing what's implemented vs pending.\n\n";
|
|
5988
|
+
output += header("Status Summary", 2);
|
|
5989
|
+
output += generateStatusSummary(projectRoot, graph);
|
|
5990
|
+
output += header("TODOs by File", 2);
|
|
5991
|
+
output += generateTodosByFile(projectRoot, graph);
|
|
5992
|
+
output += header("FIXMEs (Urgent)", 2);
|
|
5993
|
+
output += generateFixmes(projectRoot, graph);
|
|
5994
|
+
output += header("HACKs (Technical Debt)", 2);
|
|
5995
|
+
output += generateHacks(projectRoot, graph);
|
|
5996
|
+
output += header("Priority Matrix", 2);
|
|
5997
|
+
output += generatePriorityMatrix(projectRoot, graph);
|
|
5998
|
+
output += header("Deprecated Items", 2);
|
|
5999
|
+
output += generateDeprecated(projectRoot, graph);
|
|
6000
|
+
output += header("Implementation Completeness", 2);
|
|
6001
|
+
output += generateCompleteness(projectRoot, graph);
|
|
6002
|
+
return output;
|
|
6003
|
+
}
|
|
6004
|
+
function getFileCount11(graph) {
|
|
6005
|
+
const files = /* @__PURE__ */ new Set();
|
|
6006
|
+
graph.forEachNode((node, attrs) => {
|
|
6007
|
+
files.add(attrs.filePath);
|
|
6008
|
+
});
|
|
6009
|
+
return files.size;
|
|
6010
|
+
}
|
|
6011
|
+
function extractComments(projectRoot, filePath) {
|
|
6012
|
+
const comments = [];
|
|
6013
|
+
const fullPath = join9(projectRoot, filePath);
|
|
6014
|
+
if (!existsSync6(fullPath)) {
|
|
6015
|
+
return comments;
|
|
6016
|
+
}
|
|
6017
|
+
try {
|
|
6018
|
+
const content = readFileSync4(fullPath, "utf-8");
|
|
6019
|
+
const lines = content.split("\n");
|
|
6020
|
+
const patterns = [
|
|
6021
|
+
{ type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
|
|
6022
|
+
{ type: "FIXME", regex: /(?:\/\/|#|\/\*)\s*FIXME:?\s*(.+)/i },
|
|
6023
|
+
{ type: "HACK", regex: /(?:\/\/|#|\/\*)\s*HACK:?\s*(.+)/i },
|
|
6024
|
+
{ type: "XXX", regex: /(?:\/\/|#|\/\*)\s*XXX:?\s*(.+)/i },
|
|
6025
|
+
{ type: "NOTE", regex: /(?:\/\/|#|\/\*)\s*NOTE:?\s*(.+)/i },
|
|
6026
|
+
{ type: "OPTIMIZE", regex: /(?:\/\/|#|\/\*)\s*OPTIMIZE:?\s*(.+)/i },
|
|
6027
|
+
{ type: "DEPRECATED", regex: /(?:\/\/|#|\/\*)\s*DEPRECATED:?\s*(.+)/i }
|
|
6028
|
+
];
|
|
6029
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6030
|
+
const line = lines[i];
|
|
6031
|
+
for (const pattern of patterns) {
|
|
6032
|
+
const match = line.match(pattern.regex);
|
|
6033
|
+
if (match) {
|
|
6034
|
+
comments.push({
|
|
6035
|
+
type: pattern.type,
|
|
6036
|
+
file: filePath,
|
|
6037
|
+
line: i + 1,
|
|
6038
|
+
text: match[1].trim().replace(/\*\/.*$/, "").trim()
|
|
6039
|
+
});
|
|
6040
|
+
break;
|
|
6041
|
+
}
|
|
6042
|
+
}
|
|
6043
|
+
}
|
|
6044
|
+
} catch (err) {
|
|
6045
|
+
return comments;
|
|
6046
|
+
}
|
|
6047
|
+
return comments;
|
|
6048
|
+
}
|
|
6049
|
+
function getAllComments(projectRoot, graph) {
|
|
6050
|
+
const allComments = [];
|
|
6051
|
+
const files = /* @__PURE__ */ new Set();
|
|
6052
|
+
graph.forEachNode((node, attrs) => {
|
|
6053
|
+
files.add(attrs.filePath);
|
|
6054
|
+
});
|
|
6055
|
+
for (const file of files) {
|
|
6056
|
+
const comments = extractComments(projectRoot, file);
|
|
6057
|
+
allComments.push(...comments);
|
|
6058
|
+
}
|
|
6059
|
+
return allComments;
|
|
6060
|
+
}
|
|
6061
|
+
function generateStatusSummary(projectRoot, graph) {
|
|
6062
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6063
|
+
const counts = {
|
|
6064
|
+
TODO: 0,
|
|
6065
|
+
FIXME: 0,
|
|
6066
|
+
HACK: 0,
|
|
6067
|
+
XXX: 0,
|
|
6068
|
+
NOTE: 0,
|
|
6069
|
+
OPTIMIZE: 0,
|
|
6070
|
+
DEPRECATED: 0
|
|
6071
|
+
};
|
|
6072
|
+
for (const comment of comments) {
|
|
6073
|
+
counts[comment.type]++;
|
|
6074
|
+
}
|
|
6075
|
+
let output = "";
|
|
6076
|
+
output += `- **Total TODOs:** ${formatNumber(counts.TODO)}
|
|
6077
|
+
`;
|
|
6078
|
+
output += `- **Total FIXMEs:** ${formatNumber(counts.FIXME)}
|
|
6079
|
+
`;
|
|
6080
|
+
output += `- **Total HACKs:** ${formatNumber(counts.HACK)}
|
|
6081
|
+
`;
|
|
6082
|
+
if (counts.XXX > 0) {
|
|
6083
|
+
output += `- **Total XXXs:** ${formatNumber(counts.XXX)}
|
|
6084
|
+
`;
|
|
6085
|
+
}
|
|
6086
|
+
if (counts.NOTE > 0) {
|
|
6087
|
+
output += `- **Total NOTEs:** ${formatNumber(counts.NOTE)}
|
|
6088
|
+
`;
|
|
6089
|
+
}
|
|
6090
|
+
if (counts.OPTIMIZE > 0) {
|
|
6091
|
+
output += `- **Total OPTIMIZEs:** ${formatNumber(counts.OPTIMIZE)}
|
|
6092
|
+
`;
|
|
6093
|
+
}
|
|
6094
|
+
if (counts.DEPRECATED > 0) {
|
|
6095
|
+
output += `- **Total DEPRECATEDs:** ${formatNumber(counts.DEPRECATED)}
|
|
6096
|
+
`;
|
|
6097
|
+
}
|
|
6098
|
+
output += "\n";
|
|
6099
|
+
return output;
|
|
6100
|
+
}
|
|
6101
|
+
function generateTodosByFile(projectRoot, graph) {
|
|
6102
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6103
|
+
const todos = comments.filter((c) => c.type === "TODO");
|
|
6104
|
+
if (todos.length === 0) {
|
|
6105
|
+
return "\u2705 No TODOs found.\n\n";
|
|
6106
|
+
}
|
|
6107
|
+
const fileGroups = /* @__PURE__ */ new Map();
|
|
6108
|
+
for (const todo of todos) {
|
|
6109
|
+
if (!fileGroups.has(todo.file)) {
|
|
6110
|
+
fileGroups.set(todo.file, []);
|
|
6111
|
+
}
|
|
6112
|
+
fileGroups.get(todo.file).push(todo);
|
|
6113
|
+
}
|
|
6114
|
+
let output = `Found ${todos.length} TODO${todos.length === 1 ? "" : "s"} across ${fileGroups.size} file${fileGroups.size === 1 ? "" : "s"}:
|
|
6115
|
+
|
|
6116
|
+
`;
|
|
6117
|
+
const sortedFiles = Array.from(fileGroups.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
|
6118
|
+
for (const [file, fileTodos] of sortedFiles) {
|
|
6119
|
+
output += header(file, 3);
|
|
6120
|
+
const items = fileTodos.map((t) => `[ ] TODO: ${t.text} (line ${t.line})`);
|
|
6121
|
+
output += unorderedList(items);
|
|
6122
|
+
}
|
|
6123
|
+
return output;
|
|
6124
|
+
}
|
|
6125
|
+
function generateFixmes(projectRoot, graph) {
|
|
6126
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6127
|
+
const fixmes = comments.filter((c) => c.type === "FIXME");
|
|
6128
|
+
if (fixmes.length === 0) {
|
|
6129
|
+
return "\u2705 No FIXMEs found.\n\n";
|
|
6130
|
+
}
|
|
6131
|
+
let output = `\u26A0\uFE0F Found ${fixmes.length} FIXME${fixmes.length === 1 ? "" : "s"} (known broken or urgent issues):
|
|
6132
|
+
|
|
6133
|
+
`;
|
|
6134
|
+
fixmes.sort((a, b) => a.file.localeCompare(b.file));
|
|
6135
|
+
const items = fixmes.map((f) => {
|
|
6136
|
+
return `[ ] FIXME: ${f.text} (${code(f.file)}:${f.line})`;
|
|
6137
|
+
});
|
|
6138
|
+
output += unorderedList(items);
|
|
6139
|
+
return output;
|
|
6140
|
+
}
|
|
6141
|
+
function generateHacks(projectRoot, graph) {
|
|
6142
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6143
|
+
const hacks = comments.filter((c) => c.type === "HACK");
|
|
6144
|
+
if (hacks.length === 0) {
|
|
6145
|
+
return "\u2705 No HACKs found.\n\n";
|
|
6146
|
+
}
|
|
6147
|
+
let output = `Found ${hacks.length} HACK${hacks.length === 1 ? "" : "s"} (technical debt - works but needs proper implementation):
|
|
6148
|
+
|
|
6149
|
+
`;
|
|
6150
|
+
hacks.sort((a, b) => a.file.localeCompare(b.file));
|
|
6151
|
+
const items = hacks.map((h) => {
|
|
6152
|
+
return `[ ] HACK: ${h.text} (${code(h.file)}:${h.line})`;
|
|
6153
|
+
});
|
|
6154
|
+
output += unorderedList(items);
|
|
6155
|
+
return output;
|
|
6156
|
+
}
|
|
6157
|
+
function generatePriorityMatrix(projectRoot, graph) {
|
|
6158
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6159
|
+
const fileConnections = /* @__PURE__ */ new Map();
|
|
6160
|
+
graph.forEachEdge((edge, attrs, source, target) => {
|
|
6161
|
+
const sourceAttrs = graph.getNodeAttributes(source);
|
|
6162
|
+
const targetAttrs = graph.getNodeAttributes(target);
|
|
6163
|
+
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
6164
|
+
fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
|
|
6165
|
+
fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
|
|
6166
|
+
}
|
|
6167
|
+
});
|
|
6168
|
+
const items = [];
|
|
6169
|
+
for (const comment of comments) {
|
|
6170
|
+
if (comment.type === "TODO" || comment.type === "FIXME" || comment.type === "HACK") {
|
|
6171
|
+
const connections = fileConnections.get(comment.file) || 0;
|
|
6172
|
+
let priority = "Low";
|
|
6173
|
+
let priorityScore = 1;
|
|
6174
|
+
if (comment.type === "FIXME") {
|
|
6175
|
+
if (connections > 10) {
|
|
6176
|
+
priority = "\u{1F534} Critical";
|
|
6177
|
+
priorityScore = 4;
|
|
6178
|
+
} else if (connections > 5) {
|
|
6179
|
+
priority = "\u{1F7E1} High";
|
|
6180
|
+
priorityScore = 3;
|
|
6181
|
+
} else {
|
|
6182
|
+
priority = "\u{1F7E2} Medium";
|
|
6183
|
+
priorityScore = 2;
|
|
6184
|
+
}
|
|
6185
|
+
} else if (comment.type === "TODO") {
|
|
6186
|
+
if (connections > 10) {
|
|
6187
|
+
priority = "\u{1F7E1} High";
|
|
6188
|
+
priorityScore = 3;
|
|
6189
|
+
} else if (connections > 5) {
|
|
6190
|
+
priority = "\u{1F7E2} Medium";
|
|
6191
|
+
priorityScore = 2;
|
|
6192
|
+
} else {
|
|
6193
|
+
priority = "\u26AA Low";
|
|
6194
|
+
priorityScore = 1;
|
|
6195
|
+
}
|
|
6196
|
+
} else if (comment.type === "HACK") {
|
|
6197
|
+
if (connections > 10) {
|
|
6198
|
+
priority = "\u{1F7E1} High";
|
|
6199
|
+
priorityScore = 3;
|
|
6200
|
+
} else {
|
|
6201
|
+
priority = "\u{1F7E2} Medium";
|
|
6202
|
+
priorityScore = 2;
|
|
6203
|
+
}
|
|
6204
|
+
}
|
|
6205
|
+
items.push({
|
|
6206
|
+
comment,
|
|
6207
|
+
connections,
|
|
6208
|
+
priority
|
|
6209
|
+
});
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
if (items.length === 0) {
|
|
6213
|
+
return "No items to prioritize.\n\n";
|
|
6214
|
+
}
|
|
6215
|
+
items.sort((a, b) => {
|
|
6216
|
+
const priorityOrder = { "\u{1F534} Critical": 4, "\u{1F7E1} High": 3, "\u{1F7E2} Medium": 2, "\u26AA Low": 1 };
|
|
6217
|
+
const aPriority = priorityOrder[a.priority] || 0;
|
|
6218
|
+
const bPriority = priorityOrder[b.priority] || 0;
|
|
6219
|
+
if (aPriority !== bPriority) {
|
|
6220
|
+
return bPriority - aPriority;
|
|
6221
|
+
}
|
|
6222
|
+
return b.connections - a.connections;
|
|
6223
|
+
});
|
|
6224
|
+
let output = "Items prioritized by type and file connections:\n\n";
|
|
6225
|
+
const headers = ["Type", "File", "Line", "Connections", "Priority"];
|
|
6226
|
+
const rows = items.slice(0, 20).map((item) => [
|
|
6227
|
+
item.comment.type,
|
|
6228
|
+
`\`${item.comment.file}\``,
|
|
6229
|
+
item.comment.line.toString(),
|
|
6230
|
+
formatNumber(item.connections),
|
|
6231
|
+
item.priority
|
|
6232
|
+
]);
|
|
6233
|
+
output += table(headers, rows);
|
|
6234
|
+
if (items.length > 20) {
|
|
6235
|
+
output += `... and ${items.length - 20} more items.
|
|
6236
|
+
|
|
6237
|
+
`;
|
|
6238
|
+
}
|
|
6239
|
+
return output;
|
|
6240
|
+
}
|
|
6241
|
+
function generateDeprecated(projectRoot, graph) {
|
|
6242
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6243
|
+
const deprecated = comments.filter((c) => c.type === "DEPRECATED");
|
|
6244
|
+
if (deprecated.length === 0) {
|
|
6245
|
+
return "\u2705 No deprecated items found.\n\n";
|
|
6246
|
+
}
|
|
6247
|
+
let output = `Found ${deprecated.length} deprecated item${deprecated.length === 1 ? "" : "s"}:
|
|
6248
|
+
|
|
6249
|
+
`;
|
|
6250
|
+
deprecated.sort((a, b) => a.file.localeCompare(b.file));
|
|
6251
|
+
const items = deprecated.map((d) => {
|
|
6252
|
+
return `DEPRECATED: ${d.text} (${code(d.file)}:${d.line})`;
|
|
6253
|
+
});
|
|
6254
|
+
output += unorderedList(items);
|
|
6255
|
+
return output;
|
|
6256
|
+
}
|
|
6257
|
+
function generateCompleteness(projectRoot, graph) {
|
|
6258
|
+
const comments = getAllComments(projectRoot, graph);
|
|
6259
|
+
const fileTodos = /* @__PURE__ */ new Map();
|
|
6260
|
+
const fileSymbols = /* @__PURE__ */ new Map();
|
|
6261
|
+
for (const comment of comments) {
|
|
6262
|
+
if (comment.type === "TODO") {
|
|
6263
|
+
fileTodos.set(comment.file, (fileTodos.get(comment.file) || 0) + 1);
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
graph.forEachNode((node, attrs) => {
|
|
6267
|
+
fileSymbols.set(attrs.filePath, (fileSymbols.get(attrs.filePath) || 0) + 1);
|
|
6268
|
+
});
|
|
6269
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
6270
|
+
graph.forEachNode((node, attrs) => {
|
|
6271
|
+
allFiles.add(attrs.filePath);
|
|
6272
|
+
});
|
|
6273
|
+
const inProgress = [];
|
|
6274
|
+
const complete = [];
|
|
6275
|
+
for (const file of allFiles) {
|
|
6276
|
+
const todoCount = fileTodos.get(file) || 0;
|
|
6277
|
+
const symbolCount = fileSymbols.get(file) || 0;
|
|
6278
|
+
if (symbolCount === 0) continue;
|
|
6279
|
+
const todoRatio = todoCount / symbolCount;
|
|
6280
|
+
if (todoRatio > 0.1) {
|
|
6281
|
+
inProgress.push(file);
|
|
6282
|
+
} else if (todoCount === 0) {
|
|
6283
|
+
complete.push(file);
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
let output = "";
|
|
6287
|
+
const totalFiles = allFiles.size;
|
|
6288
|
+
const completePercent = totalFiles > 0 ? (complete.length / totalFiles * 100).toFixed(1) : "0.0";
|
|
6289
|
+
output += `- **Complete files (no TODOs):** ${formatNumber(complete.length)} (${completePercent}%)
|
|
6290
|
+
`;
|
|
6291
|
+
output += `- **In-progress files (many TODOs):** ${formatNumber(inProgress.length)}
|
|
6292
|
+
|
|
6293
|
+
`;
|
|
6294
|
+
if (inProgress.length > 0) {
|
|
6295
|
+
output += "**Files in progress (high TODO ratio):**\n\n";
|
|
6296
|
+
const items = inProgress.slice(0, 10).map((f) => {
|
|
6297
|
+
const todoCount = fileTodos.get(f) || 0;
|
|
6298
|
+
return `${code(f)} (${todoCount} TODOs)`;
|
|
6299
|
+
});
|
|
6300
|
+
output += unorderedList(items);
|
|
6301
|
+
if (inProgress.length > 10) {
|
|
6302
|
+
output += `... and ${inProgress.length - 10} more.
|
|
6303
|
+
|
|
6304
|
+
`;
|
|
6305
|
+
}
|
|
6306
|
+
}
|
|
6307
|
+
const dirTodos = /* @__PURE__ */ new Map();
|
|
6308
|
+
const dirFiles = /* @__PURE__ */ new Map();
|
|
6309
|
+
for (const file of allFiles) {
|
|
6310
|
+
const dir = file.split("/")[0];
|
|
6311
|
+
dirFiles.set(dir, (dirFiles.get(dir) || 0) + 1);
|
|
6312
|
+
const todoCount = fileTodos.get(file) || 0;
|
|
6313
|
+
if (todoCount > 0) {
|
|
6314
|
+
dirTodos.set(dir, (dirTodos.get(dir) || 0) + 1);
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
if (dirFiles.size > 1) {
|
|
6318
|
+
output += "**Completeness by directory:**\n\n";
|
|
6319
|
+
const sortedDirs = Array.from(dirFiles.entries()).sort((a, b) => b[1] - a[1]);
|
|
6320
|
+
for (const [dir, fileCount] of sortedDirs) {
|
|
6321
|
+
const todosInDir = dirTodos.get(dir) || 0;
|
|
6322
|
+
const completeInDir = fileCount - todosInDir;
|
|
6323
|
+
const percent = (completeInDir / fileCount * 100).toFixed(1);
|
|
6324
|
+
output += `- **${dir}/**: ${completeInDir}/${fileCount} files complete (${percent}%)
|
|
6325
|
+
`;
|
|
6326
|
+
}
|
|
6327
|
+
output += "\n";
|
|
6328
|
+
}
|
|
6329
|
+
return output;
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
// src/docs/metadata.ts
|
|
6333
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync } from "fs";
|
|
6334
|
+
import { join as join10 } from "path";
|
|
6335
|
+
function loadMetadata(outputDir) {
|
|
6336
|
+
const metadataPath = join10(outputDir, "metadata.json");
|
|
6337
|
+
if (!existsSync7(metadataPath)) {
|
|
6338
|
+
return null;
|
|
6339
|
+
}
|
|
6340
|
+
try {
|
|
6341
|
+
const content = readFileSync5(metadataPath, "utf-8");
|
|
6342
|
+
return JSON.parse(content);
|
|
6343
|
+
} catch (err) {
|
|
6344
|
+
console.error("Failed to load metadata:", err);
|
|
6345
|
+
return null;
|
|
6346
|
+
}
|
|
6347
|
+
}
|
|
6348
|
+
function saveMetadata(outputDir, metadata) {
|
|
6349
|
+
const metadataPath = join10(outputDir, "metadata.json");
|
|
6350
|
+
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
6351
|
+
}
|
|
6352
|
+
function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
|
|
6353
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6354
|
+
const documents = {};
|
|
6355
|
+
for (const docType of docTypes) {
|
|
6356
|
+
const fileName = docType === "architecture" ? "ARCHITECTURE.md" : docType === "conventions" ? "CONVENTIONS.md" : docType === "dependencies" ? "DEPENDENCIES.md" : docType === "onboarding" ? "ONBOARDING.md" : docType === "files" ? "FILES.md" : docType === "api_surface" ? "API_SURFACE.md" : docType === "errors" ? "ERRORS.md" : docType === "tests" ? "TESTS.md" : docType === "history" ? "HISTORY.md" : docType === "current" ? "CURRENT.md" : docType === "status" ? "STATUS.md" : `${docType.toUpperCase()}.md`;
|
|
6357
|
+
documents[docType] = {
|
|
6358
|
+
generated_at: now,
|
|
6359
|
+
file: fileName
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
return {
|
|
6363
|
+
version,
|
|
6364
|
+
generated_at: now,
|
|
6365
|
+
project_path: projectPath,
|
|
6366
|
+
file_count: fileCount,
|
|
6367
|
+
symbol_count: symbolCount,
|
|
6368
|
+
edge_count: edgeCount,
|
|
6369
|
+
documents
|
|
6370
|
+
};
|
|
6371
|
+
}
|
|
6372
|
+
function updateMetadata(existing, docTypes, fileCount, symbolCount, edgeCount) {
|
|
6373
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6374
|
+
for (const docType of docTypes) {
|
|
6375
|
+
if (existing.documents[docType]) {
|
|
6376
|
+
existing.documents[docType].generated_at = now;
|
|
6377
|
+
}
|
|
6378
|
+
}
|
|
6379
|
+
existing.file_count = fileCount;
|
|
6380
|
+
existing.symbol_count = symbolCount;
|
|
6381
|
+
existing.edge_count = edgeCount;
|
|
6382
|
+
existing.generated_at = now;
|
|
6383
|
+
return existing;
|
|
6384
|
+
}
|
|
6385
|
+
|
|
6386
|
+
// src/docs/generator.ts
|
|
6387
|
+
async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
6388
|
+
const startTime = Date.now();
|
|
6389
|
+
const generated = [];
|
|
6390
|
+
const errors = [];
|
|
6391
|
+
try {
|
|
6392
|
+
if (!existsSync8(options.outputDir)) {
|
|
6393
|
+
mkdirSync(options.outputDir, { recursive: true });
|
|
6394
|
+
if (options.verbose) {
|
|
6395
|
+
console.log(`Created output directory: ${options.outputDir}`);
|
|
6396
|
+
}
|
|
6397
|
+
}
|
|
6398
|
+
let docsToGenerate = options.include;
|
|
6399
|
+
if (options.update && options.only) {
|
|
6400
|
+
docsToGenerate = options.only;
|
|
6401
|
+
}
|
|
6402
|
+
if (docsToGenerate.includes("all")) {
|
|
6403
|
+
docsToGenerate = [
|
|
6404
|
+
"architecture",
|
|
6405
|
+
"conventions",
|
|
6406
|
+
"dependencies",
|
|
6407
|
+
"onboarding",
|
|
6408
|
+
"files",
|
|
6409
|
+
"api_surface",
|
|
6410
|
+
"errors",
|
|
6411
|
+
"tests",
|
|
6412
|
+
"history",
|
|
6413
|
+
"current",
|
|
6414
|
+
"status"
|
|
6415
|
+
];
|
|
6416
|
+
}
|
|
6417
|
+
let metadata = null;
|
|
6418
|
+
if (options.update) {
|
|
6419
|
+
metadata = loadMetadata(options.outputDir);
|
|
6420
|
+
}
|
|
6421
|
+
const fileCount = getFileCount12(graph);
|
|
6422
|
+
const symbolCount = graph.order;
|
|
6423
|
+
const edgeCount = graph.size;
|
|
6424
|
+
if (options.format === "markdown") {
|
|
6425
|
+
if (docsToGenerate.includes("architecture")) {
|
|
6426
|
+
try {
|
|
6427
|
+
if (options.verbose) console.log("Generating ARCHITECTURE.md...");
|
|
6428
|
+
const content = generateArchitecture(graph, projectRoot, version, parseTime);
|
|
6429
|
+
const filePath = join11(options.outputDir, "ARCHITECTURE.md");
|
|
6430
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6431
|
+
generated.push("ARCHITECTURE.md");
|
|
6432
|
+
} catch (err) {
|
|
6433
|
+
errors.push(`Failed to generate ARCHITECTURE.md: ${err}`);
|
|
6434
|
+
}
|
|
6435
|
+
}
|
|
6436
|
+
if (docsToGenerate.includes("conventions")) {
|
|
6437
|
+
try {
|
|
6438
|
+
if (options.verbose) console.log("Generating CONVENTIONS.md...");
|
|
6439
|
+
const content = generateConventions(graph, projectRoot, version);
|
|
6440
|
+
const filePath = join11(options.outputDir, "CONVENTIONS.md");
|
|
6441
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6442
|
+
generated.push("CONVENTIONS.md");
|
|
6443
|
+
} catch (err) {
|
|
6444
|
+
errors.push(`Failed to generate CONVENTIONS.md: ${err}`);
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
if (docsToGenerate.includes("dependencies")) {
|
|
6448
|
+
try {
|
|
6449
|
+
if (options.verbose) console.log("Generating DEPENDENCIES.md...");
|
|
6450
|
+
const content = generateDependencies(graph, projectRoot, version);
|
|
6451
|
+
const filePath = join11(options.outputDir, "DEPENDENCIES.md");
|
|
6452
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6453
|
+
generated.push("DEPENDENCIES.md");
|
|
6454
|
+
} catch (err) {
|
|
6455
|
+
errors.push(`Failed to generate DEPENDENCIES.md: ${err}`);
|
|
6456
|
+
}
|
|
6457
|
+
}
|
|
6458
|
+
if (docsToGenerate.includes("onboarding")) {
|
|
6459
|
+
try {
|
|
6460
|
+
if (options.verbose) console.log("Generating ONBOARDING.md...");
|
|
6461
|
+
const content = generateOnboarding(graph, projectRoot, version);
|
|
6462
|
+
const filePath = join11(options.outputDir, "ONBOARDING.md");
|
|
6463
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6464
|
+
generated.push("ONBOARDING.md");
|
|
6465
|
+
} catch (err) {
|
|
6466
|
+
errors.push(`Failed to generate ONBOARDING.md: ${err}`);
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
if (docsToGenerate.includes("files")) {
|
|
6470
|
+
try {
|
|
6471
|
+
if (options.verbose) console.log("Generating FILES.md...");
|
|
6472
|
+
const content = generateFiles(graph, projectRoot, version);
|
|
6473
|
+
const filePath = join11(options.outputDir, "FILES.md");
|
|
6474
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6475
|
+
generated.push("FILES.md");
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
errors.push(`Failed to generate FILES.md: ${err}`);
|
|
6478
|
+
}
|
|
6479
|
+
}
|
|
6480
|
+
if (docsToGenerate.includes("api_surface")) {
|
|
6481
|
+
try {
|
|
6482
|
+
if (options.verbose) console.log("Generating API_SURFACE.md...");
|
|
6483
|
+
const content = generateApiSurface(graph, projectRoot, version);
|
|
6484
|
+
const filePath = join11(options.outputDir, "API_SURFACE.md");
|
|
6485
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6486
|
+
generated.push("API_SURFACE.md");
|
|
6487
|
+
} catch (err) {
|
|
6488
|
+
errors.push(`Failed to generate API_SURFACE.md: ${err}`);
|
|
6489
|
+
}
|
|
6490
|
+
}
|
|
6491
|
+
if (docsToGenerate.includes("errors")) {
|
|
6492
|
+
try {
|
|
6493
|
+
if (options.verbose) console.log("Generating ERRORS.md...");
|
|
6494
|
+
const content = generateErrors(graph, projectRoot, version);
|
|
6495
|
+
const filePath = join11(options.outputDir, "ERRORS.md");
|
|
6496
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6497
|
+
generated.push("ERRORS.md");
|
|
6498
|
+
} catch (err) {
|
|
6499
|
+
errors.push(`Failed to generate ERRORS.md: ${err}`);
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
6502
|
+
if (docsToGenerate.includes("tests")) {
|
|
6503
|
+
try {
|
|
6504
|
+
if (options.verbose) console.log("Generating TESTS.md...");
|
|
6505
|
+
const content = generateTests(graph, projectRoot, version);
|
|
6506
|
+
const filePath = join11(options.outputDir, "TESTS.md");
|
|
6507
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6508
|
+
generated.push("TESTS.md");
|
|
6509
|
+
} catch (err) {
|
|
6510
|
+
errors.push(`Failed to generate TESTS.md: ${err}`);
|
|
6511
|
+
}
|
|
6512
|
+
}
|
|
6513
|
+
if (docsToGenerate.includes("history")) {
|
|
6514
|
+
try {
|
|
6515
|
+
if (options.verbose) console.log("Generating HISTORY.md...");
|
|
6516
|
+
const content = generateHistory(graph, projectRoot, version);
|
|
6517
|
+
const filePath = join11(options.outputDir, "HISTORY.md");
|
|
6518
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6519
|
+
generated.push("HISTORY.md");
|
|
6520
|
+
} catch (err) {
|
|
6521
|
+
errors.push(`Failed to generate HISTORY.md: ${err}`);
|
|
6522
|
+
}
|
|
6523
|
+
}
|
|
6524
|
+
if (docsToGenerate.includes("current")) {
|
|
6525
|
+
try {
|
|
6526
|
+
if (options.verbose) console.log("Generating CURRENT.md...");
|
|
6527
|
+
const content = generateCurrent(graph, projectRoot, version);
|
|
6528
|
+
const filePath = join11(options.outputDir, "CURRENT.md");
|
|
6529
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6530
|
+
generated.push("CURRENT.md");
|
|
6531
|
+
} catch (err) {
|
|
6532
|
+
errors.push(`Failed to generate CURRENT.md: ${err}`);
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
if (docsToGenerate.includes("status")) {
|
|
6536
|
+
try {
|
|
6537
|
+
if (options.verbose) console.log("Generating STATUS.md...");
|
|
6538
|
+
const content = generateStatus(graph, projectRoot, version);
|
|
6539
|
+
const filePath = join11(options.outputDir, "STATUS.md");
|
|
6540
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
6541
|
+
generated.push("STATUS.md");
|
|
6542
|
+
} catch (err) {
|
|
6543
|
+
errors.push(`Failed to generate STATUS.md: ${err}`);
|
|
6544
|
+
}
|
|
6545
|
+
}
|
|
6546
|
+
} else if (options.format === "json") {
|
|
6547
|
+
errors.push("JSON format not yet supported");
|
|
6548
|
+
}
|
|
6549
|
+
if (metadata && options.update) {
|
|
6550
|
+
metadata = updateMetadata(metadata, docsToGenerate, fileCount, symbolCount, edgeCount);
|
|
6551
|
+
} else {
|
|
6552
|
+
metadata = createMetadata(version, projectRoot, fileCount, symbolCount, edgeCount, docsToGenerate);
|
|
4308
6553
|
}
|
|
4309
6554
|
saveMetadata(options.outputDir, metadata);
|
|
4310
6555
|
if (options.verbose) console.log("Saved metadata.json");
|
|
@@ -4326,7 +6571,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
4326
6571
|
};
|
|
4327
6572
|
}
|
|
4328
6573
|
}
|
|
4329
|
-
function
|
|
6574
|
+
function getFileCount12(graph) {
|
|
4330
6575
|
const files = /* @__PURE__ */ new Set();
|
|
4331
6576
|
graph.forEachNode((node, attrs) => {
|
|
4332
6577
|
files.add(attrs.filePath);
|
|
@@ -4339,13 +6584,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
4339
6584
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4340
6585
|
|
|
4341
6586
|
// src/mcp/tools.ts
|
|
4342
|
-
import { dirname as
|
|
4343
|
-
import { existsSync as
|
|
6587
|
+
import { dirname as dirname12, join as join13 } from "path";
|
|
6588
|
+
import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
|
|
4344
6589
|
|
|
4345
6590
|
// src/mcp/connect.ts
|
|
4346
6591
|
import simpleGit from "simple-git";
|
|
4347
|
-
import { existsSync as
|
|
4348
|
-
import { join as
|
|
6592
|
+
import { existsSync as existsSync9 } from "fs";
|
|
6593
|
+
import { join as join12, basename as basename5, resolve as resolve2 } from "path";
|
|
4349
6594
|
import { tmpdir, homedir } from "os";
|
|
4350
6595
|
function validateProjectPath(source) {
|
|
4351
6596
|
const resolved = resolve2(source);
|
|
@@ -4358,11 +6603,11 @@ function validateProjectPath(source) {
|
|
|
4358
6603
|
"/boot",
|
|
4359
6604
|
"/proc",
|
|
4360
6605
|
"/sys",
|
|
4361
|
-
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
6606
|
+
join12(homedir(), ".ssh"),
|
|
6607
|
+
join12(homedir(), ".gnupg"),
|
|
6608
|
+
join12(homedir(), ".aws"),
|
|
6609
|
+
join12(homedir(), ".config"),
|
|
6610
|
+
join12(homedir(), ".env")
|
|
4366
6611
|
];
|
|
4367
6612
|
for (const blocked of blockedPaths) {
|
|
4368
6613
|
if (resolved.startsWith(blocked)) {
|
|
@@ -4385,11 +6630,11 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
4385
6630
|
};
|
|
4386
6631
|
}
|
|
4387
6632
|
projectName = match[1];
|
|
4388
|
-
const reposDir =
|
|
4389
|
-
const cloneDir =
|
|
6633
|
+
const reposDir = join12(tmpdir(), "depwire-repos");
|
|
6634
|
+
const cloneDir = join12(reposDir, projectName);
|
|
4390
6635
|
console.error(`Connecting to GitHub repo: ${source}`);
|
|
4391
6636
|
const git = simpleGit();
|
|
4392
|
-
if (
|
|
6637
|
+
if (existsSync9(cloneDir)) {
|
|
4393
6638
|
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
4394
6639
|
try {
|
|
4395
6640
|
await git.cwd(cloneDir).pull();
|
|
@@ -4407,7 +6652,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
4407
6652
|
};
|
|
4408
6653
|
}
|
|
4409
6654
|
}
|
|
4410
|
-
projectRoot = subdirectory ?
|
|
6655
|
+
projectRoot = subdirectory ? join12(cloneDir, subdirectory) : cloneDir;
|
|
4411
6656
|
} else {
|
|
4412
6657
|
const validation2 = validateProjectPath(source);
|
|
4413
6658
|
if (!validation2.valid) {
|
|
@@ -4416,14 +6661,14 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
4416
6661
|
message: validation2.error
|
|
4417
6662
|
};
|
|
4418
6663
|
}
|
|
4419
|
-
if (!
|
|
6664
|
+
if (!existsSync9(source)) {
|
|
4420
6665
|
return {
|
|
4421
6666
|
error: "Directory not found",
|
|
4422
6667
|
message: `Directory does not exist: ${source}`
|
|
4423
6668
|
};
|
|
4424
6669
|
}
|
|
4425
|
-
projectRoot = subdirectory ?
|
|
4426
|
-
projectName =
|
|
6670
|
+
projectRoot = subdirectory ? join12(source, subdirectory) : source;
|
|
6671
|
+
projectName = basename5(projectRoot);
|
|
4427
6672
|
}
|
|
4428
6673
|
const validation = validateProjectPath(projectRoot);
|
|
4429
6674
|
if (!validation.valid) {
|
|
@@ -4432,7 +6677,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
4432
6677
|
message: validation.error
|
|
4433
6678
|
};
|
|
4434
6679
|
}
|
|
4435
|
-
if (!
|
|
6680
|
+
if (!existsSync9(projectRoot)) {
|
|
4436
6681
|
return {
|
|
4437
6682
|
error: "Project root not found",
|
|
4438
6683
|
message: `Directory does not exist: ${projectRoot}`
|
|
@@ -5087,7 +7332,7 @@ function handleGetArchitectureSummary(graph) {
|
|
|
5087
7332
|
const dirMap = /* @__PURE__ */ new Map();
|
|
5088
7333
|
const languageBreakdown = {};
|
|
5089
7334
|
fileSummary.forEach((f) => {
|
|
5090
|
-
const dir = f.filePath.includes("/") ?
|
|
7335
|
+
const dir = f.filePath.includes("/") ? dirname12(f.filePath) : ".";
|
|
5091
7336
|
if (!dirMap.has(dir)) {
|
|
5092
7337
|
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
5093
7338
|
}
|
|
@@ -5174,8 +7419,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
|
5174
7419
|
};
|
|
5175
7420
|
}
|
|
5176
7421
|
async function handleGetProjectDocs(docType, state) {
|
|
5177
|
-
const docsDir =
|
|
5178
|
-
if (!
|
|
7422
|
+
const docsDir = join13(state.projectRoot, ".depwire");
|
|
7423
|
+
if (!existsSync10(docsDir)) {
|
|
5179
7424
|
const errorMessage = `Project documentation has not been generated yet.
|
|
5180
7425
|
|
|
5181
7426
|
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
@@ -5205,12 +7450,12 @@ Available document types:
|
|
|
5205
7450
|
missing.push(doc);
|
|
5206
7451
|
continue;
|
|
5207
7452
|
}
|
|
5208
|
-
const filePath =
|
|
5209
|
-
if (!
|
|
7453
|
+
const filePath = join13(docsDir, metadata.documents[doc].file);
|
|
7454
|
+
if (!existsSync10(filePath)) {
|
|
5210
7455
|
missing.push(doc);
|
|
5211
7456
|
continue;
|
|
5212
7457
|
}
|
|
5213
|
-
const content =
|
|
7458
|
+
const content = readFileSync6(filePath, "utf-8");
|
|
5214
7459
|
if (docsToReturn.length > 1) {
|
|
5215
7460
|
output += `
|
|
5216
7461
|
|
|
@@ -5235,16 +7480,16 @@ Available document types:
|
|
|
5235
7480
|
}
|
|
5236
7481
|
async function handleUpdateProjectDocs(docType, state) {
|
|
5237
7482
|
const startTime = Date.now();
|
|
5238
|
-
const docsDir =
|
|
7483
|
+
const docsDir = join13(state.projectRoot, ".depwire");
|
|
5239
7484
|
console.error("Regenerating project documentation...");
|
|
5240
|
-
const parsedFiles = parseProject(state.projectRoot);
|
|
7485
|
+
const parsedFiles = await parseProject(state.projectRoot);
|
|
5241
7486
|
const graph = buildGraph(parsedFiles);
|
|
5242
7487
|
const parseTime = (Date.now() - startTime) / 1e3;
|
|
5243
7488
|
state.graph = graph;
|
|
5244
|
-
const packageJsonPath =
|
|
5245
|
-
const packageJson = JSON.parse(
|
|
7489
|
+
const packageJsonPath = join13(__dirname, "../../package.json");
|
|
7490
|
+
const packageJson = JSON.parse(readFileSync6(packageJsonPath, "utf-8"));
|
|
5246
7491
|
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
5247
|
-
const docsExist =
|
|
7492
|
+
const docsExist = existsSync10(docsDir);
|
|
5248
7493
|
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
5249
7494
|
outputDir: docsDir,
|
|
5250
7495
|
format: "markdown",
|