@harness-engineering/core 0.26.4 → 0.27.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.
@@ -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
+ };