@mechanai/deepreview 1.0.1 → 2.0.1

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.
Files changed (32) hide show
  1. package/.opencode/agents/deepreview-review-formatter.md +66 -0
  2. package/.opencode/commands/_deepreview-pipeline.md +57 -0
  3. package/.opencode/commands/deepreview-pr-review.md +61 -0
  4. package/.opencode/plugins/deepreview.ts +45 -0
  5. package/README.md +48 -100
  6. package/package.json +29 -6
  7. package/src/diff-classifier.test.ts +45 -0
  8. package/src/diff-classifier.ts +63 -0
  9. package/src/graphql.ts +183 -0
  10. package/src/parse-threads.test.ts +58 -0
  11. package/src/parse-threads.ts +117 -0
  12. package/src/post-review.test.ts +52 -0
  13. package/src/post-review.ts +325 -0
  14. package/src/review-api.ts +253 -0
  15. package/src/review-helpers.ts +65 -0
  16. package/src/cli.js +0 -103
  17. /package/{agents → .opencode/agents}/deepreview-applier.md +0 -0
  18. /package/{agents → .opencode/agents}/deepreview-architecture.md +0 -0
  19. /package/{agents → .opencode/agents}/deepreview-compatibility.md +0 -0
  20. /package/{agents → .opencode/agents}/deepreview-correctness.md +0 -0
  21. /package/{agents → .opencode/agents}/deepreview-docs.md +0 -0
  22. /package/{agents → .opencode/agents}/deepreview-planner.md +0 -0
  23. /package/{agents → .opencode/agents}/deepreview-security.md +0 -0
  24. /package/{agents → .opencode/agents}/deepreview-spec-completeness.md +0 -0
  25. /package/{agents → .opencode/agents}/deepreview-spec-consistency.md +0 -0
  26. /package/{agents → .opencode/agents}/deepreview-spec-feasibility.md +0 -0
  27. /package/{agents → .opencode/agents}/deepreview-synthesizer.md +0 -0
  28. /package/{agents → .opencode/agents}/deepreview-validator.md +0 -0
  29. /package/{commands → .opencode/commands}/deepreview-loop.md +0 -0
  30. /package/{commands → .opencode/commands}/deepreview-spec-loop.md +0 -0
  31. /package/{commands → .opencode/commands}/deepreview-spec.md +0 -0
  32. /package/{commands → .opencode/commands}/deepreview.md +0 -0
@@ -0,0 +1,66 @@
1
+ ---
2
+ description: "Formats review synthesis into individual thread findings for GitHub PR review posting. Part of the deepreview pipeline."
3
+ mode: subagent
4
+ temperature: 0.1
5
+ permission:
6
+ edit: allow
7
+ bash: deny
8
+ ---
9
+
10
+ You are a formatter that converts a code review synthesis into individual, postable comment threads for a GitHub PR review.
11
+
12
+ ## Input
13
+
14
+ You will receive:
15
+
16
+ 1. A path to `synthesis.md` — the unified review synthesis
17
+ 2. A path to `input.txt` — the PR diff
18
+ 3. The PR head commit SHA — provided inline in the prompt text
19
+
20
+ Read both files.
21
+
22
+ ## Process
23
+
24
+ 1. Read the synthesis and identify every individual finding (each bullet or paragraph that describes a distinct issue)
25
+ 2. For each finding, determine:
26
+ - `path`: the file path (relative to repo root) the finding refers to
27
+ - `line`: the specific line number (new-side of diff). If the synthesis gives a range, use the end line.
28
+ - `startLine`: if the finding spans multiple lines, use the start of the range. Omit if single-line.
29
+ 3. Read `input.txt` (the diff) to:
30
+ - Verify line references are correct
31
+ - Generate ` ```suggestion ` blocks where a concrete fix is obvious and fits within the diff
32
+ 4. Write each finding as a document in the output file
33
+
34
+ ## Output format
35
+
36
+ Write to the output path provided. Use this exact format — one document per finding, separated by `---`:
37
+
38
+ ```
39
+ ---
40
+ path: <file path>
41
+ startLine: <start line, omit if single-line>
42
+ line: <line number>
43
+ ---
44
+ <markdown body of the comment>
45
+ ```
46
+
47
+ ## Content rules
48
+
49
+ - One finding per document. Never bundle multiple issues.
50
+ - No stats, severity counts, or framing ("3 critical issues found")
51
+ - No references to local file paths, session directories, AI tooling, or the deepreview pipeline
52
+ - Use permalinks for code references: `https://github.com/OWNER/REPO/blob/$PR_HEAD_SHA/<path>#L<line>`
53
+ - Get OWNER/REPO from the diff header or from `input.txt` context
54
+ - Use ` ```suggestion ` blocks where a concrete fix is obvious
55
+ - American English. Succinct. No filler.
56
+ - Do NOT classify findings into tiers — the posting pipeline handles placement
57
+
58
+ ## Line number rules
59
+
60
+ - All line numbers refer to the NEW (right) side of the diff
61
+ - If the synthesis says "line 42" but looking at the diff that corresponds to a different new-side line, use the correct new-side line
62
+ - If you cannot determine an exact line, use the first line of the relevant function/block
63
+
64
+ ## Response contract
65
+
66
+ After writing the threads file, your ONLY response must be the absolute path to your output file and a count line (e.g., "12 threads written"). Do not summarize findings.
@@ -0,0 +1,57 @@
1
+ ---
2
+ internal: true
3
+ description: "Shared pipeline stages for deepreview commands (review → validate → synthesize)"
4
+ ---
5
+
6
+ # Shared Pipeline: Review → Validate → Synthesize
7
+
8
+ This template defines 3 stages used by deepreview orchestrator commands. The orchestrator must set SESSION_DIR and INPUT_DESCRIPTION before invoking these stages.
9
+
10
+ ## STAGE 1: INITIAL REVIEW (one task per review perspective)
11
+
12
+ Dispatch ALL of these Task tool calls simultaneously in a single message:
13
+
14
+ Task — Use the Task tool with subagent_type="deepreview-correctness":
15
+ "You are reviewing $INPUT_DESCRIPTION. Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-correctness.md."
16
+
17
+ Task — Use the Task tool with subagent_type="deepreview-security":
18
+ "You are reviewing $INPUT_DESCRIPTION. Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-security.md."
19
+
20
+ Task — Use the Task tool with subagent_type="deepreview-architecture":
21
+ "You are reviewing $INPUT_DESCRIPTION. Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-architecture.md."
22
+
23
+ Task — Use the Task tool with subagent_type="deepreview-docs":
24
+ "You are reviewing $INPUT_DESCRIPTION. Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-docs.md."
25
+
26
+ Task — Use the Task tool with subagent_type="deepreview-compatibility":
27
+ "You are reviewing $INPUT_DESCRIPTION. Read the content at $SESSION_DIR/input.txt. Write your review to $SESSION_DIR/review-compatibility.md."
28
+
29
+ Wait for all 5 to return. Record which succeeded and which failed.
30
+
31
+ ## STAGE 2: CROSS-VALIDATION (5 parallel tasks)
32
+
33
+ Only proceed with reviews that exist. For each validator below, replace $REVIEW_FILE_LIST with ONLY the review files that were successfully created in Stage 1. If a review file failed, omit it from the list entirely. Dispatch validators simultaneously:
34
+
35
+ Task — Use the Task tool with subagent_type="deepreview-validator":
36
+ "Your perspective: correctness. Read all available review files at: $REVIEW_FILE_LIST. Also read the original input at $SESSION_DIR/input.txt for context. Write your validated review to $SESSION_DIR/validated-correctness.md."
37
+
38
+ Task — Use the Task tool with subagent_type="deepreview-validator":
39
+ "Your perspective: security. Read all available review files at: $REVIEW_FILE_LIST. Also read the original input at $SESSION_DIR/input.txt for context. Write your validated review to $SESSION_DIR/validated-security.md."
40
+
41
+ Task — Use the Task tool with subagent_type="deepreview-validator":
42
+ "Your perspective: architecture. Read all available review files at: $REVIEW_FILE_LIST. Also read the original input at $SESSION_DIR/input.txt for context. Write your validated review to $SESSION_DIR/validated-architecture.md."
43
+
44
+ Task — Use the Task tool with subagent_type="deepreview-validator":
45
+ "Your perspective: docs. Read all available review files at: $REVIEW_FILE_LIST. Also read the original input at $SESSION_DIR/input.txt for context. Write your validated review to $SESSION_DIR/validated-docs.md."
46
+
47
+ Task — Use the Task tool with subagent_type="deepreview-validator":
48
+ "Your perspective: compatibility. Read all available review files at: $REVIEW_FILE_LIST. Also read the original input at $SESSION_DIR/input.txt for context. Write your validated review to $SESSION_DIR/validated-compatibility.md."
49
+
50
+ Wait for all 5 to return.
51
+
52
+ ## STAGE 3: SYNTHESIS (1 task)
53
+
54
+ Task — Use the Task tool with subagent_type="deepreview-synthesizer":
55
+ "Read the validated reviews at: $SESSION_DIR/validated-correctness.md, $SESSION_DIR/validated-security.md, $SESSION_DIR/validated-architecture.md, $SESSION_DIR/validated-docs.md, $SESSION_DIR/validated-compatibility.md (skip any that don't exist). Write the synthesis to $SESSION_DIR/synthesis.md."
56
+
57
+ Record the stats line from its return.
@@ -0,0 +1,61 @@
1
+ ---
2
+ description: "Multi-agent parallel code review posted as a pending GitHub PR review"
3
+ ---
4
+
5
+ You are an orchestrator for a multi-agent code review pipeline that posts findings as a pending GitHub PR review. Follow these steps EXACTLY. Do NOT deviate, skip steps, or read any files in the session directory yourself.
6
+
7
+ STEP 1: VALIDATE INPUT
8
+ $ARGUMENTS must be a PR number (integer). If it is not a number, tell the user "Usage: /deepreview-pr-review <PR_NUMBER>" and STOP.
9
+
10
+ Set SESSION_DIR=".ai/deepreview/$ARGUMENTS-review-$(date +%Y-%m-%d-%H%M%S)"
11
+ Create the directory with `mkdir -p $SESSION_DIR`
12
+ Set INPUT_DESCRIPTION="a PR diff (code changes)"
13
+
14
+ STEP 2: PREPARE INPUT
15
+ Run `gh pr diff $ARGUMENTS > $SESSION_DIR/input.txt`
16
+ Check if input.txt is empty (0 bytes). If empty, tell the user "Nothing to review — PR has no diff." and STOP.
17
+
18
+ Get and store the PR head SHA:
19
+ Run `gh pr view $ARGUMENTS --json headRefOid --jq .headRefOid` and save the output as PR_HEAD_SHA.
20
+
21
+ ## Stage 1-3: Shared Pipeline
22
+
23
+ Follow the shared pipeline at `commands/_deepreview-pipeline.md` with:
24
+
25
+ - INPUT: `$SESSION_DIR/input.txt`
26
+ - OUTPUT: `$SESSION_DIR/synthesis.md`
27
+
28
+ If stats show 0 critical, 0 warnings, 0 suggestions, tell the user "No findings to post. PR looks good!" and STOP.
29
+
30
+ STEP 3: FORMAT THREADS (1 task)
31
+
32
+ Get the repo owner/name:
33
+ Run `gh repo view --json owner,name --jq '.owner.login + "/" + .name'` and save as OWNER_REPO.
34
+
35
+ Task — Use the Task tool with subagent_type="deepreview-review-formatter":
36
+ "Read the synthesis at $SESSION_DIR/synthesis.md and the diff at $SESSION_DIR/input.txt. The PR is $OWNER_REPO#$ARGUMENTS, head SHA is $PR_HEAD_SHA. Write the formatted threads to $SESSION_DIR/threads.md."
37
+
38
+ Wait for it to return.
39
+
40
+ STEP 4: POST REVIEW
41
+ Use the `deepreview-post-review` tool:
42
+
43
+ - `threads_path`: The absolute path to `$SESSION_DIR/threads.md`
44
+ - `pr_number`: $ARGUMENTS (the PR number)
45
+
46
+ STEP 5: PRESENT RESULTS
47
+ Show the user:
48
+
49
+ - Session directory: $SESSION_DIR/
50
+ - Which reviewers completed (and any that failed)
51
+ - Stats from synthesis (the stats line from Stage 3)
52
+ - Output from the posting script (how many threads posted, any demotions)
53
+ - Remind: "The review is PENDING. Submit it via the GitHub UI when ready."
54
+
55
+ IMPORTANT RULES:
56
+
57
+ - Do NOT read any files in $SESSION_DIR yourself. Ever.
58
+ - Use ONLY the file paths and stats/summary lines returned by subagents.
59
+ - If a subagent fails, note which one failed and continue with what you have.
60
+ - If all 5 reviewers fail in Stage 1, tell the user and STOP.
61
+ - Do NOT submit the review. It stays pending.
@@ -0,0 +1,45 @@
1
+ import { type Plugin, type PluginInput, tool } from "@opencode-ai/plugin";
2
+ import { postReview } from "@mechanai/deepreview/api";
3
+
4
+ // oxlint-disable-next-line require-await -- Why: Plugin type signature requires async but this plugin has no async initialization
5
+ export const server: Plugin = async (_input: PluginInput) => {
6
+ return {
7
+ tool: {
8
+ "deepreview-post-review": tool({
9
+ description:
10
+ "Post a GitHub PR review from a threads.md file. " +
11
+ "Parses findings, classifies them into line-level/file-level/review-body " +
12
+ "tiers based on the PR diff, and submits via GitHub GraphQL API. " +
13
+ "Returns a summary of what was posted.",
14
+ args: {
15
+ threads_path: tool.schema
16
+ .string()
17
+ .describe("Relative path to the threads.md file (from workspace root)"),
18
+ pr_number: tool.schema.number().int().positive().describe("Pull request number"),
19
+ dry_run: tool.schema
20
+ .boolean()
21
+ .optional()
22
+ .describe("Print what would be posted without submitting"),
23
+ skip_ids: tool.schema
24
+ .array(tool.schema.string())
25
+ .optional()
26
+ .describe("Finding IDs to skip (for retrying partial failures)"),
27
+ },
28
+ async execute(args, context) {
29
+ try {
30
+ const result = await postReview({
31
+ threadsPath: args.threads_path,
32
+ prNumber: args.pr_number,
33
+ dryRun: args.dry_run ?? false,
34
+ skipIds: args.skip_ids,
35
+ cwd: context.directory,
36
+ });
37
+ return result.summary;
38
+ } catch (err) {
39
+ throw err instanceof Error ? err : new Error(String(err));
40
+ }
41
+ },
42
+ }),
43
+ },
44
+ };
45
+ };
package/README.md CHANGED
@@ -4,132 +4,80 @@ Multi-agent parallel code/spec review for [OpenCode](https://opencode.ai). Spawn
4
4
  review agents, cross-validates findings, synthesizes results, and produces an actionable
5
5
  implementation plan.
6
6
 
7
- ## How it works
8
-
9
- ### Code review (`/deepreview`)
10
-
11
- ```
12
- Stage 1: 5 parallel reviewers (correctness, security, architecture, docs, compatibility)
13
- Stage 2: 5 parallel cross-validators (try to disprove each finding)
14
- Stage 3: Synthesizer (deduplicate, rank, produce unified report)
15
- Stage 4: Planner (write exact code fixes)
16
- Stage 5: Applier (apply fixes — user-gated)
17
- ```
18
-
19
- ### Spec/plan review (`/deepreview-spec`)
20
-
21
- ```
22
- Stage 1: 5 parallel reviewers (completeness, consistency, feasibility, docs, architecture)
23
- Stage 2: 5 parallel cross-validators (try to disprove each finding)
24
- Stage 3: Synthesizer (deduplicate, rank, produce unified report)
25
- Stage 4: Planner (write spec/plan fixes, not code fixes)
26
- Stage 5: Applier (apply fixes — user-gated)
27
- ```
28
-
29
- All communication between stages happens via files on disk. The orchestrator never reads
30
- review content into its own context, keeping token usage minimal.
31
-
32
7
  ## Install
33
8
 
34
- ```bash
35
- npx @mechanai/deepreview install
36
- ```
37
-
38
- This copies agent and command files into `~/.config/opencode/` and adds `.ai/deepreview/`
39
- to the local `.gitignore` (where review output is written). Run it again after updating
40
- the package to sync changes.
9
+ Add to your `opencode.json` (project-level or global):
41
10
 
42
- To add the gitignore entry to your global gitignore instead:
43
-
44
- ```bash
45
- npx @mechanai/deepreview install --gitignore-global
11
+ ```jsonc
12
+ {
13
+ "plugin": ["@mechanai/deepreview"],
14
+ }
46
15
  ```
47
16
 
48
- To remove:
17
+ OpenCode installs the package automatically at startup.
18
+
19
+ ## Usage
49
20
 
50
- ```bash
51
- npx @mechanai/deepreview uninstall
52
21
  ```
22
+ /deepreview # Review current branch vs main
23
+ /deepreview 123 # Review PR #123
24
+ /deepreview file1.ts file2.ts # Review specific files
53
25
 
54
- ## Usage
26
+ /deepreview-loop # Review + fix loop (repeats until clean or 5 iterations)
27
+ /deepreview-loop 123 # Same, targeting a PR
55
28
 
56
- In any OpenCode session inside a git repo:
29
+ /deepreview-pr-review 123 # Review PR and post findings as a pending GitHub review
57
30
 
31
+ /deepreview-spec spec.md # Spec-focused review (completeness, consistency, feasibility)
32
+ /deepreview-spec-loop spec.md # Spec review + fix loop
58
33
  ```
59
- /deepreview # Review current branch vs main
60
- /deepreview 123 # Review PR #123
61
- /deepreview path/to/spec.md # Review a spec or plan
62
- /deepreview doc1.md doc2.md # Review multiple files
63
34
 
64
- /deepreview-loop # Review + fix loop until clean
65
- /deepreview-loop 123 # Same, targeting a PR
66
- /deepreview-loop spec.md # Same, targeting files
35
+ All commands accept a branch diff, PR number, or file path(s). The `-loop` variants
36
+ apply fixes automatically and re-review until no findings remain. Pauses on plateaus
37
+ (same finding persists across iterations).
67
38
 
68
- /deepreview-spec spec.md # Spec-focused review (completeness, consistency, feasibility)
69
- /deepreview-spec a.md b.md # Review multiple spec/plan files
39
+ ## Pipeline
70
40
 
71
- /deepreview-spec-loop spec.md # Review + fix loop for specs until clean
72
- /deepreview-spec-loop a.md b.md # Same, targeting multiple files
41
+ ```mermaid
42
+ graph LR
43
+ A[5 Reviewers] --> B[5 Validators]
44
+ B --> C[Synthesizer]
45
+ C --> D[Planner]
46
+ D --> E[Applier]
73
47
  ```
74
48
 
75
- `/deepreview-loop` runs the full code review, applies all fixes automatically, then
76
- re-reviews. It repeats until no findings remain or hits the iteration limit (5,
77
- extendable). Pauses on decision deadlocks (same finding persists across iterations).
49
+ Stages communicate via files on disk the orchestrator never reads review content into
50
+ its own context, keeping token usage minimal.
78
51
 
79
- `/deepreview-spec-loop` does the same for spec/plan files, applying spec fixes (not code
80
- fixes) each iteration. Includes plateau detection to stop when findings oscillate rather
81
- than converge.
52
+ ### Review agents
82
53
 
83
- The pipeline runs automatically. At the end, you'll see a summary and be asked whether
84
- to apply the fixes.
54
+ | Agent | Code review | Spec review |
55
+ | --------------------------- | -------------------------------------- | -------------------------------------------- |
56
+ | correctness / completeness | Logic bugs, edge cases, error handling | Gaps, missing edge cases, undefined behavior |
57
+ | security / consistency | Vulnerabilities, performance | Contradictions, name mismatches, type drift |
58
+ | architecture | Patterns, coupling, complexity | Patterns, coupling, complexity |
59
+ | docs | Comment quality, stale claims | Comment quality, stale claims |
60
+ | compatibility / feasibility | Breaking changes, API contracts | Implicit dependencies, can it be built |
85
61
 
86
62
  ## Requirements
87
63
 
88
64
  - [OpenCode](https://opencode.ai)
89
- - `git` (for diffs)
90
- - `gh` CLI (only if reviewing PRs by number)
65
+ - `git`
66
+ - `gh` CLI (only for PR commands)
91
67
 
92
- ## Review agents
68
+ > [!NOTE]
69
+ > If upgrading from the old `npx @anthropic/deepreview install` workflow, remove
70
+ > `~/.config/opencode/agents/deepreview*` files — they are no longer used.
93
71
 
94
- ### Code review
72
+ ## Development
95
73
 
96
- | Agent | Focus |
97
- | ------------- | ----------------------------------------------------- |
98
- | correctness | Logic bugs, edge cases, error handling, missing tests |
99
- | security | Vulnerabilities, auth issues, performance bottlenecks |
100
- | architecture | Patterns, coupling, abstractions, complexity |
101
- | docs | Comment quality, stale claims, duplicate content |
102
- | compatibility | Breaking changes, API contract violations |
74
+ This project uses [Bun](https://bun.sh/) as its runtime and package manager.
103
75
 
104
- ### Spec/plan review
105
-
106
- | Agent | Focus |
107
- | ----------------- | -------------------------------------------------- |
108
- | spec-completeness | Gaps, missing edge cases, undefined behavior |
109
- | spec-consistency | Contradictions, name mismatches, type drift |
110
- | spec-feasibility | Can it be built, implicit dependencies, complexity |
111
- | docs | Comment quality, stale claims, duplicate content |
112
- | architecture | Patterns, coupling, abstractions, complexity |
113
-
114
- ## Output
115
-
116
- All review artifacts are saved to `.ai/deepreview/<branch-or-PR>-<date>/`:
117
-
118
- ```
119
- .ai/deepreview/feature-xyz-2025-05-10/
120
- ├── diff.txt
121
- ├── review-correctness.md
122
- ├── review-security.md
123
- ├── review-architecture.md
124
- ├── review-docs.md
125
- ├── review-compatibility.md
126
- ├── validated-correctness.md
127
- ├── validated-security.md
128
- ├── validated-architecture.md
129
- ├── validated-docs.md
130
- ├── validated-compatibility.md
131
- ├── synthesis.md
132
- └── implementation-plan.md
76
+ ```bash
77
+ bun install
78
+ mise run test
79
+ mise run lint
80
+ mise run fmt
133
81
  ```
134
82
 
135
83
  ## License
package/package.json CHANGED
@@ -1,24 +1,47 @@
1
1
  {
2
2
  "name": "@mechanai/deepreview",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "Multi-agent parallel code/spec review for OpenCode",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/mechanai/deepreview"
9
9
  },
10
- "bin": "./src/cli.js",
11
10
  "files": [
12
11
  "src/",
13
- "agents/",
14
- "commands/"
12
+ ".opencode/"
15
13
  ],
14
+ "type": "module",
15
+ "main": "./.opencode/plugins/deepreview.ts",
16
+ "exports": {
17
+ ".": "./.opencode/plugins/deepreview.ts",
18
+ "./api": "./src/post-review.ts"
19
+ },
16
20
  "publishConfig": {
17
21
  "access": "public"
18
22
  },
23
+ "dependencies": {
24
+ "gray-matter": "4.0.3",
25
+ "js-yaml": "4.1.0",
26
+ "parse-diff": "0.11.1"
27
+ },
19
28
  "devDependencies": {
29
+ "@opencode-ai/plugin": "1.4.7",
30
+ "@types/js-yaml": "^4.0.9",
31
+ "bun-types": "^1.3.14",
20
32
  "oxfmt": "0.52.0",
21
- "oxlint": "1.67.0"
33
+ "oxlint": "1.67.0",
34
+ "oxlint-tsgolint": "^0.23.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@opencode-ai/plugin": ">=1.4.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@opencode-ai/plugin": {
41
+ "optional": true
42
+ }
22
43
  },
23
- "packageManager": "yarn@4.15.0"
44
+ "engines": {
45
+ "bun": ">=1.2.0"
46
+ }
24
47
  }
@@ -0,0 +1,45 @@
1
+ import { describe, it } from "bun:test";
2
+ import assert from "node:assert/strict";
3
+ import { classifyFindings } from "./diff-classifier.ts";
4
+
5
+ const SAMPLE_DIFF = [
6
+ "diff --git a/pkg/server/handler.go b/pkg/server/handler.go",
7
+ "index abc1234..def5678 100644",
8
+ "--- a/pkg/server/handler.go",
9
+ "+++ b/pkg/server/handler.go",
10
+ "@@ -40,6 +40,10 @@ func Handle() {",
11
+ " existing()",
12
+ "+ newLine1()",
13
+ "+ newLine2()",
14
+ "+ newLine3()",
15
+ "+ newLine4()",
16
+ " more()",
17
+ "diff --git a/README.md b/README.md",
18
+ "index 111..222 100644",
19
+ "--- a/README.md",
20
+ "+++ b/README.md",
21
+ "@@ -1,3 +1,4 @@",
22
+ " # Title",
23
+ "+New line",
24
+ " Rest",
25
+ ].join("\n");
26
+
27
+ describe("classifyFindings", () => {
28
+ it("tier 1: line within a diff hunk", () => {
29
+ const findings = [{ path: "pkg/server/handler.go", line: 42, body: "issue" }];
30
+ const result = classifyFindings(findings, SAMPLE_DIFF);
31
+ assert.equal(result[0].tier, 1);
32
+ });
33
+
34
+ it("tier 2: file in diff but line not in any hunk", () => {
35
+ const findings = [{ path: "pkg/server/handler.go", line: 200, body: "issue" }];
36
+ const result = classifyFindings(findings, SAMPLE_DIFF);
37
+ assert.equal(result[0].tier, 2);
38
+ });
39
+
40
+ it("tier 3: file not in diff at all", () => {
41
+ const findings = [{ path: "internal/other.go", line: 10, body: "issue" }];
42
+ const result = classifyFindings(findings, SAMPLE_DIFF);
43
+ assert.equal(result[0].tier, 3);
44
+ });
45
+ });
@@ -0,0 +1,63 @@
1
+ import parseDiff from "parse-diff";
2
+
3
+ export interface Finding {
4
+ path: string;
5
+ line: number;
6
+ startLine?: number;
7
+ body: string;
8
+ }
9
+
10
+ export interface ClassifiedFinding extends Finding {
11
+ tier: 1 | 2 | 3;
12
+ }
13
+
14
+ interface FileHunks {
15
+ hunks: { newStart: number; newLines: number }[];
16
+ }
17
+
18
+ /**
19
+ * Classify findings into placement tiers based on the PR diff.
20
+ *
21
+ * Tier 1: line-level (line within a diff hunk's new-side range)
22
+ * Tier 2: file-level (file in diff but line not in any hunk)
23
+ * Tier 3: review body (file not in diff)
24
+ */
25
+ export function classifyFindings(findings: Finding[], diffText: string): ClassifiedFinding[] {
26
+ const parsed = parseDiff(diffText);
27
+ const fileMap = buildFileMap(parsed);
28
+
29
+ return findings.map((finding) => {
30
+ const file = fileMap.get(finding.path);
31
+ if (!file) {
32
+ return { ...finding, tier: 3 as const };
33
+ }
34
+
35
+ const lineInHunk = file.hunks.some((hunk) => {
36
+ return finding.line >= hunk.newStart && finding.line < hunk.newStart + hunk.newLines;
37
+ });
38
+
39
+ if (lineInHunk) {
40
+ return { ...finding, tier: 1 as const };
41
+ }
42
+ return { ...finding, tier: 2 as const };
43
+ });
44
+ }
45
+
46
+ /**
47
+ * Build a map of file path → {hunks: [{newStart, newLines}]}
48
+ */
49
+ function buildFileMap(parsedDiff: parseDiff.File[]): Map<string, FileHunks> {
50
+ const map = new Map<string, FileHunks>();
51
+ for (const file of parsedDiff) {
52
+ const filePath = file.to === "/dev/null" ? file.from : file.to;
53
+ if (filePath === undefined || filePath === "") continue;
54
+
55
+ const hunks = file.chunks.map((chunk) => ({
56
+ newStart: chunk.newStart,
57
+ newLines: chunk.newLines,
58
+ }));
59
+
60
+ map.set(filePath, { hunks });
61
+ }
62
+ return map;
63
+ }