depwire-cli 0.7.1 → 0.9.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/LICENSE +2 -0
- package/README.md +138 -9
- package/dist/{chunk-65H7HCM4.js → chunk-MGMDHXPB.js} +1059 -144
- package/dist/index.js +30 -1
- package/dist/mcpb-entry.js +1 -1
- package/dist/parser/grammars/tree-sitter-rust.wasm +0 -0
- package/package.json +4 -3
|
@@ -29,7 +29,8 @@ function scanDirectory(rootDir, baseDir = rootDir) {
|
|
|
29
29
|
const isJavaScript = entry.endsWith(".js") || entry.endsWith(".jsx") || entry.endsWith(".mjs") || entry.endsWith(".cjs");
|
|
30
30
|
const isPython = entry.endsWith(".py");
|
|
31
31
|
const isGo = entry.endsWith(".go") && !entry.endsWith("_test.go");
|
|
32
|
-
|
|
32
|
+
const isRust = entry.endsWith(".rs");
|
|
33
|
+
if (isTypeScript || isJavaScript || isPython || isGo || isRust) {
|
|
33
34
|
files.push(relative(rootDir, fullPath));
|
|
34
35
|
}
|
|
35
36
|
}
|
|
@@ -54,6 +55,8 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
54
55
|
// TypeScript
|
|
55
56
|
"go.mod",
|
|
56
57
|
// Go
|
|
58
|
+
"Cargo.toml",
|
|
59
|
+
// Rust
|
|
57
60
|
"pyproject.toml",
|
|
58
61
|
// Python (modern)
|
|
59
62
|
"setup.py",
|
|
@@ -80,8 +83,8 @@ function findProjectRoot(startDir = process.cwd()) {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
// src/parser/index.ts
|
|
83
|
-
import { readFileSync as
|
|
84
|
-
import { join as
|
|
86
|
+
import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
87
|
+
import { join as join7 } from "path";
|
|
85
88
|
|
|
86
89
|
// src/parser/detect.ts
|
|
87
90
|
import { extname as extname3 } from "path";
|
|
@@ -109,7 +112,8 @@ async function initParser() {
|
|
|
109
112
|
"tsx": "tree-sitter-tsx.wasm",
|
|
110
113
|
"javascript": "tree-sitter-javascript.wasm",
|
|
111
114
|
"python": "tree-sitter-python.wasm",
|
|
112
|
-
"go": "tree-sitter-go.wasm"
|
|
115
|
+
"go": "tree-sitter-go.wasm",
|
|
116
|
+
"rust": "tree-sitter-rust.wasm"
|
|
113
117
|
};
|
|
114
118
|
for (const [name, file] of Object.entries(grammarFiles)) {
|
|
115
119
|
const wasmPath = path.join(grammarsDir, file);
|
|
@@ -1972,12 +1976,429 @@ var goParser = {
|
|
|
1972
1976
|
parseFile: parseGoFile
|
|
1973
1977
|
};
|
|
1974
1978
|
|
|
1979
|
+
// src/parser/rust.ts
|
|
1980
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1981
|
+
import { join as join6, dirname as dirname5, relative as relative3 } from "path";
|
|
1982
|
+
function parseRustFile(filePath, sourceCode, projectRoot) {
|
|
1983
|
+
const parser = getParser("rust");
|
|
1984
|
+
const tree = parser.parse(sourceCode, null, { bufferSize: 1024 * 1024 });
|
|
1985
|
+
const context = {
|
|
1986
|
+
filePath,
|
|
1987
|
+
projectRoot,
|
|
1988
|
+
sourceCode,
|
|
1989
|
+
symbols: [],
|
|
1990
|
+
edges: [],
|
|
1991
|
+
currentScope: [],
|
|
1992
|
+
currentModule: []
|
|
1993
|
+
};
|
|
1994
|
+
walkNode5(tree.rootNode, context);
|
|
1995
|
+
return {
|
|
1996
|
+
filePath,
|
|
1997
|
+
symbols: context.symbols,
|
|
1998
|
+
edges: context.edges
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
function walkNode5(node, context) {
|
|
2002
|
+
processNode5(node, context);
|
|
2003
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2004
|
+
const child = node.child(i);
|
|
2005
|
+
if (child) {
|
|
2006
|
+
walkNode5(child, context);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
function processNode5(node, context) {
|
|
2011
|
+
const type = node.type;
|
|
2012
|
+
switch (type) {
|
|
2013
|
+
case "function_item":
|
|
2014
|
+
processFunctionItem(node, context);
|
|
2015
|
+
break;
|
|
2016
|
+
case "struct_item":
|
|
2017
|
+
processStructItem(node, context);
|
|
2018
|
+
break;
|
|
2019
|
+
case "enum_item":
|
|
2020
|
+
processEnumItem(node, context);
|
|
2021
|
+
break;
|
|
2022
|
+
case "trait_item":
|
|
2023
|
+
processTraitItem(node, context);
|
|
2024
|
+
break;
|
|
2025
|
+
case "impl_item":
|
|
2026
|
+
processImplItem(node, context);
|
|
2027
|
+
break;
|
|
2028
|
+
case "const_item":
|
|
2029
|
+
processConstItem(node, context);
|
|
2030
|
+
break;
|
|
2031
|
+
case "type_item":
|
|
2032
|
+
processTypeItem(node, context);
|
|
2033
|
+
break;
|
|
2034
|
+
case "use_declaration":
|
|
2035
|
+
processUseDeclaration(node, context);
|
|
2036
|
+
break;
|
|
2037
|
+
case "mod_item":
|
|
2038
|
+
processModItem(node, context);
|
|
2039
|
+
break;
|
|
2040
|
+
case "call_expression":
|
|
2041
|
+
processCallExpression5(node, context);
|
|
2042
|
+
break;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
function processFunctionItem(node, context) {
|
|
2046
|
+
const nameNode = node.childForFieldName("name");
|
|
2047
|
+
if (!nameNode) return;
|
|
2048
|
+
const name = nodeText4(nameNode, context);
|
|
2049
|
+
const exported = hasVisibility(node, "pub");
|
|
2050
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2051
|
+
context.symbols.push({
|
|
2052
|
+
id: symbolId,
|
|
2053
|
+
name,
|
|
2054
|
+
kind: "function",
|
|
2055
|
+
filePath: context.filePath,
|
|
2056
|
+
startLine: node.startPosition.row + 1,
|
|
2057
|
+
endLine: node.endPosition.row + 1,
|
|
2058
|
+
exported
|
|
2059
|
+
});
|
|
2060
|
+
context.currentScope.push(name);
|
|
2061
|
+
const body = node.childForFieldName("body");
|
|
2062
|
+
if (body) {
|
|
2063
|
+
walkNode5(body, context);
|
|
2064
|
+
}
|
|
2065
|
+
context.currentScope.pop();
|
|
2066
|
+
}
|
|
2067
|
+
function processStructItem(node, context) {
|
|
2068
|
+
const nameNode = node.childForFieldName("name");
|
|
2069
|
+
if (!nameNode) return;
|
|
2070
|
+
const name = nodeText4(nameNode, context);
|
|
2071
|
+
const exported = hasVisibility(node, "pub");
|
|
2072
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2073
|
+
context.symbols.push({
|
|
2074
|
+
id: symbolId,
|
|
2075
|
+
name,
|
|
2076
|
+
kind: "class",
|
|
2077
|
+
// Consistent with other parsers
|
|
2078
|
+
filePath: context.filePath,
|
|
2079
|
+
startLine: node.startPosition.row + 1,
|
|
2080
|
+
endLine: node.endPosition.row + 1,
|
|
2081
|
+
exported
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
function processEnumItem(node, context) {
|
|
2085
|
+
const nameNode = node.childForFieldName("name");
|
|
2086
|
+
if (!nameNode) return;
|
|
2087
|
+
const name = nodeText4(nameNode, context);
|
|
2088
|
+
const exported = hasVisibility(node, "pub");
|
|
2089
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2090
|
+
context.symbols.push({
|
|
2091
|
+
id: symbolId,
|
|
2092
|
+
name,
|
|
2093
|
+
kind: "enum",
|
|
2094
|
+
filePath: context.filePath,
|
|
2095
|
+
startLine: node.startPosition.row + 1,
|
|
2096
|
+
endLine: node.endPosition.row + 1,
|
|
2097
|
+
exported
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
function processTraitItem(node, context) {
|
|
2101
|
+
const nameNode = node.childForFieldName("name");
|
|
2102
|
+
if (!nameNode) return;
|
|
2103
|
+
const name = nodeText4(nameNode, context);
|
|
2104
|
+
const exported = hasVisibility(node, "pub");
|
|
2105
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2106
|
+
context.symbols.push({
|
|
2107
|
+
id: symbolId,
|
|
2108
|
+
name,
|
|
2109
|
+
kind: "interface",
|
|
2110
|
+
filePath: context.filePath,
|
|
2111
|
+
startLine: node.startPosition.row + 1,
|
|
2112
|
+
endLine: node.endPosition.row + 1,
|
|
2113
|
+
exported
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
function processImplItem(node, context) {
|
|
2117
|
+
const typeNode = node.childForFieldName("type");
|
|
2118
|
+
if (!typeNode) return;
|
|
2119
|
+
const typeName = extractTypeName2(typeNode, context);
|
|
2120
|
+
if (!typeName) return;
|
|
2121
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2122
|
+
const child = node.child(i);
|
|
2123
|
+
if (child && child.type === "function_item") {
|
|
2124
|
+
const nameNode = child.childForFieldName("name");
|
|
2125
|
+
if (!nameNode) continue;
|
|
2126
|
+
const name = nodeText4(nameNode, context);
|
|
2127
|
+
const exported = hasVisibility(child, "pub");
|
|
2128
|
+
const symbolId = `${context.filePath}::${typeName}.${name}`;
|
|
2129
|
+
context.symbols.push({
|
|
2130
|
+
id: symbolId,
|
|
2131
|
+
name,
|
|
2132
|
+
kind: "method",
|
|
2133
|
+
filePath: context.filePath,
|
|
2134
|
+
startLine: child.startPosition.row + 1,
|
|
2135
|
+
endLine: child.endPosition.row + 1,
|
|
2136
|
+
exported,
|
|
2137
|
+
scope: typeName
|
|
2138
|
+
});
|
|
2139
|
+
context.currentScope.push(`${typeName}.${name}`);
|
|
2140
|
+
const body = child.childForFieldName("body");
|
|
2141
|
+
if (body) {
|
|
2142
|
+
walkNode5(body, context);
|
|
2143
|
+
}
|
|
2144
|
+
context.currentScope.pop();
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function processConstItem(node, context) {
|
|
2149
|
+
const nameNode = node.childForFieldName("name");
|
|
2150
|
+
if (!nameNode) return;
|
|
2151
|
+
const name = nodeText4(nameNode, context);
|
|
2152
|
+
const exported = hasVisibility(node, "pub");
|
|
2153
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2154
|
+
context.symbols.push({
|
|
2155
|
+
id: symbolId,
|
|
2156
|
+
name,
|
|
2157
|
+
kind: "constant",
|
|
2158
|
+
filePath: context.filePath,
|
|
2159
|
+
startLine: node.startPosition.row + 1,
|
|
2160
|
+
endLine: node.endPosition.row + 1,
|
|
2161
|
+
exported
|
|
2162
|
+
});
|
|
2163
|
+
}
|
|
2164
|
+
function processTypeItem(node, context) {
|
|
2165
|
+
const nameNode = node.childForFieldName("name");
|
|
2166
|
+
if (!nameNode) return;
|
|
2167
|
+
const name = nodeText4(nameNode, context);
|
|
2168
|
+
const exported = hasVisibility(node, "pub");
|
|
2169
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2170
|
+
context.symbols.push({
|
|
2171
|
+
id: symbolId,
|
|
2172
|
+
name,
|
|
2173
|
+
kind: "type_alias",
|
|
2174
|
+
filePath: context.filePath,
|
|
2175
|
+
startLine: node.startPosition.row + 1,
|
|
2176
|
+
endLine: node.endPosition.row + 1,
|
|
2177
|
+
exported
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
function processUseDeclaration(node, context) {
|
|
2181
|
+
let pathNode = findChildByType5(node, "scoped_identifier");
|
|
2182
|
+
if (!pathNode) {
|
|
2183
|
+
pathNode = findChildByType5(node, "identifier");
|
|
2184
|
+
}
|
|
2185
|
+
if (!pathNode) {
|
|
2186
|
+
pathNode = findChildByType5(node, "use_as_clause");
|
|
2187
|
+
if (pathNode) {
|
|
2188
|
+
pathNode = pathNode.childForFieldName("path");
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (!pathNode) {
|
|
2192
|
+
return;
|
|
2193
|
+
}
|
|
2194
|
+
let pathText = nodeText4(pathNode, context);
|
|
2195
|
+
if (!pathText.startsWith("crate::") && !pathText.startsWith("super::") && !pathText.startsWith("self::")) {
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2198
|
+
const segments = pathText.split("::");
|
|
2199
|
+
if (segments.length > 1) {
|
|
2200
|
+
segments.pop();
|
|
2201
|
+
pathText = segments.join("::");
|
|
2202
|
+
}
|
|
2203
|
+
const resolvedFiles = resolveRustImport(pathText, context);
|
|
2204
|
+
if (resolvedFiles.length === 0) return;
|
|
2205
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
2206
|
+
for (const targetFile of resolvedFiles) {
|
|
2207
|
+
const targetId = `${targetFile}::__file__`;
|
|
2208
|
+
context.edges.push({
|
|
2209
|
+
source: sourceId,
|
|
2210
|
+
target: targetId,
|
|
2211
|
+
kind: "imports",
|
|
2212
|
+
filePath: context.filePath,
|
|
2213
|
+
line: node.startPosition.row + 1
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function processModItem(node, context) {
|
|
2218
|
+
const nameNode = node.childForFieldName("name");
|
|
2219
|
+
if (!nameNode) return;
|
|
2220
|
+
const name = nodeText4(nameNode, context);
|
|
2221
|
+
const body = node.childForFieldName("body");
|
|
2222
|
+
if (!body) {
|
|
2223
|
+
const resolvedFiles = resolveModuleFile(name, context);
|
|
2224
|
+
if (resolvedFiles.length > 0) {
|
|
2225
|
+
const sourceId = `${context.filePath}::__file__`;
|
|
2226
|
+
for (const targetFile of resolvedFiles) {
|
|
2227
|
+
const targetId = `${targetFile}::__file__`;
|
|
2228
|
+
context.edges.push({
|
|
2229
|
+
source: sourceId,
|
|
2230
|
+
target: targetId,
|
|
2231
|
+
kind: "imports",
|
|
2232
|
+
filePath: context.filePath,
|
|
2233
|
+
line: node.startPosition.row + 1
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
const exported = hasVisibility(node, "pub");
|
|
2239
|
+
const symbolId = `${context.filePath}::${name}`;
|
|
2240
|
+
context.symbols.push({
|
|
2241
|
+
id: symbolId,
|
|
2242
|
+
name,
|
|
2243
|
+
kind: "module",
|
|
2244
|
+
filePath: context.filePath,
|
|
2245
|
+
startLine: node.startPosition.row + 1,
|
|
2246
|
+
endLine: node.endPosition.row + 1,
|
|
2247
|
+
exported
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
function processCallExpression5(node, context) {
|
|
2251
|
+
const functionNode = node.childForFieldName("function");
|
|
2252
|
+
if (!functionNode) return;
|
|
2253
|
+
const calleeName = extractCalleeNameFromNode(functionNode, context);
|
|
2254
|
+
if (!calleeName) return;
|
|
2255
|
+
const builtins = ["println!", "print!", "eprintln!", "eprint!", "format!", "panic!", "assert!", "assert_eq!", "assert_ne!", "vec!"];
|
|
2256
|
+
if (builtins.includes(calleeName)) return;
|
|
2257
|
+
const callerId = getCurrentSymbolId5(context);
|
|
2258
|
+
if (!callerId) return;
|
|
2259
|
+
const calleeId = resolveSymbol4(calleeName, context);
|
|
2260
|
+
if (!calleeId) return;
|
|
2261
|
+
context.edges.push({
|
|
2262
|
+
source: callerId,
|
|
2263
|
+
target: calleeId,
|
|
2264
|
+
kind: "calls",
|
|
2265
|
+
filePath: context.filePath,
|
|
2266
|
+
line: node.startPosition.row + 1
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
2269
|
+
function resolveRustImport(importPath, context) {
|
|
2270
|
+
if (importPath.startsWith("crate::")) {
|
|
2271
|
+
const relativePath = importPath.replace("crate::", "").replace(/::/g, "/");
|
|
2272
|
+
const possibleFiles = [
|
|
2273
|
+
join6(context.projectRoot, "src", `${relativePath}.rs`),
|
|
2274
|
+
join6(context.projectRoot, "src", relativePath, "mod.rs")
|
|
2275
|
+
];
|
|
2276
|
+
return possibleFiles.filter((f) => existsSync6(f)).map((f) => relative3(context.projectRoot, f));
|
|
2277
|
+
}
|
|
2278
|
+
if (importPath.startsWith("super::")) {
|
|
2279
|
+
const currentFileAbs = join6(context.projectRoot, context.filePath);
|
|
2280
|
+
const currentDir = dirname5(currentFileAbs);
|
|
2281
|
+
const parentDir = dirname5(currentDir);
|
|
2282
|
+
const relativePath = importPath.replace("super::", "").replace(/::/g, "/");
|
|
2283
|
+
const possibleFiles = [
|
|
2284
|
+
join6(parentDir, `${relativePath}.rs`),
|
|
2285
|
+
join6(parentDir, relativePath, "mod.rs")
|
|
2286
|
+
];
|
|
2287
|
+
return possibleFiles.filter((f) => existsSync6(f)).map((f) => relative3(context.projectRoot, f));
|
|
2288
|
+
}
|
|
2289
|
+
if (importPath.startsWith("self::")) {
|
|
2290
|
+
const currentFileAbs = join6(context.projectRoot, context.filePath);
|
|
2291
|
+
const currentDir = dirname5(currentFileAbs);
|
|
2292
|
+
const relativePath = importPath.replace("self::", "").replace(/::/g, "/");
|
|
2293
|
+
const possibleFiles = [
|
|
2294
|
+
join6(currentDir, `${relativePath}.rs`),
|
|
2295
|
+
join6(currentDir, relativePath, "mod.rs")
|
|
2296
|
+
];
|
|
2297
|
+
return possibleFiles.filter((f) => existsSync6(f)).map((f) => relative3(context.projectRoot, f));
|
|
2298
|
+
}
|
|
2299
|
+
return [];
|
|
2300
|
+
}
|
|
2301
|
+
function resolveModuleFile(moduleName, context) {
|
|
2302
|
+
const currentFileAbs = join6(context.projectRoot, context.filePath);
|
|
2303
|
+
const currentDir = dirname5(currentFileAbs);
|
|
2304
|
+
const possibleFiles = [
|
|
2305
|
+
join6(currentDir, `${moduleName}.rs`),
|
|
2306
|
+
join6(currentDir, moduleName, "mod.rs")
|
|
2307
|
+
];
|
|
2308
|
+
return possibleFiles.filter((f) => existsSync6(f)).map((f) => relative3(context.projectRoot, f));
|
|
2309
|
+
}
|
|
2310
|
+
function hasVisibility(node, visibility) {
|
|
2311
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2312
|
+
const child = node.child(i);
|
|
2313
|
+
if (child && child.type === "visibility_modifier") {
|
|
2314
|
+
const text = nodeText4(child, { sourceCode: node.text });
|
|
2315
|
+
return text === visibility;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
return false;
|
|
2319
|
+
}
|
|
2320
|
+
function extractTypeName2(typeNode, context) {
|
|
2321
|
+
if (typeNode.type === "type_identifier") {
|
|
2322
|
+
return nodeText4(typeNode, context);
|
|
2323
|
+
}
|
|
2324
|
+
if (typeNode.type === "generic_type") {
|
|
2325
|
+
const typeId = findChildByType5(typeNode, "type_identifier");
|
|
2326
|
+
if (typeId) {
|
|
2327
|
+
return nodeText4(typeId, context);
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2330
|
+
for (let i = 0; i < typeNode.childCount; i++) {
|
|
2331
|
+
const child = typeNode.child(i);
|
|
2332
|
+
if (child && child.type === "type_identifier") {
|
|
2333
|
+
return nodeText4(child, context);
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
function extractCalleeNameFromNode(functionNode, context) {
|
|
2339
|
+
if (functionNode.type === "identifier") {
|
|
2340
|
+
return nodeText4(functionNode, context);
|
|
2341
|
+
}
|
|
2342
|
+
if (functionNode.type === "field_expression") {
|
|
2343
|
+
const field = functionNode.childForFieldName("field");
|
|
2344
|
+
if (field) {
|
|
2345
|
+
return nodeText4(field, context);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (functionNode.type === "scoped_identifier") {
|
|
2349
|
+
const name = functionNode.childForFieldName("name");
|
|
2350
|
+
if (name) {
|
|
2351
|
+
return nodeText4(name, context);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
function resolveSymbol4(name, context) {
|
|
2357
|
+
const currentFileId = context.filePath;
|
|
2358
|
+
const symbol = context.symbols.find((s) => s.name === name && s.filePath === currentFileId);
|
|
2359
|
+
if (symbol) {
|
|
2360
|
+
return symbol.id;
|
|
2361
|
+
}
|
|
2362
|
+
if (context.currentScope.length > 0) {
|
|
2363
|
+
for (let i = context.currentScope.length - 1; i >= 0; i--) {
|
|
2364
|
+
const scopedId = `${currentFileId}::${context.currentScope[i]}.${name}`;
|
|
2365
|
+
const scopedSymbol = context.symbols.find((s) => s.id === scopedId);
|
|
2366
|
+
if (scopedSymbol) {
|
|
2367
|
+
return scopedId;
|
|
2368
|
+
}
|
|
2369
|
+
}
|
|
2370
|
+
}
|
|
2371
|
+
return null;
|
|
2372
|
+
}
|
|
2373
|
+
function findChildByType5(node, type) {
|
|
2374
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2375
|
+
const child = node.child(i);
|
|
2376
|
+
if (child && child.type === type) {
|
|
2377
|
+
return child;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
return null;
|
|
2381
|
+
}
|
|
2382
|
+
function nodeText4(node, context) {
|
|
2383
|
+
return context.sourceCode.slice(node.startIndex, node.endIndex);
|
|
2384
|
+
}
|
|
2385
|
+
function getCurrentSymbolId5(context) {
|
|
2386
|
+
if (context.currentScope.length === 0) return null;
|
|
2387
|
+
return `${context.filePath}::${context.currentScope.join(".")}`;
|
|
2388
|
+
}
|
|
2389
|
+
var rustParser = {
|
|
2390
|
+
language: "rust",
|
|
2391
|
+
extensions: [".rs"],
|
|
2392
|
+
parseFile: parseRustFile
|
|
2393
|
+
};
|
|
2394
|
+
|
|
1975
2395
|
// src/parser/detect.ts
|
|
1976
2396
|
var parsers = [
|
|
1977
2397
|
typescriptParser,
|
|
1978
2398
|
pythonParser,
|
|
1979
2399
|
javascriptParser,
|
|
1980
|
-
goParser
|
|
2400
|
+
goParser,
|
|
2401
|
+
rustParser
|
|
1981
2402
|
];
|
|
1982
2403
|
function getParserForFile(filePath) {
|
|
1983
2404
|
const ext = extname3(filePath).toLowerCase();
|
|
@@ -2007,12 +2428,12 @@ async function parseProject(projectRoot, options) {
|
|
|
2007
2428
|
let errorFiles = 0;
|
|
2008
2429
|
for (const file of files) {
|
|
2009
2430
|
try {
|
|
2010
|
-
const fullPath =
|
|
2431
|
+
const fullPath = join7(projectRoot, file);
|
|
2011
2432
|
if (options?.exclude) {
|
|
2012
|
-
const
|
|
2433
|
+
const shouldExclude2 = options.exclude.some(
|
|
2013
2434
|
(pattern) => minimatch(file, pattern, { matchBase: true })
|
|
2014
2435
|
);
|
|
2015
|
-
if (
|
|
2436
|
+
if (shouldExclude2) {
|
|
2016
2437
|
if (options.verbose) {
|
|
2017
2438
|
console.error(`[Parser] Excluded: ${file}`);
|
|
2018
2439
|
}
|
|
@@ -2033,7 +2454,7 @@ async function parseProject(projectRoot, options) {
|
|
|
2033
2454
|
skippedFiles++;
|
|
2034
2455
|
continue;
|
|
2035
2456
|
}
|
|
2036
|
-
const sourceCode =
|
|
2457
|
+
const sourceCode = readFileSync4(fullPath, "utf-8");
|
|
2037
2458
|
const parsed = parser.parseFile(file, sourceCode, projectRoot);
|
|
2038
2459
|
parsedFiles.push(parsed);
|
|
2039
2460
|
} catch (err) {
|
|
@@ -2089,6 +2510,18 @@ function buildGraph(parsedFiles) {
|
|
|
2089
2510
|
exported: false
|
|
2090
2511
|
});
|
|
2091
2512
|
}
|
|
2513
|
+
if (edge.target.endsWith("::__file__") && !fileNodes.has(edge.target)) {
|
|
2514
|
+
fileNodes.add(edge.target);
|
|
2515
|
+
const filePath = edge.target.replace("::__file__", "");
|
|
2516
|
+
graph.addNode(edge.target, {
|
|
2517
|
+
name: "__file__",
|
|
2518
|
+
kind: "import",
|
|
2519
|
+
filePath,
|
|
2520
|
+
startLine: 1,
|
|
2521
|
+
endLine: 1,
|
|
2522
|
+
exported: false
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2092
2525
|
}
|
|
2093
2526
|
}
|
|
2094
2527
|
for (const file of parsedFiles) {
|
|
@@ -2414,7 +2847,7 @@ function watchProject(projectRoot, callbacks) {
|
|
|
2414
2847
|
const watcher = chokidar.watch(projectRoot, watcherOptions);
|
|
2415
2848
|
console.error("[Watcher] Attaching event listeners...");
|
|
2416
2849
|
watcher.on("change", (absolutePath) => {
|
|
2417
|
-
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2850
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs"];
|
|
2418
2851
|
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2419
2852
|
if (absolutePath.endsWith("_test.go")) return;
|
|
2420
2853
|
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
@@ -2422,7 +2855,7 @@ function watchProject(projectRoot, callbacks) {
|
|
|
2422
2855
|
callbacks.onFileChanged(relativePath);
|
|
2423
2856
|
});
|
|
2424
2857
|
watcher.on("add", (absolutePath) => {
|
|
2425
|
-
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2858
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs"];
|
|
2426
2859
|
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2427
2860
|
if (absolutePath.endsWith("_test.go")) return;
|
|
2428
2861
|
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
@@ -2430,7 +2863,7 @@ function watchProject(projectRoot, callbacks) {
|
|
|
2430
2863
|
callbacks.onFileAdded(relativePath);
|
|
2431
2864
|
});
|
|
2432
2865
|
watcher.on("unlink", (absolutePath) => {
|
|
2433
|
-
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go"];
|
|
2866
|
+
const validExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".py", ".go", ".rs"];
|
|
2434
2867
|
if (!validExtensions.some((ext) => absolutePath.endsWith(ext))) return;
|
|
2435
2868
|
if (absolutePath.endsWith("_test.go")) return;
|
|
2436
2869
|
const relativePath = absolutePath.replace(projectRoot + "/", "");
|
|
@@ -2460,10 +2893,10 @@ function watchProject(projectRoot, callbacks) {
|
|
|
2460
2893
|
import express from "express";
|
|
2461
2894
|
import open from "open";
|
|
2462
2895
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2463
|
-
import { dirname as
|
|
2896
|
+
import { dirname as dirname6, join as join8 } from "path";
|
|
2464
2897
|
import { WebSocketServer } from "ws";
|
|
2465
2898
|
var __filename = fileURLToPath2(import.meta.url);
|
|
2466
|
-
var __dirname2 =
|
|
2899
|
+
var __dirname2 = dirname6(__filename);
|
|
2467
2900
|
var activeServer = null;
|
|
2468
2901
|
async function findAvailablePort(startPort, maxAttempts = 10) {
|
|
2469
2902
|
const net = await import("net");
|
|
@@ -2501,7 +2934,7 @@ async function startVizServer(initialVizData, graph, projectRoot, port = 3333, s
|
|
|
2501
2934
|
const availablePort = await findAvailablePort(port);
|
|
2502
2935
|
const app = express();
|
|
2503
2936
|
let vizData = initialVizData;
|
|
2504
|
-
const publicDir =
|
|
2937
|
+
const publicDir = join8(__dirname2, "viz", "public");
|
|
2505
2938
|
app.use(express.static(publicDir));
|
|
2506
2939
|
app.get("/api/graph", (req, res) => {
|
|
2507
2940
|
res.json(vizData);
|
|
@@ -2617,7 +3050,7 @@ function isProjectLoaded(state) {
|
|
|
2617
3050
|
}
|
|
2618
3051
|
|
|
2619
3052
|
// src/graph/updater.ts
|
|
2620
|
-
import { join as
|
|
3053
|
+
import { join as join9 } from "path";
|
|
2621
3054
|
function removeFileFromGraph(graph, filePath) {
|
|
2622
3055
|
const nodesToRemove = [];
|
|
2623
3056
|
graph.forEachNode((node, attrs) => {
|
|
@@ -2661,7 +3094,7 @@ function addFileToGraph(graph, parsedFile) {
|
|
|
2661
3094
|
}
|
|
2662
3095
|
async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
2663
3096
|
removeFileFromGraph(graph, relativeFilePath);
|
|
2664
|
-
const absolutePath =
|
|
3097
|
+
const absolutePath = join9(projectRoot, relativeFilePath);
|
|
2665
3098
|
try {
|
|
2666
3099
|
const parsedFile = parseTypeScriptFile(absolutePath, relativeFilePath);
|
|
2667
3100
|
addFileToGraph(graph, parsedFile);
|
|
@@ -2671,7 +3104,7 @@ async function updateFileInGraph(graph, projectRoot, relativeFilePath) {
|
|
|
2671
3104
|
}
|
|
2672
3105
|
|
|
2673
3106
|
// src/health/metrics.ts
|
|
2674
|
-
import { dirname as
|
|
3107
|
+
import { dirname as dirname7 } from "path";
|
|
2675
3108
|
function scoreToGrade(score) {
|
|
2676
3109
|
if (score >= 90) return "A";
|
|
2677
3110
|
if (score >= 80) return "B";
|
|
@@ -2704,8 +3137,8 @@ function calculateCouplingScore(graph) {
|
|
|
2704
3137
|
totalEdges++;
|
|
2705
3138
|
fileConnections.set(sourceAttrs.filePath, (fileConnections.get(sourceAttrs.filePath) || 0) + 1);
|
|
2706
3139
|
fileConnections.set(targetAttrs.filePath, (fileConnections.get(targetAttrs.filePath) || 0) + 1);
|
|
2707
|
-
const sourceDir =
|
|
2708
|
-
const targetDir =
|
|
3140
|
+
const sourceDir = dirname7(sourceAttrs.filePath).split("/")[0];
|
|
3141
|
+
const targetDir = dirname7(targetAttrs.filePath).split("/")[0];
|
|
2709
3142
|
if (sourceDir !== targetDir) {
|
|
2710
3143
|
crossDirEdges++;
|
|
2711
3144
|
}
|
|
@@ -2752,8 +3185,8 @@ function calculateCohesionScore(graph) {
|
|
|
2752
3185
|
const sourceAttrs = graph.getNodeAttributes(source);
|
|
2753
3186
|
const targetAttrs = graph.getNodeAttributes(target);
|
|
2754
3187
|
if (sourceAttrs.filePath !== targetAttrs.filePath) {
|
|
2755
|
-
const sourceDir =
|
|
2756
|
-
const targetDir =
|
|
3188
|
+
const sourceDir = dirname7(sourceAttrs.filePath);
|
|
3189
|
+
const targetDir = dirname7(targetAttrs.filePath);
|
|
2757
3190
|
if (!dirEdges.has(sourceDir)) {
|
|
2758
3191
|
dirEdges.set(sourceDir, { internal: 0, total: 0 });
|
|
2759
3192
|
}
|
|
@@ -2820,11 +3253,11 @@ function calculateCircularDepsScore(graph) {
|
|
|
2820
3253
|
const visited = /* @__PURE__ */ new Set();
|
|
2821
3254
|
const recStack = /* @__PURE__ */ new Set();
|
|
2822
3255
|
const cycles = [];
|
|
2823
|
-
function dfs(node,
|
|
3256
|
+
function dfs(node, path6) {
|
|
2824
3257
|
if (recStack.has(node)) {
|
|
2825
|
-
const cycleStart =
|
|
3258
|
+
const cycleStart = path6.indexOf(node);
|
|
2826
3259
|
if (cycleStart >= 0) {
|
|
2827
|
-
cycles.push(
|
|
3260
|
+
cycles.push(path6.slice(cycleStart));
|
|
2828
3261
|
}
|
|
2829
3262
|
return;
|
|
2830
3263
|
}
|
|
@@ -2833,11 +3266,11 @@ function calculateCircularDepsScore(graph) {
|
|
|
2833
3266
|
}
|
|
2834
3267
|
visited.add(node);
|
|
2835
3268
|
recStack.add(node);
|
|
2836
|
-
|
|
3269
|
+
path6.push(node);
|
|
2837
3270
|
const neighbors = fileGraph.get(node);
|
|
2838
3271
|
if (neighbors) {
|
|
2839
3272
|
for (const neighbor of neighbors) {
|
|
2840
|
-
dfs(neighbor, [...
|
|
3273
|
+
dfs(neighbor, [...path6]);
|
|
2841
3274
|
}
|
|
2842
3275
|
}
|
|
2843
3276
|
recStack.delete(node);
|
|
@@ -2939,25 +3372,40 @@ function calculateOrphansScore(graph) {
|
|
|
2939
3372
|
});
|
|
2940
3373
|
const orphanCount = files.size - connectedFiles.size;
|
|
2941
3374
|
const orphanPercent = files.size > 0 ? orphanCount / files.size * 100 : 0;
|
|
3375
|
+
const totalSymbols = graph.order;
|
|
3376
|
+
let deadSymbolCount = 0;
|
|
3377
|
+
graph.forEachNode((node) => {
|
|
3378
|
+
const inDegree = graph.inDegree(node);
|
|
3379
|
+
if (inDegree === 0) {
|
|
3380
|
+
deadSymbolCount++;
|
|
3381
|
+
}
|
|
3382
|
+
});
|
|
3383
|
+
const deadCodePercent = totalSymbols > 0 ? deadSymbolCount / totalSymbols * 100 : 0;
|
|
3384
|
+
const combinedScore = (orphanPercent + deadCodePercent) / 2;
|
|
2942
3385
|
let score = 100;
|
|
2943
|
-
if (
|
|
3386
|
+
if (combinedScore === 0) {
|
|
2944
3387
|
score = 100;
|
|
2945
|
-
} else if (
|
|
3388
|
+
} else if (combinedScore <= 5) {
|
|
2946
3389
|
score = 80;
|
|
2947
|
-
} else if (
|
|
3390
|
+
} else if (combinedScore <= 10) {
|
|
2948
3391
|
score = 60;
|
|
2949
|
-
} else if (
|
|
3392
|
+
} else if (combinedScore <= 20) {
|
|
2950
3393
|
score = 40;
|
|
2951
3394
|
} else {
|
|
2952
3395
|
score = 20;
|
|
2953
3396
|
}
|
|
2954
3397
|
return {
|
|
2955
|
-
name: "
|
|
3398
|
+
name: "Orphans & Dead Code",
|
|
2956
3399
|
score,
|
|
2957
3400
|
weight: 0.1,
|
|
2958
3401
|
grade: scoreToGrade(score),
|
|
2959
|
-
details:
|
|
2960
|
-
metrics: {
|
|
3402
|
+
details: `${orphanCount} orphan file${orphanCount === 1 ? "" : "s"} (${orphanPercent.toFixed(0)}%), ${deadSymbolCount} dead symbols (${deadCodePercent.toFixed(1)}%)`,
|
|
3403
|
+
metrics: {
|
|
3404
|
+
orphans: orphanCount,
|
|
3405
|
+
orphanPercentage: parseFloat(orphanPercent.toFixed(1)),
|
|
3406
|
+
deadSymbols: deadSymbolCount,
|
|
3407
|
+
deadCodePercentage: parseFloat(deadCodePercent.toFixed(1))
|
|
3408
|
+
}
|
|
2961
3409
|
};
|
|
2962
3410
|
}
|
|
2963
3411
|
function calculateDepthScore(graph) {
|
|
@@ -3020,8 +3468,8 @@ function calculateDepthScore(graph) {
|
|
|
3020
3468
|
}
|
|
3021
3469
|
|
|
3022
3470
|
// src/health/index.ts
|
|
3023
|
-
import { readFileSync as
|
|
3024
|
-
import { join as
|
|
3471
|
+
import { readFileSync as readFileSync5, writeFileSync, existsSync as existsSync7, mkdirSync } from "fs";
|
|
3472
|
+
import { join as join10, dirname as dirname8 } from "path";
|
|
3025
3473
|
function calculateHealthScore(graph, projectRoot) {
|
|
3026
3474
|
const coupling = calculateCouplingScore(graph);
|
|
3027
3475
|
const cohesion = calculateCohesionScore(graph);
|
|
@@ -3120,7 +3568,7 @@ function getHealthTrend(projectRoot, currentScore) {
|
|
|
3120
3568
|
}
|
|
3121
3569
|
}
|
|
3122
3570
|
function saveHealthHistory(projectRoot, report) {
|
|
3123
|
-
const historyFile =
|
|
3571
|
+
const historyFile = join10(projectRoot, ".depwire", "health-history.json");
|
|
3124
3572
|
const entry = {
|
|
3125
3573
|
timestamp: report.timestamp,
|
|
3126
3574
|
score: report.overall,
|
|
@@ -3132,9 +3580,9 @@ function saveHealthHistory(projectRoot, report) {
|
|
|
3132
3580
|
}))
|
|
3133
3581
|
};
|
|
3134
3582
|
let history = [];
|
|
3135
|
-
if (
|
|
3583
|
+
if (existsSync7(historyFile)) {
|
|
3136
3584
|
try {
|
|
3137
|
-
const content =
|
|
3585
|
+
const content = readFileSync5(historyFile, "utf-8");
|
|
3138
3586
|
history = JSON.parse(content);
|
|
3139
3587
|
} catch {
|
|
3140
3588
|
}
|
|
@@ -3143,28 +3591,326 @@ function saveHealthHistory(projectRoot, report) {
|
|
|
3143
3591
|
if (history.length > 50) {
|
|
3144
3592
|
history = history.slice(-50);
|
|
3145
3593
|
}
|
|
3146
|
-
mkdirSync(
|
|
3594
|
+
mkdirSync(dirname8(historyFile), { recursive: true });
|
|
3147
3595
|
writeFileSync(historyFile, JSON.stringify(history, null, 2), "utf-8");
|
|
3148
3596
|
}
|
|
3149
3597
|
function loadHealthHistory(projectRoot) {
|
|
3150
|
-
const historyFile =
|
|
3151
|
-
if (!
|
|
3598
|
+
const historyFile = join10(projectRoot, ".depwire", "health-history.json");
|
|
3599
|
+
if (!existsSync7(historyFile)) {
|
|
3152
3600
|
return [];
|
|
3153
3601
|
}
|
|
3154
3602
|
try {
|
|
3155
|
-
const content =
|
|
3603
|
+
const content = readFileSync5(historyFile, "utf-8");
|
|
3156
3604
|
return JSON.parse(content);
|
|
3157
3605
|
} catch {
|
|
3158
3606
|
return [];
|
|
3159
3607
|
}
|
|
3160
3608
|
}
|
|
3161
3609
|
|
|
3610
|
+
// src/dead-code/detector.ts
|
|
3611
|
+
import path2 from "path";
|
|
3612
|
+
function findDeadSymbols(graph, projectRoot, includeTests = false) {
|
|
3613
|
+
const deadSymbols = [];
|
|
3614
|
+
const context = { graph, projectRoot };
|
|
3615
|
+
for (const node of graph.nodes()) {
|
|
3616
|
+
const attrs = graph.getNodeAttributes(node);
|
|
3617
|
+
if (!attrs.file || !attrs.name) continue;
|
|
3618
|
+
const inDegree = graph.inDegree(node);
|
|
3619
|
+
if (inDegree === 0) {
|
|
3620
|
+
if (shouldExclude(attrs, context, includeTests)) {
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
deadSymbols.push({
|
|
3624
|
+
name: attrs.name,
|
|
3625
|
+
kind: attrs.kind || "unknown",
|
|
3626
|
+
file: attrs.file,
|
|
3627
|
+
line: attrs.startLine || 0,
|
|
3628
|
+
exported: attrs.exported || false,
|
|
3629
|
+
dependents: 0,
|
|
3630
|
+
confidence: "high",
|
|
3631
|
+
reason: "Zero dependents"
|
|
3632
|
+
});
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
return deadSymbols;
|
|
3636
|
+
}
|
|
3637
|
+
function shouldExclude(attrs, context, includeTests) {
|
|
3638
|
+
const filePath = attrs.file;
|
|
3639
|
+
const relativePath = path2.relative(context.projectRoot, filePath);
|
|
3640
|
+
if (!includeTests && isTestFile(relativePath)) {
|
|
3641
|
+
return true;
|
|
3642
|
+
}
|
|
3643
|
+
if (isEntryPoint(relativePath)) {
|
|
3644
|
+
return true;
|
|
3645
|
+
}
|
|
3646
|
+
if (isConfigFile(relativePath)) {
|
|
3647
|
+
return true;
|
|
3648
|
+
}
|
|
3649
|
+
if (isTypeDeclarationFile(relativePath)) {
|
|
3650
|
+
return true;
|
|
3651
|
+
}
|
|
3652
|
+
if (attrs.kind === "default") {
|
|
3653
|
+
return true;
|
|
3654
|
+
}
|
|
3655
|
+
if (isFrameworkAutoLoadedFile(relativePath)) {
|
|
3656
|
+
return true;
|
|
3657
|
+
}
|
|
3658
|
+
return false;
|
|
3659
|
+
}
|
|
3660
|
+
function isTestFile(filePath) {
|
|
3661
|
+
return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
|
|
3662
|
+
}
|
|
3663
|
+
function isEntryPoint(filePath) {
|
|
3664
|
+
const basename6 = path2.basename(filePath);
|
|
3665
|
+
return basename6 === "index.ts" || basename6 === "index.js" || basename6 === "main.ts" || basename6 === "main.js" || basename6 === "app.ts" || basename6 === "app.js" || basename6 === "server.ts" || basename6 === "server.js";
|
|
3666
|
+
}
|
|
3667
|
+
function isConfigFile(filePath) {
|
|
3668
|
+
return filePath.includes(".config.") || filePath.includes("config/") || filePath.includes("vite.config") || filePath.includes("rollup.config") || filePath.includes("webpack.config");
|
|
3669
|
+
}
|
|
3670
|
+
function isTypeDeclarationFile(filePath) {
|
|
3671
|
+
return filePath.endsWith(".d.ts");
|
|
3672
|
+
}
|
|
3673
|
+
function isFrameworkAutoLoadedFile(filePath) {
|
|
3674
|
+
return filePath.includes("/pages/") || filePath.includes("/routes/") || filePath.includes("/middleware/") || filePath.includes("/commands/") || filePath.includes("/api/") || filePath.includes("/app/");
|
|
3675
|
+
}
|
|
3676
|
+
|
|
3677
|
+
// src/dead-code/classifier.ts
|
|
3678
|
+
import path3 from "path";
|
|
3679
|
+
function classifyDeadSymbols(symbols, graph) {
|
|
3680
|
+
return symbols.map((symbol) => {
|
|
3681
|
+
const confidence = calculateConfidence(symbol, graph);
|
|
3682
|
+
const reason = generateReason(symbol, confidence);
|
|
3683
|
+
return {
|
|
3684
|
+
...symbol,
|
|
3685
|
+
confidence,
|
|
3686
|
+
reason
|
|
3687
|
+
};
|
|
3688
|
+
});
|
|
3689
|
+
}
|
|
3690
|
+
function calculateConfidence(symbol, graph) {
|
|
3691
|
+
if (!symbol.exported && symbol.dependents === 0) {
|
|
3692
|
+
return "high";
|
|
3693
|
+
}
|
|
3694
|
+
if (symbol.exported && symbol.dependents === 0 && !isBarrelFile(symbol.file)) {
|
|
3695
|
+
return "high";
|
|
3696
|
+
}
|
|
3697
|
+
if (symbol.exported && symbol.dependents === 0 && isBarrelFile(symbol.file)) {
|
|
3698
|
+
return "medium";
|
|
3699
|
+
}
|
|
3700
|
+
const dependents = getSymbolDependents(symbol, graph);
|
|
3701
|
+
if (dependents.length === 1 && isTestFile2(dependents[0])) {
|
|
3702
|
+
return "medium";
|
|
3703
|
+
}
|
|
3704
|
+
if (symbol.exported && isPackageEntryPoint(symbol.file)) {
|
|
3705
|
+
return "low";
|
|
3706
|
+
}
|
|
3707
|
+
if ((symbol.kind === "interface" || symbol.kind === "type") && symbol.dependents === 0) {
|
|
3708
|
+
return "low";
|
|
3709
|
+
}
|
|
3710
|
+
if (isLikelyDynamicUsage(symbol)) {
|
|
3711
|
+
return "low";
|
|
3712
|
+
}
|
|
3713
|
+
return "medium";
|
|
3714
|
+
}
|
|
3715
|
+
function generateReason(symbol, confidence) {
|
|
3716
|
+
if (!symbol.exported && symbol.dependents === 0) {
|
|
3717
|
+
return "Not exported, zero references";
|
|
3718
|
+
}
|
|
3719
|
+
if (symbol.exported && symbol.dependents === 0 && !isBarrelFile(symbol.file)) {
|
|
3720
|
+
return "Exported, zero dependents";
|
|
3721
|
+
}
|
|
3722
|
+
if (symbol.exported && symbol.dependents === 0 && isBarrelFile(symbol.file)) {
|
|
3723
|
+
return "Exported from barrel file, zero dependents (might be used externally)";
|
|
3724
|
+
}
|
|
3725
|
+
if (confidence === "medium") {
|
|
3726
|
+
return "Low usage, might be dead";
|
|
3727
|
+
}
|
|
3728
|
+
if (confidence === "low") {
|
|
3729
|
+
if (symbol.kind === "interface" || symbol.kind === "type") {
|
|
3730
|
+
return "Type with zero dependents (might be used via import type)";
|
|
3731
|
+
}
|
|
3732
|
+
if (isPackageEntryPoint(symbol.file)) {
|
|
3733
|
+
return "Exported from package entry point (might be public API)";
|
|
3734
|
+
}
|
|
3735
|
+
if (isLikelyDynamicUsage(symbol)) {
|
|
3736
|
+
return "In dynamic-use pattern directory (might be auto-loaded)";
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
return "Potentially unused";
|
|
3740
|
+
}
|
|
3741
|
+
function isBarrelFile(filePath) {
|
|
3742
|
+
const basename6 = path3.basename(filePath);
|
|
3743
|
+
return basename6 === "index.ts" || basename6 === "index.js";
|
|
3744
|
+
}
|
|
3745
|
+
function isTestFile2(filePath) {
|
|
3746
|
+
return filePath.includes("__tests__/") || filePath.includes(".test.") || filePath.includes(".spec.") || filePath.includes("/test/") || filePath.includes("/tests/");
|
|
3747
|
+
}
|
|
3748
|
+
function isPackageEntryPoint(filePath) {
|
|
3749
|
+
return filePath.includes("/src/index.") || filePath.includes("/lib/index.") || filePath.endsWith("/index.ts") || filePath.endsWith("/index.js");
|
|
3750
|
+
}
|
|
3751
|
+
function isLikelyDynamicUsage(symbol) {
|
|
3752
|
+
const filePath = symbol.file;
|
|
3753
|
+
return filePath.includes("/routes/") || filePath.includes("/pages/") || filePath.includes("/middleware/") || filePath.includes("/commands/") || filePath.includes("/handlers/") || filePath.includes("/api/");
|
|
3754
|
+
}
|
|
3755
|
+
function getSymbolDependents(symbol, graph) {
|
|
3756
|
+
const dependents = [];
|
|
3757
|
+
for (const node of graph.nodes()) {
|
|
3758
|
+
const attrs = graph.getNodeAttributes(node);
|
|
3759
|
+
if (attrs.file === symbol.file && attrs.name === symbol.name) {
|
|
3760
|
+
const inNeighbors = graph.inNeighbors(node);
|
|
3761
|
+
for (const neighbor of inNeighbors) {
|
|
3762
|
+
const neighborAttrs = graph.getNodeAttributes(neighbor);
|
|
3763
|
+
if (neighborAttrs.file) {
|
|
3764
|
+
dependents.push(neighborAttrs.file);
|
|
3765
|
+
}
|
|
3766
|
+
}
|
|
3767
|
+
break;
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
return dependents;
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// src/dead-code/display.ts
|
|
3774
|
+
import chalk from "chalk";
|
|
3775
|
+
import path4 from "path";
|
|
3776
|
+
function displayDeadCodeReport(report, options, projectRoot) {
|
|
3777
|
+
if (options.json) {
|
|
3778
|
+
console.log(JSON.stringify(report, null, 2));
|
|
3779
|
+
return;
|
|
3780
|
+
}
|
|
3781
|
+
console.log(chalk.cyan.bold("\n\u{1F50D} Dead Code Analysis\n"));
|
|
3782
|
+
const { high, medium, low } = report.byConfidence;
|
|
3783
|
+
console.log(
|
|
3784
|
+
`Found ${chalk.yellow.bold(report.deadSymbols)} potentially dead symbols (${chalk.red(high)} high, ${chalk.yellow(medium)} medium, ${chalk.gray(low)} low confidence)
|
|
3785
|
+
`
|
|
3786
|
+
);
|
|
3787
|
+
const symbolsByConfidence = groupByConfidence(report.symbols);
|
|
3788
|
+
if (symbolsByConfidence.high.length > 0) {
|
|
3789
|
+
displayConfidenceGroup("HIGH", symbolsByConfidence.high, options.verbose, projectRoot);
|
|
3790
|
+
}
|
|
3791
|
+
if (symbolsByConfidence.medium.length > 0) {
|
|
3792
|
+
displayConfidenceGroup("MEDIUM", symbolsByConfidence.medium, options.verbose, projectRoot);
|
|
3793
|
+
}
|
|
3794
|
+
if (symbolsByConfidence.low.length > 0) {
|
|
3795
|
+
displayConfidenceGroup("LOW", symbolsByConfidence.low, options.verbose, projectRoot);
|
|
3796
|
+
}
|
|
3797
|
+
if (options.stats) {
|
|
3798
|
+
displayStats(report);
|
|
3799
|
+
}
|
|
3800
|
+
}
|
|
3801
|
+
function groupByConfidence(symbols) {
|
|
3802
|
+
return symbols.reduce(
|
|
3803
|
+
(acc, symbol) => {
|
|
3804
|
+
acc[symbol.confidence].push(symbol);
|
|
3805
|
+
return acc;
|
|
3806
|
+
},
|
|
3807
|
+
{ high: [], medium: [], low: [] }
|
|
3808
|
+
);
|
|
3809
|
+
}
|
|
3810
|
+
function displayConfidenceGroup(level, symbols, verbose, projectRoot) {
|
|
3811
|
+
const emoji = level === "HIGH" ? "\u{1F534}" : level === "MEDIUM" ? "\u{1F7E1}" : "\u26AA";
|
|
3812
|
+
const color = level === "HIGH" ? chalk.red : level === "MEDIUM" ? chalk.yellow : chalk.gray;
|
|
3813
|
+
console.log(
|
|
3814
|
+
color.bold(`
|
|
3815
|
+
${emoji} ${level} CONFIDENCE `) + chalk.gray(`(${level === "HIGH" ? "definitely" : level === "MEDIUM" ? "probably" : "might be"} dead)`)
|
|
3816
|
+
);
|
|
3817
|
+
const headers = ["Symbol", "Kind", "File", verbose ? "Reason" : ""];
|
|
3818
|
+
const rows = symbols.map((symbol) => {
|
|
3819
|
+
const relativePath = path4.relative(projectRoot, symbol.file);
|
|
3820
|
+
return [
|
|
3821
|
+
chalk.bold(symbol.name),
|
|
3822
|
+
symbol.kind,
|
|
3823
|
+
`${relativePath}:${symbol.line}`,
|
|
3824
|
+
verbose ? symbol.reason : ""
|
|
3825
|
+
];
|
|
3826
|
+
});
|
|
3827
|
+
displayTable(headers, rows.filter((row) => row[3] !== ""));
|
|
3828
|
+
}
|
|
3829
|
+
function displayTable(headers, rows) {
|
|
3830
|
+
if (rows.length === 0) return;
|
|
3831
|
+
const columnWidths = headers.map((header2, i) => {
|
|
3832
|
+
const maxRowWidth = Math.max(...rows.map((row) => stripAnsi(row[i]).length));
|
|
3833
|
+
return Math.max(header2.length, maxRowWidth);
|
|
3834
|
+
});
|
|
3835
|
+
const separator = "\u250C" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u252C") + "\u2510";
|
|
3836
|
+
const headerRow = "\u2502 " + headers.map((h, i) => h.padEnd(columnWidths[i])).join(" \u2502 ") + " \u2502";
|
|
3837
|
+
const divider = "\u251C" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C") + "\u2524";
|
|
3838
|
+
const footer = "\u2514" + columnWidths.map((w) => "\u2500".repeat(w + 2)).join("\u2534") + "\u2518";
|
|
3839
|
+
console.log(separator);
|
|
3840
|
+
console.log(headerRow);
|
|
3841
|
+
console.log(divider);
|
|
3842
|
+
for (const row of rows) {
|
|
3843
|
+
const formattedRow = "\u2502 " + row.map((cell, i) => {
|
|
3844
|
+
const stripped = stripAnsi(cell);
|
|
3845
|
+
const padding = columnWidths[i] - stripped.length;
|
|
3846
|
+
return cell + " ".repeat(padding);
|
|
3847
|
+
}).join(" \u2502 ") + " \u2502";
|
|
3848
|
+
console.log(formattedRow);
|
|
3849
|
+
}
|
|
3850
|
+
console.log(footer);
|
|
3851
|
+
}
|
|
3852
|
+
function displayStats(report) {
|
|
3853
|
+
console.log(chalk.cyan.bold("\n\u{1F4CA} Summary\n"));
|
|
3854
|
+
console.log(` Total symbols analyzed: ${chalk.bold(report.totalSymbols.toLocaleString())}`);
|
|
3855
|
+
console.log(` Potentially dead: ${chalk.yellow.bold(report.deadSymbols)} (${report.deadPercentage.toFixed(1)}%)`);
|
|
3856
|
+
console.log(
|
|
3857
|
+
` By confidence: ${chalk.red(report.byConfidence.high)} high, ${chalk.yellow(report.byConfidence.medium)} medium, ${chalk.gray(report.byConfidence.low)} low`
|
|
3858
|
+
);
|
|
3859
|
+
const estimatedLines = report.deadSymbols * 18;
|
|
3860
|
+
console.log(` Estimated dead code: ${chalk.gray(`~${estimatedLines.toLocaleString()} lines`)}
|
|
3861
|
+
`);
|
|
3862
|
+
}
|
|
3863
|
+
function stripAnsi(str) {
|
|
3864
|
+
return str.replace(
|
|
3865
|
+
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
|
|
3866
|
+
""
|
|
3867
|
+
);
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// src/dead-code/index.ts
|
|
3871
|
+
function analyzeDeadCode(graph, projectRoot, options = {}) {
|
|
3872
|
+
const opts = {
|
|
3873
|
+
confidence: options.confidence || "medium",
|
|
3874
|
+
includeTests: options.includeTests || false,
|
|
3875
|
+
verbose: options.verbose || false,
|
|
3876
|
+
stats: options.stats || false,
|
|
3877
|
+
json: options.json || false
|
|
3878
|
+
};
|
|
3879
|
+
const rawDeadSymbols = findDeadSymbols(graph, projectRoot, opts.includeTests);
|
|
3880
|
+
const classifiedSymbols = classifyDeadSymbols(rawDeadSymbols, graph);
|
|
3881
|
+
const filteredSymbols = filterByConfidence(classifiedSymbols, opts.confidence);
|
|
3882
|
+
const totalSymbols = graph.order;
|
|
3883
|
+
const byConfidence = {
|
|
3884
|
+
high: classifiedSymbols.filter((s) => s.confidence === "high").length,
|
|
3885
|
+
medium: classifiedSymbols.filter((s) => s.confidence === "medium").length,
|
|
3886
|
+
low: classifiedSymbols.filter((s) => s.confidence === "low").length
|
|
3887
|
+
};
|
|
3888
|
+
const report = {
|
|
3889
|
+
totalSymbols,
|
|
3890
|
+
deadSymbols: filteredSymbols.length,
|
|
3891
|
+
deadPercentage: filteredSymbols.length / totalSymbols * 100,
|
|
3892
|
+
byConfidence,
|
|
3893
|
+
symbols: filteredSymbols
|
|
3894
|
+
};
|
|
3895
|
+
if (!opts.json) {
|
|
3896
|
+
displayDeadCodeReport(report, opts, projectRoot);
|
|
3897
|
+
}
|
|
3898
|
+
return report;
|
|
3899
|
+
}
|
|
3900
|
+
function filterByConfidence(symbols, minConfidence) {
|
|
3901
|
+
const confidenceLevels = { high: 3, medium: 2, low: 1 };
|
|
3902
|
+
const minLevel = confidenceLevels[minConfidence];
|
|
3903
|
+
return symbols.filter(
|
|
3904
|
+
(s) => confidenceLevels[s.confidence] >= minLevel
|
|
3905
|
+
);
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3162
3908
|
// src/docs/generator.ts
|
|
3163
|
-
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as
|
|
3164
|
-
import { join as
|
|
3909
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync10 } from "fs";
|
|
3910
|
+
import { join as join13 } from "path";
|
|
3165
3911
|
|
|
3166
3912
|
// src/docs/architecture.ts
|
|
3167
|
-
import { dirname as
|
|
3913
|
+
import { dirname as dirname9 } from "path";
|
|
3168
3914
|
|
|
3169
3915
|
// src/docs/templates.ts
|
|
3170
3916
|
function header(text, level = 1) {
|
|
@@ -3315,7 +4061,7 @@ function generateModuleStructure(graph) {
|
|
|
3315
4061
|
function getDirectoryStats(graph) {
|
|
3316
4062
|
const dirMap = /* @__PURE__ */ new Map();
|
|
3317
4063
|
graph.forEachNode((node, attrs) => {
|
|
3318
|
-
const dir =
|
|
4064
|
+
const dir = dirname9(attrs.filePath);
|
|
3319
4065
|
if (dir === ".") return;
|
|
3320
4066
|
if (!dirMap.has(dir)) {
|
|
3321
4067
|
dirMap.set(dir, {
|
|
@@ -3340,7 +4086,7 @@ function getDirectoryStats(graph) {
|
|
|
3340
4086
|
});
|
|
3341
4087
|
const filesPerDir = /* @__PURE__ */ new Map();
|
|
3342
4088
|
graph.forEachNode((node, attrs) => {
|
|
3343
|
-
const dir =
|
|
4089
|
+
const dir = dirname9(attrs.filePath);
|
|
3344
4090
|
if (!filesPerDir.has(dir)) {
|
|
3345
4091
|
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
3346
4092
|
}
|
|
@@ -3355,8 +4101,8 @@ function getDirectoryStats(graph) {
|
|
|
3355
4101
|
graph.forEachEdge((edge, attrs, source, target) => {
|
|
3356
4102
|
const sourceAttrs = graph.getNodeAttributes(source);
|
|
3357
4103
|
const targetAttrs = graph.getNodeAttributes(target);
|
|
3358
|
-
const sourceDir =
|
|
3359
|
-
const targetDir =
|
|
4104
|
+
const sourceDir = dirname9(sourceAttrs.filePath);
|
|
4105
|
+
const targetDir = dirname9(targetAttrs.filePath);
|
|
3360
4106
|
if (sourceDir !== targetDir) {
|
|
3361
4107
|
if (!dirEdges.has(sourceDir)) {
|
|
3362
4108
|
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
@@ -4208,20 +4954,20 @@ function findLongestPaths(graph, limit) {
|
|
|
4208
4954
|
}
|
|
4209
4955
|
const allPaths = [];
|
|
4210
4956
|
const visited = /* @__PURE__ */ new Set();
|
|
4211
|
-
function dfs(file,
|
|
4957
|
+
function dfs(file, path6) {
|
|
4212
4958
|
visited.add(file);
|
|
4213
|
-
|
|
4959
|
+
path6.push(file);
|
|
4214
4960
|
const neighbors = fileGraph.get(file);
|
|
4215
4961
|
if (!neighbors || neighbors.size === 0) {
|
|
4216
|
-
allPaths.push([...
|
|
4962
|
+
allPaths.push([...path6]);
|
|
4217
4963
|
} else {
|
|
4218
4964
|
for (const neighbor of neighbors) {
|
|
4219
4965
|
if (!visited.has(neighbor)) {
|
|
4220
|
-
dfs(neighbor,
|
|
4966
|
+
dfs(neighbor, path6);
|
|
4221
4967
|
}
|
|
4222
4968
|
}
|
|
4223
4969
|
}
|
|
4224
|
-
|
|
4970
|
+
path6.pop();
|
|
4225
4971
|
visited.delete(file);
|
|
4226
4972
|
}
|
|
4227
4973
|
for (const root of roots.slice(0, 10)) {
|
|
@@ -4337,7 +5083,7 @@ function detectCyclesDetailed(graph) {
|
|
|
4337
5083
|
}
|
|
4338
5084
|
|
|
4339
5085
|
// src/docs/onboarding.ts
|
|
4340
|
-
import { dirname as
|
|
5086
|
+
import { dirname as dirname10 } from "path";
|
|
4341
5087
|
function generateOnboarding(graph, projectRoot, version) {
|
|
4342
5088
|
let output = "";
|
|
4343
5089
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -4396,7 +5142,7 @@ function generateQuickOrientation(graph) {
|
|
|
4396
5142
|
const primaryLang = Object.entries(languages2).sort((a, b) => b[1] - a[1])[0];
|
|
4397
5143
|
const dirs = /* @__PURE__ */ new Set();
|
|
4398
5144
|
graph.forEachNode((node, attrs) => {
|
|
4399
|
-
const dir =
|
|
5145
|
+
const dir = dirname10(attrs.filePath);
|
|
4400
5146
|
if (dir !== ".") {
|
|
4401
5147
|
const topLevel = dir.split("/")[0];
|
|
4402
5148
|
dirs.add(topLevel);
|
|
@@ -4500,7 +5246,7 @@ function generateModuleMap(graph) {
|
|
|
4500
5246
|
function getDirectoryStats2(graph) {
|
|
4501
5247
|
const dirMap = /* @__PURE__ */ new Map();
|
|
4502
5248
|
graph.forEachNode((node, attrs) => {
|
|
4503
|
-
const dir =
|
|
5249
|
+
const dir = dirname10(attrs.filePath);
|
|
4504
5250
|
if (dir === ".") return;
|
|
4505
5251
|
if (!dirMap.has(dir)) {
|
|
4506
5252
|
dirMap.set(dir, {
|
|
@@ -4515,7 +5261,7 @@ function getDirectoryStats2(graph) {
|
|
|
4515
5261
|
});
|
|
4516
5262
|
const filesPerDir = /* @__PURE__ */ new Map();
|
|
4517
5263
|
graph.forEachNode((node, attrs) => {
|
|
4518
|
-
const dir =
|
|
5264
|
+
const dir = dirname10(attrs.filePath);
|
|
4519
5265
|
if (!filesPerDir.has(dir)) {
|
|
4520
5266
|
filesPerDir.set(dir, /* @__PURE__ */ new Set());
|
|
4521
5267
|
}
|
|
@@ -4530,8 +5276,8 @@ function getDirectoryStats2(graph) {
|
|
|
4530
5276
|
graph.forEachEdge((edge, attrs, source, target) => {
|
|
4531
5277
|
const sourceAttrs = graph.getNodeAttributes(source);
|
|
4532
5278
|
const targetAttrs = graph.getNodeAttributes(target);
|
|
4533
|
-
const sourceDir =
|
|
4534
|
-
const targetDir =
|
|
5279
|
+
const sourceDir = dirname10(sourceAttrs.filePath);
|
|
5280
|
+
const targetDir = dirname10(targetAttrs.filePath);
|
|
4535
5281
|
if (sourceDir !== targetDir) {
|
|
4536
5282
|
if (!dirEdges.has(sourceDir)) {
|
|
4537
5283
|
dirEdges.set(sourceDir, { in: 0, out: 0 });
|
|
@@ -4612,7 +5358,7 @@ function detectClusters(graph) {
|
|
|
4612
5358
|
const dirFiles = /* @__PURE__ */ new Map();
|
|
4613
5359
|
const fileEdges = /* @__PURE__ */ new Map();
|
|
4614
5360
|
graph.forEachNode((node, attrs) => {
|
|
4615
|
-
const dir =
|
|
5361
|
+
const dir = dirname10(attrs.filePath);
|
|
4616
5362
|
if (!dirFiles.has(dir)) {
|
|
4617
5363
|
dirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
4618
5364
|
}
|
|
@@ -4666,8 +5412,8 @@ function inferClusterName(files) {
|
|
|
4666
5412
|
if (sortedWords.length > 0 && sortedWords[0][1] > 1) {
|
|
4667
5413
|
return capitalizeFirst2(sortedWords[0][0]);
|
|
4668
5414
|
}
|
|
4669
|
-
const commonDir =
|
|
4670
|
-
if (files.every((f) =>
|
|
5415
|
+
const commonDir = dirname10(files[0]);
|
|
5416
|
+
if (files.every((f) => dirname10(f) === commonDir)) {
|
|
4671
5417
|
return capitalizeFirst2(commonDir.split("/").pop() || "Core");
|
|
4672
5418
|
}
|
|
4673
5419
|
return "Core";
|
|
@@ -4725,7 +5471,7 @@ function generateDepwireUsage(projectRoot) {
|
|
|
4725
5471
|
}
|
|
4726
5472
|
|
|
4727
5473
|
// src/docs/files.ts
|
|
4728
|
-
import { dirname as
|
|
5474
|
+
import { dirname as dirname11, basename as basename3 } from "path";
|
|
4729
5475
|
function generateFiles(graph, projectRoot, version) {
|
|
4730
5476
|
let output = "";
|
|
4731
5477
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -4829,7 +5575,7 @@ function generateDirectoryBreakdown(graph) {
|
|
|
4829
5575
|
const fileStats = getFileStats2(graph);
|
|
4830
5576
|
const dirMap = /* @__PURE__ */ new Map();
|
|
4831
5577
|
for (const file of fileStats) {
|
|
4832
|
-
const dir =
|
|
5578
|
+
const dir = dirname11(file.filePath);
|
|
4833
5579
|
const topDir = dir === "." ? "." : dir.split("/")[0];
|
|
4834
5580
|
if (!dirMap.has(topDir)) {
|
|
4835
5581
|
dirMap.set(topDir, {
|
|
@@ -5472,7 +6218,7 @@ function generateRecommendations(graph) {
|
|
|
5472
6218
|
}
|
|
5473
6219
|
|
|
5474
6220
|
// src/docs/tests.ts
|
|
5475
|
-
import { basename as basename4, dirname as
|
|
6221
|
+
import { basename as basename4, dirname as dirname12 } from "path";
|
|
5476
6222
|
function generateTests(graph, projectRoot, version) {
|
|
5477
6223
|
let output = "";
|
|
5478
6224
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -5499,9 +6245,9 @@ function getFileCount8(graph) {
|
|
|
5499
6245
|
});
|
|
5500
6246
|
return files.size;
|
|
5501
6247
|
}
|
|
5502
|
-
function
|
|
6248
|
+
function isTestFile3(filePath) {
|
|
5503
6249
|
const fileName = basename4(filePath).toLowerCase();
|
|
5504
|
-
const dirPath =
|
|
6250
|
+
const dirPath = dirname12(filePath).toLowerCase();
|
|
5505
6251
|
if (dirPath.includes("test") || dirPath.includes("spec") || dirPath.includes("__tests__")) {
|
|
5506
6252
|
return true;
|
|
5507
6253
|
}
|
|
@@ -5513,7 +6259,7 @@ function isTestFile(filePath) {
|
|
|
5513
6259
|
function getTestFiles(graph) {
|
|
5514
6260
|
const testFiles = /* @__PURE__ */ new Map();
|
|
5515
6261
|
graph.forEachNode((node, attrs) => {
|
|
5516
|
-
if (
|
|
6262
|
+
if (isTestFile3(attrs.filePath)) {
|
|
5517
6263
|
if (!testFiles.has(attrs.filePath)) {
|
|
5518
6264
|
testFiles.set(attrs.filePath, {
|
|
5519
6265
|
filePath: attrs.filePath,
|
|
@@ -5560,21 +6306,21 @@ function generateTestFileInventory(graph) {
|
|
|
5560
6306
|
}
|
|
5561
6307
|
function matchTestToSource(testFile) {
|
|
5562
6308
|
const testFileName = basename4(testFile);
|
|
5563
|
-
const testDir =
|
|
6309
|
+
const testDir = dirname12(testFile);
|
|
5564
6310
|
let sourceFileName = testFileName.replace(/\.test\./g, ".").replace(/\.spec\./g, ".").replace(/_test\./g, ".").replace(/_spec\./g, ".");
|
|
5565
6311
|
const possiblePaths = [];
|
|
5566
6312
|
possiblePaths.push(testDir + "/" + sourceFileName);
|
|
5567
6313
|
if (testDir.endsWith("/test") || testDir.endsWith("/tests") || testDir.endsWith("/__tests__")) {
|
|
5568
|
-
const parentDir =
|
|
6314
|
+
const parentDir = dirname12(testDir);
|
|
5569
6315
|
possiblePaths.push(parentDir + "/" + sourceFileName);
|
|
5570
6316
|
}
|
|
5571
6317
|
if (testDir.includes("test")) {
|
|
5572
6318
|
const srcDir = testDir.replace(/test[s]?/g, "src");
|
|
5573
6319
|
possiblePaths.push(srcDir + "/" + sourceFileName);
|
|
5574
6320
|
}
|
|
5575
|
-
for (const
|
|
5576
|
-
if (!
|
|
5577
|
-
return
|
|
6321
|
+
for (const path6 of possiblePaths) {
|
|
6322
|
+
if (!isTestFile3(path6)) {
|
|
6323
|
+
return path6;
|
|
5578
6324
|
}
|
|
5579
6325
|
}
|
|
5580
6326
|
return null;
|
|
@@ -5629,7 +6375,7 @@ function generateUntestedFiles(graph) {
|
|
|
5629
6375
|
allFiles.add(attrs.filePath);
|
|
5630
6376
|
});
|
|
5631
6377
|
for (const file of allFiles) {
|
|
5632
|
-
if (!
|
|
6378
|
+
if (!isTestFile3(file)) {
|
|
5633
6379
|
sourceFiles.push(file);
|
|
5634
6380
|
}
|
|
5635
6381
|
}
|
|
@@ -5688,7 +6434,7 @@ function generateTestCoverageMap(graph) {
|
|
|
5688
6434
|
allFiles.add(attrs.filePath);
|
|
5689
6435
|
});
|
|
5690
6436
|
for (const file of allFiles) {
|
|
5691
|
-
if (!
|
|
6437
|
+
if (!isTestFile3(file)) {
|
|
5692
6438
|
sourceFiles.push(file);
|
|
5693
6439
|
}
|
|
5694
6440
|
}
|
|
@@ -5740,7 +6486,7 @@ function generateTestStatistics(graph) {
|
|
|
5740
6486
|
allFiles.add(attrs.filePath);
|
|
5741
6487
|
});
|
|
5742
6488
|
for (const file of allFiles) {
|
|
5743
|
-
if (!
|
|
6489
|
+
if (!isTestFile3(file)) {
|
|
5744
6490
|
sourceFiles.push(file);
|
|
5745
6491
|
}
|
|
5746
6492
|
}
|
|
@@ -5762,7 +6508,7 @@ function generateTestStatistics(graph) {
|
|
|
5762
6508
|
`;
|
|
5763
6509
|
const dirTestCoverage = /* @__PURE__ */ new Map();
|
|
5764
6510
|
for (const sourceFile of sourceFiles) {
|
|
5765
|
-
const dir =
|
|
6511
|
+
const dir = dirname12(sourceFile).split("/")[0];
|
|
5766
6512
|
if (!dirTestCoverage.has(dir)) {
|
|
5767
6513
|
dirTestCoverage.set(dir, { total: 0, tested: 0 });
|
|
5768
6514
|
}
|
|
@@ -5785,7 +6531,7 @@ function generateTestStatistics(graph) {
|
|
|
5785
6531
|
}
|
|
5786
6532
|
|
|
5787
6533
|
// src/docs/history.ts
|
|
5788
|
-
import { dirname as
|
|
6534
|
+
import { dirname as dirname13 } from "path";
|
|
5789
6535
|
import { execSync } from "child_process";
|
|
5790
6536
|
function generateHistory(graph, projectRoot, version) {
|
|
5791
6537
|
let output = "";
|
|
@@ -6066,7 +6812,7 @@ function generateFeatureClusters(graph) {
|
|
|
6066
6812
|
const dirFiles = /* @__PURE__ */ new Map();
|
|
6067
6813
|
const fileEdges = /* @__PURE__ */ new Map();
|
|
6068
6814
|
graph.forEachNode((node, attrs) => {
|
|
6069
|
-
const dir =
|
|
6815
|
+
const dir = dirname13(attrs.filePath);
|
|
6070
6816
|
if (!dirFiles.has(dir)) {
|
|
6071
6817
|
dirFiles.set(dir, /* @__PURE__ */ new Set());
|
|
6072
6818
|
}
|
|
@@ -6148,7 +6894,7 @@ function capitalizeFirst3(str) {
|
|
|
6148
6894
|
}
|
|
6149
6895
|
|
|
6150
6896
|
// src/docs/current.ts
|
|
6151
|
-
import { dirname as
|
|
6897
|
+
import { dirname as dirname14 } from "path";
|
|
6152
6898
|
function generateCurrent(graph, projectRoot, version) {
|
|
6153
6899
|
let output = "";
|
|
6154
6900
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -6286,7 +7032,7 @@ function generateCompleteFileIndex(graph) {
|
|
|
6286
7032
|
fileInfos.sort((a, b) => a.filePath.localeCompare(b.filePath));
|
|
6287
7033
|
const dirGroups = /* @__PURE__ */ new Map();
|
|
6288
7034
|
for (const info of fileInfos) {
|
|
6289
|
-
const dir =
|
|
7035
|
+
const dir = dirname14(info.filePath);
|
|
6290
7036
|
const topDir = dir === "." ? "root" : dir.split("/")[0];
|
|
6291
7037
|
if (!dirGroups.has(topDir)) {
|
|
6292
7038
|
dirGroups.set(topDir, []);
|
|
@@ -6497,8 +7243,8 @@ function getTopLevelDir2(filePath) {
|
|
|
6497
7243
|
}
|
|
6498
7244
|
|
|
6499
7245
|
// src/docs/status.ts
|
|
6500
|
-
import { readFileSync as
|
|
6501
|
-
import { join as
|
|
7246
|
+
import { readFileSync as readFileSync6, existsSync as existsSync8 } from "fs";
|
|
7247
|
+
import { join as join11 } from "path";
|
|
6502
7248
|
function generateStatus(graph, projectRoot, version) {
|
|
6503
7249
|
let output = "";
|
|
6504
7250
|
const now = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
@@ -6531,12 +7277,12 @@ function getFileCount11(graph) {
|
|
|
6531
7277
|
}
|
|
6532
7278
|
function extractComments(projectRoot, filePath) {
|
|
6533
7279
|
const comments = [];
|
|
6534
|
-
const fullPath =
|
|
6535
|
-
if (!
|
|
7280
|
+
const fullPath = join11(projectRoot, filePath);
|
|
7281
|
+
if (!existsSync8(fullPath)) {
|
|
6536
7282
|
return comments;
|
|
6537
7283
|
}
|
|
6538
7284
|
try {
|
|
6539
|
-
const content =
|
|
7285
|
+
const content = readFileSync6(fullPath, "utf-8");
|
|
6540
7286
|
const lines = content.split("\n");
|
|
6541
7287
|
const patterns = [
|
|
6542
7288
|
{ type: "TODO", regex: /(?:\/\/|#|\/\*)\s*TODO:?\s*(.+)/i },
|
|
@@ -7005,16 +7751,108 @@ function generateDetailedMetrics(dimensions) {
|
|
|
7005
7751
|
return output;
|
|
7006
7752
|
}
|
|
7007
7753
|
|
|
7754
|
+
// src/docs/dead-code.ts
|
|
7755
|
+
import path5 from "path";
|
|
7756
|
+
function generateDeadCode(graph, projectRoot, projectName) {
|
|
7757
|
+
const report = analyzeDeadCode(graph, projectRoot, {
|
|
7758
|
+
confidence: "low",
|
|
7759
|
+
includeTests: false,
|
|
7760
|
+
verbose: true,
|
|
7761
|
+
stats: true,
|
|
7762
|
+
json: true
|
|
7763
|
+
});
|
|
7764
|
+
let output = "";
|
|
7765
|
+
output += header(`${projectName} - Dead Code Analysis`, 1);
|
|
7766
|
+
output += timestamp();
|
|
7767
|
+
output += "\n";
|
|
7768
|
+
output += header("Summary", 2);
|
|
7769
|
+
output += `Total symbols analyzed: **${formatNumber(report.totalSymbols)}**
|
|
7770
|
+
|
|
7771
|
+
`;
|
|
7772
|
+
output += `Potentially dead symbols: **${formatNumber(report.deadSymbols)}** (${report.deadPercentage.toFixed(1)}%)
|
|
7773
|
+
|
|
7774
|
+
`;
|
|
7775
|
+
output += `- \u{1F534} High confidence (definitely dead): **${report.byConfidence.high}**
|
|
7776
|
+
`;
|
|
7777
|
+
output += `- \u{1F7E1} Medium confidence (probably dead): **${report.byConfidence.medium}**
|
|
7778
|
+
`;
|
|
7779
|
+
output += `- \u26AA Low confidence (might be dead): **${report.byConfidence.low}**
|
|
7780
|
+
|
|
7781
|
+
`;
|
|
7782
|
+
const estimatedLines = report.deadSymbols * 18;
|
|
7783
|
+
output += `Estimated dead code: **~${formatNumber(estimatedLines)} lines**
|
|
7784
|
+
|
|
7785
|
+
`;
|
|
7786
|
+
const symbolsByConfidence = groupByConfidence2(report.symbols);
|
|
7787
|
+
if (symbolsByConfidence.high.length > 0) {
|
|
7788
|
+
output += generateConfidenceSection(
|
|
7789
|
+
"High Confidence",
|
|
7790
|
+
"definitely dead",
|
|
7791
|
+
symbolsByConfidence.high,
|
|
7792
|
+
projectRoot
|
|
7793
|
+
);
|
|
7794
|
+
}
|
|
7795
|
+
if (symbolsByConfidence.medium.length > 0) {
|
|
7796
|
+
output += generateConfidenceSection(
|
|
7797
|
+
"Medium Confidence",
|
|
7798
|
+
"probably dead",
|
|
7799
|
+
symbolsByConfidence.medium,
|
|
7800
|
+
projectRoot
|
|
7801
|
+
);
|
|
7802
|
+
}
|
|
7803
|
+
if (symbolsByConfidence.low.length > 0) {
|
|
7804
|
+
output += generateConfidenceSection(
|
|
7805
|
+
"Low Confidence",
|
|
7806
|
+
"might be dead",
|
|
7807
|
+
symbolsByConfidence.low,
|
|
7808
|
+
projectRoot
|
|
7809
|
+
);
|
|
7810
|
+
}
|
|
7811
|
+
output += "\n---\n\n";
|
|
7812
|
+
output += "_This document was auto-generated by Depwire. It reflects the current state of the codebase and should be reviewed before taking action._\n";
|
|
7813
|
+
return output;
|
|
7814
|
+
}
|
|
7815
|
+
function groupByConfidence2(symbols) {
|
|
7816
|
+
return symbols.reduce(
|
|
7817
|
+
(acc, symbol) => {
|
|
7818
|
+
acc[symbol.confidence].push(symbol);
|
|
7819
|
+
return acc;
|
|
7820
|
+
},
|
|
7821
|
+
{ high: [], medium: [], low: [] }
|
|
7822
|
+
);
|
|
7823
|
+
}
|
|
7824
|
+
function generateConfidenceSection(title, description, symbols, projectRoot) {
|
|
7825
|
+
let output = "";
|
|
7826
|
+
output += header(`${title} (${description})`, 2);
|
|
7827
|
+
output += `Found **${symbols.length}** symbol${symbols.length === 1 ? "" : "s"}.
|
|
7828
|
+
|
|
7829
|
+
`;
|
|
7830
|
+
const headers = ["Symbol", "Kind", "File", "Exported", "Reason"];
|
|
7831
|
+
const rows = symbols.map((s) => {
|
|
7832
|
+
const relativePath = path5.relative(projectRoot, s.file);
|
|
7833
|
+
return [
|
|
7834
|
+
code(s.name),
|
|
7835
|
+
s.kind,
|
|
7836
|
+
`${relativePath}:${s.line}`,
|
|
7837
|
+
s.exported ? "Yes" : "No",
|
|
7838
|
+
s.reason
|
|
7839
|
+
];
|
|
7840
|
+
});
|
|
7841
|
+
output += table(headers, rows);
|
|
7842
|
+
output += "\n";
|
|
7843
|
+
return output;
|
|
7844
|
+
}
|
|
7845
|
+
|
|
7008
7846
|
// src/docs/metadata.ts
|
|
7009
|
-
import { existsSync as
|
|
7010
|
-
import { join as
|
|
7847
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "fs";
|
|
7848
|
+
import { join as join12 } from "path";
|
|
7011
7849
|
function loadMetadata(outputDir) {
|
|
7012
|
-
const metadataPath =
|
|
7013
|
-
if (!
|
|
7850
|
+
const metadataPath = join12(outputDir, "metadata.json");
|
|
7851
|
+
if (!existsSync9(metadataPath)) {
|
|
7014
7852
|
return null;
|
|
7015
7853
|
}
|
|
7016
7854
|
try {
|
|
7017
|
-
const content =
|
|
7855
|
+
const content = readFileSync7(metadataPath, "utf-8");
|
|
7018
7856
|
return JSON.parse(content);
|
|
7019
7857
|
} catch (err) {
|
|
7020
7858
|
console.error("Failed to load metadata:", err);
|
|
@@ -7022,7 +7860,7 @@ function loadMetadata(outputDir) {
|
|
|
7022
7860
|
}
|
|
7023
7861
|
}
|
|
7024
7862
|
function saveMetadata(outputDir, metadata) {
|
|
7025
|
-
const metadataPath =
|
|
7863
|
+
const metadataPath = join12(outputDir, "metadata.json");
|
|
7026
7864
|
writeFileSync2(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
|
7027
7865
|
}
|
|
7028
7866
|
function createMetadata(version, projectPath, fileCount, symbolCount, edgeCount, docTypes) {
|
|
@@ -7065,7 +7903,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7065
7903
|
const generated = [];
|
|
7066
7904
|
const errors = [];
|
|
7067
7905
|
try {
|
|
7068
|
-
if (!
|
|
7906
|
+
if (!existsSync10(options.outputDir)) {
|
|
7069
7907
|
mkdirSync2(options.outputDir, { recursive: true });
|
|
7070
7908
|
if (options.verbose) {
|
|
7071
7909
|
console.log(`Created output directory: ${options.outputDir}`);
|
|
@@ -7088,7 +7926,8 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7088
7926
|
"history",
|
|
7089
7927
|
"current",
|
|
7090
7928
|
"status",
|
|
7091
|
-
"health"
|
|
7929
|
+
"health",
|
|
7930
|
+
"dead_code"
|
|
7092
7931
|
];
|
|
7093
7932
|
}
|
|
7094
7933
|
let metadata = null;
|
|
@@ -7103,7 +7942,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7103
7942
|
try {
|
|
7104
7943
|
if (options.verbose) console.log("Generating ARCHITECTURE.md...");
|
|
7105
7944
|
const content = generateArchitecture(graph, projectRoot, version, parseTime);
|
|
7106
|
-
const filePath =
|
|
7945
|
+
const filePath = join13(options.outputDir, "ARCHITECTURE.md");
|
|
7107
7946
|
writeFileSync3(filePath, content, "utf-8");
|
|
7108
7947
|
generated.push("ARCHITECTURE.md");
|
|
7109
7948
|
} catch (err) {
|
|
@@ -7114,7 +7953,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7114
7953
|
try {
|
|
7115
7954
|
if (options.verbose) console.log("Generating CONVENTIONS.md...");
|
|
7116
7955
|
const content = generateConventions(graph, projectRoot, version);
|
|
7117
|
-
const filePath =
|
|
7956
|
+
const filePath = join13(options.outputDir, "CONVENTIONS.md");
|
|
7118
7957
|
writeFileSync3(filePath, content, "utf-8");
|
|
7119
7958
|
generated.push("CONVENTIONS.md");
|
|
7120
7959
|
} catch (err) {
|
|
@@ -7125,7 +7964,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7125
7964
|
try {
|
|
7126
7965
|
if (options.verbose) console.log("Generating DEPENDENCIES.md...");
|
|
7127
7966
|
const content = generateDependencies(graph, projectRoot, version);
|
|
7128
|
-
const filePath =
|
|
7967
|
+
const filePath = join13(options.outputDir, "DEPENDENCIES.md");
|
|
7129
7968
|
writeFileSync3(filePath, content, "utf-8");
|
|
7130
7969
|
generated.push("DEPENDENCIES.md");
|
|
7131
7970
|
} catch (err) {
|
|
@@ -7136,7 +7975,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7136
7975
|
try {
|
|
7137
7976
|
if (options.verbose) console.log("Generating ONBOARDING.md...");
|
|
7138
7977
|
const content = generateOnboarding(graph, projectRoot, version);
|
|
7139
|
-
const filePath =
|
|
7978
|
+
const filePath = join13(options.outputDir, "ONBOARDING.md");
|
|
7140
7979
|
writeFileSync3(filePath, content, "utf-8");
|
|
7141
7980
|
generated.push("ONBOARDING.md");
|
|
7142
7981
|
} catch (err) {
|
|
@@ -7147,7 +7986,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7147
7986
|
try {
|
|
7148
7987
|
if (options.verbose) console.log("Generating FILES.md...");
|
|
7149
7988
|
const content = generateFiles(graph, projectRoot, version);
|
|
7150
|
-
const filePath =
|
|
7989
|
+
const filePath = join13(options.outputDir, "FILES.md");
|
|
7151
7990
|
writeFileSync3(filePath, content, "utf-8");
|
|
7152
7991
|
generated.push("FILES.md");
|
|
7153
7992
|
} catch (err) {
|
|
@@ -7158,7 +7997,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7158
7997
|
try {
|
|
7159
7998
|
if (options.verbose) console.log("Generating API_SURFACE.md...");
|
|
7160
7999
|
const content = generateApiSurface(graph, projectRoot, version);
|
|
7161
|
-
const filePath =
|
|
8000
|
+
const filePath = join13(options.outputDir, "API_SURFACE.md");
|
|
7162
8001
|
writeFileSync3(filePath, content, "utf-8");
|
|
7163
8002
|
generated.push("API_SURFACE.md");
|
|
7164
8003
|
} catch (err) {
|
|
@@ -7169,7 +8008,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7169
8008
|
try {
|
|
7170
8009
|
if (options.verbose) console.log("Generating ERRORS.md...");
|
|
7171
8010
|
const content = generateErrors(graph, projectRoot, version);
|
|
7172
|
-
const filePath =
|
|
8011
|
+
const filePath = join13(options.outputDir, "ERRORS.md");
|
|
7173
8012
|
writeFileSync3(filePath, content, "utf-8");
|
|
7174
8013
|
generated.push("ERRORS.md");
|
|
7175
8014
|
} catch (err) {
|
|
@@ -7180,7 +8019,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7180
8019
|
try {
|
|
7181
8020
|
if (options.verbose) console.log("Generating TESTS.md...");
|
|
7182
8021
|
const content = generateTests(graph, projectRoot, version);
|
|
7183
|
-
const filePath =
|
|
8022
|
+
const filePath = join13(options.outputDir, "TESTS.md");
|
|
7184
8023
|
writeFileSync3(filePath, content, "utf-8");
|
|
7185
8024
|
generated.push("TESTS.md");
|
|
7186
8025
|
} catch (err) {
|
|
@@ -7191,7 +8030,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7191
8030
|
try {
|
|
7192
8031
|
if (options.verbose) console.log("Generating HISTORY.md...");
|
|
7193
8032
|
const content = generateHistory(graph, projectRoot, version);
|
|
7194
|
-
const filePath =
|
|
8033
|
+
const filePath = join13(options.outputDir, "HISTORY.md");
|
|
7195
8034
|
writeFileSync3(filePath, content, "utf-8");
|
|
7196
8035
|
generated.push("HISTORY.md");
|
|
7197
8036
|
} catch (err) {
|
|
@@ -7202,7 +8041,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7202
8041
|
try {
|
|
7203
8042
|
if (options.verbose) console.log("Generating CURRENT.md...");
|
|
7204
8043
|
const content = generateCurrent(graph, projectRoot, version);
|
|
7205
|
-
const filePath =
|
|
8044
|
+
const filePath = join13(options.outputDir, "CURRENT.md");
|
|
7206
8045
|
writeFileSync3(filePath, content, "utf-8");
|
|
7207
8046
|
generated.push("CURRENT.md");
|
|
7208
8047
|
} catch (err) {
|
|
@@ -7213,7 +8052,7 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7213
8052
|
try {
|
|
7214
8053
|
if (options.verbose) console.log("Generating STATUS.md...");
|
|
7215
8054
|
const content = generateStatus(graph, projectRoot, version);
|
|
7216
|
-
const filePath =
|
|
8055
|
+
const filePath = join13(options.outputDir, "STATUS.md");
|
|
7217
8056
|
writeFileSync3(filePath, content, "utf-8");
|
|
7218
8057
|
generated.push("STATUS.md");
|
|
7219
8058
|
} catch (err) {
|
|
@@ -7224,13 +8063,24 @@ async function generateDocs(graph, projectRoot, version, parseTime, options) {
|
|
|
7224
8063
|
try {
|
|
7225
8064
|
if (options.verbose) console.log("Generating HEALTH.md...");
|
|
7226
8065
|
const content = generateHealth(graph, projectRoot, version);
|
|
7227
|
-
const filePath =
|
|
8066
|
+
const filePath = join13(options.outputDir, "HEALTH.md");
|
|
7228
8067
|
writeFileSync3(filePath, content, "utf-8");
|
|
7229
8068
|
generated.push("HEALTH.md");
|
|
7230
8069
|
} catch (err) {
|
|
7231
8070
|
errors.push(`Failed to generate HEALTH.md: ${err}`);
|
|
7232
8071
|
}
|
|
7233
8072
|
}
|
|
8073
|
+
if (docsToGenerate.includes("dead_code")) {
|
|
8074
|
+
try {
|
|
8075
|
+
if (options.verbose) console.log("Generating DEAD_CODE.md...");
|
|
8076
|
+
const content = generateDeadCode(graph, projectRoot, version);
|
|
8077
|
+
const filePath = join13(options.outputDir, "DEAD_CODE.md");
|
|
8078
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
8079
|
+
generated.push("DEAD_CODE.md");
|
|
8080
|
+
} catch (err) {
|
|
8081
|
+
errors.push(`Failed to generate DEAD_CODE.md: ${err}`);
|
|
8082
|
+
}
|
|
8083
|
+
}
|
|
7234
8084
|
} else if (options.format === "json") {
|
|
7235
8085
|
errors.push("JSON format not yet supported");
|
|
7236
8086
|
}
|
|
@@ -7272,13 +8122,13 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7272
8122
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
7273
8123
|
|
|
7274
8124
|
// src/mcp/tools.ts
|
|
7275
|
-
import { dirname as
|
|
7276
|
-
import { existsSync as
|
|
8125
|
+
import { dirname as dirname15, join as join16 } from "path";
|
|
8126
|
+
import { existsSync as existsSync13, readFileSync as readFileSync9 } from "fs";
|
|
7277
8127
|
|
|
7278
8128
|
// src/mcp/connect.ts
|
|
7279
8129
|
import simpleGit from "simple-git";
|
|
7280
|
-
import { existsSync as
|
|
7281
|
-
import { join as
|
|
8130
|
+
import { existsSync as existsSync11 } from "fs";
|
|
8131
|
+
import { join as join14, basename as basename5, resolve as resolve2 } from "path";
|
|
7282
8132
|
import { tmpdir, homedir } from "os";
|
|
7283
8133
|
function validateProjectPath(source) {
|
|
7284
8134
|
const resolved = resolve2(source);
|
|
@@ -7291,11 +8141,11 @@ function validateProjectPath(source) {
|
|
|
7291
8141
|
"/boot",
|
|
7292
8142
|
"/proc",
|
|
7293
8143
|
"/sys",
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
8144
|
+
join14(homedir(), ".ssh"),
|
|
8145
|
+
join14(homedir(), ".gnupg"),
|
|
8146
|
+
join14(homedir(), ".aws"),
|
|
8147
|
+
join14(homedir(), ".config"),
|
|
8148
|
+
join14(homedir(), ".env")
|
|
7299
8149
|
];
|
|
7300
8150
|
for (const blocked of blockedPaths) {
|
|
7301
8151
|
if (resolved.startsWith(blocked)) {
|
|
@@ -7318,11 +8168,11 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
7318
8168
|
};
|
|
7319
8169
|
}
|
|
7320
8170
|
projectName = match[1];
|
|
7321
|
-
const reposDir =
|
|
7322
|
-
const cloneDir =
|
|
8171
|
+
const reposDir = join14(tmpdir(), "depwire-repos");
|
|
8172
|
+
const cloneDir = join14(reposDir, projectName);
|
|
7323
8173
|
console.error(`Connecting to GitHub repo: ${source}`);
|
|
7324
8174
|
const git = simpleGit();
|
|
7325
|
-
if (
|
|
8175
|
+
if (existsSync11(cloneDir)) {
|
|
7326
8176
|
console.error(`Repo already cloned at ${cloneDir}, pulling latest changes...`);
|
|
7327
8177
|
try {
|
|
7328
8178
|
await git.cwd(cloneDir).pull();
|
|
@@ -7340,7 +8190,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
7340
8190
|
};
|
|
7341
8191
|
}
|
|
7342
8192
|
}
|
|
7343
|
-
projectRoot = subdirectory ?
|
|
8193
|
+
projectRoot = subdirectory ? join14(cloneDir, subdirectory) : cloneDir;
|
|
7344
8194
|
} else {
|
|
7345
8195
|
const validation2 = validateProjectPath(source);
|
|
7346
8196
|
if (!validation2.valid) {
|
|
@@ -7349,13 +8199,13 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
7349
8199
|
message: validation2.error
|
|
7350
8200
|
};
|
|
7351
8201
|
}
|
|
7352
|
-
if (!
|
|
8202
|
+
if (!existsSync11(source)) {
|
|
7353
8203
|
return {
|
|
7354
8204
|
error: "Directory not found",
|
|
7355
8205
|
message: `Directory does not exist: ${source}`
|
|
7356
8206
|
};
|
|
7357
8207
|
}
|
|
7358
|
-
projectRoot = subdirectory ?
|
|
8208
|
+
projectRoot = subdirectory ? join14(source, subdirectory) : source;
|
|
7359
8209
|
projectName = basename5(projectRoot);
|
|
7360
8210
|
}
|
|
7361
8211
|
const validation = validateProjectPath(projectRoot);
|
|
@@ -7365,7 +8215,7 @@ async function connectToRepo(source, subdirectory, state) {
|
|
|
7365
8215
|
message: validation.error
|
|
7366
8216
|
};
|
|
7367
8217
|
}
|
|
7368
|
-
if (!
|
|
8218
|
+
if (!existsSync11(projectRoot)) {
|
|
7369
8219
|
return {
|
|
7370
8220
|
error: "Project root not found",
|
|
7371
8221
|
message: `Directory does not exist: ${projectRoot}`
|
|
@@ -7646,24 +8496,24 @@ function getWeekNumber(date) {
|
|
|
7646
8496
|
}
|
|
7647
8497
|
|
|
7648
8498
|
// src/temporal/snapshots.ts
|
|
7649
|
-
import { writeFileSync as writeFileSync4, readFileSync as
|
|
7650
|
-
import { join as
|
|
8499
|
+
import { writeFileSync as writeFileSync4, readFileSync as readFileSync8, mkdirSync as mkdirSync3, existsSync as existsSync12, readdirSync as readdirSync4 } from "fs";
|
|
8500
|
+
import { join as join15 } from "path";
|
|
7651
8501
|
function saveSnapshot(snapshot, outputDir) {
|
|
7652
|
-
if (!
|
|
8502
|
+
if (!existsSync12(outputDir)) {
|
|
7653
8503
|
mkdirSync3(outputDir, { recursive: true });
|
|
7654
8504
|
}
|
|
7655
8505
|
const filename = `${snapshot.commitHash.substring(0, 8)}.json`;
|
|
7656
|
-
const filepath =
|
|
8506
|
+
const filepath = join15(outputDir, filename);
|
|
7657
8507
|
writeFileSync4(filepath, JSON.stringify(snapshot, null, 2), "utf-8");
|
|
7658
8508
|
}
|
|
7659
8509
|
function loadSnapshot(commitHash, outputDir) {
|
|
7660
8510
|
const shortHash = commitHash.substring(0, 8);
|
|
7661
|
-
const filepath =
|
|
7662
|
-
if (!
|
|
8511
|
+
const filepath = join15(outputDir, `${shortHash}.json`);
|
|
8512
|
+
if (!existsSync12(filepath)) {
|
|
7663
8513
|
return null;
|
|
7664
8514
|
}
|
|
7665
8515
|
try {
|
|
7666
|
-
const content =
|
|
8516
|
+
const content = readFileSync8(filepath, "utf-8");
|
|
7667
8517
|
return JSON.parse(content);
|
|
7668
8518
|
} catch {
|
|
7669
8519
|
return null;
|
|
@@ -7689,8 +8539,8 @@ function createSnapshot(graph, commitHash, commitDate, commitMessage, commitAuth
|
|
|
7689
8539
|
}
|
|
7690
8540
|
}
|
|
7691
8541
|
}
|
|
7692
|
-
const files = Array.from(fileMap.entries()).map(([
|
|
7693
|
-
path:
|
|
8542
|
+
const files = Array.from(fileMap.entries()).map(([path6, data]) => ({
|
|
8543
|
+
path: path6,
|
|
7694
8544
|
symbols: data.symbols,
|
|
7695
8545
|
connections: data.inbound + data.outbound
|
|
7696
8546
|
}));
|
|
@@ -7927,6 +8777,21 @@ function getToolsList() {
|
|
|
7927
8777
|
}
|
|
7928
8778
|
}
|
|
7929
8779
|
}
|
|
8780
|
+
},
|
|
8781
|
+
{
|
|
8782
|
+
name: "find_dead_code",
|
|
8783
|
+
description: "Find potentially dead code \u2014 symbols that are defined but never referenced anywhere in the codebase. Returns symbols categorized by confidence level (high, medium, low). High confidence means definitely unused. Use this to identify cleanup opportunities.",
|
|
8784
|
+
inputSchema: {
|
|
8785
|
+
type: "object",
|
|
8786
|
+
properties: {
|
|
8787
|
+
confidence: {
|
|
8788
|
+
type: "string",
|
|
8789
|
+
enum: ["high", "medium", "low"],
|
|
8790
|
+
description: "Minimum confidence level to return (default: medium)",
|
|
8791
|
+
default: "medium"
|
|
8792
|
+
}
|
|
8793
|
+
}
|
|
8794
|
+
}
|
|
7930
8795
|
}
|
|
7931
8796
|
];
|
|
7932
8797
|
}
|
|
@@ -7989,6 +8854,15 @@ async function handleToolCall(name, args, state) {
|
|
|
7989
8854
|
} else {
|
|
7990
8855
|
result = await handleGetTemporalGraph(state, args.commits || 10, args.strategy || "even");
|
|
7991
8856
|
}
|
|
8857
|
+
} else if (name === "find_dead_code") {
|
|
8858
|
+
if (!isProjectLoaded(state)) {
|
|
8859
|
+
result = {
|
|
8860
|
+
error: "No project loaded",
|
|
8861
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
8862
|
+
};
|
|
8863
|
+
} else {
|
|
8864
|
+
result = handleFindDeadCode(state, args.confidence || "medium");
|
|
8865
|
+
}
|
|
7992
8866
|
} else {
|
|
7993
8867
|
if (!isProjectLoaded(state)) {
|
|
7994
8868
|
result = {
|
|
@@ -8326,7 +9200,7 @@ function handleGetArchitectureSummary(graph) {
|
|
|
8326
9200
|
const dirMap = /* @__PURE__ */ new Map();
|
|
8327
9201
|
const languageBreakdown = {};
|
|
8328
9202
|
fileSummary.forEach((f) => {
|
|
8329
|
-
const dir = f.filePath.includes("/") ?
|
|
9203
|
+
const dir = f.filePath.includes("/") ? dirname15(f.filePath) : ".";
|
|
8330
9204
|
if (!dirMap.has(dir)) {
|
|
8331
9205
|
dirMap.set(dir, { fileCount: 0, symbolCount: 0 });
|
|
8332
9206
|
}
|
|
@@ -8413,8 +9287,8 @@ The server will keep running until you end the MCP session or press Ctrl+C.`;
|
|
|
8413
9287
|
};
|
|
8414
9288
|
}
|
|
8415
9289
|
async function handleGetProjectDocs(docType, state) {
|
|
8416
|
-
const docsDir =
|
|
8417
|
-
if (!
|
|
9290
|
+
const docsDir = join16(state.projectRoot, ".depwire");
|
|
9291
|
+
if (!existsSync13(docsDir)) {
|
|
8418
9292
|
const errorMessage = `Project documentation has not been generated yet.
|
|
8419
9293
|
|
|
8420
9294
|
Run \`depwire docs ${state.projectRoot}\` to generate codebase documentation.
|
|
@@ -8444,12 +9318,12 @@ Available document types:
|
|
|
8444
9318
|
missing.push(doc);
|
|
8445
9319
|
continue;
|
|
8446
9320
|
}
|
|
8447
|
-
const filePath =
|
|
8448
|
-
if (!
|
|
9321
|
+
const filePath = join16(docsDir, metadata.documents[doc].file);
|
|
9322
|
+
if (!existsSync13(filePath)) {
|
|
8449
9323
|
missing.push(doc);
|
|
8450
9324
|
continue;
|
|
8451
9325
|
}
|
|
8452
|
-
const content =
|
|
9326
|
+
const content = readFileSync9(filePath, "utf-8");
|
|
8453
9327
|
if (docsToReturn.length > 1) {
|
|
8454
9328
|
output += `
|
|
8455
9329
|
|
|
@@ -8474,16 +9348,16 @@ Available document types:
|
|
|
8474
9348
|
}
|
|
8475
9349
|
async function handleUpdateProjectDocs(docType, state) {
|
|
8476
9350
|
const startTime = Date.now();
|
|
8477
|
-
const docsDir =
|
|
9351
|
+
const docsDir = join16(state.projectRoot, ".depwire");
|
|
8478
9352
|
console.error("Regenerating project documentation...");
|
|
8479
9353
|
const parsedFiles = await parseProject(state.projectRoot);
|
|
8480
9354
|
const graph = buildGraph(parsedFiles);
|
|
8481
9355
|
const parseTime = (Date.now() - startTime) / 1e3;
|
|
8482
9356
|
state.graph = graph;
|
|
8483
|
-
const packageJsonPath =
|
|
8484
|
-
const packageJson = JSON.parse(
|
|
9357
|
+
const packageJsonPath = join16(__dirname, "../../package.json");
|
|
9358
|
+
const packageJson = JSON.parse(readFileSync9(packageJsonPath, "utf-8"));
|
|
8485
9359
|
const docsToGenerate = docType === "all" ? ["architecture", "conventions", "dependencies", "onboarding"] : [docType];
|
|
8486
|
-
const docsExist =
|
|
9360
|
+
const docsExist = existsSync13(docsDir);
|
|
8487
9361
|
const result = await generateDocs(graph, state.projectRoot, packageJson.version, parseTime, {
|
|
8488
9362
|
outputDir: docsDir,
|
|
8489
9363
|
format: "markdown",
|
|
@@ -8542,7 +9416,7 @@ async function handleGetTemporalGraph(state, commits, strategy) {
|
|
|
8542
9416
|
}
|
|
8543
9417
|
const sampledCommits = sampleCommits(allCommits, commits, strategy);
|
|
8544
9418
|
const snapshots = [];
|
|
8545
|
-
const outputDir =
|
|
9419
|
+
const outputDir = join16(projectRoot, ".depwire", "temporal");
|
|
8546
9420
|
for (const commit of sampledCommits) {
|
|
8547
9421
|
const existing = loadSnapshot(commit.hash, outputDir);
|
|
8548
9422
|
if (existing) {
|
|
@@ -8604,6 +9478,46 @@ async function handleGetTemporalGraph(state, commits, strategy) {
|
|
|
8604
9478
|
};
|
|
8605
9479
|
}
|
|
8606
9480
|
}
|
|
9481
|
+
function handleFindDeadCode(state, confidence) {
|
|
9482
|
+
if (!state.graph || !state.projectRoot) {
|
|
9483
|
+
return {
|
|
9484
|
+
error: "No project loaded",
|
|
9485
|
+
message: "Use connect_repo to connect to a codebase first"
|
|
9486
|
+
};
|
|
9487
|
+
}
|
|
9488
|
+
try {
|
|
9489
|
+
const report = analyzeDeadCode(state.graph, state.projectRoot, {
|
|
9490
|
+
confidence,
|
|
9491
|
+
includeTests: false,
|
|
9492
|
+
verbose: false,
|
|
9493
|
+
stats: false,
|
|
9494
|
+
json: true
|
|
9495
|
+
});
|
|
9496
|
+
return {
|
|
9497
|
+
status: "success",
|
|
9498
|
+
totalSymbols: report.totalSymbols,
|
|
9499
|
+
deadSymbols: report.deadSymbols,
|
|
9500
|
+
deadPercentage: report.deadPercentage,
|
|
9501
|
+
byConfidence: report.byConfidence,
|
|
9502
|
+
symbols: report.symbols.map((s) => ({
|
|
9503
|
+
name: s.name,
|
|
9504
|
+
kind: s.kind,
|
|
9505
|
+
file: s.file,
|
|
9506
|
+
line: s.line,
|
|
9507
|
+
exported: s.exported,
|
|
9508
|
+
dependents: s.dependents,
|
|
9509
|
+
confidence: s.confidence,
|
|
9510
|
+
reason: s.reason
|
|
9511
|
+
})),
|
|
9512
|
+
summary: `Found ${report.deadSymbols} potentially dead symbols (${report.byConfidence.high} high, ${report.byConfidence.medium} medium, ${report.byConfidence.low} low confidence) out of ${report.totalSymbols} total symbols (${report.deadPercentage.toFixed(1)}% dead code).`
|
|
9513
|
+
};
|
|
9514
|
+
} catch (error) {
|
|
9515
|
+
return {
|
|
9516
|
+
error: "Failed to analyze dead code",
|
|
9517
|
+
message: String(error)
|
|
9518
|
+
};
|
|
9519
|
+
}
|
|
9520
|
+
}
|
|
8607
9521
|
|
|
8608
9522
|
// src/mcp/server.ts
|
|
8609
9523
|
async function startMcpServer(state) {
|
|
@@ -8652,6 +9566,7 @@ export {
|
|
|
8652
9566
|
updateFileInGraph,
|
|
8653
9567
|
calculateHealthScore,
|
|
8654
9568
|
getHealthTrend,
|
|
9569
|
+
analyzeDeadCode,
|
|
8655
9570
|
generateDocs,
|
|
8656
9571
|
getCommitLog,
|
|
8657
9572
|
getCurrentBranch,
|