coderoast 0.1.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,256 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runRoastNarratorAgent = runRoastNarratorAgent;
4
+ const gemini_client_1 = require("./gemini-client");
5
+ function formatEvidenceItem(item) {
6
+ const metrics = item.metrics
7
+ .map((metric) => `${metric.type}=${metric.value}`)
8
+ .join(", ");
9
+ return `${item.file}:${item.startLine}-${item.endLine} (${metrics})`;
10
+ }
11
+ function getMetricValue(metrics, type) {
12
+ return metrics.find((metric) => metric.type === type)?.value;
13
+ }
14
+ function formatEvidenceExample(item) {
15
+ const loc = getMetricValue(item.metrics, "loc");
16
+ const locText = typeof loc === "number" ? ` (${loc} lines)` : "";
17
+ return `${item.file} lines ${item.startLine}-${item.endLine}${locText}`;
18
+ }
19
+ function buildLongFunctionMessage(issue) {
20
+ const example = issue.evidence[0];
21
+ if (!example) {
22
+ return "Long functions detected, but the evidence list is empty.";
23
+ }
24
+ const extraCount = issue.evidence.length - 1;
25
+ const extraText = extraCount > 0 ? ` (+${extraCount} more)` : "";
26
+ return `Very long functions can be hard to maintain, for example ${formatEvidenceExample(example)}${extraText}.`;
27
+ }
28
+ function buildDuplicateMessage(issue) {
29
+ const byHash = new Map();
30
+ for (const item of issue.evidence) {
31
+ const hash = String(getMetricValue(item.metrics, "hash") ?? "unknown");
32
+ const count = getMetricValue(item.metrics, "count");
33
+ const loc = getMetricValue(item.metrics, "loc");
34
+ const entry = byHash.get(hash) ?? {
35
+ count: typeof count === "number" ? count : undefined,
36
+ loc: typeof loc === "number" ? loc : undefined,
37
+ examples: [],
38
+ };
39
+ entry.examples.push(item);
40
+ if (typeof count === "number") {
41
+ entry.count = count;
42
+ }
43
+ if (typeof loc === "number") {
44
+ entry.loc = loc;
45
+ }
46
+ byHash.set(hash, entry);
47
+ }
48
+ const firstBlock = [...byHash.values()].sort((a, b) => {
49
+ const countA = a.count ?? 0;
50
+ const countB = b.count ?? 0;
51
+ return countB - countA;
52
+ })[0];
53
+ if (!firstBlock || firstBlock.examples.length === 0) {
54
+ return "Repeated code detected, but the evidence list is empty.";
55
+ }
56
+ const example = firstBlock.examples[0];
57
+ const countText = typeof firstBlock.count === "number"
58
+ ? ` (${firstBlock.count} total copies)`
59
+ : "";
60
+ const locText = typeof firstBlock.loc === "number" ? ` (~${firstBlock.loc} lines)` : "";
61
+ if (!example) {
62
+ return `Repeated code${locText} appears in multiple places${countText}.`;
63
+ }
64
+ return `Repeated code${locText} appears in multiple places${countText}, for example ${formatEvidenceExample(example)}.`;
65
+ }
66
+ function buildCircularMessage(issue) {
67
+ const unique = Array.from(new Map(issue.evidence.map((item) => [item.file, item])).values());
68
+ if (unique.length === 0) {
69
+ return "Possible circular dependency detected, but the evidence list is empty.";
70
+ }
71
+ const examples = unique.slice(0, 2).map(formatEvidenceExample);
72
+ if (examples.length === 1) {
73
+ return `Possible circular dependency involving ${examples[0]}.`;
74
+ }
75
+ return `Possible circular dependency between ${examples[0]} and ${examples[1]}.`;
76
+ }
77
+ function buildLaymanMessage(issue) {
78
+ if (!issue.evidenceComplete) {
79
+ return "not enough data";
80
+ }
81
+ switch (issue.signal) {
82
+ case "longFunctions":
83
+ return buildLongFunctionMessage(issue);
84
+ case "duplicateBlocks":
85
+ return buildDuplicateMessage(issue);
86
+ case "circularDependencies":
87
+ return buildCircularMessage(issue);
88
+ case "testPresence":
89
+ return "not enough data";
90
+ default: {
91
+ const example = issue.evidence[0];
92
+ if (!example) {
93
+ return "Issue detected, but the evidence list is empty.";
94
+ }
95
+ return `Potential issue spotted around ${formatEvidenceExample(example)}.`;
96
+ }
97
+ }
98
+ }
99
+ function buildActionItem(issue) {
100
+ if (!issue.evidenceComplete) {
101
+ if (issue.signal === "testPresence") {
102
+ return "Add at least one test file to cover critical paths.";
103
+ }
104
+ return null;
105
+ }
106
+ const example = issue.evidence[0];
107
+ switch (issue.signal) {
108
+ case "longFunctions": {
109
+ if (!example) {
110
+ return "Split long functions into smaller helpers.";
111
+ }
112
+ return `Split ${example.file} around lines ${example.startLine}-${example.endLine} into smaller helpers.`;
113
+ }
114
+ case "duplicateBlocks": {
115
+ if (!example) {
116
+ return "Extract repeated logic into a shared helper.";
117
+ }
118
+ return `Extract repeated logic around ${example.file} lines ${example.startLine}-${example.endLine} into a shared helper.`;
119
+ }
120
+ case "circularDependencies": {
121
+ if (!example) {
122
+ return "Break circular dependencies by moving shared code into a lower-level module.";
123
+ }
124
+ return `Break the dependency loop involving ${example.file} (lines ${example.startLine}-${example.endLine}).`;
125
+ }
126
+ case "testPresence":
127
+ return "Add at least one test file to cover critical paths.";
128
+ default:
129
+ if (!example) {
130
+ return "Create a focused refactor for the flagged area.";
131
+ }
132
+ return `Review ${example.file} lines ${example.startLine}-${example.endLine} for a focused refactor.`;
133
+ }
134
+ }
135
+ function formatEvidenceList(issue, limit) {
136
+ if (issue.evidence.length === 0) {
137
+ return " Evidence: none";
138
+ }
139
+ const maxItems = limit === undefined ? 3 : limit <= 0 ? issue.evidence.length : limit;
140
+ const shown = issue.evidence.slice(0, maxItems);
141
+ const lines = shown.map((item) => ` - ${formatEvidenceItem(item)}`);
142
+ const remaining = issue.evidence.length - shown.length;
143
+ if (remaining > 0) {
144
+ lines.push(` - ... +${remaining} more`);
145
+ }
146
+ return [" Evidence:", ...lines].join("\n");
147
+ }
148
+ function formatIssueLine(index, issue, message, showDetails, detailsLimit) {
149
+ const base = `${index + 1}. [${issue.type}] ${message}`;
150
+ if (!showDetails) {
151
+ return base;
152
+ }
153
+ return `${base}\n${formatEvidenceList(issue, detailsLimit)}`;
154
+ }
155
+ function buildGeminiPrompt(config, issues) {
156
+ const payload = issues.map((issue) => ({
157
+ id: issue.id,
158
+ type: issue.type,
159
+ signal: issue.signal,
160
+ confidence: issue.confidence,
161
+ evidenceComplete: issue.evidenceComplete,
162
+ evidence: issue.evidence,
163
+ }));
164
+ return [
165
+ "You are CodeRoast, a strict evidence-only code review narrator.",
166
+ "Use plain, non-technical language that a non-engineer can understand.",
167
+ "Use only the evidence provided in the JSON below.",
168
+ "Do not invent details or add new issues.",
169
+ "If evidenceComplete is false or evidence is empty, output \"not enough data\".",
170
+ `Tone: ${config.severity}. Focus: ${config.focus}.`,
171
+ "Return ONLY valid JSON, no markdown or extra text.",
172
+ "Output format: [{\"id\":1,\"text\":\"...\"}].",
173
+ "Each text must be one sentence and should reference file paths and line ranges.",
174
+ "",
175
+ "Issues JSON:",
176
+ JSON.stringify(payload, null, 2),
177
+ ].join("\n");
178
+ }
179
+ function extractJsonArray(raw) {
180
+ const start = raw.indexOf("[");
181
+ const end = raw.lastIndexOf("]");
182
+ if (start === -1 || end === -1 || end <= start) {
183
+ throw new Error("Gemini response missing JSON array");
184
+ }
185
+ const jsonText = raw.slice(start, end + 1);
186
+ const parsed = JSON.parse(jsonText);
187
+ if (!Array.isArray(parsed)) {
188
+ throw new Error("Gemini response JSON is not an array");
189
+ }
190
+ return parsed;
191
+ }
192
+ function normalizeGeminiText(value) {
193
+ return value.replace(/\s+/g, " ").trim();
194
+ }
195
+ async function runRoastNarratorAgent(config, insights) {
196
+ if (insights.issues.length === 0) {
197
+ return {
198
+ content: `No issues detected for ${config.focus}. Add analyzers to produce evidence-bound findings.`,
199
+ usedGemini: false,
200
+ };
201
+ }
202
+ const fallbackLines = insights.issues.map((issue, index) => formatIssueLine(index, issue, buildLaymanMessage(issue), config.showDetails, config.detailsLimit));
203
+ const actionItems = insights.issues
204
+ .map(buildActionItem)
205
+ .filter((item) => Boolean(item));
206
+ const apiKey = (0, gemini_client_1.getGeminiApiKey)();
207
+ if (!apiKey) {
208
+ return {
209
+ content: fallbackLines.join("\n\n"),
210
+ usedGemini: false,
211
+ actionItems,
212
+ };
213
+ }
214
+ const model = process.env.GEMINI_MODEL ?? "gemini-2.5-flash";
215
+ const issuesWithId = insights.issues.map((issue, index) => ({
216
+ ...issue,
217
+ id: index + 1,
218
+ }));
219
+ try {
220
+ const prompt = buildGeminiPrompt(config, issuesWithId);
221
+ const responseText = await (0, gemini_client_1.callGeminiNarrator)({
222
+ apiKey,
223
+ model,
224
+ prompt,
225
+ });
226
+ const parsed = extractJsonArray(responseText);
227
+ const byId = new Map();
228
+ for (const item of parsed) {
229
+ if (typeof item?.id !== "number" || typeof item?.text !== "string") {
230
+ continue;
231
+ }
232
+ byId.set(item.id, normalizeGeminiText(item.text));
233
+ }
234
+ const lines = issuesWithId.map((issue, index) => {
235
+ if (!issue.evidenceComplete) {
236
+ return formatIssueLine(index, issue, "not enough data", config.showDetails, config.detailsLimit);
237
+ }
238
+ const geminiText = byId.get(issue.id);
239
+ if (!geminiText) {
240
+ return fallbackLines[index];
241
+ }
242
+ if (geminiText.toLowerCase() === "not enough data") {
243
+ return fallbackLines[index];
244
+ }
245
+ return formatIssueLine(index, issue, geminiText, config.showDetails, config.detailsLimit);
246
+ });
247
+ return { content: lines.join("\n\n"), usedGemini: true };
248
+ }
249
+ catch {
250
+ return {
251
+ content: fallbackLines.join("\n\n"),
252
+ usedGemini: false,
253
+ actionItems,
254
+ };
255
+ }
256
+ }
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ require("dotenv/config");
5
+ const pipeline_1 = require("./pipeline");
6
+ (0, pipeline_1.runPipeline)(process.argv.slice(2))
7
+ .then((output) => {
8
+ process.stdout.write(output);
9
+ })
10
+ .catch((error) => {
11
+ const message = error instanceof Error ? error.message : String(error);
12
+ process.stderr.write(`${message}\n`);
13
+ process.exitCode = 1;
14
+ });
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runPipeline = runPipeline;
4
+ const cli_agent_1 = require("./agents/cli-agent");
5
+ const code_analysis_agent_1 = require("./agents/code-analysis-agent");
6
+ const evidence_guard_agent_1 = require("./agents/evidence-guard-agent");
7
+ const fix_apply_agent_1 = require("./agents/fix-apply-agent");
8
+ const fix_it_agent_1 = require("./agents/fix-it-agent");
9
+ const insight_aggregator_agent_1 = require("./agents/insight-aggregator-agent");
10
+ const output_formatter_agent_1 = require("./agents/output-formatter-agent");
11
+ const repo_scanner_agent_1 = require("./agents/repo-scanner-agent");
12
+ const roast_narrator_agent_1 = require("./agents/roast-narrator-agent");
13
+ async function runPipeline(argv) {
14
+ const cliConfig = (0, cli_agent_1.runCliAgent)(argv);
15
+ const scanResult = await (0, repo_scanner_agent_1.runRepoScannerAgent)(cliConfig);
16
+ const analysisResult = await (0, code_analysis_agent_1.runCodeAnalysisAgent)(cliConfig, scanResult);
17
+ const insights = (0, insight_aggregator_agent_1.runInsightAggregatorAgent)(scanResult, analysisResult);
18
+ const guardedInsights = (0, evidence_guard_agent_1.runEvidenceGuardAgent)(insights);
19
+ const fixResult = cliConfig.enableFixes
20
+ ? await (0, fix_it_agent_1.runFixItAgent)(cliConfig, scanResult, analysisResult, guardedInsights)
21
+ : undefined;
22
+ if (fixResult && cliConfig.applyFixes) {
23
+ fixResult.applyResult = await (0, fix_apply_agent_1.runFixApplyAgent)(cliConfig, fixResult);
24
+ }
25
+ const roast = await (0, roast_narrator_agent_1.runRoastNarratorAgent)(cliConfig, guardedInsights);
26
+ const formatted = (0, output_formatter_agent_1.runOutputFormatterAgent)(cliConfig, roast, fixResult, analysisResult);
27
+ return formatted.text;
28
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "coderoast",
3
+ "version": "0.1.0",
4
+ "scripts": {
5
+ "build": "tsc",
6
+ "prepublishOnly": "npm run build",
7
+ "lint": "eslint .",
8
+ "lint:fix": "eslint . --fix",
9
+ "demo:fix": "node scripts/demo-fix.js",
10
+ "demo:judge": "node scripts/demo-judge.js",
11
+ "start": "node dist/index.js",
12
+ "typecheck": "tsc --noEmit",
13
+ "test": "npm run build && node --test tests/**/*.test.js"
14
+ },
15
+ "bin": {
16
+ "coderoast": "dist/index.js"
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "Agents.md",
22
+ "LICENSE"
23
+ ],
24
+ "dependencies": {
25
+ "@google/genai": "^1.38.0",
26
+ "dotenv": "^17.2.3",
27
+ "ignore": "^5.3.1",
28
+ "typescript": "^5.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@eslint/js": "^9.0.0",
32
+ "@types/node": "^20.11.0",
33
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
34
+ "@typescript-eslint/parser": "^8.0.0",
35
+ "eslint": "^9.0.0",
36
+ "globals": "^15.0.0"
37
+ }
38
+ }