@harness-engineering/core 0.26.4 → 0.28.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/dist/analyzer-VY6DJVOU.mjs +8 -0
- package/dist/architecture/matchers.d.mts +1 -1
- package/dist/architecture/matchers.d.ts +1 -1
- package/dist/architecture/matchers.mjs +4 -1
- package/dist/chunk-7P6ASYW6.mjs +9 -0
- package/dist/chunk-BTUDWWB4.mjs +1729 -0
- package/dist/chunk-FYTJQ2ZY.mjs +1171 -0
- package/dist/chunk-IIEDD47I.mjs +125 -0
- package/dist/{chunk-JIOBXIVB.mjs → chunk-MUWJHO2S.mjs} +600 -1886
- package/dist/chunk-XKLVJIPL.mjs +261 -0
- package/dist/index.d.mts +185 -30
- package/dist/index.d.ts +185 -30
- package/dist/index.js +7500 -6731
- package/dist/index.mjs +1269 -2781
- package/dist/timeline-manager-FPYKJRHR.mjs +8 -0
- package/package.json +2 -2
- package/dist/{matchers-DSibUtbV.d.mts → matchers-OE6gO1nh.d.mts} +6 -6
- package/dist/{matchers-DSibUtbV.d.ts → matchers-OE6gO1nh.d.ts} +6 -6
|
@@ -0,0 +1,1729 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Err,
|
|
3
|
+
Ok,
|
|
4
|
+
buildDependencyGraph,
|
|
5
|
+
createEntropyError,
|
|
6
|
+
detectComplexityViolations,
|
|
7
|
+
detectCouplingViolations,
|
|
8
|
+
fileExists,
|
|
9
|
+
findFiles,
|
|
10
|
+
getDefaultRegistry,
|
|
11
|
+
readFileContent,
|
|
12
|
+
relativePosix
|
|
13
|
+
} from "./chunk-MUWJHO2S.mjs";
|
|
14
|
+
|
|
15
|
+
// src/entropy/snapshot.ts
|
|
16
|
+
import { skipDirGlobs } from "@harness-engineering/graph";
|
|
17
|
+
import { resolve as resolve2 } from "path";
|
|
18
|
+
import { minimatch } from "minimatch";
|
|
19
|
+
|
|
20
|
+
// src/entropy/entry-points.ts
|
|
21
|
+
import { join, resolve } from "path";
|
|
22
|
+
function collectFieldEntries(rootDir, field) {
|
|
23
|
+
if (typeof field === "string") return [resolve(rootDir, field)];
|
|
24
|
+
if (typeof field === "object" && field !== null) {
|
|
25
|
+
return Object.values(field).filter((v) => typeof v === "string").map((v) => resolve(rootDir, v));
|
|
26
|
+
}
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
function extractPackageEntries(rootDir, pkg) {
|
|
30
|
+
const entries = [];
|
|
31
|
+
entries.push(...collectFieldEntries(rootDir, pkg["exports"]));
|
|
32
|
+
if (entries.length === 0 && typeof pkg["main"] === "string") {
|
|
33
|
+
entries.push(resolve(rootDir, pkg["main"]));
|
|
34
|
+
}
|
|
35
|
+
if (pkg["bin"]) entries.push(...collectFieldEntries(rootDir, pkg["bin"]));
|
|
36
|
+
return entries;
|
|
37
|
+
}
|
|
38
|
+
var TS_HINTS = ['Add "exports" or "main" to package.json', "Create src/index.ts"];
|
|
39
|
+
var TS_CONVENTIONS = ["src/index.ts", "src/main.ts", "src/index.tsx", "index.ts", "main.ts"];
|
|
40
|
+
async function readPackageJsonEntries(rootDir, pkgPath) {
|
|
41
|
+
const content = await readFileContent(pkgPath);
|
|
42
|
+
if (!content.ok) return [];
|
|
43
|
+
try {
|
|
44
|
+
const pkg = JSON.parse(content.value);
|
|
45
|
+
return extractPackageEntries(rootDir, pkg);
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function resolveTypeScript(rootDir) {
|
|
51
|
+
const pkgPath = join(rootDir, "package.json");
|
|
52
|
+
const detected = await fileExists(pkgPath);
|
|
53
|
+
if (detected) {
|
|
54
|
+
const entries = await readPackageJsonEntries(rootDir, pkgPath);
|
|
55
|
+
if (entries.length > 0) {
|
|
56
|
+
return { language: "typescript", detected: true, entries, hints: TS_HINTS };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
for (const conv of TS_CONVENTIONS) {
|
|
60
|
+
const p = join(rootDir, conv);
|
|
61
|
+
if (await fileExists(p)) {
|
|
62
|
+
return { language: "typescript", detected: true, entries: [p], hints: TS_HINTS };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { language: "typescript", detected, entries: [], hints: TS_HINTS };
|
|
66
|
+
}
|
|
67
|
+
var PYTHON_HINTS = [
|
|
68
|
+
"Add an entry to [project.scripts] in pyproject.toml",
|
|
69
|
+
"Create main.py or <package>/__main__.py"
|
|
70
|
+
];
|
|
71
|
+
var PYTHON_CONVENTIONS = [
|
|
72
|
+
"__main__.py",
|
|
73
|
+
"main.py",
|
|
74
|
+
"app.py",
|
|
75
|
+
"src/__main__.py",
|
|
76
|
+
"src/main.py",
|
|
77
|
+
"src/app.py"
|
|
78
|
+
];
|
|
79
|
+
async function detectPython(rootDir) {
|
|
80
|
+
return await fileExists(join(rootDir, "pyproject.toml")) || await fileExists(join(rootDir, "setup.py")) || await fileExists(join(rootDir, "requirements.txt"));
|
|
81
|
+
}
|
|
82
|
+
async function readPyProject(rootDir) {
|
|
83
|
+
const pyproject = join(rootDir, "pyproject.toml");
|
|
84
|
+
if (!await fileExists(pyproject)) return { scriptTargets: [] };
|
|
85
|
+
const content = await readFileContent(pyproject);
|
|
86
|
+
if (!content.ok) return { scriptTargets: [] };
|
|
87
|
+
return parsePyProject(content.value);
|
|
88
|
+
}
|
|
89
|
+
async function resolveScriptTargetEntry(rootDir, target) {
|
|
90
|
+
const mod = target.split(":")[0];
|
|
91
|
+
if (!mod) return void 0;
|
|
92
|
+
const relPath = mod.replaceAll(".", "/") + ".py";
|
|
93
|
+
for (const candidate of [join(rootDir, relPath), join(rootDir, "src", relPath)]) {
|
|
94
|
+
if (await fileExists(candidate)) return candidate;
|
|
95
|
+
}
|
|
96
|
+
return void 0;
|
|
97
|
+
}
|
|
98
|
+
async function resolvePythonFromScripts(rootDir, targets) {
|
|
99
|
+
const entries = [];
|
|
100
|
+
for (const target of targets) {
|
|
101
|
+
const entry = await resolveScriptTargetEntry(rootDir, target);
|
|
102
|
+
if (entry) entries.push(entry);
|
|
103
|
+
}
|
|
104
|
+
return entries;
|
|
105
|
+
}
|
|
106
|
+
async function resolvePythonFromProjectName(rootDir, projectName) {
|
|
107
|
+
if (!projectName) return [];
|
|
108
|
+
const normalized = projectName.replaceAll("-", "_");
|
|
109
|
+
const candidates = [
|
|
110
|
+
join(rootDir, normalized, "__init__.py"),
|
|
111
|
+
join(rootDir, normalized, "__main__.py"),
|
|
112
|
+
join(rootDir, "src", normalized, "__init__.py"),
|
|
113
|
+
join(rootDir, "src", normalized, "__main__.py")
|
|
114
|
+
];
|
|
115
|
+
const entries = [];
|
|
116
|
+
for (const c of candidates) {
|
|
117
|
+
if (await fileExists(c)) entries.push(c);
|
|
118
|
+
}
|
|
119
|
+
return entries;
|
|
120
|
+
}
|
|
121
|
+
async function resolvePythonConventions(rootDir) {
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const conv of PYTHON_CONVENTIONS) {
|
|
124
|
+
const p = join(rootDir, conv);
|
|
125
|
+
if (await fileExists(p)) entries.push(p);
|
|
126
|
+
}
|
|
127
|
+
return entries;
|
|
128
|
+
}
|
|
129
|
+
async function findPythonTopLevelPackages(rootDir) {
|
|
130
|
+
const found = await findFiles("*/__init__.py", rootDir);
|
|
131
|
+
if (found.length > 0) return found;
|
|
132
|
+
return findFiles("src/*/__init__.py", rootDir);
|
|
133
|
+
}
|
|
134
|
+
async function resolvePython(rootDir) {
|
|
135
|
+
if (!await detectPython(rootDir)) {
|
|
136
|
+
return { language: "python", detected: false, entries: [], hints: PYTHON_HINTS };
|
|
137
|
+
}
|
|
138
|
+
const info = await readPyProject(rootDir);
|
|
139
|
+
const strategies = [
|
|
140
|
+
() => resolvePythonFromScripts(rootDir, info.scriptTargets),
|
|
141
|
+
() => resolvePythonFromProjectName(rootDir, info.projectName),
|
|
142
|
+
() => resolvePythonConventions(rootDir),
|
|
143
|
+
() => findPythonTopLevelPackages(rootDir)
|
|
144
|
+
];
|
|
145
|
+
for (const strategy of strategies) {
|
|
146
|
+
const entries = await strategy();
|
|
147
|
+
if (entries.length > 0) {
|
|
148
|
+
return { language: "python", detected: true, entries, hints: PYTHON_HINTS };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return { language: "python", detected: true, entries: [], hints: PYTHON_HINTS };
|
|
152
|
+
}
|
|
153
|
+
async function resolveGo(rootDir) {
|
|
154
|
+
const hints = ["Create main.go at the project root, or use the cmd/<name>/main.go layout"];
|
|
155
|
+
const detected = await fileExists(join(rootDir, "go.mod"));
|
|
156
|
+
if (!detected) return { language: "go", detected: false, entries: [], hints };
|
|
157
|
+
const entries = [];
|
|
158
|
+
const mainGo = join(rootDir, "main.go");
|
|
159
|
+
if (await fileExists(mainGo)) entries.push(mainGo);
|
|
160
|
+
entries.push(...await findFiles("cmd/*/main.go", rootDir));
|
|
161
|
+
return { language: "go", detected: true, entries, hints };
|
|
162
|
+
}
|
|
163
|
+
async function resolveRust(rootDir) {
|
|
164
|
+
const hints = [
|
|
165
|
+
"Create src/main.rs or src/lib.rs",
|
|
166
|
+
"Declare [[bin]] entries with a `path` in Cargo.toml"
|
|
167
|
+
];
|
|
168
|
+
const cargoPath = join(rootDir, "Cargo.toml");
|
|
169
|
+
const detected = await fileExists(cargoPath);
|
|
170
|
+
if (!detected) return { language: "rust", detected: false, entries: [], hints };
|
|
171
|
+
const entries = [];
|
|
172
|
+
const content = await readFileContent(cargoPath);
|
|
173
|
+
if (content.ok) {
|
|
174
|
+
for (const bp of parseCargoBinPaths(content.value)) {
|
|
175
|
+
const abs = resolve(rootDir, bp);
|
|
176
|
+
if (await fileExists(abs)) entries.push(abs);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (entries.length === 0) {
|
|
180
|
+
for (const conv of ["src/main.rs", "src/lib.rs"]) {
|
|
181
|
+
const p = join(rootDir, conv);
|
|
182
|
+
if (await fileExists(p)) entries.push(p);
|
|
183
|
+
}
|
|
184
|
+
entries.push(...await findFiles("src/bin/*.rs", rootDir));
|
|
185
|
+
}
|
|
186
|
+
return { language: "rust", detected: true, entries, hints };
|
|
187
|
+
}
|
|
188
|
+
async function resolveJava(rootDir) {
|
|
189
|
+
const hints = [
|
|
190
|
+
"Place an entry class at src/main/java/**/Main.java (or *Application.java for Spring Boot)"
|
|
191
|
+
];
|
|
192
|
+
const detected = await fileExists(join(rootDir, "pom.xml")) || await fileExists(join(rootDir, "build.gradle")) || await fileExists(join(rootDir, "build.gradle.kts"));
|
|
193
|
+
if (!detected) return { language: "java", detected: false, entries: [], hints };
|
|
194
|
+
const entries = [];
|
|
195
|
+
entries.push(...await findFiles("src/main/java/**/Main.java", rootDir));
|
|
196
|
+
entries.push(...await findFiles("src/main/java/**/*Application.java", rootDir));
|
|
197
|
+
return { language: "java", detected: true, entries, hints };
|
|
198
|
+
}
|
|
199
|
+
function parseTomlLine(raw) {
|
|
200
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
201
|
+
if (!line) return {};
|
|
202
|
+
const sectionMatch = /^\[([^\]]+)\]$/.exec(line);
|
|
203
|
+
if (sectionMatch) return { section: sectionMatch[1] ?? "" };
|
|
204
|
+
const eq = line.indexOf("=");
|
|
205
|
+
if (eq <= 0) return {};
|
|
206
|
+
return {
|
|
207
|
+
key: line.slice(0, eq).trim(),
|
|
208
|
+
value: stripTomlString(line.slice(eq + 1).trim())
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function parsePyProject(content) {
|
|
212
|
+
const result = { scriptTargets: [] };
|
|
213
|
+
let section = null;
|
|
214
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
215
|
+
const parsed = parseTomlLine(raw);
|
|
216
|
+
if (parsed.section !== void 0) {
|
|
217
|
+
section = parsed.section;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (parsed.key === void 0 || parsed.value === void 0) continue;
|
|
221
|
+
if (section === "project" && parsed.key === "name") result.projectName = parsed.value;
|
|
222
|
+
else if (section === "project.scripts") result.scriptTargets.push(parsed.value);
|
|
223
|
+
}
|
|
224
|
+
return result;
|
|
225
|
+
}
|
|
226
|
+
function parseCargoBinPaths(content) {
|
|
227
|
+
const paths = [];
|
|
228
|
+
let inBin = false;
|
|
229
|
+
for (const raw of content.split(/\r?\n/)) {
|
|
230
|
+
const line = raw.replace(/(^|\s)#.*$/, "").trim();
|
|
231
|
+
if (!line) continue;
|
|
232
|
+
if (line === "[[bin]]") {
|
|
233
|
+
inBin = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (line.startsWith("[")) {
|
|
237
|
+
inBin = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!inBin) continue;
|
|
241
|
+
const eq = line.indexOf("=");
|
|
242
|
+
if (eq <= 0) continue;
|
|
243
|
+
const key = line.slice(0, eq).trim();
|
|
244
|
+
if (key === "path") paths.push(stripTomlString(line.slice(eq + 1).trim()));
|
|
245
|
+
}
|
|
246
|
+
return paths;
|
|
247
|
+
}
|
|
248
|
+
function stripTomlString(value) {
|
|
249
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
250
|
+
return value.slice(1, -1);
|
|
251
|
+
}
|
|
252
|
+
return value;
|
|
253
|
+
}
|
|
254
|
+
async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
255
|
+
if (explicitEntries && explicitEntries.length > 0) {
|
|
256
|
+
return Ok(explicitEntries.map((e) => resolve(rootDir, e)));
|
|
257
|
+
}
|
|
258
|
+
const resolvers = [resolveTypeScript, resolvePython, resolveGo, resolveRust, resolveJava];
|
|
259
|
+
const resolutions = [];
|
|
260
|
+
for (const resolver of resolvers) {
|
|
261
|
+
const res = await resolver(rootDir);
|
|
262
|
+
resolutions.push(res);
|
|
263
|
+
if (res.entries.length > 0) return Ok(res.entries);
|
|
264
|
+
}
|
|
265
|
+
const detectedLangs = resolutions.filter((r) => r.detected);
|
|
266
|
+
const suggestions = detectedLangs.length > 0 ? detectedLangs.flatMap((r) => r.hints) : resolutions.flatMap((r) => r.hints);
|
|
267
|
+
suggestions.push("Specify entryPoints in config");
|
|
268
|
+
const reason = detectedLangs.length > 0 ? `Detected ${detectedLangs.map((r) => r.language).join(", ")} project but found no entry points` : "No language manifest (package.json, pyproject.toml, go.mod, Cargo.toml, pom.xml) and no conventional entry files found";
|
|
269
|
+
return Err(
|
|
270
|
+
createEntropyError(
|
|
271
|
+
"ENTRY_POINT_NOT_FOUND",
|
|
272
|
+
"Could not resolve entry points",
|
|
273
|
+
{ reason },
|
|
274
|
+
suggestions
|
|
275
|
+
)
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// src/entropy/snapshot.ts
|
|
280
|
+
var DEFAULT_INCLUDE_PATTERNS = [
|
|
281
|
+
"**/*.ts",
|
|
282
|
+
"**/*.tsx",
|
|
283
|
+
"**/*.js",
|
|
284
|
+
"**/*.jsx",
|
|
285
|
+
"**/*.mjs",
|
|
286
|
+
"**/*.cjs",
|
|
287
|
+
"**/*.py",
|
|
288
|
+
"**/*.go",
|
|
289
|
+
"**/*.rs",
|
|
290
|
+
"**/*.java"
|
|
291
|
+
];
|
|
292
|
+
function extractCodeBlocks(content) {
|
|
293
|
+
const blocks = [];
|
|
294
|
+
const lines = content.split("\n");
|
|
295
|
+
for (let i = 0; i < lines.length; i++) {
|
|
296
|
+
const line = lines[i];
|
|
297
|
+
if (line !== void 0 && line.startsWith("```")) {
|
|
298
|
+
const langMatch = line.match(/```(\w*)/);
|
|
299
|
+
const language = langMatch?.[1] || "text";
|
|
300
|
+
let codeContent = "";
|
|
301
|
+
let j = i + 1;
|
|
302
|
+
let currentLine = lines[j];
|
|
303
|
+
while (j < lines.length && currentLine !== void 0 && !currentLine.startsWith("```")) {
|
|
304
|
+
codeContent += currentLine + "\n";
|
|
305
|
+
j++;
|
|
306
|
+
currentLine = lines[j];
|
|
307
|
+
}
|
|
308
|
+
blocks.push({
|
|
309
|
+
language,
|
|
310
|
+
content: codeContent.trim(),
|
|
311
|
+
line: i + 1
|
|
312
|
+
});
|
|
313
|
+
i = j;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return blocks;
|
|
317
|
+
}
|
|
318
|
+
function extractInlineRefs(content) {
|
|
319
|
+
const refs = [];
|
|
320
|
+
const lines = content.split("\n");
|
|
321
|
+
for (let i = 0; i < lines.length; i++) {
|
|
322
|
+
const line = lines[i];
|
|
323
|
+
if (line === void 0) continue;
|
|
324
|
+
const regex = /`([^`]+)`/g;
|
|
325
|
+
let match;
|
|
326
|
+
while ((match = regex.exec(line)) !== null) {
|
|
327
|
+
const reference = match[1];
|
|
328
|
+
if (reference === void 0) continue;
|
|
329
|
+
if (reference.match(/^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)*(\(.*\))?$/)) {
|
|
330
|
+
refs.push({
|
|
331
|
+
reference: reference.replace(/\(.*\)$/, ""),
|
|
332
|
+
// Remove function parens
|
|
333
|
+
line: i + 1,
|
|
334
|
+
column: match.index
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return refs;
|
|
340
|
+
}
|
|
341
|
+
async function parseDocumentationFile(path) {
|
|
342
|
+
const contentResult = await readFileContent(path);
|
|
343
|
+
if (!contentResult.ok) {
|
|
344
|
+
return Err(
|
|
345
|
+
createEntropyError(
|
|
346
|
+
"PARSE_ERROR",
|
|
347
|
+
`Failed to read documentation file: ${path}`,
|
|
348
|
+
{ file: path },
|
|
349
|
+
["Check that the file exists"]
|
|
350
|
+
)
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
const content = contentResult.value;
|
|
354
|
+
const type = path.endsWith(".md") ? "markdown" : "text";
|
|
355
|
+
return Ok({
|
|
356
|
+
path,
|
|
357
|
+
type,
|
|
358
|
+
content,
|
|
359
|
+
codeBlocks: extractCodeBlocks(content),
|
|
360
|
+
inlineRefs: extractInlineRefs(content)
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function makeInternalSymbol(name, type, line) {
|
|
364
|
+
return { name, type, line, references: 0, calledBy: [] };
|
|
365
|
+
}
|
|
366
|
+
function extractFunctionSymbol(node, line) {
|
|
367
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "function", line)];
|
|
368
|
+
return [];
|
|
369
|
+
}
|
|
370
|
+
function extractVariableSymbols(node, line) {
|
|
371
|
+
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
372
|
+
}
|
|
373
|
+
function extractClassSymbol(node, line) {
|
|
374
|
+
if (node.id?.name) return [makeInternalSymbol(node.id.name, "class", line)];
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
377
|
+
function extractSymbolsFromNode(node) {
|
|
378
|
+
const line = node.loc?.start?.line || 0;
|
|
379
|
+
if (node.type === "FunctionDeclaration") return extractFunctionSymbol(node, line);
|
|
380
|
+
if (node.type === "VariableDeclaration") return extractVariableSymbols(node, line);
|
|
381
|
+
if (node.type === "ClassDeclaration") return extractClassSymbol(node, line);
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
function extractInternalSymbols(ast) {
|
|
385
|
+
if (ast.language !== "typescript" && ast.language !== "javascript") return [];
|
|
386
|
+
const body = ast.body;
|
|
387
|
+
if (!body?.body) return [];
|
|
388
|
+
const nodes = body.body;
|
|
389
|
+
return nodes.flatMap(extractSymbolsFromNode);
|
|
390
|
+
}
|
|
391
|
+
function toJSDocComment(comment) {
|
|
392
|
+
if (comment.type !== "Block" || !comment.value?.startsWith("*")) return null;
|
|
393
|
+
return { content: comment.value, line: comment.loc?.start?.line || 0 };
|
|
394
|
+
}
|
|
395
|
+
function extractJSDocComments(ast) {
|
|
396
|
+
if (ast.language !== "typescript" && ast.language !== "javascript") return [];
|
|
397
|
+
const body = ast.body;
|
|
398
|
+
if (!body?.comments) return [];
|
|
399
|
+
return body.comments.flatMap((c) => {
|
|
400
|
+
const doc = toJSDocComment(c);
|
|
401
|
+
return doc ? [doc] : [];
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function buildExportMap(files) {
|
|
405
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
406
|
+
const byName = /* @__PURE__ */ new Map();
|
|
407
|
+
for (const file of files) {
|
|
408
|
+
byFile.set(file.path, file.exports);
|
|
409
|
+
for (const exp of file.exports) {
|
|
410
|
+
const existing = byName.get(exp.name) || [];
|
|
411
|
+
existing.push({ file: file.path, export: exp });
|
|
412
|
+
byName.set(exp.name, existing);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { byFile, byName };
|
|
416
|
+
}
|
|
417
|
+
var CODE_BLOCK_LANGUAGES = /* @__PURE__ */ new Set(["typescript", "ts", "javascript", "js"]);
|
|
418
|
+
function refsFromInlineRefs(doc) {
|
|
419
|
+
return doc.inlineRefs.map((inlineRef) => ({
|
|
420
|
+
docFile: doc.path,
|
|
421
|
+
line: inlineRef.line,
|
|
422
|
+
column: inlineRef.column,
|
|
423
|
+
reference: inlineRef.reference,
|
|
424
|
+
context: "inline"
|
|
425
|
+
}));
|
|
426
|
+
}
|
|
427
|
+
function refsFromCodeBlock(docPath, block) {
|
|
428
|
+
if (!CODE_BLOCK_LANGUAGES.has(block.language)) return [];
|
|
429
|
+
const refs = [];
|
|
430
|
+
const importRegex = /import\s+\{([^}]+)\}\s+from/g;
|
|
431
|
+
let match;
|
|
432
|
+
while ((match = importRegex.exec(block.content)) !== null) {
|
|
433
|
+
const group = match[1];
|
|
434
|
+
if (group === void 0) continue;
|
|
435
|
+
for (const name of group.split(",").map((n) => n.trim())) {
|
|
436
|
+
refs.push({
|
|
437
|
+
docFile: docPath,
|
|
438
|
+
line: block.line,
|
|
439
|
+
column: 0,
|
|
440
|
+
reference: name,
|
|
441
|
+
context: "code-block"
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return refs;
|
|
446
|
+
}
|
|
447
|
+
function refsFromCodeBlocks(doc) {
|
|
448
|
+
return doc.codeBlocks.flatMap((block) => refsFromCodeBlock(doc.path, block));
|
|
449
|
+
}
|
|
450
|
+
function extractAllCodeReferences(docs) {
|
|
451
|
+
return docs.flatMap((doc) => [...refsFromInlineRefs(doc), ...refsFromCodeBlocks(doc)]);
|
|
452
|
+
}
|
|
453
|
+
async function buildSnapshot(config) {
|
|
454
|
+
const startTime = Date.now();
|
|
455
|
+
const rootDir = resolve2(config.rootDir);
|
|
456
|
+
const entryPointsResult = await resolveEntryPoints(rootDir, config.entryPoints);
|
|
457
|
+
if (!entryPointsResult.ok) {
|
|
458
|
+
return Err(entryPointsResult.error);
|
|
459
|
+
}
|
|
460
|
+
const registry = getDefaultRegistry();
|
|
461
|
+
const singleParser = config.parser;
|
|
462
|
+
const parserForFile = (filePath) => singleParser ?? registry.getForFile(filePath);
|
|
463
|
+
const includePatterns = config.include || DEFAULT_INCLUDE_PATTERNS;
|
|
464
|
+
const excludePatterns = config.exclude || [...skipDirGlobs(), "**/*.test.ts", "**/*.spec.ts"];
|
|
465
|
+
let sourceFilePaths = [];
|
|
466
|
+
for (const pattern of includePatterns) {
|
|
467
|
+
const files2 = await findFiles(pattern, rootDir);
|
|
468
|
+
sourceFilePaths.push(...files2);
|
|
469
|
+
}
|
|
470
|
+
sourceFilePaths = sourceFilePaths.filter((f) => {
|
|
471
|
+
const rel = relativePosix(rootDir, f);
|
|
472
|
+
return !excludePatterns.some((p) => minimatch(rel, p));
|
|
473
|
+
});
|
|
474
|
+
const files = [];
|
|
475
|
+
for (const filePath of sourceFilePaths) {
|
|
476
|
+
const fileParser = parserForFile(filePath);
|
|
477
|
+
if (!fileParser) continue;
|
|
478
|
+
const parseResult = await fileParser.parseFile(filePath);
|
|
479
|
+
if (!parseResult.ok) continue;
|
|
480
|
+
const importsResult = fileParser.extractImports(parseResult.value);
|
|
481
|
+
const exportsResult = fileParser.extractExports(parseResult.value);
|
|
482
|
+
const internalSymbols = extractInternalSymbols(parseResult.value);
|
|
483
|
+
const jsDocComments = extractJSDocComments(parseResult.value);
|
|
484
|
+
files.push({
|
|
485
|
+
path: filePath,
|
|
486
|
+
ast: parseResult.value,
|
|
487
|
+
imports: importsResult.ok ? importsResult.value : [],
|
|
488
|
+
exports: exportsResult.ok ? exportsResult.value : [],
|
|
489
|
+
internalSymbols,
|
|
490
|
+
jsDocComments
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
const graphResult = await buildDependencyGraph(sourceFilePaths, singleParser ?? registry);
|
|
494
|
+
const dependencyGraph = graphResult.ok ? graphResult.value : { nodes: [], edges: [] };
|
|
495
|
+
const docPatterns = config.docPaths || ["docs/**/*.md", "README.md", "**/README.md"];
|
|
496
|
+
let docFilePaths = [];
|
|
497
|
+
for (const pattern of docPatterns) {
|
|
498
|
+
const docFiles = await findFiles(pattern, rootDir);
|
|
499
|
+
docFilePaths.push(...docFiles);
|
|
500
|
+
}
|
|
501
|
+
docFilePaths = [...new Set(docFilePaths)];
|
|
502
|
+
const docs = [];
|
|
503
|
+
for (const docPath of docFilePaths) {
|
|
504
|
+
const docResult = await parseDocumentationFile(docPath);
|
|
505
|
+
if (docResult.ok) {
|
|
506
|
+
docs.push(docResult.value);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const exportMap = buildExportMap(files);
|
|
510
|
+
const codeReferences = extractAllCodeReferences(docs);
|
|
511
|
+
const buildTime = Date.now() - startTime;
|
|
512
|
+
return Ok({
|
|
513
|
+
files,
|
|
514
|
+
dependencyGraph,
|
|
515
|
+
exportMap,
|
|
516
|
+
docs,
|
|
517
|
+
codeReferences,
|
|
518
|
+
entryPoints: entryPointsResult.value,
|
|
519
|
+
rootDir,
|
|
520
|
+
config,
|
|
521
|
+
buildTime
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/entropy/detectors/drift.ts
|
|
526
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
527
|
+
function initLevenshteinMatrix(aLen, bLen) {
|
|
528
|
+
const matrix = [];
|
|
529
|
+
for (let i = 0; i <= bLen; i++) {
|
|
530
|
+
matrix[i] = [i];
|
|
531
|
+
}
|
|
532
|
+
const firstRow = matrix[0];
|
|
533
|
+
if (firstRow) {
|
|
534
|
+
for (let j = 0; j <= aLen; j++) {
|
|
535
|
+
firstRow[j] = j;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
return matrix;
|
|
539
|
+
}
|
|
540
|
+
function computeLevenshteinCell(row, prevRow, j, charsMatch) {
|
|
541
|
+
if (charsMatch) {
|
|
542
|
+
row[j] = prevRow[j - 1] ?? 0;
|
|
543
|
+
} else {
|
|
544
|
+
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
function levenshteinDistance(a, b) {
|
|
548
|
+
const matrix = initLevenshteinMatrix(a.length, b.length);
|
|
549
|
+
for (let i = 1; i <= b.length; i++) {
|
|
550
|
+
for (let j = 1; j <= a.length; j++) {
|
|
551
|
+
const row = matrix[i];
|
|
552
|
+
const prevRow = matrix[i - 1];
|
|
553
|
+
if (!row || !prevRow) continue;
|
|
554
|
+
computeLevenshteinCell(row, prevRow, j, b.charAt(i - 1) === a.charAt(j - 1));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
const lastRow = matrix[b.length];
|
|
558
|
+
return lastRow?.[a.length] ?? 0;
|
|
559
|
+
}
|
|
560
|
+
function findPossibleMatches(reference, exportNames, maxDistance = 5) {
|
|
561
|
+
const matches = [];
|
|
562
|
+
const refLower = reference.toLowerCase();
|
|
563
|
+
for (const name of exportNames) {
|
|
564
|
+
const nameLower = name.toLowerCase();
|
|
565
|
+
if (nameLower === refLower) {
|
|
566
|
+
matches.push({ name, score: 0 });
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (nameLower.includes(refLower) || refLower.includes(nameLower)) {
|
|
570
|
+
matches.push({ name, score: 1 });
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
const distance = levenshteinDistance(refLower, nameLower);
|
|
574
|
+
if (distance <= maxDistance) {
|
|
575
|
+
matches.push({ name, score: distance });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return matches.sort((a, b) => a.score - b.score).slice(0, 3).map((m) => m.name);
|
|
579
|
+
}
|
|
580
|
+
var DEFAULT_DRIFT_CONFIG = {
|
|
581
|
+
docPaths: [],
|
|
582
|
+
checkApiSignatures: true,
|
|
583
|
+
checkExamples: true,
|
|
584
|
+
checkStructure: true,
|
|
585
|
+
ignorePatterns: []
|
|
586
|
+
};
|
|
587
|
+
function checkApiSignatureDrift(snapshot, config) {
|
|
588
|
+
const drifts = [];
|
|
589
|
+
const exportNames = Array.from(snapshot.exportMap.byName.keys());
|
|
590
|
+
for (const ref of snapshot.codeReferences) {
|
|
591
|
+
if (config.ignorePatterns.some((p) => ref.reference.match(new RegExp(p)))) {
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!snapshot.exportMap.byName.has(ref.reference)) {
|
|
595
|
+
const possibleMatches = findPossibleMatches(ref.reference, exportNames);
|
|
596
|
+
const confidence = possibleMatches.length > 0 ? "high" : "medium";
|
|
597
|
+
const drift = {
|
|
598
|
+
type: "api-signature",
|
|
599
|
+
docFile: ref.docFile,
|
|
600
|
+
line: ref.line,
|
|
601
|
+
reference: ref.reference,
|
|
602
|
+
context: ref.context,
|
|
603
|
+
issue: possibleMatches.length > 0 ? "RENAMED" : "NOT_FOUND",
|
|
604
|
+
details: possibleMatches.length > 0 ? `Symbol "${ref.reference}" not found. Similar: ${possibleMatches.join(", ")}` : `Symbol "${ref.reference}" not found in codebase`,
|
|
605
|
+
suggestion: possibleMatches.length > 0 ? `Did you mean "${possibleMatches[0]}"?` : "Remove reference or add the missing export",
|
|
606
|
+
confidence
|
|
607
|
+
};
|
|
608
|
+
if (possibleMatches.length > 0) {
|
|
609
|
+
drift.possibleMatches = possibleMatches;
|
|
610
|
+
}
|
|
611
|
+
drifts.push(drift);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return drifts;
|
|
615
|
+
}
|
|
616
|
+
function extractFileLinks(content) {
|
|
617
|
+
const links = [];
|
|
618
|
+
const lines = content.split("\n");
|
|
619
|
+
for (let i = 0; i < lines.length; i++) {
|
|
620
|
+
const line = lines[i];
|
|
621
|
+
if (!line) continue;
|
|
622
|
+
const linkRegex = /\[([^\]]*)\]\(([^)]+)\)/g;
|
|
623
|
+
let match;
|
|
624
|
+
while ((match = linkRegex.exec(line)) !== null) {
|
|
625
|
+
const linkPath = match[2];
|
|
626
|
+
if (linkPath && !linkPath.startsWith("http") && !linkPath.startsWith("#") && (linkPath.includes(".") || linkPath.startsWith(".."))) {
|
|
627
|
+
links.push({ link: linkPath, line: i + 1 });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return links;
|
|
632
|
+
}
|
|
633
|
+
async function checkStructureDrift(snapshot, _config) {
|
|
634
|
+
const drifts = [];
|
|
635
|
+
for (const doc of snapshot.docs) {
|
|
636
|
+
const fileLinks = extractFileLinks(doc.content);
|
|
637
|
+
for (const { link, line } of fileLinks) {
|
|
638
|
+
const resolvedPath = resolve3(dirname(doc.path), link);
|
|
639
|
+
const exists = await fileExists(resolvedPath);
|
|
640
|
+
if (!exists) {
|
|
641
|
+
drifts.push({
|
|
642
|
+
type: "structure",
|
|
643
|
+
docFile: doc.path,
|
|
644
|
+
line,
|
|
645
|
+
reference: link,
|
|
646
|
+
context: "link",
|
|
647
|
+
issue: "NOT_FOUND",
|
|
648
|
+
details: `File "${link}" referenced in documentation does not exist`,
|
|
649
|
+
suggestion: "Update the link or remove the reference",
|
|
650
|
+
confidence: "high"
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return drifts;
|
|
656
|
+
}
|
|
657
|
+
function computeDriftSeverity(driftCount) {
|
|
658
|
+
if (driftCount === 0) return "none";
|
|
659
|
+
if (driftCount <= 3) return "low";
|
|
660
|
+
if (driftCount <= 10) return "medium";
|
|
661
|
+
return "high";
|
|
662
|
+
}
|
|
663
|
+
function buildGraphDriftReport(graphDriftData) {
|
|
664
|
+
const drifts = [];
|
|
665
|
+
for (const target of graphDriftData.missingTargets) {
|
|
666
|
+
drifts.push({
|
|
667
|
+
type: "api-signature",
|
|
668
|
+
docFile: target,
|
|
669
|
+
line: 0,
|
|
670
|
+
reference: target,
|
|
671
|
+
context: "graph-missing-target",
|
|
672
|
+
issue: "NOT_FOUND",
|
|
673
|
+
details: `Graph node "${target}" has no matching code target`,
|
|
674
|
+
confidence: "high"
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
for (const edge of graphDriftData.staleEdges) {
|
|
678
|
+
drifts.push({
|
|
679
|
+
type: "api-signature",
|
|
680
|
+
docFile: edge.docNodeId,
|
|
681
|
+
line: 0,
|
|
682
|
+
reference: edge.codeNodeId,
|
|
683
|
+
context: `graph-stale-edge:${edge.edgeType}`,
|
|
684
|
+
issue: "NOT_FOUND",
|
|
685
|
+
details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
|
|
686
|
+
confidence: "medium"
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
return Ok({
|
|
690
|
+
drifts,
|
|
691
|
+
stats: {
|
|
692
|
+
docsScanned: graphDriftData.staleEdges.length,
|
|
693
|
+
referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
|
|
694
|
+
driftsFound: drifts.length,
|
|
695
|
+
byType: { api: drifts.length, example: 0, structure: 0 }
|
|
696
|
+
},
|
|
697
|
+
severity: computeDriftSeverity(drifts.length)
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async function detectDocDrift(snapshot, config, graphDriftData) {
|
|
701
|
+
if (graphDriftData) {
|
|
702
|
+
return buildGraphDriftReport(graphDriftData);
|
|
703
|
+
}
|
|
704
|
+
const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
|
|
705
|
+
const drifts = [];
|
|
706
|
+
if (fullConfig.checkApiSignatures) {
|
|
707
|
+
drifts.push(...checkApiSignatureDrift(snapshot, fullConfig));
|
|
708
|
+
}
|
|
709
|
+
if (fullConfig.checkStructure) {
|
|
710
|
+
drifts.push(...await checkStructureDrift(snapshot, fullConfig));
|
|
711
|
+
}
|
|
712
|
+
const apiDrifts = drifts.filter((d) => d.type === "api-signature").length;
|
|
713
|
+
const exampleDrifts = drifts.filter((d) => d.type === "example-code").length;
|
|
714
|
+
const structureDrifts = drifts.filter((d) => d.type === "structure").length;
|
|
715
|
+
const severity = drifts.length === 0 ? "none" : drifts.length <= 3 ? "low" : drifts.length <= 10 ? "medium" : "high";
|
|
716
|
+
return Ok({
|
|
717
|
+
drifts,
|
|
718
|
+
stats: {
|
|
719
|
+
docsScanned: snapshot.docs.length,
|
|
720
|
+
referencesChecked: snapshot.codeReferences.length,
|
|
721
|
+
driftsFound: drifts.length,
|
|
722
|
+
byType: { api: apiDrifts, example: exampleDrifts, structure: structureDrifts }
|
|
723
|
+
},
|
|
724
|
+
severity
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/entropy/detectors/dead-code.ts
|
|
729
|
+
import { dirname as dirname2, extname, resolve as resolve4 } from "path";
|
|
730
|
+
var JS_EXT_FALLBACKS = {
|
|
731
|
+
".js": [".ts", ".tsx", ".jsx"],
|
|
732
|
+
".jsx": [".tsx"],
|
|
733
|
+
".mjs": [".mts"],
|
|
734
|
+
".cjs": [".cts"]
|
|
735
|
+
};
|
|
736
|
+
function buildFileIndex(snapshot) {
|
|
737
|
+
const index = /* @__PURE__ */ new Map();
|
|
738
|
+
for (const file of snapshot.files) {
|
|
739
|
+
index.set(file.path, file);
|
|
740
|
+
}
|
|
741
|
+
return index;
|
|
742
|
+
}
|
|
743
|
+
function resolveImportToFile(importSource, fromFile, snapshot, fileIndex) {
|
|
744
|
+
if (!importSource.startsWith(".")) {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
const hasFile = fileIndex ? (p) => fileIndex.has(p) : (p) => snapshot.files.some((f) => f.path === p);
|
|
748
|
+
const fromDir = dirname2(fromFile);
|
|
749
|
+
const resolved = resolve4(fromDir, importSource);
|
|
750
|
+
const sourceExt = extname(resolved);
|
|
751
|
+
const fallbacks = JS_EXT_FALLBACKS[sourceExt];
|
|
752
|
+
if (fallbacks) {
|
|
753
|
+
const base = resolved.slice(0, -sourceExt.length);
|
|
754
|
+
for (const ext of fallbacks) {
|
|
755
|
+
const candidate = base + ext;
|
|
756
|
+
if (hasFile(candidate)) return candidate;
|
|
757
|
+
}
|
|
758
|
+
for (const indexExt of [".ts", ".tsx", ".jsx"]) {
|
|
759
|
+
const indexPath = resolve4(base, "index" + indexExt);
|
|
760
|
+
if (hasFile(indexPath)) return indexPath;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (hasFile(resolved)) return resolved;
|
|
764
|
+
if (!sourceExt) {
|
|
765
|
+
for (const ext of [".ts", ".tsx"]) {
|
|
766
|
+
const candidate = resolved + ext;
|
|
767
|
+
if (hasFile(candidate)) return candidate;
|
|
768
|
+
}
|
|
769
|
+
for (const indexExt of [".ts", ".tsx"]) {
|
|
770
|
+
const indexPath = resolve4(resolved, "index" + indexExt);
|
|
771
|
+
if (hasFile(indexPath)) return indexPath;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
function enqueueResolved(sources, current, snapshot, visited, queue, fileIndex) {
|
|
777
|
+
for (const item of sources) {
|
|
778
|
+
if (!item.source) continue;
|
|
779
|
+
const resolved = resolveImportToFile(item.source, current, snapshot, fileIndex);
|
|
780
|
+
if (resolved && !visited.has(resolved)) {
|
|
781
|
+
queue.push(resolved);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function processReachabilityNode(current, snapshot, reachability, visited, queue, fileIndex) {
|
|
786
|
+
reachability.set(current, true);
|
|
787
|
+
const sourceFile = fileIndex ? fileIndex.get(current) : snapshot.files.find((f) => f.path === current);
|
|
788
|
+
if (!sourceFile) return;
|
|
789
|
+
enqueueResolved(sourceFile.imports, current, snapshot, visited, queue, fileIndex);
|
|
790
|
+
const reExports = sourceFile.exports.filter((e) => e.isReExport);
|
|
791
|
+
enqueueResolved(reExports, current, snapshot, visited, queue, fileIndex);
|
|
792
|
+
}
|
|
793
|
+
function buildReachabilityMap(snapshot) {
|
|
794
|
+
const fileIndex = buildFileIndex(snapshot);
|
|
795
|
+
const reachability = /* @__PURE__ */ new Map();
|
|
796
|
+
for (const file of snapshot.files) {
|
|
797
|
+
reachability.set(file.path, false);
|
|
798
|
+
}
|
|
799
|
+
const queue = [...snapshot.entryPoints];
|
|
800
|
+
const visited = /* @__PURE__ */ new Set();
|
|
801
|
+
while (queue.length > 0) {
|
|
802
|
+
const current = queue.shift();
|
|
803
|
+
if (visited.has(current)) continue;
|
|
804
|
+
visited.add(current);
|
|
805
|
+
processReachabilityNode(current, snapshot, reachability, visited, queue, fileIndex);
|
|
806
|
+
}
|
|
807
|
+
return reachability;
|
|
808
|
+
}
|
|
809
|
+
function buildExportUsageMap(snapshot) {
|
|
810
|
+
const fileIndex = buildFileIndex(snapshot);
|
|
811
|
+
const usageMap = /* @__PURE__ */ new Map();
|
|
812
|
+
for (const file of snapshot.files) {
|
|
813
|
+
for (const exp of file.exports) {
|
|
814
|
+
const key = `${file.path}:${exp.name}`;
|
|
815
|
+
usageMap.set(key, { importers: [], isReExported: exp.isReExport });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
for (const file of snapshot.files) {
|
|
819
|
+
for (const imp of file.imports) {
|
|
820
|
+
const resolvedFile = resolveImportToFile(imp.source, file.path, snapshot, fileIndex);
|
|
821
|
+
if (!resolvedFile) continue;
|
|
822
|
+
const sourceFile = fileIndex.get(resolvedFile);
|
|
823
|
+
if (!sourceFile) continue;
|
|
824
|
+
for (const specifier of imp.specifiers) {
|
|
825
|
+
const matchingExport = sourceFile.exports.find(
|
|
826
|
+
(e) => e.name === specifier || specifier === "default" && e.type === "default"
|
|
827
|
+
);
|
|
828
|
+
if (matchingExport) {
|
|
829
|
+
const key = `${resolvedFile}:${matchingExport.name}`;
|
|
830
|
+
const usage = usageMap.get(key);
|
|
831
|
+
if (usage) {
|
|
832
|
+
usage.importers.push(file.path);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return usageMap;
|
|
839
|
+
}
|
|
840
|
+
function findDeadExports(snapshot, usageMap, reachability) {
|
|
841
|
+
const deadExports = [];
|
|
842
|
+
for (const file of snapshot.files) {
|
|
843
|
+
if (snapshot.entryPoints.includes(file.path)) continue;
|
|
844
|
+
for (const exp of file.exports) {
|
|
845
|
+
if (exp.isReExport) continue;
|
|
846
|
+
const key = `${file.path}:${exp.name}`;
|
|
847
|
+
const usage = usageMap.get(key);
|
|
848
|
+
if (!usage || usage.importers.length === 0) {
|
|
849
|
+
deadExports.push({
|
|
850
|
+
file: file.path,
|
|
851
|
+
name: exp.name,
|
|
852
|
+
line: exp.location.line,
|
|
853
|
+
type: "variable",
|
|
854
|
+
// Default type since Export doesn't track declaration kind
|
|
855
|
+
isDefault: exp.type === "default",
|
|
856
|
+
reason: "NO_IMPORTERS"
|
|
857
|
+
});
|
|
858
|
+
} else {
|
|
859
|
+
const allImportersDead = usage.importers.every((importer) => !reachability.get(importer));
|
|
860
|
+
if (allImportersDead) {
|
|
861
|
+
deadExports.push({
|
|
862
|
+
file: file.path,
|
|
863
|
+
name: exp.name,
|
|
864
|
+
line: exp.location.line,
|
|
865
|
+
type: "variable",
|
|
866
|
+
// Default type since Export doesn't track declaration kind
|
|
867
|
+
isDefault: exp.type === "default",
|
|
868
|
+
reason: "IMPORTERS_ALSO_DEAD"
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return deadExports;
|
|
875
|
+
}
|
|
876
|
+
function maxLineOfValue(value) {
|
|
877
|
+
if (Array.isArray(value)) {
|
|
878
|
+
return value.reduce((m, item) => Math.max(m, findMaxLineInNode(item)), 0);
|
|
879
|
+
}
|
|
880
|
+
if (value && typeof value === "object") {
|
|
881
|
+
return findMaxLineInNode(value);
|
|
882
|
+
}
|
|
883
|
+
return 0;
|
|
884
|
+
}
|
|
885
|
+
function maxLineOfNodeKeys(node) {
|
|
886
|
+
let max = 0;
|
|
887
|
+
for (const key of Object.keys(node)) {
|
|
888
|
+
max = Math.max(max, maxLineOfValue(node[key]));
|
|
889
|
+
}
|
|
890
|
+
return max;
|
|
891
|
+
}
|
|
892
|
+
function findMaxLineInNode(node) {
|
|
893
|
+
if (!node || typeof node !== "object") return 0;
|
|
894
|
+
const n = node;
|
|
895
|
+
const locLine = n.loc?.end?.line ?? 0;
|
|
896
|
+
return Math.max(locLine, maxLineOfNodeKeys(node));
|
|
897
|
+
}
|
|
898
|
+
function countLinesFromAST(ast) {
|
|
899
|
+
if (!ast.body || !Array.isArray(ast.body)) return 1;
|
|
900
|
+
const maxLine = findMaxLineInNode(ast);
|
|
901
|
+
if (maxLine > 0) return maxLine;
|
|
902
|
+
return Math.max(ast.body.length * 3, 1);
|
|
903
|
+
}
|
|
904
|
+
function findDeadFiles(snapshot, reachability) {
|
|
905
|
+
const deadFiles = [];
|
|
906
|
+
for (const file of snapshot.files) {
|
|
907
|
+
const isReachable = reachability.get(file.path) ?? false;
|
|
908
|
+
if (!isReachable) {
|
|
909
|
+
deadFiles.push({
|
|
910
|
+
path: file.path,
|
|
911
|
+
reason: "NO_IMPORTERS",
|
|
912
|
+
exportCount: file.exports.filter((e) => !e.isReExport).length,
|
|
913
|
+
lineCount: countLinesFromAST(file.ast)
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return deadFiles;
|
|
918
|
+
}
|
|
919
|
+
function isIdentifierUsedInAST(ast, identifier, skipImportDeclaration = true) {
|
|
920
|
+
const astString = JSON.stringify(
|
|
921
|
+
ast,
|
|
922
|
+
(_key, value) => typeof value === "bigint" ? value.toString() : value
|
|
923
|
+
);
|
|
924
|
+
const identifierPattern = new RegExp(`"name"\\s*:\\s*"${identifier}"`, "g");
|
|
925
|
+
const matches = astString.match(identifierPattern);
|
|
926
|
+
if (!matches) return false;
|
|
927
|
+
if (skipImportDeclaration) {
|
|
928
|
+
return matches.length > 2;
|
|
929
|
+
}
|
|
930
|
+
return matches.length > 0;
|
|
931
|
+
}
|
|
932
|
+
function findUnusedImports(snapshot) {
|
|
933
|
+
const unusedImports = [];
|
|
934
|
+
for (const file of snapshot.files) {
|
|
935
|
+
for (const imp of file.imports) {
|
|
936
|
+
const unusedSpecifiers = [];
|
|
937
|
+
for (const specifier of imp.specifiers) {
|
|
938
|
+
if (!isIdentifierUsedInAST(file.ast, specifier, true)) {
|
|
939
|
+
unusedSpecifiers.push(specifier);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (unusedSpecifiers.length > 0) {
|
|
943
|
+
unusedImports.push({
|
|
944
|
+
file: file.path,
|
|
945
|
+
line: imp.location.line,
|
|
946
|
+
source: imp.source,
|
|
947
|
+
specifiers: unusedSpecifiers,
|
|
948
|
+
isFullyUnused: unusedSpecifiers.length === imp.specifiers.length
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return unusedImports;
|
|
954
|
+
}
|
|
955
|
+
function findDeadInternals(snapshot, _reachability) {
|
|
956
|
+
const deadInternals = [];
|
|
957
|
+
for (const file of snapshot.files) {
|
|
958
|
+
for (const symbol of file.internalSymbols) {
|
|
959
|
+
if (symbol.type === "type") continue;
|
|
960
|
+
if (symbol.references === 0 && symbol.calledBy.length === 0) {
|
|
961
|
+
deadInternals.push({
|
|
962
|
+
file: file.path,
|
|
963
|
+
name: symbol.name,
|
|
964
|
+
line: symbol.line,
|
|
965
|
+
type: symbol.type,
|
|
966
|
+
reason: "NEVER_CALLED"
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
return deadInternals;
|
|
972
|
+
}
|
|
973
|
+
var FILE_TYPES = /* @__PURE__ */ new Set(["file", "module"]);
|
|
974
|
+
var EXPORT_TYPES = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
|
|
975
|
+
function classifyUnreachableNode(node, deadFiles, deadExports) {
|
|
976
|
+
if (FILE_TYPES.has(node.type)) {
|
|
977
|
+
deadFiles.push({
|
|
978
|
+
path: node.path || node.id,
|
|
979
|
+
reason: "NO_IMPORTERS",
|
|
980
|
+
exportCount: 0,
|
|
981
|
+
lineCount: 0
|
|
982
|
+
});
|
|
983
|
+
} else if (EXPORT_TYPES.has(node.type)) {
|
|
984
|
+
const exportType = node.type === "method" ? "function" : node.type;
|
|
985
|
+
deadExports.push({
|
|
986
|
+
file: node.path || node.id,
|
|
987
|
+
name: node.name,
|
|
988
|
+
line: 0,
|
|
989
|
+
type: exportType,
|
|
990
|
+
isDefault: false,
|
|
991
|
+
reason: "NO_IMPORTERS"
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function computeGraphReportStats(data, deadFiles, deadExports) {
|
|
996
|
+
const reachableCount = data.reachableNodeIds instanceof Set ? data.reachableNodeIds.size : data.reachableNodeIds.length;
|
|
997
|
+
const fileNodes = data.unreachableNodes.filter((n) => FILE_TYPES.has(n.type));
|
|
998
|
+
const exportNodes = data.unreachableNodes.filter((n) => EXPORT_TYPES.has(n.type));
|
|
999
|
+
const totalFiles = reachableCount + fileNodes.length;
|
|
1000
|
+
const totalExports = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
|
|
1001
|
+
return {
|
|
1002
|
+
filesAnalyzed: totalFiles,
|
|
1003
|
+
entryPointsUsed: [],
|
|
1004
|
+
totalExports,
|
|
1005
|
+
deadExportCount: deadExports.length,
|
|
1006
|
+
totalFiles,
|
|
1007
|
+
deadFileCount: deadFiles.length,
|
|
1008
|
+
estimatedDeadLines: 0
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
function buildReportFromGraph(data) {
|
|
1012
|
+
const deadFiles = [];
|
|
1013
|
+
const deadExports = [];
|
|
1014
|
+
for (const node of data.unreachableNodes) {
|
|
1015
|
+
classifyUnreachableNode(node, deadFiles, deadExports);
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
deadExports,
|
|
1019
|
+
deadFiles,
|
|
1020
|
+
deadInternals: [],
|
|
1021
|
+
unusedImports: [],
|
|
1022
|
+
stats: computeGraphReportStats(data, deadFiles, deadExports)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
function buildReportFromSnapshot(snapshot) {
|
|
1026
|
+
const reachability = buildReachabilityMap(snapshot);
|
|
1027
|
+
const usageMap = buildExportUsageMap(snapshot);
|
|
1028
|
+
const deadExports = findDeadExports(snapshot, usageMap, reachability);
|
|
1029
|
+
const deadFiles = findDeadFiles(snapshot, reachability);
|
|
1030
|
+
const unusedImports = findUnusedImports(snapshot);
|
|
1031
|
+
const deadInternals = findDeadInternals(snapshot, reachability);
|
|
1032
|
+
const totalExports = snapshot.files.reduce(
|
|
1033
|
+
(acc, file) => acc + file.exports.filter((e) => !e.isReExport).length,
|
|
1034
|
+
0
|
|
1035
|
+
);
|
|
1036
|
+
const estimatedDeadLines = deadFiles.reduce((acc, file) => acc + file.lineCount, 0);
|
|
1037
|
+
return {
|
|
1038
|
+
deadExports,
|
|
1039
|
+
deadFiles,
|
|
1040
|
+
deadInternals,
|
|
1041
|
+
unusedImports,
|
|
1042
|
+
stats: {
|
|
1043
|
+
filesAnalyzed: snapshot.files.length,
|
|
1044
|
+
entryPointsUsed: snapshot.entryPoints,
|
|
1045
|
+
totalExports,
|
|
1046
|
+
deadExportCount: deadExports.length,
|
|
1047
|
+
totalFiles: snapshot.files.length,
|
|
1048
|
+
deadFileCount: deadFiles.length,
|
|
1049
|
+
estimatedDeadLines
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
function filterProtectedFindings(report, regions) {
|
|
1054
|
+
const deadExports = report.deadExports.filter(
|
|
1055
|
+
(e) => !regions.isProtected(e.file, e.line, "entropy")
|
|
1056
|
+
);
|
|
1057
|
+
const deadFiles = report.deadFiles.filter((f) => regions.getRegions(f.path).length === 0);
|
|
1058
|
+
const unusedImports = report.unusedImports.filter(
|
|
1059
|
+
(i) => !regions.isProtected(i.file, i.line, "entropy")
|
|
1060
|
+
);
|
|
1061
|
+
const deadInternals = report.deadInternals.filter(
|
|
1062
|
+
(i) => !regions.isProtected(i.file, i.line, "entropy")
|
|
1063
|
+
);
|
|
1064
|
+
const estimatedDeadLines = deadFiles.reduce((acc, f) => acc + f.lineCount, 0);
|
|
1065
|
+
return {
|
|
1066
|
+
deadExports,
|
|
1067
|
+
deadFiles,
|
|
1068
|
+
unusedImports,
|
|
1069
|
+
deadInternals,
|
|
1070
|
+
stats: {
|
|
1071
|
+
...report.stats,
|
|
1072
|
+
deadExportCount: deadExports.length,
|
|
1073
|
+
deadFileCount: deadFiles.length,
|
|
1074
|
+
estimatedDeadLines
|
|
1075
|
+
}
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
async function detectDeadCode(snapshot, graphDeadCodeData, protectedRegions) {
|
|
1079
|
+
let report = graphDeadCodeData ? buildReportFromGraph(graphDeadCodeData) : buildReportFromSnapshot(snapshot);
|
|
1080
|
+
if (protectedRegions) {
|
|
1081
|
+
report = filterProtectedFindings(report, protectedRegions);
|
|
1082
|
+
}
|
|
1083
|
+
return Ok(report);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/entropy/detectors/patterns.ts
|
|
1087
|
+
import { minimatch as minimatch2 } from "minimatch";
|
|
1088
|
+
function fileMatchesPattern(filePath, pattern, rootDir) {
|
|
1089
|
+
const relativePath = relativePosix(rootDir, filePath);
|
|
1090
|
+
return minimatch2(relativePath, pattern);
|
|
1091
|
+
}
|
|
1092
|
+
var CONVENTION_DESCRIPTIONS = {
|
|
1093
|
+
camelCase: "camelCase (e.g., myFunction)",
|
|
1094
|
+
PascalCase: "PascalCase (e.g., MyClass)",
|
|
1095
|
+
UPPER_SNAKE: "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)",
|
|
1096
|
+
"kebab-case": "kebab-case (e.g., my-component)"
|
|
1097
|
+
};
|
|
1098
|
+
function checkMustExport(rule, file, message) {
|
|
1099
|
+
if (rule.type !== "must-export") return [];
|
|
1100
|
+
const matches = [];
|
|
1101
|
+
for (const name of rule.names) {
|
|
1102
|
+
if (!file.exports.some((e) => e.name === name)) {
|
|
1103
|
+
matches.push({
|
|
1104
|
+
line: 1,
|
|
1105
|
+
message: message || `Missing required export: "${name}"`,
|
|
1106
|
+
suggestion: `Add export for "${name}"`
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
return matches;
|
|
1111
|
+
}
|
|
1112
|
+
function checkMustExportDefault(_rule, file, message) {
|
|
1113
|
+
if (!file.exports.some((e) => e.type === "default")) {
|
|
1114
|
+
return [
|
|
1115
|
+
{
|
|
1116
|
+
line: 1,
|
|
1117
|
+
message: message || "File must have a default export",
|
|
1118
|
+
suggestion: "Add a default export"
|
|
1119
|
+
}
|
|
1120
|
+
];
|
|
1121
|
+
}
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
function checkNoExport(rule, file, message) {
|
|
1125
|
+
if (rule.type !== "no-export") return [];
|
|
1126
|
+
const matches = [];
|
|
1127
|
+
for (const name of rule.names) {
|
|
1128
|
+
const exp = file.exports.find((e) => e.name === name);
|
|
1129
|
+
if (exp) {
|
|
1130
|
+
matches.push({
|
|
1131
|
+
line: exp.location.line,
|
|
1132
|
+
message: message || `Forbidden export: "${name}"`,
|
|
1133
|
+
suggestion: `Remove export "${name}"`
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
return matches;
|
|
1138
|
+
}
|
|
1139
|
+
function checkMustImport(rule, file, message) {
|
|
1140
|
+
if (rule.type !== "must-import") return [];
|
|
1141
|
+
const hasImport = file.imports.some(
|
|
1142
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
1143
|
+
);
|
|
1144
|
+
if (!hasImport) {
|
|
1145
|
+
return [
|
|
1146
|
+
{
|
|
1147
|
+
line: 1,
|
|
1148
|
+
message: message || `Missing required import from "${rule.from}"`,
|
|
1149
|
+
suggestion: `Add import from "${rule.from}"`
|
|
1150
|
+
}
|
|
1151
|
+
];
|
|
1152
|
+
}
|
|
1153
|
+
return [];
|
|
1154
|
+
}
|
|
1155
|
+
function checkNoImport(rule, file, message) {
|
|
1156
|
+
if (rule.type !== "no-import") return [];
|
|
1157
|
+
const forbiddenImport = file.imports.find(
|
|
1158
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
1159
|
+
);
|
|
1160
|
+
if (forbiddenImport) {
|
|
1161
|
+
return [
|
|
1162
|
+
{
|
|
1163
|
+
line: forbiddenImport.location.line,
|
|
1164
|
+
message: message || `Forbidden import from "${rule.from}"`,
|
|
1165
|
+
suggestion: `Remove import from "${rule.from}"`
|
|
1166
|
+
}
|
|
1167
|
+
];
|
|
1168
|
+
}
|
|
1169
|
+
return [];
|
|
1170
|
+
}
|
|
1171
|
+
function checkNaming(rule, file, message) {
|
|
1172
|
+
if (rule.type !== "naming") return [];
|
|
1173
|
+
const regex = new RegExp(rule.match);
|
|
1174
|
+
const matches = [];
|
|
1175
|
+
for (const exp of file.exports) {
|
|
1176
|
+
if (!regex.test(exp.name)) {
|
|
1177
|
+
const expected = CONVENTION_DESCRIPTIONS[rule.convention] ?? rule.convention;
|
|
1178
|
+
matches.push({
|
|
1179
|
+
line: exp.location.line,
|
|
1180
|
+
message: message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
1181
|
+
suggestion: `Rename to follow ${expected}`
|
|
1182
|
+
});
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
return matches;
|
|
1186
|
+
}
|
|
1187
|
+
function checkMaxExports(rule, file, message) {
|
|
1188
|
+
if (rule.type !== "max-exports") return [];
|
|
1189
|
+
if (file.exports.length > rule.count) {
|
|
1190
|
+
return [
|
|
1191
|
+
{
|
|
1192
|
+
line: 1,
|
|
1193
|
+
message: message || `File has ${file.exports.length} exports, max is ${rule.count}`,
|
|
1194
|
+
suggestion: `Split into multiple files or reduce exports to ${rule.count}`
|
|
1195
|
+
}
|
|
1196
|
+
];
|
|
1197
|
+
}
|
|
1198
|
+
return [];
|
|
1199
|
+
}
|
|
1200
|
+
function checkMaxLines(_rule, _file, _message) {
|
|
1201
|
+
return [];
|
|
1202
|
+
}
|
|
1203
|
+
function checkRequireJsdoc(_rule, file, message) {
|
|
1204
|
+
if (file.jsDocComments.length === 0 && file.exports.length > 0) {
|
|
1205
|
+
return [
|
|
1206
|
+
{
|
|
1207
|
+
line: 1,
|
|
1208
|
+
message: message || "Exported symbols require JSDoc documentation",
|
|
1209
|
+
suggestion: "Add JSDoc comments to exports"
|
|
1210
|
+
}
|
|
1211
|
+
];
|
|
1212
|
+
}
|
|
1213
|
+
return [];
|
|
1214
|
+
}
|
|
1215
|
+
var RULE_CHECKERS = {
|
|
1216
|
+
"must-export": checkMustExport,
|
|
1217
|
+
"must-export-default": checkMustExportDefault,
|
|
1218
|
+
"no-export": checkNoExport,
|
|
1219
|
+
"must-import": checkMustImport,
|
|
1220
|
+
"no-import": checkNoImport,
|
|
1221
|
+
naming: checkNaming,
|
|
1222
|
+
"max-exports": checkMaxExports,
|
|
1223
|
+
"max-lines": checkMaxLines,
|
|
1224
|
+
"require-jsdoc": checkRequireJsdoc
|
|
1225
|
+
};
|
|
1226
|
+
function checkConfigPattern(pattern, file, rootDir) {
|
|
1227
|
+
const fileMatches = pattern.files.some((glob) => fileMatchesPattern(file.path, glob, rootDir));
|
|
1228
|
+
if (!fileMatches) return [];
|
|
1229
|
+
const checker = RULE_CHECKERS[pattern.rule.type];
|
|
1230
|
+
if (!checker) return [];
|
|
1231
|
+
return checker(pattern.rule, file, pattern.message);
|
|
1232
|
+
}
|
|
1233
|
+
async function detectPatternViolations(snapshot, config) {
|
|
1234
|
+
const violations = [];
|
|
1235
|
+
const patterns = config?.patterns || [];
|
|
1236
|
+
for (const file of snapshot.files) {
|
|
1237
|
+
for (const pattern of patterns) {
|
|
1238
|
+
const matches = checkConfigPattern(pattern, file, snapshot.rootDir);
|
|
1239
|
+
for (const match of matches) {
|
|
1240
|
+
violations.push({
|
|
1241
|
+
pattern: pattern.name,
|
|
1242
|
+
file: file.path,
|
|
1243
|
+
line: match.line,
|
|
1244
|
+
message: match.message,
|
|
1245
|
+
suggestion: match.suggestion || "Review and fix this pattern violation",
|
|
1246
|
+
severity: pattern.severity
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (config?.customPatterns) {
|
|
1252
|
+
for (const file of snapshot.files) {
|
|
1253
|
+
for (const custom of config.customPatterns) {
|
|
1254
|
+
const matches = custom.check(file, snapshot);
|
|
1255
|
+
for (const match of matches) {
|
|
1256
|
+
violations.push({
|
|
1257
|
+
pattern: custom.name,
|
|
1258
|
+
file: file.path,
|
|
1259
|
+
line: match.line,
|
|
1260
|
+
message: match.message,
|
|
1261
|
+
suggestion: match.suggestion || "Review and fix this pattern violation",
|
|
1262
|
+
severity: custom.severity
|
|
1263
|
+
});
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
1269
|
+
const warningCount = violations.filter((v) => v.severity === "warning").length;
|
|
1270
|
+
const customCount = config?.customPatterns?.length ?? 0;
|
|
1271
|
+
const allPatternsCount = patterns.length + customCount;
|
|
1272
|
+
const totalChecks = snapshot.files.length * allPatternsCount;
|
|
1273
|
+
const passRate = totalChecks > 0 ? Math.max(0, (totalChecks - violations.length) / totalChecks) : 1;
|
|
1274
|
+
return Ok({
|
|
1275
|
+
violations,
|
|
1276
|
+
stats: {
|
|
1277
|
+
filesChecked: snapshot.files.length,
|
|
1278
|
+
patternsApplied: allPatternsCount,
|
|
1279
|
+
violationCount: violations.length,
|
|
1280
|
+
errorCount,
|
|
1281
|
+
warningCount
|
|
1282
|
+
},
|
|
1283
|
+
passRate
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/entropy/detectors/size-budget.ts
|
|
1288
|
+
import { readdirSync, statSync } from "fs";
|
|
1289
|
+
import { join as join2 } from "path";
|
|
1290
|
+
import { DEFAULT_SKIP_DIRS } from "@harness-engineering/graph";
|
|
1291
|
+
function parseSize(size) {
|
|
1292
|
+
const match = size.trim().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB|B)?$/i);
|
|
1293
|
+
if (!match) return 0;
|
|
1294
|
+
const value = parseFloat(match[1]);
|
|
1295
|
+
const unit = (match[2] || "B").toUpperCase();
|
|
1296
|
+
switch (unit) {
|
|
1297
|
+
case "KB":
|
|
1298
|
+
return Math.round(value * 1024);
|
|
1299
|
+
case "MB":
|
|
1300
|
+
return Math.round(value * 1024 * 1024);
|
|
1301
|
+
case "GB":
|
|
1302
|
+
return Math.round(value * 1024 * 1024 * 1024);
|
|
1303
|
+
default:
|
|
1304
|
+
return Math.round(value);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
function dirSize(dirPath) {
|
|
1308
|
+
let total = 0;
|
|
1309
|
+
let entries;
|
|
1310
|
+
try {
|
|
1311
|
+
entries = readdirSync(dirPath);
|
|
1312
|
+
} catch {
|
|
1313
|
+
return 0;
|
|
1314
|
+
}
|
|
1315
|
+
for (const entry of entries) {
|
|
1316
|
+
if (DEFAULT_SKIP_DIRS.has(entry)) continue;
|
|
1317
|
+
const fullPath = join2(dirPath, entry);
|
|
1318
|
+
try {
|
|
1319
|
+
const stat = statSync(fullPath);
|
|
1320
|
+
if (stat.isDirectory()) {
|
|
1321
|
+
total += dirSize(fullPath);
|
|
1322
|
+
} else if (stat.isFile()) {
|
|
1323
|
+
total += stat.size;
|
|
1324
|
+
}
|
|
1325
|
+
} catch {
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
return total;
|
|
1330
|
+
}
|
|
1331
|
+
async function detectSizeBudgetViolations(rootDir, config) {
|
|
1332
|
+
const budgets = config?.budgets ?? {};
|
|
1333
|
+
const violations = [];
|
|
1334
|
+
let packagesChecked = 0;
|
|
1335
|
+
for (const [pkgPath, budget] of Object.entries(budgets)) {
|
|
1336
|
+
packagesChecked++;
|
|
1337
|
+
const distPath = join2(rootDir, pkgPath, "dist");
|
|
1338
|
+
const currentSize = dirSize(distPath);
|
|
1339
|
+
if (budget.warn) {
|
|
1340
|
+
const budgetBytes = parseSize(budget.warn);
|
|
1341
|
+
if (budgetBytes > 0 && currentSize > budgetBytes) {
|
|
1342
|
+
violations.push({
|
|
1343
|
+
package: pkgPath,
|
|
1344
|
+
currentSize,
|
|
1345
|
+
budgetSize: budgetBytes,
|
|
1346
|
+
unit: "bytes",
|
|
1347
|
+
tier: 2,
|
|
1348
|
+
severity: "warning"
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const warningCount = violations.filter((v) => v.severity === "warning").length;
|
|
1354
|
+
const infoCount = violations.filter((v) => v.severity === "info").length;
|
|
1355
|
+
return Ok({
|
|
1356
|
+
violations,
|
|
1357
|
+
stats: {
|
|
1358
|
+
packagesChecked,
|
|
1359
|
+
violationCount: violations.length,
|
|
1360
|
+
warningCount,
|
|
1361
|
+
infoCount
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/entropy/fixers/suggestions.ts
|
|
1367
|
+
function deadFileSuggestion(file) {
|
|
1368
|
+
return {
|
|
1369
|
+
type: "delete",
|
|
1370
|
+
priority: "high",
|
|
1371
|
+
source: "dead-code",
|
|
1372
|
+
relatedIssues: [`dead-file:${file.path}`],
|
|
1373
|
+
title: `Remove dead file: ${file.path.split("/").pop()}`,
|
|
1374
|
+
description: `This file is not imported by any other file and can be safely removed.`,
|
|
1375
|
+
files: [file.path],
|
|
1376
|
+
steps: [`Delete ${file.path}`, "Run tests to verify no regressions"],
|
|
1377
|
+
whyManual: "File deletion requires verification that no dynamic imports exist"
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1380
|
+
function deadExportSuggestion(exp) {
|
|
1381
|
+
return {
|
|
1382
|
+
type: "refactor",
|
|
1383
|
+
priority: "medium",
|
|
1384
|
+
source: "dead-code",
|
|
1385
|
+
relatedIssues: [`dead-export:${exp.file}:${exp.name}`],
|
|
1386
|
+
title: `Remove unused export: ${exp.name}`,
|
|
1387
|
+
description: `The export "${exp.name}" is not used anywhere. Consider removing it.`,
|
|
1388
|
+
files: [exp.file],
|
|
1389
|
+
steps: [`Remove export "${exp.name}" from ${exp.file}`, "Run tests to verify no regressions"],
|
|
1390
|
+
whyManual: "Export removal may affect external consumers not in scope"
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function unusedImportSuggestion(imp) {
|
|
1394
|
+
const plural = imp.specifiers.length > 1;
|
|
1395
|
+
return {
|
|
1396
|
+
type: "delete",
|
|
1397
|
+
priority: "medium",
|
|
1398
|
+
source: "dead-code",
|
|
1399
|
+
relatedIssues: [`unused-import:${imp.file}:${imp.specifiers.join(",")}`],
|
|
1400
|
+
title: `Remove unused import${plural ? "s" : ""}: ${imp.specifiers.join(", ")}`,
|
|
1401
|
+
description: `The import${plural ? "s" : ""} from "${imp.source}" ${plural ? "are" : "is"} not used.`,
|
|
1402
|
+
files: [imp.file],
|
|
1403
|
+
steps: imp.isFullyUnused ? [`Remove entire import line from ${imp.file}`] : [`Remove unused specifiers (${imp.specifiers.join(", ")}) from import statement`],
|
|
1404
|
+
whyManual: "Import removal can be auto-fixed"
|
|
1405
|
+
};
|
|
1406
|
+
}
|
|
1407
|
+
function generateDeadCodeSuggestions(report) {
|
|
1408
|
+
return [
|
|
1409
|
+
...report.deadFiles.map(deadFileSuggestion),
|
|
1410
|
+
...report.deadExports.map(deadExportSuggestion),
|
|
1411
|
+
...report.unusedImports.map(unusedImportSuggestion)
|
|
1412
|
+
];
|
|
1413
|
+
}
|
|
1414
|
+
function generateDriftSuggestions(report) {
|
|
1415
|
+
const suggestions = [];
|
|
1416
|
+
for (const drift of report.drifts) {
|
|
1417
|
+
const priority = drift.confidence === "high" ? "high" : "medium";
|
|
1418
|
+
suggestions.push({
|
|
1419
|
+
type: "update-docs",
|
|
1420
|
+
priority,
|
|
1421
|
+
source: "drift",
|
|
1422
|
+
relatedIssues: [`drift:${drift.docFile}:${drift.reference}`],
|
|
1423
|
+
title: `Fix documentation drift: ${drift.reference}`,
|
|
1424
|
+
description: drift.details,
|
|
1425
|
+
files: [drift.docFile],
|
|
1426
|
+
steps: [
|
|
1427
|
+
drift.suggestion || "Review and update documentation",
|
|
1428
|
+
"Review documentation for accuracy"
|
|
1429
|
+
],
|
|
1430
|
+
whyManual: "Documentation updates require human judgment for accuracy"
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
return suggestions;
|
|
1434
|
+
}
|
|
1435
|
+
function generatePatternSuggestions(report) {
|
|
1436
|
+
const suggestions = [];
|
|
1437
|
+
for (const violation of report.violations) {
|
|
1438
|
+
suggestions.push({
|
|
1439
|
+
type: "refactor",
|
|
1440
|
+
priority: violation.severity === "error" ? "high" : "low",
|
|
1441
|
+
source: "pattern",
|
|
1442
|
+
relatedIssues: [`pattern:${violation.pattern}:${violation.file}`],
|
|
1443
|
+
title: `Fix pattern violation: ${violation.pattern}`,
|
|
1444
|
+
description: violation.message,
|
|
1445
|
+
files: [violation.file],
|
|
1446
|
+
steps: [violation.suggestion || "Follow pattern guidelines"],
|
|
1447
|
+
whyManual: "Pattern violations often require architectural decisions"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
return suggestions;
|
|
1451
|
+
}
|
|
1452
|
+
function generateSuggestions(deadCode, drift, patterns) {
|
|
1453
|
+
const suggestions = [];
|
|
1454
|
+
if (deadCode) {
|
|
1455
|
+
suggestions.push(...generateDeadCodeSuggestions(deadCode));
|
|
1456
|
+
}
|
|
1457
|
+
if (drift) {
|
|
1458
|
+
suggestions.push(...generateDriftSuggestions(drift));
|
|
1459
|
+
}
|
|
1460
|
+
if (patterns) {
|
|
1461
|
+
suggestions.push(...generatePatternSuggestions(patterns));
|
|
1462
|
+
}
|
|
1463
|
+
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
|
1464
|
+
suggestions.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
|
1465
|
+
const byPriority = {
|
|
1466
|
+
high: suggestions.filter((s) => s.priority === "high"),
|
|
1467
|
+
medium: suggestions.filter((s) => s.priority === "medium"),
|
|
1468
|
+
low: suggestions.filter((s) => s.priority === "low")
|
|
1469
|
+
};
|
|
1470
|
+
let estimatedEffort;
|
|
1471
|
+
if (suggestions.length === 0) {
|
|
1472
|
+
estimatedEffort = "trivial";
|
|
1473
|
+
} else if (suggestions.length <= 5) {
|
|
1474
|
+
estimatedEffort = "small";
|
|
1475
|
+
} else if (suggestions.length <= 20) {
|
|
1476
|
+
estimatedEffort = "medium";
|
|
1477
|
+
} else {
|
|
1478
|
+
estimatedEffort = "large";
|
|
1479
|
+
}
|
|
1480
|
+
return {
|
|
1481
|
+
suggestions,
|
|
1482
|
+
byPriority,
|
|
1483
|
+
estimatedEffort
|
|
1484
|
+
};
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
// src/entropy/analyzer.ts
|
|
1488
|
+
var EntropyAnalyzer = class {
|
|
1489
|
+
config;
|
|
1490
|
+
snapshot;
|
|
1491
|
+
report;
|
|
1492
|
+
constructor(config) {
|
|
1493
|
+
this.config = { ...config };
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Run full entropy analysis.
|
|
1497
|
+
* When graphOptions is provided, passes graph data to drift and dead code detectors
|
|
1498
|
+
* for graph-enhanced analysis instead of snapshot-based analysis.
|
|
1499
|
+
*/
|
|
1500
|
+
async analyze(graphOptions) {
|
|
1501
|
+
const startTime = Date.now();
|
|
1502
|
+
const needsSnapshot = !graphOptions || !graphOptions.graphDriftData || !graphOptions.graphDeadCodeData;
|
|
1503
|
+
if (needsSnapshot) {
|
|
1504
|
+
const snapshotResult = await buildSnapshot(this.config);
|
|
1505
|
+
if (!snapshotResult.ok) {
|
|
1506
|
+
return Err(snapshotResult.error);
|
|
1507
|
+
}
|
|
1508
|
+
this.snapshot = snapshotResult.value;
|
|
1509
|
+
} else {
|
|
1510
|
+
this.snapshot = {
|
|
1511
|
+
files: [],
|
|
1512
|
+
dependencyGraph: { nodes: [], edges: [] },
|
|
1513
|
+
exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
|
|
1514
|
+
docs: [],
|
|
1515
|
+
codeReferences: [],
|
|
1516
|
+
entryPoints: [],
|
|
1517
|
+
rootDir: this.config.rootDir,
|
|
1518
|
+
config: this.config,
|
|
1519
|
+
buildTime: 0
|
|
1520
|
+
};
|
|
1521
|
+
}
|
|
1522
|
+
let driftReport;
|
|
1523
|
+
let deadCodeReport;
|
|
1524
|
+
let patternReport;
|
|
1525
|
+
const analysisErrors = [];
|
|
1526
|
+
if (this.config.analyze.drift) {
|
|
1527
|
+
const driftConfig = typeof this.config.analyze.drift === "object" ? this.config.analyze.drift : {};
|
|
1528
|
+
const result = await detectDocDrift(this.snapshot, driftConfig, graphOptions?.graphDriftData);
|
|
1529
|
+
if (result.ok) {
|
|
1530
|
+
driftReport = result.value;
|
|
1531
|
+
} else {
|
|
1532
|
+
analysisErrors.push({ analyzer: "drift", error: result.error });
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if (this.config.analyze.deadCode) {
|
|
1536
|
+
const result = await detectDeadCode(
|
|
1537
|
+
this.snapshot,
|
|
1538
|
+
graphOptions?.graphDeadCodeData,
|
|
1539
|
+
this.config.protectedRegions
|
|
1540
|
+
);
|
|
1541
|
+
if (result.ok) {
|
|
1542
|
+
deadCodeReport = result.value;
|
|
1543
|
+
} else {
|
|
1544
|
+
analysisErrors.push({ analyzer: "deadCode", error: result.error });
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (this.config.analyze.patterns) {
|
|
1548
|
+
const patternConfig = typeof this.config.analyze.patterns === "object" ? this.config.analyze.patterns : { patterns: [] };
|
|
1549
|
+
const result = await detectPatternViolations(this.snapshot, patternConfig);
|
|
1550
|
+
if (result.ok) {
|
|
1551
|
+
patternReport = result.value;
|
|
1552
|
+
} else {
|
|
1553
|
+
analysisErrors.push({ analyzer: "patterns", error: result.error });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
let complexityReport;
|
|
1557
|
+
if (this.config.analyze.complexity) {
|
|
1558
|
+
const complexityConfig = typeof this.config.analyze.complexity === "object" ? this.config.analyze.complexity : {};
|
|
1559
|
+
const result = await detectComplexityViolations(
|
|
1560
|
+
this.snapshot,
|
|
1561
|
+
complexityConfig,
|
|
1562
|
+
graphOptions?.graphComplexityData
|
|
1563
|
+
);
|
|
1564
|
+
if (result.ok) {
|
|
1565
|
+
complexityReport = result.value;
|
|
1566
|
+
} else {
|
|
1567
|
+
analysisErrors.push({ analyzer: "complexity", error: result.error });
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
let couplingReport;
|
|
1571
|
+
if (this.config.analyze.coupling) {
|
|
1572
|
+
const couplingConfig = typeof this.config.analyze.coupling === "object" ? this.config.analyze.coupling : {};
|
|
1573
|
+
const result = await detectCouplingViolations(
|
|
1574
|
+
this.snapshot,
|
|
1575
|
+
couplingConfig,
|
|
1576
|
+
graphOptions?.graphCouplingData
|
|
1577
|
+
);
|
|
1578
|
+
if (result.ok) {
|
|
1579
|
+
couplingReport = result.value;
|
|
1580
|
+
} else {
|
|
1581
|
+
analysisErrors.push({ analyzer: "coupling", error: result.error });
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
let sizeBudgetReport;
|
|
1585
|
+
if (this.config.analyze.sizeBudget) {
|
|
1586
|
+
const sizeBudgetConfig = typeof this.config.analyze.sizeBudget === "object" ? this.config.analyze.sizeBudget : {};
|
|
1587
|
+
const result = await detectSizeBudgetViolations(this.config.rootDir, sizeBudgetConfig);
|
|
1588
|
+
if (result.ok) {
|
|
1589
|
+
sizeBudgetReport = result.value;
|
|
1590
|
+
} else {
|
|
1591
|
+
analysisErrors.push({ analyzer: "sizeBudget", error: result.error });
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
const driftIssues = driftReport?.drifts.length || 0;
|
|
1595
|
+
const deadCodeIssues = (deadCodeReport?.deadExports.length || 0) + (deadCodeReport?.deadFiles.length || 0) + (deadCodeReport?.unusedImports.length || 0);
|
|
1596
|
+
const patternIssues = patternReport?.violations.length || 0;
|
|
1597
|
+
const patternErrors = patternReport?.stats.errorCount || 0;
|
|
1598
|
+
const patternWarnings = patternReport?.stats.warningCount || 0;
|
|
1599
|
+
const complexityIssues = complexityReport?.violations.length || 0;
|
|
1600
|
+
const couplingIssues = couplingReport?.violations.length || 0;
|
|
1601
|
+
const sizeBudgetIssues = sizeBudgetReport?.violations.length || 0;
|
|
1602
|
+
const complexityErrors = complexityReport?.stats.errorCount || 0;
|
|
1603
|
+
const complexityWarnings = complexityReport?.stats.warningCount || 0;
|
|
1604
|
+
const couplingWarnings = couplingReport?.stats.warningCount || 0;
|
|
1605
|
+
const sizeBudgetWarnings = sizeBudgetReport?.stats.warningCount || 0;
|
|
1606
|
+
const totalIssues = driftIssues + deadCodeIssues + patternIssues + complexityIssues + couplingIssues + sizeBudgetIssues;
|
|
1607
|
+
const fixableCount = (deadCodeReport?.deadFiles.length || 0) + (deadCodeReport?.unusedImports.length || 0);
|
|
1608
|
+
const suggestions = generateSuggestions(deadCodeReport, driftReport, patternReport);
|
|
1609
|
+
const duration = Date.now() - startTime;
|
|
1610
|
+
const report = {
|
|
1611
|
+
snapshot: this.snapshot,
|
|
1612
|
+
analysisErrors,
|
|
1613
|
+
summary: {
|
|
1614
|
+
totalIssues,
|
|
1615
|
+
errors: patternErrors + complexityErrors,
|
|
1616
|
+
warnings: patternWarnings + driftIssues + complexityWarnings + couplingWarnings + sizeBudgetWarnings,
|
|
1617
|
+
fixableCount,
|
|
1618
|
+
suggestionCount: suggestions.suggestions.length
|
|
1619
|
+
},
|
|
1620
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1621
|
+
duration
|
|
1622
|
+
};
|
|
1623
|
+
if (driftReport) {
|
|
1624
|
+
report.drift = driftReport;
|
|
1625
|
+
}
|
|
1626
|
+
if (deadCodeReport) {
|
|
1627
|
+
report.deadCode = deadCodeReport;
|
|
1628
|
+
}
|
|
1629
|
+
if (patternReport) {
|
|
1630
|
+
report.patterns = patternReport;
|
|
1631
|
+
}
|
|
1632
|
+
if (complexityReport) {
|
|
1633
|
+
report.complexity = complexityReport;
|
|
1634
|
+
}
|
|
1635
|
+
if (couplingReport) {
|
|
1636
|
+
report.coupling = couplingReport;
|
|
1637
|
+
}
|
|
1638
|
+
if (sizeBudgetReport) {
|
|
1639
|
+
report.sizeBudget = sizeBudgetReport;
|
|
1640
|
+
}
|
|
1641
|
+
this.report = report;
|
|
1642
|
+
return Ok(report);
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Get the built snapshot (must call analyze first)
|
|
1646
|
+
*/
|
|
1647
|
+
getSnapshot() {
|
|
1648
|
+
return this.snapshot;
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* Get the last report (must call analyze first)
|
|
1652
|
+
*/
|
|
1653
|
+
getReport() {
|
|
1654
|
+
return this.report;
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* Generate suggestions from the last analysis
|
|
1658
|
+
*/
|
|
1659
|
+
getSuggestions() {
|
|
1660
|
+
if (!this.report) {
|
|
1661
|
+
return {
|
|
1662
|
+
suggestions: [],
|
|
1663
|
+
byPriority: { high: [], medium: [], low: [] },
|
|
1664
|
+
estimatedEffort: "trivial"
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
return generateSuggestions(this.report.deadCode, this.report.drift, this.report.patterns);
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Build snapshot without running analysis
|
|
1671
|
+
*/
|
|
1672
|
+
async buildSnapshot() {
|
|
1673
|
+
const result = await buildSnapshot(this.config);
|
|
1674
|
+
if (result.ok) {
|
|
1675
|
+
this.snapshot = result.value;
|
|
1676
|
+
}
|
|
1677
|
+
return result;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Ensure snapshot is built, returning the snapshot or an error
|
|
1681
|
+
*/
|
|
1682
|
+
async ensureSnapshot() {
|
|
1683
|
+
if (this.snapshot) {
|
|
1684
|
+
return Ok(this.snapshot);
|
|
1685
|
+
}
|
|
1686
|
+
return this.buildSnapshot();
|
|
1687
|
+
}
|
|
1688
|
+
/**
|
|
1689
|
+
* Run drift detection only (snapshot must be built first)
|
|
1690
|
+
*/
|
|
1691
|
+
async detectDrift(config, graphDriftData) {
|
|
1692
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
1693
|
+
if (!snapshotResult.ok) {
|
|
1694
|
+
return Err(snapshotResult.error);
|
|
1695
|
+
}
|
|
1696
|
+
return detectDocDrift(snapshotResult.value, config || {}, graphDriftData);
|
|
1697
|
+
}
|
|
1698
|
+
/**
|
|
1699
|
+
* Run dead code detection only (snapshot must be built first)
|
|
1700
|
+
*/
|
|
1701
|
+
async detectDeadCode(graphDeadCodeData) {
|
|
1702
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
1703
|
+
if (!snapshotResult.ok) {
|
|
1704
|
+
return Err(snapshotResult.error);
|
|
1705
|
+
}
|
|
1706
|
+
return detectDeadCode(snapshotResult.value, graphDeadCodeData, this.config.protectedRegions);
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Run pattern detection only (snapshot must be built first)
|
|
1710
|
+
*/
|
|
1711
|
+
async detectPatterns(config) {
|
|
1712
|
+
const snapshotResult = await this.ensureSnapshot();
|
|
1713
|
+
if (!snapshotResult.ok) {
|
|
1714
|
+
return Err(snapshotResult.error);
|
|
1715
|
+
}
|
|
1716
|
+
return detectPatternViolations(snapshotResult.value, config);
|
|
1717
|
+
}
|
|
1718
|
+
};
|
|
1719
|
+
|
|
1720
|
+
export {
|
|
1721
|
+
buildSnapshot,
|
|
1722
|
+
detectDocDrift,
|
|
1723
|
+
detectDeadCode,
|
|
1724
|
+
detectPatternViolations,
|
|
1725
|
+
parseSize,
|
|
1726
|
+
detectSizeBudgetViolations,
|
|
1727
|
+
generateSuggestions,
|
|
1728
|
+
EntropyAnalyzer
|
|
1729
|
+
};
|