codebrief 1.1.0 → 1.2.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/index.js +192 -90
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -79,6 +79,7 @@ var init_utils = __esm({
|
|
|
79
79
|
});
|
|
80
80
|
|
|
81
81
|
// src/index.ts
|
|
82
|
+
init_utils();
|
|
82
83
|
import path8 from "path";
|
|
83
84
|
import * as p4 from "@clack/prompts";
|
|
84
85
|
import pc3 from "picocolors";
|
|
@@ -305,7 +306,10 @@ async function detectContext(rootDir, onProgress) {
|
|
|
305
306
|
"**/vendor/**",
|
|
306
307
|
"**/__pycache__/**",
|
|
307
308
|
"**/venv/**",
|
|
308
|
-
"**/.venv/**"
|
|
309
|
+
"**/.venv/**",
|
|
310
|
+
"**/.Trash/**",
|
|
311
|
+
"**/Library/**",
|
|
312
|
+
"**/.git/**"
|
|
309
313
|
],
|
|
310
314
|
stats: true
|
|
311
315
|
}
|
|
@@ -859,21 +863,34 @@ function computePageRank(files, edges, iterations = 5, damping = 0.85) {
|
|
|
859
863
|
}
|
|
860
864
|
async function buildImportGraph(rootDir, language, onProgress) {
|
|
861
865
|
const globs = getSourceGlob(language);
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
866
|
+
let files;
|
|
867
|
+
try {
|
|
868
|
+
files = await fg2(globs, {
|
|
869
|
+
cwd: rootDir,
|
|
870
|
+
ignore: [
|
|
871
|
+
"**/node_modules/**",
|
|
872
|
+
"**/dist/**",
|
|
873
|
+
"**/build/**",
|
|
874
|
+
"**/.next/**",
|
|
875
|
+
"**/target/**",
|
|
876
|
+
"**/vendor/**",
|
|
877
|
+
"**/__pycache__/**",
|
|
878
|
+
"**/venv/**",
|
|
879
|
+
"**/.venv/**",
|
|
880
|
+
"**/.Trash/**",
|
|
881
|
+
"**/Library/**",
|
|
882
|
+
"**/.git/**"
|
|
883
|
+
],
|
|
884
|
+
absolute: false
|
|
885
|
+
});
|
|
886
|
+
} catch (err) {
|
|
887
|
+
const code = err.code;
|
|
888
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
889
|
+
onProgress?.("Warning: permission error scanning files \u2014 returning empty graph");
|
|
890
|
+
return { edges: [], inDegree: /* @__PURE__ */ new Map(), centrality: /* @__PURE__ */ new Map(), externalImportCounts: /* @__PURE__ */ new Map() };
|
|
891
|
+
}
|
|
892
|
+
throw err;
|
|
893
|
+
}
|
|
877
894
|
onProgress?.(`Found ${files.length} source files to analyze`);
|
|
878
895
|
const fileSet = new Set(files);
|
|
879
896
|
const edges = [];
|
|
@@ -1401,7 +1418,10 @@ async function generateSnapshot(ctx, customPaths, graph, maxTokens, onProgress,
|
|
|
1401
1418
|
"**/build/**",
|
|
1402
1419
|
"**/*.test.*",
|
|
1403
1420
|
"**/*.spec.*",
|
|
1404
|
-
"**/__tests__/**"
|
|
1421
|
+
"**/__tests__/**",
|
|
1422
|
+
"**/.Trash/**",
|
|
1423
|
+
"**/Library/**",
|
|
1424
|
+
"**/.git/**"
|
|
1405
1425
|
],
|
|
1406
1426
|
absolute: false
|
|
1407
1427
|
});
|
|
@@ -2963,47 +2983,21 @@ function printSummary(files, ctx, snapshot, analysis) {
|
|
|
2963
2983
|
const savings = Math.round(
|
|
2964
2984
|
(explorationTokens - afterTotal) / explorationTokens * 100
|
|
2965
2985
|
);
|
|
2966
|
-
const
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
const
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
const afterContentWidth = maxCostLabel + 3 + maxCostDesc + 2 + maxCostValue;
|
|
2977
|
-
const beforeValue = `~${formatNumber(explorationTokens)} tokens`;
|
|
2978
|
-
const beforeLabel = "Exploration to understand codebase";
|
|
2979
|
-
const beforeGap = afterContentWidth - beforeLabel.length - beforeValue.length;
|
|
2980
|
-
console.log(pc.dim(" Before (no context files):"));
|
|
2981
|
-
if (beforeGap >= 2) {
|
|
2982
|
-
console.log(
|
|
2983
|
-
` ${beforeLabel}${" ".repeat(beforeGap)}${pc.red(beforeValue)}`
|
|
2984
|
-
);
|
|
2985
|
-
} else {
|
|
2986
|
-
console.log(
|
|
2987
|
-
` ${beforeLabel} ${pc.red(beforeValue)}`
|
|
2988
|
-
);
|
|
2989
|
-
}
|
|
2990
|
-
console.log("");
|
|
2991
|
-
console.log(pc.dim(" After:"));
|
|
2992
|
-
for (const row of costRows) {
|
|
2993
|
-
console.log(
|
|
2994
|
-
` ${row.label.padEnd(maxCostLabel)} ${row.desc.padEnd(maxCostDesc)} ${pc.green(row.value.padStart(maxCostValue))}`
|
|
2995
|
-
);
|
|
2996
|
-
}
|
|
2997
|
-
const totalText = `Total: ~${formatNumber(afterTotal)} tokens`;
|
|
2998
|
-
const totalPad = afterContentWidth > totalText.length ? " ".repeat(afterContentWidth - totalText.length) : "";
|
|
2999
|
-
console.log(
|
|
3000
|
-
` ${totalPad}${pc.bold(totalText)}`
|
|
3001
|
-
);
|
|
2986
|
+
const BAR_MAX = 40;
|
|
2987
|
+
const maxVal = Math.max(explorationTokens, afterTotal);
|
|
2988
|
+
const beforeBarLen = Math.max(1, Math.round(explorationTokens / maxVal * BAR_MAX));
|
|
2989
|
+
const afterBarLen = Math.max(1, Math.round(afterTotal / maxVal * BAR_MAX));
|
|
2990
|
+
const BLOCK = "\u2588";
|
|
2991
|
+
const beforeBar = BLOCK.repeat(beforeBarLen);
|
|
2992
|
+
const afterBar = BLOCK.repeat(afterBarLen);
|
|
2993
|
+
const afterPad = " ".repeat(Math.max(0, beforeBarLen - afterBarLen));
|
|
2994
|
+
console.log(` Before: ${pc.red(beforeBar)} ~${formatNumber(explorationTokens)} tokens`);
|
|
2995
|
+
console.log(` After: ${pc.green(afterBar)}${afterPad} ~${formatNumber(afterTotal)} tokens`);
|
|
3002
2996
|
console.log("");
|
|
3003
2997
|
if (savings > 0) {
|
|
3004
2998
|
console.log(
|
|
3005
2999
|
pc.green(
|
|
3006
|
-
` Estimated savings: ~${savings}% fewer tokens
|
|
3000
|
+
` Estimated savings: ~${savings}% fewer tokens`
|
|
3007
3001
|
)
|
|
3008
3002
|
);
|
|
3009
3003
|
}
|
|
@@ -3047,20 +3041,39 @@ function printSummary(files, ctx, snapshot, analysis) {
|
|
|
3047
3041
|
);
|
|
3048
3042
|
}
|
|
3049
3043
|
}
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
);
|
|
3044
|
+
if (analysis) {
|
|
3045
|
+
const findings = [];
|
|
3046
|
+
if (analysis.circularDeps.length > 0) {
|
|
3047
|
+
for (const c of analysis.circularDeps.slice(0, 3)) {
|
|
3048
|
+
const names = c.chain.map((f) => f.split("/").pop()?.replace(/\.[jt]sx?$/, "") ?? f);
|
|
3049
|
+
findings.push(`${analysis.circularDeps.length > 1 ? "" : ""}1 circular dependency chain (${names.slice(0, 2).join(" \u2194 ")})`);
|
|
3050
|
+
}
|
|
3051
|
+
if (analysis.circularDeps.length > 1) {
|
|
3052
|
+
findings[0] = `${analysis.circularDeps.length} circular dependency chain${analysis.circularDeps.length === 1 ? "" : "s"}`;
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
const highInstabilityFiles = analysis.instabilities.filter((f) => f.instability > 0.8);
|
|
3056
|
+
if (highInstabilityFiles.length > 0) {
|
|
3057
|
+
findings.push(`${highInstabilityFiles.length} high-instability file${highInstabilityFiles.length === 1 ? "" : "s"}`);
|
|
3058
|
+
}
|
|
3059
|
+
const ec = analysis.exportCoverage;
|
|
3060
|
+
if (ec && ec.length > 0) {
|
|
3061
|
+
const totalExports = ec.reduce((sum, e) => sum + e.totalExports, 0);
|
|
3062
|
+
const totalUsed = ec.reduce((sum, e) => sum + e.usedExports, 0);
|
|
3063
|
+
const unusedExports = totalExports - totalUsed;
|
|
3064
|
+
const filesWithUnused = ec.filter((e) => e.usedExports < e.totalExports).length;
|
|
3065
|
+
if (unusedExports > 0) {
|
|
3066
|
+
findings.push(`${unusedExports} unused export${unusedExports === 1 ? "" : "s"} in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"}`);
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
console.log("");
|
|
3070
|
+
if (findings.length > 0) {
|
|
3071
|
+
console.log(pc.yellow(` \u26A0 ${findings.length} finding${findings.length === 1 ? "" : "s"}`));
|
|
3072
|
+
for (const f of findings) {
|
|
3073
|
+
console.log(pc.dim(` \u25CF ${f}`));
|
|
3074
|
+
}
|
|
3075
|
+
} else {
|
|
3076
|
+
console.log(pc.green(` \u2713 No structural issues detected`));
|
|
3064
3077
|
}
|
|
3065
3078
|
}
|
|
3066
3079
|
console.log("");
|
|
@@ -3395,6 +3408,19 @@ async function main() {
|
|
|
3395
3408
|
const maxTokens = maxTokensArg ? parseInt(maxTokensArg.split("=")[1], 10) : void 0;
|
|
3396
3409
|
const targetDir = args.find((a) => !a.startsWith("-") && a !== "-v") ?? process.cwd();
|
|
3397
3410
|
const rootDir = path8.resolve(targetDir);
|
|
3411
|
+
const PROJECT_MARKERS = ["package.json", "go.mod", "Cargo.toml", "pyproject.toml", "requirements.txt"];
|
|
3412
|
+
const hasProjectMarker = (await Promise.all(
|
|
3413
|
+
PROJECT_MARKERS.map((f) => fileExists(path8.join(rootDir, f)))
|
|
3414
|
+
)).some(Boolean);
|
|
3415
|
+
if (!hasProjectMarker) {
|
|
3416
|
+
console.log("");
|
|
3417
|
+
p4.intro(pc3.bold(" codebrief "));
|
|
3418
|
+
p4.log.error(`No project found at ${pc3.cyan(rootDir)}`);
|
|
3419
|
+
p4.log.info(`Run ${pc3.bold("npx codebrief")} from a project directory, or pass a path:
|
|
3420
|
+
${pc3.dim("npx codebrief ./my-project")}`);
|
|
3421
|
+
p4.outro("");
|
|
3422
|
+
process.exit(1);
|
|
3423
|
+
}
|
|
3398
3424
|
const verboseLog = (msg) => {
|
|
3399
3425
|
if (verbose) p4.log.info(pc3.dim(msg));
|
|
3400
3426
|
};
|
|
@@ -3471,35 +3497,111 @@ async function main() {
|
|
|
3471
3497
|
const fileCount = graph.centrality.size;
|
|
3472
3498
|
spinner3.start(`Running PageRank on ${fileCount} files...`);
|
|
3473
3499
|
const hubFiles = getHubFiles(graph);
|
|
3474
|
-
|
|
3475
|
-
spinner3.
|
|
3500
|
+
const topHubName = hubFiles[0]?.path ?? "";
|
|
3501
|
+
spinner3.stop(
|
|
3502
|
+
hubFiles.length > 0 ? `${pc3.green("PageRank")} found ${pc3.bold(String(hubFiles.length))} hub files` + (topHubName ? pc3.dim(` (top: ${topHubName})`) : "") : `${pc3.green("PageRank")} ${pc3.dim("no hub files detected")}`
|
|
3503
|
+
);
|
|
3504
|
+
if (verbose && hubFiles.length > 0) {
|
|
3505
|
+
for (const h of hubFiles.slice(0, 5)) {
|
|
3506
|
+
p4.log.info(pc3.dim(` ${h.path} (centrality: ${h.centrality.toFixed(3)}, imported by ${h.importedBy})`));
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
spinner3.start("Finding circular dependencies...");
|
|
3476
3510
|
const circularDeps = findCircularDeps(graph);
|
|
3477
|
-
|
|
3478
|
-
|
|
3511
|
+
spinner3.stop(
|
|
3512
|
+
circularDeps.length === 0 ? `${pc3.green("Tarjan SCC")} no cycles found ${pc3.green("\u2713")}` : `${pc3.yellow("Tarjan SCC")} ${pc3.bold(String(circularDeps.length))} cycle${circularDeps.length === 1 ? "" : "s"} found`
|
|
3513
|
+
);
|
|
3514
|
+
if (verbose && circularDeps.length > 0) {
|
|
3515
|
+
for (const c of circularDeps.slice(0, 3)) {
|
|
3516
|
+
p4.log.info(pc3.dim(` ${c.chain.join(" \u2192 ")}`));
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
spinner3.start("Detecting architecture layers...");
|
|
3479
3520
|
const { layers, layerEdges } = detectArchitecturalLayers(graph);
|
|
3480
|
-
|
|
3481
|
-
|
|
3521
|
+
spinner3.stop(
|
|
3522
|
+
layers.length > 0 ? `${pc3.green("Layers")} ${layers.map((l) => l.name).join(" \u2192 ")}` : `${pc3.green("Layers")} ${pc3.dim("no clear layers detected")}`
|
|
3523
|
+
);
|
|
3524
|
+
if (verbose && layers.length > 0) {
|
|
3525
|
+
for (const l of layers) {
|
|
3526
|
+
p4.log.info(pc3.dim(` ${l.name}: ${l.files.length} files, depends on: ${l.dependsOn.join(", ") || "none"}`));
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
spinner3.start("Computing instability metrics...");
|
|
3482
3530
|
const instabilities = computeInstability(graph);
|
|
3483
|
-
|
|
3484
|
-
spinner3.
|
|
3531
|
+
const highInstability = instabilities.filter((f) => f.instability > 0.8);
|
|
3532
|
+
spinner3.stop(
|
|
3533
|
+
highInstability.length > 0 ? `${pc3.yellow("Instability")} ${pc3.bold(String(highInstability.length))} high-risk file${highInstability.length === 1 ? "" : "s"}` : `${pc3.green("Instability")} ${pc3.dim("all files within healthy range")} ${pc3.green("\u2713")}`
|
|
3534
|
+
);
|
|
3535
|
+
if (verbose && highInstability.length > 0) {
|
|
3536
|
+
for (const f of highInstability.slice(0, 5)) {
|
|
3537
|
+
p4.log.info(pc3.dim(` ${f.path} (I=${f.instability.toFixed(2)}, fan-in=${f.fanIn}, fan-out=${f.fanOut})`));
|
|
3538
|
+
}
|
|
3539
|
+
}
|
|
3540
|
+
spinner3.start("Detecting module communities...");
|
|
3485
3541
|
const communities = detectCommunities(graph);
|
|
3486
|
-
|
|
3487
|
-
|
|
3542
|
+
spinner3.stop(
|
|
3543
|
+
communities.length > 0 ? `${pc3.green("Communities")} ${pc3.bold(String(communities.length))} module cluster${communities.length === 1 ? "" : "s"}` : `${pc3.green("Communities")} ${pc3.dim("single cohesive module")}`
|
|
3544
|
+
);
|
|
3545
|
+
if (verbose && communities.length > 0) {
|
|
3546
|
+
for (const c of communities.slice(0, 5)) {
|
|
3547
|
+
p4.log.info(pc3.dim(` ${c.label} (${c.files.length} files)`));
|
|
3548
|
+
}
|
|
3549
|
+
}
|
|
3550
|
+
spinner3.start("Computing export coverage...");
|
|
3488
3551
|
const exportCoverage = computeExportCoverage(graph);
|
|
3489
|
-
|
|
3490
|
-
|
|
3552
|
+
{
|
|
3553
|
+
const totalExp = exportCoverage.reduce((s, e) => s + e.totalExports, 0);
|
|
3554
|
+
const totalUsed = exportCoverage.reduce((s, e) => s + e.usedExports, 0);
|
|
3555
|
+
const unusedCount = totalExp - totalUsed;
|
|
3556
|
+
const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
|
|
3557
|
+
spinner3.stop(
|
|
3558
|
+
unusedCount > 0 ? `${pc3.yellow("Exports")} ${pc3.bold(String(unusedCount))} unused export${unusedCount === 1 ? "" : "s"} in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"}` : `${pc3.green("Exports")} ${pc3.dim("all exports used")} ${pc3.green("\u2713")}`
|
|
3559
|
+
);
|
|
3560
|
+
if (verbose && unusedCount > 0) {
|
|
3561
|
+
for (const e of exportCoverage.filter((e2) => e2.usedExports < e2.totalExports).slice(0, 5)) {
|
|
3562
|
+
p4.log.info(pc3.dim(` ${e.file}: ${e.totalExports - e.usedExports} unused of ${e.totalExports}`));
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
spinner3.start("Analyzing git history...");
|
|
3491
3567
|
const gitActivity = detected.isGitRepo ? analyzeGitActivity(rootDir, verbose ? verboseLog : spinnerProgress) : null;
|
|
3492
|
-
if (gitActivity)
|
|
3568
|
+
if (gitActivity) {
|
|
3569
|
+
const coupledPairs = gitActivity.changeCoupling.length;
|
|
3570
|
+
spinner3.stop(
|
|
3571
|
+
`${pc3.green("Git (90d)")} ${pc3.bold(String(gitActivity.hotFiles.length))} active file${gitActivity.hotFiles.length === 1 ? "" : "s"}, ${pc3.bold(String(coupledPairs))} coupled pair${coupledPairs === 1 ? "" : "s"}`
|
|
3572
|
+
);
|
|
3573
|
+
if (verbose) {
|
|
3574
|
+
for (const h of gitActivity.hotFiles.slice(0, 5)) {
|
|
3575
|
+
p4.log.info(pc3.dim(` ${h.path} (${h.commits} commits, last: ${h.lastChanged})`));
|
|
3576
|
+
}
|
|
3577
|
+
}
|
|
3578
|
+
} else {
|
|
3579
|
+
spinner3.stop(`${pc3.green("Git")} ${pc3.dim("not a git repo \u2014 skipped")}`);
|
|
3580
|
+
}
|
|
3493
3581
|
const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3582
|
+
{
|
|
3583
|
+
const reportLines = [];
|
|
3584
|
+
reportLines.push(` Files analyzed: ${fileCount}`);
|
|
3585
|
+
reportLines.push(` Import edges: ${graph.edges.length}`);
|
|
3586
|
+
reportLines.push(` External pkgs: ${graph.externalImportCounts.size}`);
|
|
3587
|
+
if (hubFiles.length > 0) {
|
|
3588
|
+
reportLines.push(` Hub files: ${hubFiles.length}` + (hubFiles[0] ? ` (most connected: ${hubFiles[0].path})` : ""));
|
|
3589
|
+
}
|
|
3590
|
+
if (layers.length > 0) {
|
|
3591
|
+
reportLines.push(` Architecture: ${layers.map((l) => l.name).join(" \u2192 ")}`);
|
|
3592
|
+
}
|
|
3593
|
+
reportLines.push(` Circular deps: ${circularDeps.length === 0 ? "none" : `${circularDeps.length} chain${circularDeps.length === 1 ? "" : "s"}`}`);
|
|
3594
|
+
if (gitActivity) {
|
|
3595
|
+
reportLines.push(` Hot files (90d): ${gitActivity.hotFiles.length}`);
|
|
3596
|
+
}
|
|
3597
|
+
p4.note(reportLines.join("\n"), "Analysis Report");
|
|
3598
|
+
if (circularDeps.length > 0) {
|
|
3599
|
+
for (const c of circularDeps.slice(0, 2)) {
|
|
3600
|
+
const shortChain = c.chain.map((f) => f.split("/").pop() ?? f);
|
|
3601
|
+
p4.log.warn(pc3.yellow(`Cycle: ${shortChain.join(" \u2192 ")}`));
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3503
3605
|
const savedConfig = await loadConfig(rootDir);
|
|
3504
3606
|
if (savedConfig?.snapshotHash) {
|
|
3505
3607
|
const currentHash = await computeSnapshotHash(rootDir, detected.language);
|