Skip to content

Commit cecb90c

Browse files
committed
Merge branch 'dev' of https://github.com/HUMBLEF0OL/grotto into staging
2 parents cac3a4f + 3d0ff08 commit cecb90c

97 files changed

Lines changed: 12839 additions & 6819 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@git-compass/core": minor
3+
"@git-compass/web": minor
4+
---
5+
6+
NPM Publication Readiness:
7+
- Turn `@git-compass/web` into a global CLI tool out of the box.
8+
- Create `bin/git-compass.js` wrapper with dynamic `REPO_PATH` resolution.
9+
- Update `package.json` metadata (description, keywords, bin) for professional NPM distribution.
10+
- Align `@git-compass/core` metadata for registry discoverability.

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"scripts": {
4242
"build": "tsc",
4343
"dev": "tsc --watch",
44+
"cli": "tsx src/bin/git-compass.ts",
4445
"lint": "eslint .",
4546
"test": "vitest run"
4647
},

packages/cli/report.txt

-9.85 KB
Binary file not shown.

packages/cli/src/commands/analyze-all.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import {
3333
saveCache,
3434
} from "../utils/cache.js";
3535
import { ensureGitIgnore } from "../utils/gitignore.js";
36+
import { printConsoleReport } from "../formatters/console.js";
37+
3638

3739
export const analyzeAllCommand = new Command("analyze-all")
3840
.description("Scan a directory for Git repositories and analyze them all")
@@ -156,6 +158,10 @@ export const analyzeAllCommand = new Command("analyze-all")
156158
`${repoName}: ${result.meta.commitCount} commits, ${highRiskCount} high-risk files.`,
157159
),
158160
);
161+
162+
if (options.detailLevel === "verbose") {
163+
printConsoleReport(result, "normal", !!options.ai);
164+
}
159165
} catch (err) {
160166
repoSpinner.fail(chalk.red(`Failed to analyze ${repoName}: ${(err as Error).message}`));
161167
}

packages/cli/src/commands/analyze.ts

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,16 @@ export const analyzeCommand = new Command("analyze")
111111
: await getCommits(git, {
112112
branch: options.branch,
113113
maxCount: parseInt(options.maxCommits, 10),
114+
since: options.window !== "all" ? options.window : undefined,
114115
});
115116

116117
if (!cachedResult && commits.length === 0) {
117118
spinner.fail(chalk.red("No commits found in the specified window/branch."));
118119
return;
119120
}
120121

122+
spinner.text = `Performing deep analysis on ${commits.length} commits...`;
123+
121124
const result: AnalysisResult = cachedResult || {
122125
meta: {
123126
repoPath,
@@ -126,42 +129,38 @@ export const analyzeCommand = new Command("analyze")
126129
commitCount: commits.length,
127130
generatedAt: new Date(),
128131
},
129-
hotspots: analyzeHotspots(commits, options.window as any),
130-
riskScores: computeRiskScores(analyzeHotspots(commits, options.window as any)), // Simplified for rebuild
131-
churn: analyzeChurn(commits, options.window as any),
132-
contributors: analyzeContributors(commits),
133-
burnout: analyzeBurnout(commits),
134-
coupling: analyzeCoupling(commits),
135-
knowledge: analyzeKnowledge(commits),
136-
impact: analyzeImpact(commits),
137-
rot: analyzeRot(commits),
138-
compass: analyzeCompass(commits),
139-
health: analyzeHealth(
140-
commits,
141-
analyzeChurn(commits, options.window as any),
142-
analyzeCoupling(commits),
143-
),
144-
contributorTimeline: analyzeContributorTimeline(commits),
132+
hotspots: [],
133+
riskScores: [],
134+
churn: [],
135+
contributors: [],
136+
contributorTimeline: [],
137+
burnout: { flags: [], afterHoursCommits: 0, weekendCommits: 0, contributors: [] },
138+
coupling: [],
139+
knowledge: [],
140+
impact: [],
141+
rot: [],
142+
compass: { essentials: [], components: [] },
143+
health: { stability: 0, velocity: 0, simplicity: 0, coverage: 0, decoupling: 0 },
145144
};
146145

147-
// Re-calculate hotspots/risk if we don't have cached result (above logic is a bit messy, let's fix)
148146
if (!cachedResult) {
149-
spinner.text = `Analyzing ${commits.length} commits...`;
150-
const h = analyzeHotspots(commits, options.window as any);
151-
result.hotspots = h;
152-
result.riskScores = computeRiskScores(h);
147+
const hotspots = analyzeHotspots(commits, options.window as any);
148+
const riskScores = computeRiskScores(hotspots);
153149
const churn = analyzeChurn(commits, options.window as any);
154150
const coupling = analyzeCoupling(commits);
151+
152+
result.hotspots = hotspots;
153+
result.riskScores = riskScores;
155154
result.churn = churn;
155+
result.coupling = coupling;
156156
result.contributors = analyzeContributors(commits);
157+
result.contributorTimeline = analyzeContributorTimeline(commits);
157158
result.burnout = analyzeBurnout(commits);
158-
result.coupling = coupling;
159159
result.knowledge = analyzeKnowledge(commits);
160160
result.impact = analyzeImpact(commits);
161161
result.rot = analyzeRot(commits);
162162
result.compass = analyzeCompass(commits);
163163
result.health = analyzeHealth(commits, churn, coupling);
164-
result.contributorTimeline = analyzeContributorTimeline(commits);
165164
}
166165

167166
if (options.ai) {

packages/cli/src/constants/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,9 @@ export const ENV_VARS = {
1515
GEMINI_API_KEY: "GEMINI_API_KEY",
1616
AI_PROVIDER: "GIT_COMPASS_AI_PROVIDER",
1717
} as const;
18+
19+
export const HEALTH_THRESHOLDS = {
20+
GOOD: 70,
21+
WARNING: 40,
22+
} as const;
23+

packages/cli/src/formatters/console.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import chalk from "chalk";
22
import boxen from "boxen";
33
import { table, getBorderCharacters } from "table";
44
import type { AnalysisResult } from "@git-compass/core";
5+
import { HEALTH_THRESHOLDS } from "../constants/index.js";
56

67
export function printConsoleReport(
78
result: AnalysisResult,
@@ -36,7 +37,11 @@ export function printConsoleReport(
3637
5,
3738
);
3839
const healthColor =
39-
healthScore > 70 ? chalk.green : healthScore > 40 ? chalk.yellow : chalk.red;
40+
healthScore > HEALTH_THRESHOLDS.GOOD
41+
? chalk.green
42+
: healthScore > HEALTH_THRESHOLDS.WARNING
43+
? chalk.yellow
44+
: chalk.red;
4045

4146
console.log(
4247
boxen(
@@ -86,7 +91,7 @@ export function printConsoleReport(
8691
}
8792

8893
if (isSummary) {
89-
printHealthIndicators(health, impact, rot, !!result.aiSummary);
94+
printHealthIndicators(health, impact, rot, result.churn, !!result.aiSummary);
9095
return;
9196
}
9297

@@ -129,8 +134,13 @@ export function printConsoleReport(
129134
const riskLevel = fileRisk?.level || "low";
130135
const riskColor = getRiskColor(riskLevel);
131136

137+
// Explanatory factors
138+
const factors = fileRisk?.factors
139+
? `(${chalk.gray(`freq:${fileRisk.factors.changeFrequency},auth:${fileRisk.factors.uniqueAuthors},rec:${fileRisk.factors.recentActivity}`)})`
140+
: "";
141+
132142
hotspotData.push([
133-
chalk.white(h.path),
143+
chalk.white(h.path) + (factors ? "\n" + factors : ""),
134144
chalk.cyan(h.changeCount.toString()),
135145
chalk.magenta(h.uniqueAuthors.toString()),
136146
chalk.hex(riskColor).bold(riskLevel.toUpperCase()),
@@ -209,20 +219,30 @@ export function printConsoleReport(
209219
});
210220
}
211221

222+
const highImpact = [...(impact || [])].sort((a, b) => b.blastRadius - a.blastRadius);
223+
if (highImpact.length > 0) {
224+
insightsData.push([chalk.bold("High Blast Radius (Impact)")]);
225+
highImpact.slice(0, 5).forEach((i) => {
226+
insightsData.push([
227+
` ${chalk.white(i.path)}\n ${chalk.gray("Avg Change Ripple: ")}${chalk.yellow(i.blastRadius.toFixed(1) + " files")}`,
228+
]);
229+
});
230+
}
231+
212232
if (insightsData.length > 0) {
213233
console.log(chalk.blue.bold("\nDeep Architecture Insights"));
214234
console.log(table(insightsData, { border: getBorderCharacters("ramac") }));
215235
}
216236
}
217237

218238
// 7. Health Indicators & Footer Tip
219-
printHealthIndicators(health, impact, rot, showAI);
239+
printHealthIndicators(health, impact, rot, result.churn, showAI);
220240
} catch (err) {
221241
console.error(chalk.red("\nError printing report:"), err);
222242
}
223243
}
224244

225-
function printHealthIndicators(health: any, impact: any[], rot: any[], showAI: boolean) {
245+
function printHealthIndicators(health: any, impact: any[], rot: any[], churn: any[], showAI: boolean) {
226246
const avgImpact =
227247
impact.length > 0
228248
? (impact.reduce((acc: number, i: any) => acc + i.blastRadius, 0) / impact.length).toFixed(2)
@@ -234,11 +254,26 @@ function printHealthIndicators(health: any, impact: any[], rot: any[], showAI: b
234254
`${chalk.white("Stability: ")} ${chalk.cyan(health.stability + "%")}`,
235255
`${chalk.white("Velocity: ")} ${chalk.cyan(health.velocity + "%")}`,
236256
`${chalk.white("Complexity: ")} ${chalk.cyan(health.simplicity + "%")}`,
257+
`${chalk.white("Coverage: ")} ${chalk.cyan(health.coverage + "%")}`,
237258
`${chalk.white("Decoupling: ")} ${chalk.cyan(health.decoupling + "%")}`,
238259
`${chalk.white("Blast Radius:")} ${chalk.yellow(avgImpact + " files")}`,
239260
`${chalk.white("Code Rot: ")} ${chalk.red(rot.length + " abandoned files")}`,
240261
];
241262

263+
// Churn trend summary
264+
if (churn && churn.length > 0) {
265+
const sortedChurn = [...churn].sort(
266+
(a: any, b: any) => b.linesAdded + b.linesRemoved - (a.linesAdded + a.linesRemoved),
267+
);
268+
const peak = sortedChurn[0];
269+
const peakDate = new Date(peak.date).toLocaleDateString();
270+
footerContent.splice(
271+
2,
272+
0,
273+
`${chalk.white("Peak Churn: ")} ${chalk.magenta(peakDate)} ${chalk.gray(`(${peak.linesAdded + peak.linesRemoved} lines)`)}`,
274+
);
275+
}
276+
242277
if (!showAI) {
243278
footerContent.push("");
244279
footerContent.push(

0 commit comments

Comments
 (0)