Skip to content

Commit 7246692

Browse files
authored
Merge pull request #1 from thinkgrid-labs/dev
feat: implement local RAG phase 1 and v0.3.0 release
2 parents 7d0e718 + b52d65f commit 7246692

9 files changed

Lines changed: 323 additions & 22 deletions

File tree

README.md

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,16 @@ Provide requirements or ticket descriptions to verify that the code actually mee
7878
diffmind --branch develop --context ticket.md
7979
```
8080

81+
### Architectural-Aware Reviews (Local RAG)
82+
Build an index of your project's symbols to allow the AI to "look up" function and type definitions referenced in your diff:
83+
```bash
84+
# 1. Build the index (only needed once or after large changes)
85+
diffmind index
86+
87+
# 2. Run your review — context is automatically retrieved
88+
diffmind --branch develop
89+
```
90+
8191
### Custom Diff (Stdin)
8292
```bash
8393
git diff main...HEAD | diffmind --stdin
@@ -104,11 +114,11 @@ git diff main...HEAD | diffmind --stdin
104114

105115
I am committed to making Diffmind the ultimate local-first AI companion for developers. As a developer, I am building the foundation for a privacy-first engineering future, and I am actively looking for contributors to join this mission!
106116

107-
- [ ] **Custom Rule Engine**: Define project-specific review standards using simple YAML.
108-
- [ ] **VS Code Extension**: Get real-time AI feedback directly in your editor as you code.
109-
- [ ] **Local RAG Integration**: Context-aware reviews that understand your entire repository's architecture.
110-
- [ ] **Multi-Language Support**: Expanding beyond TypeScript to Go, Python, and Rust.
111-
- [ ] **Advanced CI Guards**: Pre-built action templates to block PRs with high-severity findings globally.
117+
- [ ] **Local RAG Integration**: Context-aware reviews that understand your entire repository's architecture by indexing local files.
118+
- [ ] **VS Code Extension**: Real-time AI feedback directly in your editor, transforming code reviews into a fluid, pair-programming experience.
119+
- [ ] **Custom Rule Engine**: Define project-specific review standards and architectural boundaries using simple YAML configurations.
120+
- [ ] **Advanced CI Guards**: Ready-to-use GitHub Action templates to enforce security baselines and block high-severity PRs automatically.
121+
- [ ] **Multi-Language Support**: Expanding the deep-reasoning review capabilities to Go, Python, Rust, and Java.
112122

113123
---
114124

apps/local-cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@diffmind/cli",
3-
"version": "0.2.0",
3+
"version": "0.3.0",
44
"description": "diffmind CLI — local-first AI code review for your git diffs",
55
"author": "Thinkgrid Labs <hello@thinkgrid.com>",
66
"license": "MIT",

apps/local-cli/src/index.ts

Lines changed: 90 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
* Local-first AI code review for your git diffs.
66
* Powered by Qwen2.5-Coder-3B running entirely on-device via WebAssembly.
77
*
8-
* Usage:
9-
* diffmind --branch main
10-
* diffmind --branch develop --format json
118
* git diff main...HEAD | diffmind --stdin
129
*/
1310

11+
import { Indexer } from "./indexer";
1412
import { Command } from "commander";
1513
import chalk from "chalk";
1614
import ora from "ora";
@@ -46,10 +44,17 @@ const TOKENIZER_URL =
4644

4745
const program = new Command();
4846

47+
program
48+
.command("index")
49+
.description("Build a symbol index of the local repository for context-aware reviews")
50+
.action(async () => {
51+
await runIndexer();
52+
});
53+
4954
program
5055
.name("diffmind")
5156
.description("Local-first AI code review for your git diffs")
52-
.version("0.2.0")
57+
.version("0.3.0")
5358
.option("-b, --branch <name>", "Target branch to diff against", "main")
5459
.option(
5560
"-f, --format <type>",
@@ -67,8 +72,26 @@ program
6772
"low"
6873
)
6974
.option("--stdin", "Read git diff from stdin instead of running git diff")
70-
.option("--no-color", "Disable colored output")
71-
.parse(process.argv);
75+
.option("--no-color", "Disable colored output");
76+
77+
// ─── Entry Point ──────────────────────────────────────────────────────────────
78+
79+
function run() {
80+
program.parse(process.argv);
81+
82+
// If no subcommand is used, run the default analysis
83+
if (!program.args.length || program.args[0] !== "index") {
84+
main().catch((err) => {
85+
console.error(chalk.red(`Fatal Error: ${err.message}`));
86+
process.exit(1);
87+
});
88+
}
89+
}
90+
91+
// Only run if executed directly
92+
if (require.main === module) {
93+
run();
94+
}
7295

7396
const opts = program.opts<{
7497
branch: string;
@@ -80,12 +103,10 @@ const opts = program.opts<{
80103
context?: string;
81104
}>();
82105

83-
// ─── Main ─────────────────────────────────────────────────────────────────────
106+
// ─── Main Logic ───────────────────────────────────────────────────────────────
84107

85108
import { Worker } from "worker_threads";
86109

87-
// ─── Main ─────────────────────────────────────────────────────────────────────
88-
89110
async function main(): Promise<void> {
90111
printBanner();
91112

@@ -109,7 +130,13 @@ async function main(): Promise<void> {
109130
}
110131
}
111132

112-
// 4. Run analysis in a background worker
133+
// 4. Retrieve architectural context (Local RAG Phase 1)
134+
const ragContext = await getRagContext(diff);
135+
if (ragContext) {
136+
context = `${context}\n\n### Architectural Reference (Local RAG):\n${ragContext}`;
137+
}
138+
139+
// 5. Run analysis in a background worker
113140
// This keeps the process responsive and the spinner animated.
114141
const analyzeSpinner = ora("Initializing engine & analyzing diff...").start();
115142

@@ -132,7 +159,7 @@ async function main(): Promise<void> {
132159
process.exit(1);
133160
}
134161

135-
// 5. Format and output results
162+
// 6. Format and output results
136163
const output =
137164
opts.format === "json"
138165
? formatJson(report)
@@ -190,6 +217,57 @@ function runAnalysisInWorker(workerData: {
190217
});
191218
}
192219

220+
// ─── Local RAG ────────────────────────────────────────────────────────────────
221+
222+
async function runIndexer(): Promise<void> {
223+
const spinner = ora("Scanning repository for symbols...").start();
224+
try {
225+
const indexer = new Indexer(process.cwd());
226+
const index = await indexer.buildIndex();
227+
indexer.save(index);
228+
spinner.succeed(`Index built: ${Object.keys(index.symbols).length} symbols found`);
229+
} catch (err) {
230+
spinner.fail("Indexing failed");
231+
console.error(chalk.red(String(err)));
232+
process.exit(1);
233+
}
234+
}
235+
236+
async function getRagContext(diff: string): Promise<string | null> {
237+
const index = Indexer.load(process.cwd());
238+
if (!index) return null;
239+
240+
const foundSymbols = new Set<string>();
241+
const lines = diff.split("\n");
242+
243+
// Simple heuristic: find all words that match a known symbol name in added/modified lines
244+
for (const line of lines) {
245+
if (!line.startsWith("+") || line.startsWith("+++")) continue;
246+
247+
const words = line.match(/[a-zA-Z0-9_$]+/g);
248+
if (!words) continue;
249+
250+
for (const word of words) {
251+
if (index.symbols[word]) {
252+
foundSymbols.add(word);
253+
}
254+
}
255+
}
256+
257+
if (foundSymbols.size === 0) return null;
258+
259+
let contexts = "";
260+
// Pick top 10 symbols to avoid prompt overflow
261+
const symbolsToInclude = Array.from(foundSymbols).slice(0, 10);
262+
263+
for (const symName of symbolsToInclude) {
264+
const def = index.symbols[symName];
265+
contexts += `\n--- Symbol: ${symName} (from ${def.file}) ---\n${def.snippet}\n`;
266+
}
267+
268+
return contexts;
269+
}
270+
193271
// ─── Diff Acquisition ─────────────────────────────────────────────────────────
194272

195273
async function getDiff(): Promise<string> {
@@ -395,7 +473,7 @@ export function categoryBadge(category: Category): string {
395473
}
396474

397475
function printBanner(): void {
398-
console.log(chalk.cyan.bold("\n diffmind") + chalk.dim(" v0.2.0 — local-first AI code review"));
476+
console.log(chalk.cyan.bold("\n diffmind") + chalk.dim(" v0.3.0 — local-first AI code review"));
399477
console.log(chalk.dim(" Model: Qwen2.5-Coder-3B-Instruct Q4_K_M | Inference: on-device Wasm\n"));
400478
}
401479

apps/local-cli/src/indexer.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Indexer } from "./indexer";
2+
import * as fs from "fs";
3+
import * as path from "path";
4+
5+
describe("Indexer", () => {
6+
const testRoot = path.join(__dirname, "test-repo");
7+
8+
beforeAll(() => {
9+
if (!fs.existsSync(testRoot)) {
10+
fs.mkdirSync(testRoot, { recursive: true });
11+
}
12+
13+
// Create a mock TypeScript file with various exports
14+
const content = `
15+
export function add(a: number, b: number) { return a + b; }
16+
export async function fetchUser(id: string) { return { id }; }
17+
export class ReviewAnalyzer { analyze() {} }
18+
export interface User { id: string; }
19+
export type Severity = "high" | "low";
20+
export const VERSION = "1.0.0";
21+
`;
22+
fs.writeFileSync(path.join(testRoot, "index.ts"), content);
23+
});
24+
25+
afterAll(() => {
26+
fs.rmSync(testRoot, { recursive: true, force: true });
27+
});
28+
29+
it("should match all supported export types", async () => {
30+
const indexer = new Indexer(testRoot);
31+
const index = await indexer.buildIndex();
32+
33+
expect(index.symbols["add"]).toBeDefined();
34+
expect(index.symbols["add"].type).toBe("function");
35+
36+
expect(index.symbols["fetchUser"]).toBeDefined();
37+
expect(index.symbols["fetchUser"].type).toBe("function");
38+
39+
expect(index.symbols["ReviewAnalyzer"]).toBeDefined();
40+
expect(index.symbols["ReviewAnalyzer"].type).toBe("class");
41+
42+
expect(index.symbols["User"]).toBeDefined();
43+
expect(index.symbols["User"].type).toBe("interface");
44+
45+
expect(index.symbols["Severity"]).toBeDefined();
46+
expect(index.symbols["Severity"].type).toBe("type");
47+
48+
expect(index.symbols["VERSION"]).toBeDefined();
49+
expect(index.symbols["VERSION"].type).toBe("const");
50+
});
51+
52+
it("should include code snippets in the index", async () => {
53+
const indexer = new Indexer(testRoot);
54+
const index = await indexer.buildIndex();
55+
56+
expect(index.symbols["add"].snippet).toContain("function add(a: number, b: number)");
57+
});
58+
});

0 commit comments

Comments
 (0)