codebrief 1.1.1 → 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 CHANGED
@@ -2983,47 +2983,21 @@ function printSummary(files, ctx, snapshot, analysis) {
2983
2983
  const savings = Math.round(
2984
2984
  (explorationTokens - afterTotal) / explorationTokens * 100
2985
2985
  );
2986
- const costRows = [
2987
- { label: "Always loaded", desc: "main context + global rule", value: `~${formatNumber(alwaysOnTokens)} tokens` }
2988
- ];
2989
- if (avgScopedTokens > 0) {
2990
- costRows.push({ label: "Per-task (avg)", desc: "1 scoped rule", value: `~${formatNumber(avgScopedTokens)} tokens` });
2991
- }
2992
- costRows.push({ label: "Exploration", desc: "mostly eliminated", value: `~${formatNumber(residualExploration)} tokens` });
2993
- const maxCostLabel = Math.max(...costRows.map((r) => r.label.length));
2994
- const maxCostDesc = Math.max(...costRows.map((r) => r.desc.length));
2995
- const maxCostValue = Math.max(...costRows.map((r) => r.value.length));
2996
- const afterContentWidth = maxCostLabel + 3 + maxCostDesc + 2 + maxCostValue;
2997
- const beforeValue = `~${formatNumber(explorationTokens)} tokens`;
2998
- const beforeLabel = "Exploration to understand codebase";
2999
- const beforeGap = afterContentWidth - beforeLabel.length - beforeValue.length;
3000
- console.log(pc.dim(" Before (no context files):"));
3001
- if (beforeGap >= 2) {
3002
- console.log(
3003
- ` ${beforeLabel}${" ".repeat(beforeGap)}${pc.red(beforeValue)}`
3004
- );
3005
- } else {
3006
- console.log(
3007
- ` ${beforeLabel} ${pc.red(beforeValue)}`
3008
- );
3009
- }
3010
- console.log("");
3011
- console.log(pc.dim(" After:"));
3012
- for (const row of costRows) {
3013
- console.log(
3014
- ` ${row.label.padEnd(maxCostLabel)} ${row.desc.padEnd(maxCostDesc)} ${pc.green(row.value.padStart(maxCostValue))}`
3015
- );
3016
- }
3017
- const totalText = `Total: ~${formatNumber(afterTotal)} tokens`;
3018
- const totalPad = afterContentWidth > totalText.length ? " ".repeat(afterContentWidth - totalText.length) : "";
3019
- console.log(
3020
- ` ${totalPad}${pc.bold(totalText)}`
3021
- );
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`);
3022
2996
  console.log("");
3023
2997
  if (savings > 0) {
3024
2998
  console.log(
3025
2999
  pc.green(
3026
- ` Estimated savings: ~${savings}% fewer tokens before real work begins`
3000
+ ` Estimated savings: ~${savings}% fewer tokens`
3027
3001
  )
3028
3002
  );
3029
3003
  }
@@ -3067,20 +3041,39 @@ function printSummary(files, ctx, snapshot, analysis) {
3067
3041
  );
3068
3042
  }
3069
3043
  }
3070
- const exportCoverage = analysis?.exportCoverage;
3071
- if (exportCoverage && exportCoverage.length > 0) {
3072
- const totalExports = exportCoverage.reduce((sum, e) => sum + e.totalExports, 0);
3073
- const totalUsed = exportCoverage.reduce((sum, e) => sum + e.usedExports, 0);
3074
- const unusedExports = totalExports - totalUsed;
3075
- const coveragePct = totalExports > 0 ? Math.round(totalUsed / totalExports * 100) : 100;
3076
- const filesWithUnused = exportCoverage.filter((e) => e.usedExports < e.totalExports).length;
3077
- if (unusedExports > 0) {
3078
- console.log("");
3079
- console.log(
3080
- pc.dim(
3081
- ` Export coverage: ${coveragePct}% of exports are used (${unusedExports} unused exports in ${filesWithUnused} file${filesWithUnused === 1 ? "" : "s"})`
3082
- )
3083
- );
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`));
3084
3077
  }
3085
3078
  }
3086
3079
  console.log("");
@@ -3504,35 +3497,111 @@ async function main() {
3504
3497
  const fileCount = graph.centrality.size;
3505
3498
  spinner3.start(`Running PageRank on ${fileCount} files...`);
3506
3499
  const hubFiles = getHubFiles(graph);
3507
- verboseLog(`PageRank: found ${hubFiles.length} hub files`);
3508
- spinner3.message("Finding circular dependencies...");
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...");
3509
3510
  const circularDeps = findCircularDeps(graph);
3510
- verboseLog(`Tarjan SCC: ${circularDeps.length === 0 ? "no cycles found" : `${circularDeps.length} cycle(s)`}`);
3511
- spinner3.message("Detecting architecture layers...");
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...");
3512
3520
  const { layers, layerEdges } = detectArchitecturalLayers(graph);
3513
- verboseLog(`Layers: ${layers.map((l) => l.name).join(", ") || "none detected"}`);
3514
- spinner3.message("Computing instability metrics...");
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...");
3515
3530
  const instabilities = computeInstability(graph);
3516
- verboseLog(`Instability: ${instabilities.length} high-risk file(s)`);
3517
- spinner3.message("Detecting module communities...");
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...");
3518
3541
  const communities = detectCommunities(graph);
3519
- verboseLog(`Communities: ${communities.length} cluster(s)`);
3520
- spinner3.message("Computing export coverage...");
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...");
3521
3551
  const exportCoverage = computeExportCoverage(graph);
3522
- verboseLog(`Export coverage: ${exportCoverage.length} files analyzed`);
3523
- spinner3.message("Analyzing git history...");
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...");
3524
3567
  const gitActivity = detected.isGitRepo ? analyzeGitActivity(rootDir, verbose ? verboseLog : spinnerProgress) : null;
3525
- if (gitActivity) verboseLog(`Git: ${gitActivity.hotFiles.length} active files, ${gitActivity.changeCoupling.length} coupled pairs`);
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
+ }
3526
3581
  const analysis = { hubFiles, circularDeps, layers, layerEdges, gitActivity, instabilities, communities, exportCoverage };
3527
- const analysisParts = [];
3528
- if (hubFiles.length > 0) analysisParts.push(`${hubFiles.length} hub files`);
3529
- if (layers.length > 0) analysisParts.push(`${layers.length} layers`);
3530
- if (circularDeps.length > 0) analysisParts.push(`${circularDeps.length} circular dep${circularDeps.length === 1 ? "" : "s"}`);
3531
- if (communities.length > 0) analysisParts.push(`${communities.length} module cluster${communities.length === 1 ? "" : "s"}`);
3532
- if (gitActivity) analysisParts.push(`${gitActivity.hotFiles.length} active files`);
3533
- spinner3.stop(
3534
- analysisParts.length > 0 ? `Analysis: ${analysisParts.join(", ")}.` : "Analysis complete."
3535
- );
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
+ }
3536
3605
  const savedConfig = await loadConfig(rootDir);
3537
3606
  if (savedConfig?.snapshotHash) {
3538
3607
  const currentHash = await computeSnapshotHash(rootDir, detected.language);