review-council 1.0.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/LICENSE +21 -0
- package/README.md +210 -0
- package/dist/config/defaults.d.ts +17 -0
- package/dist/config/defaults.js +27 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/loader.d.ts +2 -0
- package/dist/config/loader.js +66 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/schema.d.ts +79 -0
- package/dist/config/schema.js +49 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/consensus/deduper.d.ts +6 -0
- package/dist/consensus/deduper.js +127 -0
- package/dist/consensus/deduper.js.map +1 -0
- package/dist/consensus/parser.d.ts +6 -0
- package/dist/consensus/parser.js +90 -0
- package/dist/consensus/parser.js.map +1 -0
- package/dist/consensus/types.d.ts +57 -0
- package/dist/consensus/types.js +2 -0
- package/dist/consensus/types.js.map +1 -0
- package/dist/consensus/voter.d.ts +3 -0
- package/dist/consensus/voter.js +198 -0
- package/dist/consensus/voter.js.map +1 -0
- package/dist/dispatch/adapter.d.ts +15 -0
- package/dist/dispatch/adapter.js +24 -0
- package/dist/dispatch/adapter.js.map +1 -0
- package/dist/dispatch/anthropic.d.ts +9 -0
- package/dist/dispatch/anthropic.js +132 -0
- package/dist/dispatch/anthropic.js.map +1 -0
- package/dist/dispatch/google.d.ts +9 -0
- package/dist/dispatch/google.js +87 -0
- package/dist/dispatch/google.js.map +1 -0
- package/dist/dispatch/openai-compat.d.ts +18 -0
- package/dist/dispatch/openai-compat.js +90 -0
- package/dist/dispatch/openai-compat.js.map +1 -0
- package/dist/dispatch/openai.d.ts +9 -0
- package/dist/dispatch/openai.js +90 -0
- package/dist/dispatch/openai.js.map +1 -0
- package/dist/dispatch/runner.d.ts +11 -0
- package/dist/dispatch/runner.js +67 -0
- package/dist/dispatch/runner.js.map +1 -0
- package/dist/dispatch/utils.d.ts +6 -0
- package/dist/dispatch/utils.js +15 -0
- package/dist/dispatch/utils.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +262 -0
- package/dist/index.js.map +1 -0
- package/dist/output/github.d.ts +3 -0
- package/dist/output/github.js +95 -0
- package/dist/output/github.js.map +1 -0
- package/dist/output/json.d.ts +3 -0
- package/dist/output/json.js +8 -0
- package/dist/output/json.js.map +1 -0
- package/dist/output/markdown.d.ts +3 -0
- package/dist/output/markdown.js +81 -0
- package/dist/output/markdown.js.map +1 -0
- package/dist/output/terminal.d.ts +2 -0
- package/dist/output/terminal.js +141 -0
- package/dist/output/terminal.js.map +1 -0
- package/dist/prepare/chunker.d.ts +9 -0
- package/dist/prepare/chunker.js +62 -0
- package/dist/prepare/chunker.js.map +1 -0
- package/dist/prepare/language.d.ts +1 -0
- package/dist/prepare/language.js +99 -0
- package/dist/prepare/language.js.map +1 -0
- package/dist/prepare/prompt-builder.d.ts +11 -0
- package/dist/prepare/prompt-builder.js +65 -0
- package/dist/prepare/prompt-builder.js.map +1 -0
- package/dist/prompts/base.d.ts +3 -0
- package/dist/prompts/base.js +53 -0
- package/dist/prompts/base.js.map +1 -0
- package/dist/prompts/hardening.d.ts +7 -0
- package/dist/prompts/hardening.js +30 -0
- package/dist/prompts/hardening.js.map +1 -0
- package/dist/prompts/languages/elixir.d.ts +1 -0
- package/dist/prompts/languages/elixir.js +13 -0
- package/dist/prompts/languages/elixir.js.map +1 -0
- package/dist/prompts/languages/generic.d.ts +1 -0
- package/dist/prompts/languages/generic.js +11 -0
- package/dist/prompts/languages/generic.js.map +1 -0
- package/dist/prompts/languages/python.d.ts +1 -0
- package/dist/prompts/languages/python.js +14 -0
- package/dist/prompts/languages/python.js.map +1 -0
- package/dist/prompts/languages/typescript.d.ts +1 -0
- package/dist/prompts/languages/typescript.js +13 -0
- package/dist/prompts/languages/typescript.js.map +1 -0
- package/dist/resolver/github.d.ts +8 -0
- package/dist/resolver/github.js +70 -0
- package/dist/resolver/github.js.map +1 -0
- package/dist/resolver/local.d.ts +3 -0
- package/dist/resolver/local.js +75 -0
- package/dist/resolver/local.js.map +1 -0
- package/dist/resolver/types.d.ts +28 -0
- package/dist/resolver/types.js +2 -0
- package/dist/resolver/types.js.map +1 -0
- package/dist/roles/builtin.d.ts +4 -0
- package/dist/roles/builtin.js +234 -0
- package/dist/roles/builtin.js.map +1 -0
- package/dist/roles/dispatcher.d.ts +23 -0
- package/dist/roles/dispatcher.js +87 -0
- package/dist/roles/dispatcher.js.map +1 -0
- package/dist/roles/loader.d.ts +11 -0
- package/dist/roles/loader.js +107 -0
- package/dist/roles/loader.js.map +1 -0
- package/dist/roles/types.d.ts +13 -0
- package/dist/roles/types.js +2 -0
- package/dist/roles/types.js.map +1 -0
- package/package.json +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael Ströck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# review-council
|
|
2
|
+
|
|
3
|
+
> Multi-model AI code review in your terminal — many models, many roles, one consensus.
|
|
4
|
+
|
|
5
|
+
  
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g review-council
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Node.js >= 18.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# Review a GitHub PR with default models and roles
|
|
23
|
+
rcl review owner/repo#42
|
|
24
|
+
|
|
25
|
+
# Review with specific roles and post findings as a PR comment
|
|
26
|
+
rcl review owner/repo#42 --roles security-auditor,bug-hunter --post
|
|
27
|
+
|
|
28
|
+
# Review a local patch file; fail CI if critical/important findings exist
|
|
29
|
+
rcl review changes.patch --ci --markdown report.md
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Built-in Roles
|
|
35
|
+
|
|
36
|
+
| Role | Description |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| 🔍 `general` | Comprehensive review covering all dimensions |
|
|
39
|
+
| 🔒 `security-auditor` | Auth, injection, XSS, CSRF, IDOR, and sensitive data exposure |
|
|
40
|
+
| ⚡ `performance-engineer` | N+1 queries, caching, algorithmic complexity, and memory efficiency |
|
|
41
|
+
| 📐 `api-design` | API contracts, breaking changes, REST/gRPC conventions |
|
|
42
|
+
| 🧪 `test-coverage` | Missing tests, edge cases, flawed test logic |
|
|
43
|
+
| ✏️ `dx-critic` | Readability, naming, documentation, and developer ergonomics |
|
|
44
|
+
| 🏗️ `architecture` | Module boundaries, coupling, and architectural patterns |
|
|
45
|
+
| 🐛 `bug-hunter` | Logic errors, null paths, race conditions, off-by-one |
|
|
46
|
+
| ♿ `accessibility-auditor` | WCAG compliance, ARIA roles, keyboard navigation |
|
|
47
|
+
| 📋 `project-rules` | Enforces repo conventions from `AGENTS.md`, `CLAUDE.md`, etc. |
|
|
48
|
+
| 📄 `spec-compliance` | Checks implementation against a spec or plan file |
|
|
49
|
+
|
|
50
|
+
List roles in the terminal:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
rcl roles list
|
|
54
|
+
rcl roles show security-auditor
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## CLI Reference
|
|
60
|
+
|
|
61
|
+
### `rcl review <target>`
|
|
62
|
+
|
|
63
|
+
Review a PR or local diff.
|
|
64
|
+
|
|
65
|
+
**Target formats:**
|
|
66
|
+
- `owner/repo#N` — GitHub PR number
|
|
67
|
+
- GitHub PR URL
|
|
68
|
+
- Path to a `.patch` or `.diff` file
|
|
69
|
+
|
|
70
|
+
**Options:**
|
|
71
|
+
|
|
72
|
+
| Flag | Description |
|
|
73
|
+
|------|-------------|
|
|
74
|
+
| `--role <name>` | Use a single named role |
|
|
75
|
+
| `--roles <names>` | Comma-separated list of roles |
|
|
76
|
+
| `--reviewer <model:role>` | Explicit model:role pair (repeatable) |
|
|
77
|
+
| `--models <models>` | Comma-separated list of models to use |
|
|
78
|
+
| `--context <path>` | Context file or directory (repeatable) |
|
|
79
|
+
| `--spec <path>` | Specification file for `spec-compliance` role |
|
|
80
|
+
| `--focus <areas>` | Comma-separated focus areas |
|
|
81
|
+
| `--post` | Post review as a GitHub PR comment |
|
|
82
|
+
| `--json` | Print JSON output to stdout |
|
|
83
|
+
| `--json-file <path>` | Write JSON output to a file |
|
|
84
|
+
| `--markdown <path>` | Write Markdown report to a file |
|
|
85
|
+
| `--ci` | Exit non-zero if critical/important findings exist |
|
|
86
|
+
| `--config <path>` | Path to a config file |
|
|
87
|
+
|
|
88
|
+
`--role`, `--roles`, and `--reviewer` are mutually exclusive.
|
|
89
|
+
|
|
90
|
+
**Examples:**
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Use explicit model:role pairs
|
|
94
|
+
rcl review owner/repo#7 \
|
|
95
|
+
--reviewer claude-opus-4-6:security-auditor \
|
|
96
|
+
--reviewer gpt-5.4:bug-hunter
|
|
97
|
+
|
|
98
|
+
# Spec compliance review with context
|
|
99
|
+
rcl review ./feature.patch --role spec-compliance --spec SPEC.md --context src/
|
|
100
|
+
|
|
101
|
+
# Output JSON for downstream processing
|
|
102
|
+
rcl review owner/repo#99 --json > findings.json
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### `rcl roles`
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
rcl roles list # List all built-in roles
|
|
111
|
+
rcl roles show <name> # Show system prompt and details for a role
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Config File
|
|
117
|
+
|
|
118
|
+
Place `.review-council.yml` in your project root (or any parent directory). All fields are optional.
|
|
119
|
+
|
|
120
|
+
```yaml
|
|
121
|
+
# Models to use (provider-prefixed names)
|
|
122
|
+
models:
|
|
123
|
+
- anthropic/claude-opus-4-6
|
|
124
|
+
- openai/gpt-5.4
|
|
125
|
+
- google/gemini-2.5-pro
|
|
126
|
+
|
|
127
|
+
# Default roles to run
|
|
128
|
+
roles:
|
|
129
|
+
- security-auditor
|
|
130
|
+
- bug-hunter
|
|
131
|
+
- test-coverage
|
|
132
|
+
|
|
133
|
+
# Or pin explicit model:role pairs
|
|
134
|
+
reviewers:
|
|
135
|
+
- model: anthropic/claude-opus-4-6
|
|
136
|
+
role: security-auditor
|
|
137
|
+
- model: openai/gpt-5.4
|
|
138
|
+
role: bug-hunter
|
|
139
|
+
|
|
140
|
+
# Custom role overrides (extends a built-in or creates new)
|
|
141
|
+
customRoles:
|
|
142
|
+
- name: my-style-guide
|
|
143
|
+
focus: [best-practices]
|
|
144
|
+
systemPrompt: |
|
|
145
|
+
Enforce our team style guide. Flag any deviation from snake_case
|
|
146
|
+
variable names and require docstrings on all public functions.
|
|
147
|
+
|
|
148
|
+
# Consensus and deduplication thresholds
|
|
149
|
+
thresholds:
|
|
150
|
+
minConsensusScore: 0.4 # 0–1; findings below this are dropped
|
|
151
|
+
minConfidence: 0.2
|
|
152
|
+
dedupeLineWindow: 5 # lines within which findings are merged
|
|
153
|
+
jaccardThreshold: 0.3 # token-overlap threshold for deduplication
|
|
154
|
+
|
|
155
|
+
# Output defaults
|
|
156
|
+
output:
|
|
157
|
+
markdown: true
|
|
158
|
+
markdownPath: review-report.md
|
|
159
|
+
|
|
160
|
+
# Concurrency and reliability
|
|
161
|
+
concurrency: 6
|
|
162
|
+
timeout: 120000 # ms per model call
|
|
163
|
+
maxRetries: 3
|
|
164
|
+
|
|
165
|
+
# Context files to attach to every review
|
|
166
|
+
context:
|
|
167
|
+
- ARCHITECTURE.md
|
|
168
|
+
- docs/api.md
|
|
169
|
+
|
|
170
|
+
# Spec file for spec-compliance role
|
|
171
|
+
spec: SPEC.md
|
|
172
|
+
|
|
173
|
+
# GitHub token (prefer GITHUB_TOKEN env var instead)
|
|
174
|
+
# githubToken: ghp_...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Supported config file names: `.review-council.yml`, `.review-council.yaml`, `.review-council.json`, `review-council.config.js`.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## How Consensus Works
|
|
182
|
+
|
|
183
|
+
When multiple models and roles review the same diff, their findings are:
|
|
184
|
+
|
|
185
|
+
1. **Deduplicated** — findings on the same file, overlapping line range, and similar category are grouped together using Jaccard token overlap.
|
|
186
|
+
2. **Scored** — each group receives a consensus score based on three dimensions: reviewer diversity (how many distinct models and roles flagged it), role relevance (whether the reporting role specialises in that finding type), and isolation (what fraction of relevant reviewers flagged it).
|
|
187
|
+
3. **Classified** — groups are assigned a confidence band (Very High → Minimal) and a final severity, with upward elevation when multiple high-relevance roles agree.
|
|
188
|
+
4. **Filtered** — groups below `minConsensusScore` or `minConfidence` are dropped.
|
|
189
|
+
|
|
190
|
+
The result is a ranked list of findings that rewards agreement across independent reviewers and penalises noise from a single model.
|
|
191
|
+
|
|
192
|
+
For the full algorithm, see [CONSENSUS_V2_SPEC.md](./CONSENSUS_V2_SPEC.md).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## Environment Variables
|
|
197
|
+
|
|
198
|
+
| Variable | Description |
|
|
199
|
+
|----------|-------------|
|
|
200
|
+
| `ANTHROPIC_API_KEY` | API key for Claude models |
|
|
201
|
+
| `OPENAI_API_KEY` | API key for OpenAI models |
|
|
202
|
+
| `GEMINI_API_KEY` | API key for Google Gemini models |
|
|
203
|
+
| `GITHUB_TOKEN` | GitHub personal access token (PR fetch and post) |
|
|
204
|
+
| `RCL_DEBUG` | Set to any value to print full error stack traces |
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
## License
|
|
209
|
+
|
|
210
|
+
MIT © 2026 Michael Ströck
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare const DEFAULT_MODELS: readonly ["anthropic/claude-opus-4-6", "openai/gpt-5.4", "google/gemini-2.5-pro"];
|
|
2
|
+
export declare const DEFAULT_THRESHOLDS: {
|
|
3
|
+
readonly minConsensusScore: 0.4;
|
|
4
|
+
readonly minConfidence: 0.2;
|
|
5
|
+
readonly dedupeLineWindow: 5;
|
|
6
|
+
readonly jaccardThreshold: 0.3;
|
|
7
|
+
};
|
|
8
|
+
export declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
9
|
+
export declare const DEFAULT_MAX_RETRIES = 3;
|
|
10
|
+
export declare const DEFAULT_CONCURRENCY = 6;
|
|
11
|
+
export declare const DEFAULT_SEVERITY_ORDER: readonly ["critical", "important", "minor", "nitpick"];
|
|
12
|
+
export declare const CONFIDENCE_THRESHOLDS: {
|
|
13
|
+
readonly veryHigh: 0.8;
|
|
14
|
+
readonly high: 0.6;
|
|
15
|
+
readonly medium: 0.4;
|
|
16
|
+
readonly low: 0.2;
|
|
17
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const DEFAULT_MODELS = [
|
|
2
|
+
'anthropic/claude-opus-4-6',
|
|
3
|
+
'openai/gpt-5.4',
|
|
4
|
+
'google/gemini-2.5-pro',
|
|
5
|
+
];
|
|
6
|
+
export const DEFAULT_THRESHOLDS = {
|
|
7
|
+
minConsensusScore: 0.4,
|
|
8
|
+
minConfidence: 0.2,
|
|
9
|
+
dedupeLineWindow: 5,
|
|
10
|
+
jaccardThreshold: 0.3,
|
|
11
|
+
};
|
|
12
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
13
|
+
export const DEFAULT_MAX_RETRIES = 3;
|
|
14
|
+
export const DEFAULT_CONCURRENCY = 6;
|
|
15
|
+
export const DEFAULT_SEVERITY_ORDER = [
|
|
16
|
+
'critical',
|
|
17
|
+
'important',
|
|
18
|
+
'minor',
|
|
19
|
+
'nitpick',
|
|
20
|
+
];
|
|
21
|
+
export const CONFIDENCE_THRESHOLDS = {
|
|
22
|
+
veryHigh: 0.8,
|
|
23
|
+
high: 0.6,
|
|
24
|
+
medium: 0.4,
|
|
25
|
+
low: 0.2,
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=defaults.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"defaults.js","sourceRoot":"","sources":["../../src/config/defaults.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,2BAA2B;IAC3B,gBAAgB;IAChB,uBAAuB;CACf,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,iBAAiB,EAAE,GAAG;IACtB,aAAa,EAAE,GAAG;IAClB,gBAAgB,EAAE,CAAC;IACnB,gBAAgB,EAAE,GAAG;CACb,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAC1C,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AACrC,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,UAAU;IACV,WAAW;IACX,OAAO;IACP,SAAS;CACD,CAAC;AAEX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,QAAQ,EAAE,GAAG;IACb,IAAI,EAAE,GAAG;IACT,MAAM,EAAE,GAAG;IACX,GAAG,EAAE,GAAG;CACA,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { cosmiconfig } from 'cosmiconfig';
|
|
2
|
+
import { ConfigSchema } from './schema.js';
|
|
3
|
+
import { DEFAULT_MODELS, DEFAULT_THRESHOLDS, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_CONCURRENCY, } from './defaults.js';
|
|
4
|
+
const explorer = cosmiconfig('review-council', {
|
|
5
|
+
searchPlaces: [
|
|
6
|
+
'.review-council.yml',
|
|
7
|
+
'.review-council.yaml',
|
|
8
|
+
'.review-council.json',
|
|
9
|
+
'.review-council.js',
|
|
10
|
+
'.review-council.cjs',
|
|
11
|
+
'review-council.config.js',
|
|
12
|
+
'review-council.config.cjs',
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
export async function loadConfig(configPath) {
|
|
16
|
+
try {
|
|
17
|
+
const result = configPath
|
|
18
|
+
? await explorer.load(configPath)
|
|
19
|
+
: await explorer.search();
|
|
20
|
+
if (!result || result.isEmpty) {
|
|
21
|
+
return buildDefaultConfig();
|
|
22
|
+
}
|
|
23
|
+
const parsed = ConfigSchema.safeParse(result.config);
|
|
24
|
+
if (!parsed.success) {
|
|
25
|
+
console.warn('Warning: config validation errors:', parsed.error.format());
|
|
26
|
+
return buildDefaultConfig();
|
|
27
|
+
}
|
|
28
|
+
return mergeWithDefaults(parsed.data);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
if (configPath) {
|
|
32
|
+
throw new Error(`Failed to load config from ${configPath}: ${String(err)}`);
|
|
33
|
+
}
|
|
34
|
+
return buildDefaultConfig();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function buildDefaultConfig() {
|
|
38
|
+
return {
|
|
39
|
+
models: [...DEFAULT_MODELS],
|
|
40
|
+
thresholds: { ...DEFAULT_THRESHOLDS },
|
|
41
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
42
|
+
maxRetries: DEFAULT_MAX_RETRIES,
|
|
43
|
+
concurrency: DEFAULT_CONCURRENCY,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function mergeWithDefaults(config) {
|
|
47
|
+
return {
|
|
48
|
+
models: config.models ?? [...DEFAULT_MODELS],
|
|
49
|
+
roles: config.roles,
|
|
50
|
+
reviewers: config.reviewers,
|
|
51
|
+
customRoles: config.customRoles,
|
|
52
|
+
thresholds: {
|
|
53
|
+
...DEFAULT_THRESHOLDS,
|
|
54
|
+
...config.thresholds,
|
|
55
|
+
},
|
|
56
|
+
output: config.output,
|
|
57
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT_MS,
|
|
58
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
59
|
+
concurrency: config.concurrency ?? DEFAULT_CONCURRENCY,
|
|
60
|
+
githubToken: config.githubToken ?? process.env['GITHUB_TOKEN'],
|
|
61
|
+
context: config.context,
|
|
62
|
+
spec: config.spec,
|
|
63
|
+
focus: config.focus,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loader.js","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,YAAY,EAAe,MAAM,aAAa,CAAC;AACxD,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,GACpB,MAAM,eAAe,CAAC;AAEvB,MAAM,QAAQ,GAAG,WAAW,CAAC,gBAAgB,EAAE;IAC7C,YAAY,EAAE;QACZ,qBAAqB;QACrB,sBAAsB;QACtB,sBAAsB;QACtB,oBAAoB;QACpB,qBAAqB;QACrB,0BAA0B;QAC1B,2BAA2B;KAC5B;CACF,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAmB;IAClD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,UAAU;YACvB,CAAC,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,UAAU,CAAC;YACjC,CAAC,CAAC,MAAM,QAAQ,CAAC,MAAM,EAAE,CAAC;QAE5B,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC9B,OAAO,kBAAkB,EAAE,CAAC;QAC9B,CAAC;QAED,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACrD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,oCAAoC,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;YAC1E,OAAO,kBAAkB,EAAE,CAAC;QAC9B,CAAC;QAED,OAAO,iBAAiB,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,UAAU,EAAE,CAAC;YACf,MAAM,IAAI,KAAK,CAAC,8BAA8B,UAAU,KAAK,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC9E,CAAC;QACD,OAAO,kBAAkB,EAAE,CAAC;IAC9B,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO;QACL,MAAM,EAAE,CAAC,GAAG,cAAc,CAAC;QAC3B,UAAU,EAAE,EAAE,GAAG,kBAAkB,EAAE;QACrC,OAAO,EAAE,kBAAkB;QAC3B,UAAU,EAAE,mBAAmB;QAC/B,WAAW,EAAE,mBAAmB;KACjC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAc;IACvC,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM,IAAI,CAAC,GAAG,cAAc,CAAC;QAC5C,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,SAAS,EAAE,MAAM,CAAC,SAAS;QAC3B,WAAW,EAAE,MAAM,CAAC,WAAW;QAC/B,UAAU,EAAE;YACV,GAAG,kBAAkB;YACrB,GAAG,MAAM,CAAC,UAAU;SACrB;QACD,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,kBAAkB;QAC7C,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,mBAAmB;QACpD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,mBAAmB;QACtD,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC;QAC9D,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,KAAK,EAAE,MAAM,CAAC,KAAK;KACpB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const SeveritySchema: z.ZodEnum<{
|
|
3
|
+
critical: "critical";
|
|
4
|
+
important: "important";
|
|
5
|
+
minor: "minor";
|
|
6
|
+
nitpick: "nitpick";
|
|
7
|
+
}>;
|
|
8
|
+
export declare const CategorySchema: z.ZodEnum<{
|
|
9
|
+
security: "security";
|
|
10
|
+
correctness: "correctness";
|
|
11
|
+
"best-practices": "best-practices";
|
|
12
|
+
tests: "tests";
|
|
13
|
+
"api-design": "api-design";
|
|
14
|
+
}>;
|
|
15
|
+
export declare const ReviewerPairSchema: z.ZodObject<{
|
|
16
|
+
model: z.ZodString;
|
|
17
|
+
role: z.ZodString;
|
|
18
|
+
}, z.core.$strip>;
|
|
19
|
+
export declare const RoleConfigSchema: z.ZodObject<{
|
|
20
|
+
name: z.ZodString;
|
|
21
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
22
|
+
focus: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
23
|
+
severityBias: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
24
|
+
}, z.core.$strip>;
|
|
25
|
+
export declare const ThresholdsSchema: z.ZodObject<{
|
|
26
|
+
minConsensusScore: z.ZodOptional<z.ZodNumber>;
|
|
27
|
+
minConfidence: z.ZodOptional<z.ZodNumber>;
|
|
28
|
+
dedupeLineWindow: z.ZodOptional<z.ZodNumber>;
|
|
29
|
+
jaccardThreshold: z.ZodOptional<z.ZodNumber>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export declare const OutputSchema: z.ZodObject<{
|
|
32
|
+
terminal: z.ZodOptional<z.ZodBoolean>;
|
|
33
|
+
json: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
+
markdown: z.ZodOptional<z.ZodBoolean>;
|
|
35
|
+
github: z.ZodOptional<z.ZodBoolean>;
|
|
36
|
+
jsonPath: z.ZodOptional<z.ZodString>;
|
|
37
|
+
markdownPath: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>;
|
|
39
|
+
export declare const ConfigSchema: z.ZodObject<{
|
|
40
|
+
models: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
41
|
+
roles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
42
|
+
reviewers: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
43
|
+
model: z.ZodString;
|
|
44
|
+
role: z.ZodString;
|
|
45
|
+
}, z.core.$strip>>>;
|
|
46
|
+
customRoles: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
47
|
+
name: z.ZodString;
|
|
48
|
+
systemPrompt: z.ZodOptional<z.ZodString>;
|
|
49
|
+
focus: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
50
|
+
severityBias: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
51
|
+
}, z.core.$strip>>>;
|
|
52
|
+
thresholds: z.ZodOptional<z.ZodObject<{
|
|
53
|
+
minConsensusScore: z.ZodOptional<z.ZodNumber>;
|
|
54
|
+
minConfidence: z.ZodOptional<z.ZodNumber>;
|
|
55
|
+
dedupeLineWindow: z.ZodOptional<z.ZodNumber>;
|
|
56
|
+
jaccardThreshold: z.ZodOptional<z.ZodNumber>;
|
|
57
|
+
}, z.core.$strip>>;
|
|
58
|
+
output: z.ZodOptional<z.ZodObject<{
|
|
59
|
+
terminal: z.ZodOptional<z.ZodBoolean>;
|
|
60
|
+
json: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
markdown: z.ZodOptional<z.ZodBoolean>;
|
|
62
|
+
github: z.ZodOptional<z.ZodBoolean>;
|
|
63
|
+
jsonPath: z.ZodOptional<z.ZodString>;
|
|
64
|
+
markdownPath: z.ZodOptional<z.ZodString>;
|
|
65
|
+
}, z.core.$strip>>;
|
|
66
|
+
timeout: z.ZodOptional<z.ZodNumber>;
|
|
67
|
+
maxRetries: z.ZodOptional<z.ZodNumber>;
|
|
68
|
+
concurrency: z.ZodOptional<z.ZodNumber>;
|
|
69
|
+
githubToken: z.ZodOptional<z.ZodString>;
|
|
70
|
+
context: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
|
+
spec: z.ZodOptional<z.ZodString>;
|
|
72
|
+
focus: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
73
|
+
}, z.core.$strip>;
|
|
74
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
75
|
+
export type ReviewerPair = z.infer<typeof ReviewerPairSchema>;
|
|
76
|
+
export type RoleConfig = z.infer<typeof RoleConfigSchema>;
|
|
77
|
+
export type Thresholds = z.infer<typeof ThresholdsSchema>;
|
|
78
|
+
export type Severity = z.infer<typeof SeveritySchema>;
|
|
79
|
+
export type Category = z.infer<typeof CategorySchema>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export const SeveritySchema = z.enum(['critical', 'important', 'minor', 'nitpick']);
|
|
3
|
+
export const CategorySchema = z.enum([
|
|
4
|
+
'security',
|
|
5
|
+
'correctness',
|
|
6
|
+
'best-practices',
|
|
7
|
+
'tests',
|
|
8
|
+
'api-design',
|
|
9
|
+
]);
|
|
10
|
+
export const ReviewerPairSchema = z.object({
|
|
11
|
+
model: z.string(),
|
|
12
|
+
role: z.string(),
|
|
13
|
+
});
|
|
14
|
+
export const RoleConfigSchema = z.object({
|
|
15
|
+
name: z.string(),
|
|
16
|
+
systemPrompt: z.string().optional(),
|
|
17
|
+
focus: z.array(z.string()).optional(),
|
|
18
|
+
severityBias: z.record(z.string(), z.number()).optional(),
|
|
19
|
+
});
|
|
20
|
+
export const ThresholdsSchema = z.object({
|
|
21
|
+
minConsensusScore: z.number().min(0).max(1).optional(),
|
|
22
|
+
minConfidence: z.number().min(0).max(1).optional(),
|
|
23
|
+
dedupeLineWindow: z.number().int().positive().optional(),
|
|
24
|
+
jaccardThreshold: z.number().min(0).max(1).optional(),
|
|
25
|
+
});
|
|
26
|
+
export const OutputSchema = z.object({
|
|
27
|
+
terminal: z.boolean().optional(),
|
|
28
|
+
json: z.boolean().optional(),
|
|
29
|
+
markdown: z.boolean().optional(),
|
|
30
|
+
github: z.boolean().optional(),
|
|
31
|
+
jsonPath: z.string().optional(),
|
|
32
|
+
markdownPath: z.string().optional(),
|
|
33
|
+
});
|
|
34
|
+
export const ConfigSchema = z.object({
|
|
35
|
+
models: z.array(z.string()).optional(),
|
|
36
|
+
roles: z.array(z.string()).optional(),
|
|
37
|
+
reviewers: z.array(ReviewerPairSchema).optional(),
|
|
38
|
+
customRoles: z.array(RoleConfigSchema).optional(),
|
|
39
|
+
thresholds: ThresholdsSchema.optional(),
|
|
40
|
+
output: OutputSchema.optional(),
|
|
41
|
+
timeout: z.number().positive().optional(),
|
|
42
|
+
maxRetries: z.number().int().nonnegative().optional(),
|
|
43
|
+
concurrency: z.number().int().positive().optional(),
|
|
44
|
+
githubToken: z.string().optional(),
|
|
45
|
+
context: z.array(z.string()).optional(),
|
|
46
|
+
spec: z.string().optional(),
|
|
47
|
+
focus: z.array(z.string()).optional(),
|
|
48
|
+
});
|
|
49
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/config/schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModelReview, DeduplicatedGroup } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Compute Jaccard similarity between two strings (word-level tokenization)
|
|
4
|
+
*/
|
|
5
|
+
export declare function jaccardSimilarity(a: string, b: string): number;
|
|
6
|
+
export declare function deduplicateFindings(reviews: ModelReview[], jaccardThreshold?: number, lineWindow?: number): DeduplicatedGroup[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const LINE_WINDOW = 5;
|
|
2
|
+
/**
|
|
3
|
+
* Compute Jaccard similarity between two strings (word-level tokenization)
|
|
4
|
+
*/
|
|
5
|
+
export function jaccardSimilarity(a, b) {
|
|
6
|
+
const tokenize = (s) => {
|
|
7
|
+
return new Set(s
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/[^a-z0-9\s]/g, ' ')
|
|
10
|
+
.split(/\s+/)
|
|
11
|
+
.filter((t) => t.length > 2));
|
|
12
|
+
};
|
|
13
|
+
const setA = tokenize(a);
|
|
14
|
+
const setB = tokenize(b);
|
|
15
|
+
if (setA.size === 0 && setB.size === 0)
|
|
16
|
+
return 1.0;
|
|
17
|
+
if (setA.size === 0 || setB.size === 0)
|
|
18
|
+
return 0.0;
|
|
19
|
+
let intersection = 0;
|
|
20
|
+
for (const token of setA) {
|
|
21
|
+
if (setB.has(token))
|
|
22
|
+
intersection++;
|
|
23
|
+
}
|
|
24
|
+
const union = setA.size + setB.size - intersection;
|
|
25
|
+
return intersection / union;
|
|
26
|
+
}
|
|
27
|
+
function linesOverlap(a, b, window) {
|
|
28
|
+
const aStart = Math.max(0, a.startLine - window);
|
|
29
|
+
const aEnd = a.endLine + window;
|
|
30
|
+
const bStart = Math.max(0, b.startLine - window);
|
|
31
|
+
const bEnd = b.endLine + window;
|
|
32
|
+
return aStart <= bEnd && bStart <= aEnd;
|
|
33
|
+
}
|
|
34
|
+
function areSameFile(a, b) {
|
|
35
|
+
return a.file === b.file;
|
|
36
|
+
}
|
|
37
|
+
function sameCategory(a, b) {
|
|
38
|
+
return a.category === b.category;
|
|
39
|
+
}
|
|
40
|
+
function areDuplicates(a, b, jaccardThreshold, lineWindow) {
|
|
41
|
+
if (!areSameFile(a, b))
|
|
42
|
+
return false;
|
|
43
|
+
if (!sameCategory(a, b))
|
|
44
|
+
return false;
|
|
45
|
+
if (!linesOverlap(a, b, lineWindow))
|
|
46
|
+
return false;
|
|
47
|
+
const titleSim = jaccardSimilarity(a.title, b.title);
|
|
48
|
+
const descSim = jaccardSimilarity(a.description, b.description);
|
|
49
|
+
const maxSim = Math.max(titleSim, descSim);
|
|
50
|
+
return maxSim >= jaccardThreshold;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Choose the representative finding from a group:
|
|
54
|
+
* - Prefer higher severity
|
|
55
|
+
* - Break ties by description length (more detail wins)
|
|
56
|
+
*/
|
|
57
|
+
function chooseRepresentative(members) {
|
|
58
|
+
const severityOrder = { critical: 0, important: 1, minor: 2, nitpick: 3 };
|
|
59
|
+
return members.reduce((best, curr) => {
|
|
60
|
+
const bestScore = severityOrder[best.finding.severity];
|
|
61
|
+
const currScore = severityOrder[curr.finding.severity];
|
|
62
|
+
if (currScore < bestScore)
|
|
63
|
+
return curr;
|
|
64
|
+
if (currScore === bestScore && curr.finding.description.length > best.finding.description.length)
|
|
65
|
+
return curr;
|
|
66
|
+
return best;
|
|
67
|
+
}).finding;
|
|
68
|
+
}
|
|
69
|
+
export function deduplicateFindings(reviews, jaccardThreshold = 0.3, lineWindow = LINE_WINDOW) {
|
|
70
|
+
// Flatten all findings with attribution
|
|
71
|
+
const all = [];
|
|
72
|
+
for (const review of reviews) {
|
|
73
|
+
if (review.status !== 'success')
|
|
74
|
+
continue;
|
|
75
|
+
for (const finding of review.findings) {
|
|
76
|
+
all.push({ finding, model: review.model, role: review.role });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (all.length === 0)
|
|
80
|
+
return [];
|
|
81
|
+
// Union-Find style grouping
|
|
82
|
+
const groupOf = new Array(all.length).fill(-1);
|
|
83
|
+
for (let i = 0; i < all.length; i++) {
|
|
84
|
+
for (let j = i + 1; j < all.length; j++) {
|
|
85
|
+
if (areDuplicates(all[i].finding, all[j].finding, jaccardThreshold, lineWindow)) {
|
|
86
|
+
// Merge groups: find root of i
|
|
87
|
+
let rootI = i;
|
|
88
|
+
while (groupOf[rootI] !== -1)
|
|
89
|
+
rootI = groupOf[rootI];
|
|
90
|
+
let rootJ = j;
|
|
91
|
+
while (groupOf[rootJ] !== -1)
|
|
92
|
+
rootJ = groupOf[rootJ];
|
|
93
|
+
if (rootI !== rootJ) {
|
|
94
|
+
groupOf[rootJ] = rootI;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Collect groups
|
|
100
|
+
const groups = new Map();
|
|
101
|
+
for (let i = 0; i < all.length; i++) {
|
|
102
|
+
let root = i;
|
|
103
|
+
while (groupOf[root] !== -1)
|
|
104
|
+
root = groupOf[root];
|
|
105
|
+
const existing = groups.get(root) ?? [];
|
|
106
|
+
existing.push(all[i]);
|
|
107
|
+
groups.set(root, existing);
|
|
108
|
+
}
|
|
109
|
+
const result = [];
|
|
110
|
+
for (const members of groups.values()) {
|
|
111
|
+
result.push({
|
|
112
|
+
representative: chooseRepresentative(members),
|
|
113
|
+
members,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
// Sort by severity then file
|
|
117
|
+
const severityOrder = { critical: 0, important: 1, minor: 2, nitpick: 3 };
|
|
118
|
+
result.sort((a, b) => {
|
|
119
|
+
const sevDiff = severityOrder[a.representative.severity] -
|
|
120
|
+
severityOrder[b.representative.severity];
|
|
121
|
+
if (sevDiff !== 0)
|
|
122
|
+
return sevDiff;
|
|
123
|
+
return a.representative.file.localeCompare(b.representative.file);
|
|
124
|
+
});
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=deduper.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deduper.js","sourceRoot":"","sources":["../../src/consensus/deduper.ts"],"names":[],"mappings":"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"}
|