Skip to content

Commit 7d0e718

Browse files
author
Dennis P
committed
feat: business context integration & v0.2.0 release
1 parent 6922490 commit 7d0e718

10 files changed

Lines changed: 101 additions & 19 deletions

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ diffmind --branch develop
7272
diffmind --format json --min-severity high
7373
```
7474

75+
### Business-Aware Reviews
76+
Provide requirements or ticket descriptions to verify that the code actually meets the business criteria:
77+
```bash
78+
diffmind --branch develop --context ticket.md
79+
```
80+
7581
### Custom Diff (Stdin)
7682
```bash
7783
git diff main...HEAD | diffmind --stdin
@@ -87,6 +93,7 @@ git diff main...HEAD | diffmind --stdin
8793
|--------|-------------|---------|
8894
| `--branch, -b` | Base branch to compare against | `main` |
8995
| `--format, -f` | Output format (`markdown` or `json`) | `markdown` |
96+
| `--context, -c` | Business context (ticket text or file path) | `""` |
9097
| `--min-severity` | Minimum severity level (`high`, `medium`, `low`) | `low` |
9198
| `--stdin` | Read diff from stdin | `false` |
9299
| `--output, -o` | Write report to a specific file | `stdout` |

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.1.0",
3+
"version": "0.2.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: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,18 @@ const program = new Command();
4949
program
5050
.name("diffmind")
5151
.description("Local-first AI code review for your git diffs")
52-
.version("0.1.0")
52+
.version("0.2.0")
5353
.option("-b, --branch <name>", "Target branch to diff against", "main")
5454
.option(
5555
"-f, --format <type>",
5656
'Output format: "markdown" or "json"',
5757
"markdown"
5858
)
5959
.option("-o, --output <file>", "Write output to a file instead of stdout")
60+
.option(
61+
"-c, --context <text|file>",
62+
"Business context (ticket description, acceptance criteria)"
63+
)
6064
.option(
6165
"--min-severity <level>",
6266
'Minimum severity to report: "high", "medium", or "low"',
@@ -73,6 +77,7 @@ const opts = program.opts<{
7377
minSeverity: Severity;
7478
stdin: boolean;
7579
color: boolean;
80+
context?: string;
7681
}>();
7782

7883
// ─── Main ─────────────────────────────────────────────────────────────────────
@@ -94,7 +99,17 @@ async function main(): Promise<void> {
9499
process.exit(0);
95100
}
96101

97-
// 3. Run analysis in a background worker
102+
// 3. Prepare business context
103+
let context = "";
104+
if (opts.context) {
105+
if (fs.existsSync(opts.context)) {
106+
context = fs.readFileSync(opts.context, "utf-8");
107+
} else {
108+
context = opts.context;
109+
}
110+
}
111+
112+
// 4. Run analysis in a background worker
98113
// This keeps the process responsive and the spinner animated.
99114
const analyzeSpinner = ora("Initializing engine & analyzing diff...").start();
100115

@@ -103,6 +118,7 @@ async function main(): Promise<void> {
103118
modelPath: path.join(MODEL_DIR, MODEL_FILENAME),
104119
tokenizerPath: path.join(MODEL_DIR, TOKENIZER_FILENAME),
105120
diff,
121+
context,
106122
maxTokens: 2048,
107123
});
108124

@@ -116,7 +132,7 @@ async function main(): Promise<void> {
116132
process.exit(1);
117133
}
118134

119-
// 4. Format and output results
135+
// 5. Format and output results
120136
const output =
121137
opts.format === "json"
122138
? formatJson(report)
@@ -140,6 +156,7 @@ function runAnalysisInWorker(workerData: {
140156
modelPath: string;
141157
tokenizerPath: string;
142158
diff: string;
159+
context: string;
143160
maxTokens: number;
144161
}): Promise<string> {
145162
return new Promise((resolve, reject) => {
@@ -378,7 +395,7 @@ export function categoryBadge(category: Category): string {
378395
}
379396

380397
function printBanner(): void {
381-
console.log(chalk.cyan.bold("\n diffmind") + chalk.dim(" v0.1.0 — local-first AI code review"));
398+
console.log(chalk.cyan.bold("\n diffmind") + chalk.dim(" v0.2.0 — local-first AI code review"));
382399
console.log(chalk.dim(" Model: Qwen2.5-Coder-3B-Instruct Q4_K_M | Inference: on-device Wasm\n"));
383400
}
384401

apps/local-cli/src/worker.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ async function runWorker() {
1111
if (!parentPort) return;
1212

1313
try {
14-
const { modelPath, tokenizerPath, diff, maxTokens } = workerData;
14+
const { modelPath, tokenizerPath, diff, context, maxTokens } = workerData;
1515

1616
// 1. Load Wasm bindings
17-
// Note: In Node.js, we require the generated CommonJS bindings from pkg/
1817
const { ReviewAnalyzer } = require("@diffmind/core-wasm");
1918

2019
// 2. Read model files
@@ -23,7 +22,7 @@ async function runWorker() {
2322

2423
// 3. Initialize and run
2524
const analyzer = new ReviewAnalyzer(modelBytes, tokenizerBytes);
26-
const resultJson = analyzer.analyze_diff_chunked(diff, maxTokens);
25+
const resultJson = analyzer.analyze_diff_chunked(diff, context || "", maxTokens);
2726

2827
// 4. Return results
2928
parentPort.postMessage({ success: true, data: resultJson });

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "diffmind",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"private": true,
55
"description": "Local-first AI code review agent — powered by on-device Wasm inference",
66
"author": "Thinkgrid Labs <dennis@thinkgrid.dev>",
@@ -32,6 +32,7 @@
3232
"lint:cli": "pnpm --filter @diffmind/cli build",
3333
"lint": "pnpm build:wasm && pnpm lint:types && pnpm lint:cli",
3434
"test:wasm": "pnpm --filter @diffmind/core-wasm test",
35+
"sync-version": "node scripts/sync-versions.js",
3536
"test:js": "jest",
3637
"test": "pnpm test:js && pnpm test:wasm"
3738
},

packages/core-wasm/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "core-wasm"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
edition = "2021"
55
description = "diffmind Wasm core — on-device security analysis via Qwen2.5-Coder"
66

packages/core-wasm/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@diffmind/core-wasm",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "diffmind Wasm core — compiled from Rust via wasm-pack",
55
"license": "MIT",
66
"author": "Thinkgrid Labs <hello@thinkgrid.com>",

packages/core-wasm/src/lib.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -89,35 +89,36 @@ impl ReviewAnalyzer {
8989
})
9090
}
9191

92-
pub fn analyze_diff(&mut self, diff: &str) -> Result<String, JsError> {
93-
let findings = self.analyze_diff_internal(diff)?;
92+
pub fn analyze_diff(&mut self, diff: &str, context: &str) -> Result<String, JsError> {
93+
let findings = self.analyze_diff_internal(diff, context)?;
9494
serde_json::to_string(&findings)
9595
.map_err(|e| JsError::new(&format!("failed to serialize findings: {e}")))
9696
}
9797

9898
pub fn analyze_diff_chunked(
9999
&mut self,
100100
diff: &str,
101+
context: &str,
101102
_max_tokens_per_chunk: u32,
102103
) -> Result<String, JsError> {
103104
let chunks = split_diff_by_file(diff);
104105
let mut all_findings: Vec<ReviewFinding> = Vec::new();
105106

106107
for chunk in chunks {
107-
let chunk_findings = self.analyze_diff_internal(&chunk)?;
108+
let chunk_findings = self.analyze_diff_internal(&chunk, context)?;
108109
all_findings.extend(chunk_findings);
109110
}
110111

111112
serde_json::to_string(&all_findings)
112113
.map_err(|e| JsError::new(&format!("failed to serialize merged findings: {e}")))
113114
}
114115

115-
fn analyze_diff_internal(&mut self, diff: &str) -> Result<Vec<ReviewFinding>, JsError> {
116+
fn analyze_diff_internal(&mut self, diff: &str, context: &str) -> Result<Vec<ReviewFinding>, JsError> {
116117
if diff.trim().is_empty() {
117118
return Ok(Vec::new());
118119
}
119120

120-
let prompt = self.format_prompt(diff);
121+
let prompt = self.format_prompt(diff, context);
121122
let response = self.generate(&prompt, 1024)?;
122123

123124
// Extract JSON block
@@ -134,9 +135,16 @@ impl ReviewAnalyzer {
134135
}
135136

136137
impl ReviewAnalyzer {
137-
fn format_prompt(&self, diff: &str) -> String {
138+
fn format_prompt(&self, diff: &str, context: &str) -> String {
139+
let context_section = if context.is_empty() {
140+
String::new()
141+
} else {
142+
format!("\n\n### Business Context / Requirements:\n{}\n", context)
143+
};
144+
138145
format!(
139-
"<|im_start|>system\nYou are an expert Senior Software Engineer and Code Reviewer. Analyze the git diff and provide a comprehensive code review for TypeScript, NestJS, and React Native code.\nFocus on:\n1. **Security**: Vulnerabilities, secrets, insecure handling.\n2. **Quality**: Bugs, anti-patterns, logical errors.\n3. **Performance**: Bottlenecks, inefficient code.\n4. **Maintainability**: Readability, naming, complexity.\n\nReturn a JSON array ONLY. Format: [{{ \"file\": \"path\", \"line\": 12, \"severity\": \"high\"|\"medium\"|\"low\", \"category\": \"security\"|\"quality\"|\"performance\"|\"maintainability\", \"issue\": \"description\", \"suggested_fix\": \"code\" }}]\nIf no issues, return [].<|im_end|>\n<|im_start|>user\nAnalyze this diff:\n{}\n<|im_end|>\n<|im_start|>assistant\n",
146+
"<|im_start|>system\nYou are an expert Senior Software Engineer and Code Reviewer. Analyze the git diff and provide a comprehensive code review for TypeScript, NestJS, and React Native code.{} \n\nFocus on:\n1. **Security**: Vulnerabilities, secrets, insecure handling.\n2. **Quality**: Bugs, anti-patterns, logical errors.\n3. **Performance**: Bottlenecks, inefficient code.\n4. **Maintainability**: Readability, naming, complexity.\n\nReturn a JSON array ONLY. Format: [{{ \"file\": \"path\", \"line\": 12, \"severity\": \"high\"|\"medium\"|\"low\", \"category\": \"security\"|\"quality\"|\"performance\"|\"maintainability\", \"issue\": \"description\", \"suggested_fix\": \"code\" }}]\nIf no issues, return [].<|im_end|>\n<|im_start|>user\nAnalyze this diff:\n{}\n<|im_end|>\n<|im_start|>assistant\n",
147+
context_section,
140148
diff
141149
)
142150
}

packages/shared-types/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@diffmind/shared-types",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"description": "Shared TypeScript types for diffmind review findings",
55
"license": "MIT",
66
"author": "Thinkgrid Labs <hello@thinkgrid.com>",

scripts/sync-versions.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const version = process.argv[2];
5+
6+
if (!version) {
7+
console.error('Error: Please provide a version number (e.g., 0.2.0)');
8+
process.exit(1);
9+
}
10+
11+
// Clean version string (remove 'v' prefix if present)
12+
const cleanVersion = version.startsWith('v') ? version.slice(1) : version;
13+
14+
console.log(`Syncing version to v${cleanVersion}...`);
15+
16+
const filesToUpdate = [
17+
'package.json',
18+
'apps/local-cli/package.json',
19+
'packages/shared-types/package.json',
20+
'packages/core-wasm/package.json',
21+
'packages/core-wasm/Cargo.toml',
22+
'apps/local-cli/src/index.ts',
23+
];
24+
25+
filesToUpdate.forEach(file => {
26+
const absolutePath = path.join(__dirname, '..', file);
27+
if (!fs.existsSync(absolutePath)) {
28+
console.warn(`Warning: File not found - ${file}`);
29+
return;
30+
}
31+
32+
let content = fs.readFileSync(absolutePath, 'utf8');
33+
34+
if (file.endsWith('package.json')) {
35+
// Update JSON version field
36+
content = content.replace(/"version":\s*"[^"]+"/, `"version": "${cleanVersion}"`);
37+
} else if (file.endsWith('Cargo.toml')) {
38+
// Update TOML version field (usually under [package])
39+
content = content.replace(/^version\s*=\s*"[^"]+"/m, `version = "${cleanVersion}"`);
40+
} else if (file.endsWith('src/index.ts')) {
41+
// Update CLI hardcoded version string
42+
content = content.replace(/version\("[^"]+"\)/, `version("${cleanVersion}")`);
43+
content = content.replace(/v[0-9]+\.[0-9]+\.[0-9]+/, `v${cleanVersion}`);
44+
}
45+
46+
fs.writeFileSync(absolutePath, content, 'utf8');
47+
console.log(`✓ Updated ${file}`);
48+
});
49+
50+
console.log('\nSuccess! All versions synced to v' + cleanVersion);

0 commit comments

Comments
 (0)