ownkey 1.1.2 → 1.3.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/README.md +43 -0
- package/dist/ai/factory.d.ts +3 -1
- package/dist/ai/factory.d.ts.map +1 -1
- package/dist/ai/factory.js +5 -5
- package/dist/ai/factory.js.map +1 -1
- package/dist/bench/cases.d.ts +25 -0
- package/dist/bench/cases.d.ts.map +1 -0
- package/dist/bench/cases.js +142 -0
- package/dist/bench/cases.js.map +1 -0
- package/dist/bench/score.d.ts +46 -0
- package/dist/bench/score.d.ts.map +1 -0
- package/dist/bench/score.js +61 -0
- package/dist/bench/score.js.map +1 -0
- package/dist/commands/bench.d.ts +9 -0
- package/dist/commands/bench.d.ts.map +1 -0
- package/dist/commands/bench.js +169 -0
- package/dist/commands/bench.js.map +1 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +5 -24
- package/dist/commands/review.js.map +1 -1
- package/dist/core/review/run.d.ts +20 -0
- package/dist/core/review/run.d.ts.map +1 -0
- package/dist/core/review/run.js +32 -0
- package/dist/core/review/run.js.map +1 -0
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/ui/App.d.ts +17 -0
- package/dist/ui/App.d.ts.map +1 -0
- package/dist/ui/App.js +109 -0
- package/dist/ui/App.js.map +1 -0
- package/dist/ui/complete.d.ts +22 -0
- package/dist/ui/complete.d.ts.map +1 -0
- package/dist/ui/complete.js +41 -0
- package/dist/ui/complete.js.map +1 -0
- package/dist/ui/context.d.ts +11 -0
- package/dist/ui/context.d.ts.map +1 -0
- package/dist/ui/context.js +37 -0
- package/dist/ui/context.js.map +1 -0
- package/dist/ui/session.d.ts +3 -0
- package/dist/ui/session.d.ts.map +1 -0
- package/dist/ui/session.js +114 -0
- package/dist/ui/session.js.map +1 -0
- package/dist/ui/words.d.ts +6 -0
- package/dist/ui/words.d.ts.map +1 -0
- package/dist/ui/words.js +46 -0
- package/dist/ui/words.js.map +1 -0
- package/package.json +16 -12
package/README.md
CHANGED
|
@@ -102,16 +102,33 @@ ownkey undo --list # recent applies
|
|
|
102
102
|
|
|
103
103
|
For a whole-project audit of any folder, git or not, use `ownkey suggest .`
|
|
104
104
|
|
|
105
|
+
## Interactive session
|
|
106
|
+
|
|
107
|
+
Run `ownkey` with no arguments in any project folder and it opens a session for that folder. The header shows the repository, branch, how many files changed, and which model is reviewing. Then type any command without the `ownkey` prefix:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
› review --staged
|
|
111
|
+
› apply 3 --verify "npm test"
|
|
112
|
+
› undo --last
|
|
113
|
+
› bench --case off-by-one
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Tab completes commands and their flags, the arrow keys walk your history (kept in `~/.ownkey/history`), `help` lists every command, and Ctrl+D leaves. Commands run exactly as they do from the shell, including interactive prompts like the `config` wizard. Ctrl+C stops the running command without closing the session.
|
|
117
|
+
|
|
118
|
+
Outside an interactive terminal (pipes, scripts, CI), or with `OWNKEY_NO_UI=1` set, a bare `ownkey` prints help as before. Every subcommand behaves the same with or without the session.
|
|
119
|
+
|
|
105
120
|
## Commands
|
|
106
121
|
|
|
107
122
|
| Command | Purpose |
|
|
108
123
|
|---|---|
|
|
124
|
+
| `ownkey` | Open the interactive session for the current folder. |
|
|
109
125
|
| `ownkey config` | Set the AI provider, model, endpoint, and optional database. Run with no flags for an interactive wizard. |
|
|
110
126
|
| `ownkey review [path]` | Review git changes: staged, uncommitted, or a branch against its base. |
|
|
111
127
|
| `ownkey suggest [path]` | Audit a whole folder and store suggestions. |
|
|
112
128
|
| `ownkey scan [path]` | List the files OwnKey would analyze, honoring `.gitignore` and `.ownkeyignore`. |
|
|
113
129
|
| `ownkey apply [id]` | Apply one suggestion, or all pending ones with `--all`, optionally verified. |
|
|
114
130
|
| `ownkey undo [backup-id]` | Restore files from a backup created by `apply`. |
|
|
131
|
+
| `ownkey bench` | Measure review quality on seeded-bug cases with your model. |
|
|
115
132
|
|
|
116
133
|
<details>
|
|
117
134
|
<summary><strong>Full option reference</strong></summary>
|
|
@@ -147,6 +164,17 @@ For a whole-project audit of any folder, git or not, use `ownkey suggest .`
|
|
|
147
164
|
|
|
148
165
|
Exit codes: `0` success, `1` the `--fail-on` gate tripped, `2` the review could not run.
|
|
149
166
|
|
|
167
|
+
#### `ownkey bench`
|
|
168
|
+
|
|
169
|
+
| Option | Description |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `--list` | List the cases and exit. |
|
|
172
|
+
| `--case <ids>` | Comma-separated case ids to run. Default: all. |
|
|
173
|
+
| `--model <model>` | Use this model for the run instead of the configured one. |
|
|
174
|
+
| `--format <format>` | `text` (default), `json`, or `markdown`. |
|
|
175
|
+
| `--output <file>` | Write the report to a file. |
|
|
176
|
+
| `--min-recall <ratio>` | Exit with code 1 when recall is below this ratio, from `0` to `1`. |
|
|
177
|
+
|
|
150
178
|
#### `ownkey scan [path]`
|
|
151
179
|
|
|
152
180
|
| Option | Description |
|
|
@@ -190,6 +218,21 @@ Exit codes: `0` success, `1` the `--fail-on` gate tripped, `2` the review could
|
|
|
190
218
|
|
|
191
219
|
</details>
|
|
192
220
|
|
|
221
|
+
## Measuring review quality
|
|
222
|
+
|
|
223
|
+
`ownkey bench` shows how well a model reviews before you trust it with your pull requests. It runs a fixed set of small cases, each a throwaway git repository with one staged change:
|
|
224
|
+
|
|
225
|
+
- **Bug cases** plant one real defect: an off-by-one, SQL injection, a missing `await`, a null dereference, a hardcoded secret, path traversal, command injection, an inverted auth check, a mutable default argument, a numeric sort with the string comparator, a switch fallthrough, and `is` used to compare strings. They cover JavaScript, TypeScript, and Python.
|
|
226
|
+
- **Control cases** are correct changes. Anything reported there at medium severity or above is a false alarm.
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
ownkey bench # all cases with your configured model
|
|
230
|
+
ownkey bench --model claude-opus-5-5 # compare another model on the same key
|
|
231
|
+
ownkey bench --format markdown --output bench.md
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
It reports **recall** (planted bugs reported within 3 lines of the defect with a matching type), **precision** (matching findings over all findings at medium or above), and how many controls stayed clean. Low and info findings never count against a model. Bench runs in temporary directories and stores nothing, but it does send every case to your provider, so a full run costs about 15 small review calls.
|
|
235
|
+
|
|
193
236
|
## Providers
|
|
194
237
|
|
|
195
238
|
| Provider | Setup | Default model |
|
package/dist/ai/factory.d.ts
CHANGED
|
@@ -6,7 +6,9 @@ export declare class AIProviderFactory {
|
|
|
6
6
|
/**
|
|
7
7
|
* Create an AI provider instance based on configuration
|
|
8
8
|
*/
|
|
9
|
-
static createProvider(
|
|
9
|
+
static createProvider(overrides?: {
|
|
10
|
+
model?: string;
|
|
11
|
+
}): Promise<AIProvider>;
|
|
10
12
|
/**
|
|
11
13
|
* Test provider connection
|
|
12
14
|
*/
|
package/dist/ai/factory.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/ai/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAU3C;;GAEG;AACH,qBAAa,iBAAiB;IAC1B;;OAEG;WACU,cAAc,
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/ai/factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAU3C;;GAEG;AACH,qBAAa,iBAAiB;IAC1B;;OAEG;WACU,cAAc,CAAC,SAAS,GAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IA6CpF;;OAEG;WACU,YAAY,CAAC,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC;IAgBjE;;OAEG;IACH,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;IAepD;;OAEG;WACU,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;CAmBvF"}
|
package/dist/ai/factory.js
CHANGED
|
@@ -13,7 +13,7 @@ export class AIProviderFactory {
|
|
|
13
13
|
/**
|
|
14
14
|
* Create an AI provider instance based on configuration
|
|
15
15
|
*/
|
|
16
|
-
static async createProvider() {
|
|
16
|
+
static async createProvider(overrides = {}) {
|
|
17
17
|
const config = await configManager.load();
|
|
18
18
|
if (!config.activeProvider) {
|
|
19
19
|
throw new ConfigError('No active AI provider configured. Run: ownkey config');
|
|
@@ -26,23 +26,23 @@ export class AIProviderFactory {
|
|
|
26
26
|
if (!apiKey) {
|
|
27
27
|
throw new ConfigError('Gemini API key not found. Run: ownkey config');
|
|
28
28
|
}
|
|
29
|
-
const geminiModel = config.providers.gemini?.model || GeminiProvider.getRecommendedModel();
|
|
29
|
+
const geminiModel = overrides.model || config.providers.gemini?.model || GeminiProvider.getRecommendedModel();
|
|
30
30
|
return new GeminiProvider(apiKey, geminiModel);
|
|
31
31
|
case 'openai':
|
|
32
32
|
if (!apiKey) {
|
|
33
33
|
throw new ConfigError('OpenAI API key not found. Run: ownkey config');
|
|
34
34
|
}
|
|
35
|
-
const openaiModel = config.providers.openai?.model || DEFAULT_MODELS.openai;
|
|
35
|
+
const openaiModel = overrides.model || config.providers.openai?.model || DEFAULT_MODELS.openai;
|
|
36
36
|
return new OpenAIProvider(apiKey, openaiModel, config.providers.openai?.endpoint);
|
|
37
37
|
case 'anthropic':
|
|
38
38
|
if (!apiKey) {
|
|
39
39
|
throw new ConfigError('Anthropic API key not found. Run: ownkey config');
|
|
40
40
|
}
|
|
41
|
-
const anthropicModel = config.providers.anthropic?.model || DEFAULT_MODELS.anthropic;
|
|
41
|
+
const anthropicModel = overrides.model || config.providers.anthropic?.model || DEFAULT_MODELS.anthropic;
|
|
42
42
|
return new AnthropicProvider(apiKey, anthropicModel);
|
|
43
43
|
case 'ollama':
|
|
44
44
|
// Ollama doesn't require API key - runs locally
|
|
45
|
-
const ollamaModel = config.providers.ollama?.model || DEFAULT_MODELS.ollama;
|
|
45
|
+
const ollamaModel = overrides.model || config.providers.ollama?.model || DEFAULT_MODELS.ollama;
|
|
46
46
|
const ollamaBaseUrl = config.providers.ollama?.endpoint || 'http://localhost:11434';
|
|
47
47
|
return new OllamaProvider(undefined, ollamaModel, ollamaBaseUrl);
|
|
48
48
|
default:
|
package/dist/ai/factory.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/ai/factory.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C;;GAEG;AACH,MAAM,OAAO,iBAAiB;IAC1B;;OAEG;IACH,MAAM,CAAC,KAAK,CAAC,cAAc;
|
|
1
|
+
{"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/ai/factory.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SuggestionType } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Seeded-bug benchmark cases.
|
|
4
|
+
*
|
|
5
|
+
* Each case is a tiny repository: `base` is committed, then `changed` is staged and reviewed.
|
|
6
|
+
* A bug case plants exactly one defect in the change; `expect.needle` is a substring of the
|
|
7
|
+
* line that carries it. A control case is a correct change: anything the reviewer reports
|
|
8
|
+
* there at medium severity or above counts as a false alarm.
|
|
9
|
+
*/
|
|
10
|
+
export interface BenchCase {
|
|
11
|
+
id: string;
|
|
12
|
+
title: string;
|
|
13
|
+
language: string;
|
|
14
|
+
file: string;
|
|
15
|
+
base: string;
|
|
16
|
+
changed: string;
|
|
17
|
+
/** Absent for control cases */
|
|
18
|
+
expect?: {
|
|
19
|
+
needle: string;
|
|
20
|
+
/** Finding types that count as a catch */
|
|
21
|
+
types: SuggestionType[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export declare const BENCH_CASES: BenchCase[];
|
|
25
|
+
//# sourceMappingURL=cases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cases.d.ts","sourceRoot":"","sources":["../../src/bench/cases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,+BAA+B;IAC/B,MAAM,CAAC,EAAE;QACL,MAAM,EAAE,MAAM,CAAC;QACf,0CAA0C;QAC1C,KAAK,EAAE,cAAc,EAAE,CAAC;KAC3B,CAAC;CACL;AAOD,eAAO,MAAM,WAAW,EAAE,SAAS,EA4VlC,CAAC"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
const lines = (...l) => l.join('\n') + '\n';
|
|
2
|
+
/** Fake, non-working payment key for the hardcoded-secret case. */
|
|
3
|
+
const FAKE_LIVE_KEY = ['sk', 'live', '51Hx9aQ2eZvKYlo2C8b7fW3mN0pR4tUv'].join('_');
|
|
4
|
+
export const BENCH_CASES = [
|
|
5
|
+
{
|
|
6
|
+
id: 'off-by-one',
|
|
7
|
+
title: 'Loop reads one past the end of the array',
|
|
8
|
+
language: 'javascript',
|
|
9
|
+
file: 'src/cart.js',
|
|
10
|
+
base: lines('export function subtotal(items) {', ' return 0;', '}'),
|
|
11
|
+
changed: lines('export function subtotal(items) {', ' let total = 0;', ' for (let i = 0; i <= items.length; i++) {', ' total += items[i].price * items[i].qty;', ' }', ' return total;', '}'),
|
|
12
|
+
expect: { needle: 'i <= items.length', types: ['bug'] },
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'sql-injection',
|
|
16
|
+
title: 'User input concatenated into a SQL query',
|
|
17
|
+
language: 'typescript',
|
|
18
|
+
file: 'src/users.ts',
|
|
19
|
+
base: lines("import { db } from './db';", '', 'export async function findUser(id: number) {', " return db.query('SELECT id, email FROM users WHERE id = $1', [id]);", '}'),
|
|
20
|
+
changed: lines("import { db } from './db';", '', 'export async function findUser(id: number) {', " return db.query('SELECT id, email FROM users WHERE id = $1', [id]);", '}', '', 'export async function searchUsers(name: string) {', " return db.query(`SELECT id, email FROM users WHERE name LIKE '%${name}%'`);", '}'),
|
|
21
|
+
expect: { needle: "LIKE '%${name}%'", types: ['security'] },
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'missing-await',
|
|
25
|
+
title: 'Async call not awaited inside try/catch',
|
|
26
|
+
language: 'typescript',
|
|
27
|
+
file: 'src/orders.ts',
|
|
28
|
+
base: lines("import { saveOrder } from './store';", '', 'export async function placeOrder(order: { id: string }) {', ' await saveOrder(order);', ' return { ok: true };', '}'),
|
|
29
|
+
changed: lines("import { saveOrder } from './store';", "import { log } from './log';", '', 'export async function placeOrder(order: { id: string }) {', ' try {', ' saveOrder(order);', ' return { ok: true };', ' } catch (err) {', " log.error('order failed', err);", ' return { ok: false };', ' }', '}'),
|
|
30
|
+
expect: { needle: ' saveOrder(order);', types: ['bug'] },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'null-deref',
|
|
34
|
+
title: 'Optional lookup result used without a check',
|
|
35
|
+
language: 'typescript',
|
|
36
|
+
file: 'src/profile.ts',
|
|
37
|
+
base: lines('interface User { id: string; name: string; address?: { city: string } }', '', 'export function displayName(user: User): string {', ' return user.name;', '}'),
|
|
38
|
+
changed: lines('interface User { id: string; name: string; address?: { city: string } }', '', 'export function displayName(user: User): string {', ' return user.name;', '}', '', 'export function cityLabel(users: User[], id: string): string {', ' const user = users.find((u) => u.id === id);', ' return `${user.name} (${user.address.city})`;', '}'),
|
|
39
|
+
expect: { needle: '${user.name} (${user.address.city})', types: ['bug'] },
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'hardcoded-secret',
|
|
43
|
+
title: 'Live API key committed in source',
|
|
44
|
+
language: 'python',
|
|
45
|
+
file: 'app/payments.py',
|
|
46
|
+
base: lines('import os', 'import requests', '', 'def charge(amount_cents, token):', " key = os.environ['PAYMENTS_KEY']", " return requests.post('https://api.payments.example/charge', auth=(key, ''), data={'amount': amount_cents, 'source': token})"),
|
|
47
|
+
changed: lines('import requests', '',
|
|
48
|
+
// Assembled at runtime so this fake key never appears whole in OwnKey's own source
|
|
49
|
+
// (secret scanners would flag it); the case file on disk gets the full value.
|
|
50
|
+
`PAYMENTS_KEY = '${FAKE_LIVE_KEY}'`, '', 'def charge(amount_cents, token):', " return requests.post('https://api.payments.example/charge', auth=(PAYMENTS_KEY, ''), data={'amount': amount_cents, 'source': token})"),
|
|
51
|
+
expect: { needle: "PAYMENTS_KEY = '", types: ['security'] },
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'path-traversal',
|
|
55
|
+
title: 'Request parameter joined into a file path',
|
|
56
|
+
language: 'javascript',
|
|
57
|
+
file: 'src/files.js',
|
|
58
|
+
base: lines("import express from 'express';", '', 'export const router = express.Router();'),
|
|
59
|
+
changed: lines("import express from 'express';", "import path from 'path';", '', 'export const router = express.Router();', "const UPLOADS = path.resolve('uploads');", '', "router.get('/download', (req, res) => {", ' const file = path.join(UPLOADS, req.query.name);', ' res.sendFile(file);', '});'),
|
|
60
|
+
expect: { needle: 'path.join(UPLOADS, req.query.name)', types: ['security'] },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'command-injection',
|
|
64
|
+
title: 'Shell command built from user input',
|
|
65
|
+
language: 'javascript',
|
|
66
|
+
file: 'src/thumbs.js',
|
|
67
|
+
base: lines("import { execFile } from 'child_process';", '', 'export function thumbnail(src, out, cb) {', " execFile('convert', [src, '-resize', '200x200', out], cb);", '}'),
|
|
68
|
+
changed: lines("import { exec } from 'child_process';", '', 'export function thumbnail(src, out, size, cb) {', ' exec(`convert ${src} -resize ${size} ${out}`, cb);', '}'),
|
|
69
|
+
expect: { needle: 'exec(`convert ${src}', types: ['security'] },
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: 'inverted-auth',
|
|
73
|
+
title: 'Authorization check inverted',
|
|
74
|
+
language: 'typescript',
|
|
75
|
+
file: 'src/admin.ts',
|
|
76
|
+
base: lines("import type { Request, Response, NextFunction } from 'express';", '', 'export function requireAdmin(req: Request, res: Response, next: NextFunction) {', " res.status(403).end();", '}'),
|
|
77
|
+
changed: lines("import type { Request, Response, NextFunction } from 'express';", '', 'export function requireAdmin(req: Request, res: Response, next: NextFunction) {', ' const user = (req as any).user;', ' if (!user || !user.isAdmin) {', ' return next();', ' }', " res.status(403).json({ error: 'forbidden' });", '}'),
|
|
78
|
+
expect: { needle: 'if (!user || !user.isAdmin)', types: ['security', 'bug'] },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: 'mutable-default',
|
|
82
|
+
title: 'Mutable default argument shared across calls',
|
|
83
|
+
language: 'python',
|
|
84
|
+
file: 'app/tags.py',
|
|
85
|
+
base: lines('def normalize(tag):', ' return tag.strip().lower()'),
|
|
86
|
+
changed: lines('def normalize(tag):', ' return tag.strip().lower()', '', 'def add_tag(tag, tags=[]):', ' tags.append(normalize(tag))', ' return tags'),
|
|
87
|
+
expect: { needle: 'def add_tag(tag, tags=[])', types: ['bug'] },
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
id: 'numeric-sort',
|
|
91
|
+
title: 'Numbers sorted with the default string comparator',
|
|
92
|
+
language: 'javascript',
|
|
93
|
+
file: 'src/stats.js',
|
|
94
|
+
base: lines('export function mean(values) {', ' return values.reduce((a, b) => a + b, 0) / values.length;', '}'),
|
|
95
|
+
changed: lines('export function mean(values) {', ' return values.reduce((a, b) => a + b, 0) / values.length;', '}', '', 'export function median(values) {', ' const sorted = [...values].sort();', ' const mid = Math.floor(sorted.length / 2);', ' return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;', '}'),
|
|
96
|
+
expect: { needle: '[...values].sort()', types: ['bug'] },
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'switch-fallthrough',
|
|
100
|
+
title: 'Missing break makes one case fall into the next',
|
|
101
|
+
language: 'javascript',
|
|
102
|
+
file: 'src/pricing.js',
|
|
103
|
+
base: lines('export function discount(tier) {', ' return 0;', '}'),
|
|
104
|
+
changed: lines('export function discount(tier) {', ' let pct = 0;', ' switch (tier) {', " case 'gold':", ' pct = 20;', " case 'silver':", ' pct = 10;', ' break;', ' default:', ' pct = 0;', ' }', ' return pct;', '}'),
|
|
105
|
+
expect: { needle: 'pct = 20;', types: ['bug'] },
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: 'string-identity',
|
|
109
|
+
title: 'String compared with `is` instead of `==`',
|
|
110
|
+
language: 'python',
|
|
111
|
+
file: 'app/roles.py',
|
|
112
|
+
base: lines('def can_edit(user):', ' return False'),
|
|
113
|
+
changed: lines('def can_edit(user):', " role = user.get('role', '').lower()", " return role is 'editor' or role is 'admin'"),
|
|
114
|
+
expect: { needle: "role is 'editor'", types: ['bug'] },
|
|
115
|
+
},
|
|
116
|
+
// Controls: correct changes. Reports here at medium or above are false alarms.
|
|
117
|
+
{
|
|
118
|
+
id: 'clean-refactor',
|
|
119
|
+
title: 'Control: loop rewritten as reduce',
|
|
120
|
+
language: 'javascript',
|
|
121
|
+
file: 'src/cart.js',
|
|
122
|
+
base: lines('export function subtotal(items) {', ' let total = 0;', ' for (let i = 0; i < items.length; i++) {', ' total += items[i].price * items[i].qty;', ' }', ' return total;', '}'),
|
|
123
|
+
changed: lines('export function subtotal(items) {', ' return items.reduce((total, item) => total + item.price * item.qty, 0);', '}'),
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'clean-feature',
|
|
127
|
+
title: 'Control: parameterized query added',
|
|
128
|
+
language: 'typescript',
|
|
129
|
+
file: 'src/users.ts',
|
|
130
|
+
base: lines("import { db } from './db';", '', 'export async function findUser(id: number) {', " return db.query('SELECT id, email FROM users WHERE id = $1', [id]);", '}'),
|
|
131
|
+
changed: lines("import { db } from './db';", '', 'export async function findUser(id: number) {', " return db.query('SELECT id, email FROM users WHERE id = $1', [id]);", '}', '', 'export async function findByEmail(email: string) {', " return db.query('SELECT id, email FROM users WHERE email = $1', [email.trim().toLowerCase()]);", '}'),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'clean-guard',
|
|
135
|
+
title: 'Control: null check added',
|
|
136
|
+
language: 'typescript',
|
|
137
|
+
file: 'src/profile.ts',
|
|
138
|
+
base: lines('interface User { id: string; name: string }', '', 'export function greet(user: User): string {', ' return `Hello, ${user.name}`;', '}'),
|
|
139
|
+
changed: lines('interface User { id: string; name: string }', '', 'export function greet(user: User | undefined): string {', " if (!user) return 'Hello, guest';", ' return `Hello, ${user.name}`;', '}'),
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
//# sourceMappingURL=cases.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cases.js","sourceRoot":"","sources":["../../src/bench/cases.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Severity } from '../types/index.js';
|
|
2
|
+
import type { BenchCase } from './cases.js';
|
|
3
|
+
/** A reported finding, reduced to what scoring needs. */
|
|
4
|
+
export interface BenchFinding {
|
|
5
|
+
filePath: string;
|
|
6
|
+
line: number;
|
|
7
|
+
type: string;
|
|
8
|
+
severity: Severity;
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
export interface CaseResult {
|
|
12
|
+
id: string;
|
|
13
|
+
title: string;
|
|
14
|
+
kind: 'bug' | 'control';
|
|
15
|
+
/** Bug cases: the planted defect was reported */
|
|
16
|
+
caught: boolean;
|
|
17
|
+
/** Findings that matched the planted defect */
|
|
18
|
+
matched: number;
|
|
19
|
+
/** Unmatched findings at medium or above (false alarms on controls, noise on bug cases) */
|
|
20
|
+
noise: number;
|
|
21
|
+
findings: BenchFinding[];
|
|
22
|
+
error?: string;
|
|
23
|
+
durationMs: number;
|
|
24
|
+
}
|
|
25
|
+
export interface BenchSummary {
|
|
26
|
+
bugCases: number;
|
|
27
|
+
caught: number;
|
|
28
|
+
/** caught / bugCases */
|
|
29
|
+
recall: number;
|
|
30
|
+
/** matched findings / (matched + noise) over all cases; 1 when nothing was reported */
|
|
31
|
+
precision: number;
|
|
32
|
+
controlCases: number;
|
|
33
|
+
/** Control cases with at least one false alarm */
|
|
34
|
+
controlsFlagged: number;
|
|
35
|
+
errors: number;
|
|
36
|
+
}
|
|
37
|
+
/** How far (in lines) a finding may sit from the planted line and still count. */
|
|
38
|
+
export declare const LINE_TOLERANCE = 3;
|
|
39
|
+
/** Noise threshold: low/info nits never count against a reviewer. */
|
|
40
|
+
export declare const NOISE_SEVERITY: Severity;
|
|
41
|
+
/** 1-based line of the planted defect in the changed file. */
|
|
42
|
+
export declare function plantedLine(c: BenchCase): number;
|
|
43
|
+
export declare function matchesPlanted(c: BenchCase, f: BenchFinding): boolean;
|
|
44
|
+
export declare function scoreCase(c: BenchCase, findings: BenchFinding[], durationMs?: number, error?: string): CaseResult;
|
|
45
|
+
export declare function summarize(results: CaseResult[]): BenchSummary;
|
|
46
|
+
//# sourceMappingURL=score.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.d.ts","sourceRoot":"","sources":["../../src/bench/score.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,yDAAyD;AACzD,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,KAAK,GAAG,SAAS,CAAC;IACxB,iDAAiD;IACjD,MAAM,EAAE,OAAO,CAAC;IAChB,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,wBAAwB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,uFAAuF;IACvF,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAClB;AAED,kFAAkF;AAClF,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,qEAAqE;AACrE,eAAO,MAAM,cAAc,EAAE,QAAmB,CAAC;AAEjD,8DAA8D;AAC9D,wBAAgB,WAAW,CAAC,CAAC,EAAE,SAAS,GAAG,MAAM,CAKhD;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,YAAY,GAAG,OAAO,CAKrE;AAED,wBAAgB,SAAS,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,EAAE,UAAU,SAAI,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,CAkB5G;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,UAAU,EAAE,GAAG,YAAY,CAe7D"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { meetsSeverity } from '../utils/severity.js';
|
|
2
|
+
/** How far (in lines) a finding may sit from the planted line and still count. */
|
|
3
|
+
export const LINE_TOLERANCE = 3;
|
|
4
|
+
/** Noise threshold: low/info nits never count against a reviewer. */
|
|
5
|
+
export const NOISE_SEVERITY = 'medium';
|
|
6
|
+
/** 1-based line of the planted defect in the changed file. */
|
|
7
|
+
export function plantedLine(c) {
|
|
8
|
+
if (!c.expect)
|
|
9
|
+
return 0;
|
|
10
|
+
const idx = c.changed.split('\n').findIndex((l) => l.includes(c.expect.needle));
|
|
11
|
+
if (idx === -1)
|
|
12
|
+
throw new Error(`Bench case ${c.id}: needle not found in changed file`);
|
|
13
|
+
return idx + 1;
|
|
14
|
+
}
|
|
15
|
+
export function matchesPlanted(c, f) {
|
|
16
|
+
if (!c.expect)
|
|
17
|
+
return false;
|
|
18
|
+
if (f.filePath !== c.file)
|
|
19
|
+
return false;
|
|
20
|
+
if (Math.abs(f.line - plantedLine(c)) > LINE_TOLERANCE)
|
|
21
|
+
return false;
|
|
22
|
+
return c.expect.types.includes(f.type);
|
|
23
|
+
}
|
|
24
|
+
export function scoreCase(c, findings, durationMs = 0, error) {
|
|
25
|
+
let matched = 0;
|
|
26
|
+
let noise = 0;
|
|
27
|
+
for (const f of findings) {
|
|
28
|
+
if (matchesPlanted(c, f))
|
|
29
|
+
matched++;
|
|
30
|
+
else if (meetsSeverity(f.severity, NOISE_SEVERITY))
|
|
31
|
+
noise++;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
id: c.id,
|
|
35
|
+
title: c.title,
|
|
36
|
+
kind: c.expect ? 'bug' : 'control',
|
|
37
|
+
caught: matched > 0,
|
|
38
|
+
matched,
|
|
39
|
+
noise,
|
|
40
|
+
findings,
|
|
41
|
+
error,
|
|
42
|
+
durationMs,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function summarize(results) {
|
|
46
|
+
const bugs = results.filter((r) => r.kind === 'bug');
|
|
47
|
+
const controls = results.filter((r) => r.kind === 'control');
|
|
48
|
+
const caught = bugs.filter((r) => r.caught).length;
|
|
49
|
+
const matched = results.reduce((n, r) => n + r.matched, 0);
|
|
50
|
+
const noise = results.reduce((n, r) => n + r.noise, 0);
|
|
51
|
+
return {
|
|
52
|
+
bugCases: bugs.length,
|
|
53
|
+
caught,
|
|
54
|
+
recall: bugs.length ? caught / bugs.length : 0,
|
|
55
|
+
precision: matched + noise ? matched / (matched + noise) : 1,
|
|
56
|
+
controlCases: controls.length,
|
|
57
|
+
controlsFlagged: controls.filter((r) => r.noise > 0).length,
|
|
58
|
+
errors: results.filter((r) => r.error).length,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=score.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"score.js","sourceRoot":"","sources":["../../src/bench/score.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { AIProvider } from '../ai/provider.js';
|
|
3
|
+
import { type BenchCase } from '../bench/cases.js';
|
|
4
|
+
import { type BenchSummary, type CaseResult } from '../bench/score.js';
|
|
5
|
+
export declare const benchCommand: Command;
|
|
6
|
+
/** Build a throwaway repo for one case, stage the change, and review it. Nothing is persisted. */
|
|
7
|
+
export declare function runCase(c: BenchCase, provider: AIProvider): Promise<CaseResult>;
|
|
8
|
+
export declare function toMarkdown(results: CaseResult[], s: BenchSummary, model: string): string;
|
|
9
|
+
//# sourceMappingURL=bench.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bench.d.ts","sourceRoot":"","sources":["../../src/commands/bench.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,EAA2C,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAKhH,eAAO,MAAM,YAAY,SAgEnB,CAAC;AAiBP,kGAAkG;AAClG,wBAAsB,OAAO,CAAC,CAAC,EAAE,SAAS,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CA6BrF;AA4BD,wBAAgB,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,EAAE,CAAC,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAexF"}
|