co-maintainer 0.4.0-beta.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.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +9 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.js +2 -0
- package/dist/package.json +54 -0
- package/dist/src/ai/batch.d.ts +16 -0
- package/dist/src/ai/batch.js +117 -0
- package/dist/src/ai/fake.d.ts +7 -0
- package/dist/src/ai/fake.js +40 -0
- package/dist/src/ai/hetzner.d.ts +16 -0
- package/dist/src/ai/hetzner.js +69 -0
- package/dist/src/ai/mermaid.d.ts +27 -0
- package/dist/src/ai/mermaid.js +457 -0
- package/dist/src/ai/mermaid_loop.d.ts +12 -0
- package/dist/src/ai/mermaid_loop.js +100 -0
- package/dist/src/ai/openrouter.d.ts +9 -0
- package/dist/src/ai/openrouter.js +29 -0
- package/dist/src/ai/provider.d.ts +4 -0
- package/dist/src/ai/provider.js +63 -0
- package/dist/src/cli/args.d.ts +5 -0
- package/dist/src/cli/args.js +252 -0
- package/dist/src/cli/commands/probe.d.ts +2 -0
- package/dist/src/cli/commands/probe.js +122 -0
- package/dist/src/cli/commands/review.d.ts +3 -0
- package/dist/src/cli/commands/review.js +84 -0
- package/dist/src/cli/commands/serve.d.ts +18 -0
- package/dist/src/cli/commands/serve.js +183 -0
- package/dist/src/cli/commands/set.d.ts +7 -0
- package/dist/src/cli/commands/set.js +171 -0
- package/dist/src/cli/main.d.ts +1 -0
- package/dist/src/cli/main.js +27 -0
- package/dist/src/cli/prompt.d.ts +4 -0
- package/dist/src/cli/prompt.js +26 -0
- package/dist/src/cli/review_args.d.ts +28 -0
- package/dist/src/cli/review_args.js +84 -0
- package/dist/src/cli/review_output.d.ts +2 -0
- package/dist/src/cli/review_output.js +10 -0
- package/dist/src/cli/review_result.d.ts +81 -0
- package/dist/src/cli/review_result.js +281 -0
- package/dist/src/config.d.ts +108 -0
- package/dist/src/config.js +138 -0
- package/dist/src/git/default_branch.d.ts +5 -0
- package/dist/src/git/default_branch.js +22 -0
- package/dist/src/github/app.d.ts +43 -0
- package/dist/src/github/app.js +120 -0
- package/dist/src/github/client.d.ts +17 -0
- package/dist/src/github/client.js +66 -0
- package/dist/src/github/collect.d.ts +7 -0
- package/dist/src/github/collect.js +368 -0
- package/dist/src/github/gh.d.ts +8 -0
- package/dist/src/github/gh.js +63 -0
- package/dist/src/github/jwt.d.ts +12 -0
- package/dist/src/github/jwt.js +84 -0
- package/dist/src/github/oauth.d.ts +9 -0
- package/dist/src/github/oauth.js +51 -0
- package/dist/src/github/pat.d.ts +10 -0
- package/dist/src/github/pat.js +60 -0
- package/dist/src/knowledge/facts.d.ts +3 -0
- package/dist/src/knowledge/facts.js +325 -0
- package/dist/src/knowledge/guide.d.ts +6 -0
- package/dist/src/knowledge/guide.js +82 -0
- package/dist/src/knowledge/probe.d.ts +12 -0
- package/dist/src/knowledge/probe.js +144 -0
- package/dist/src/knowledge/sections.d.ts +2 -0
- package/dist/src/knowledge/sections.js +13 -0
- package/dist/src/knowledge/skill.d.ts +10 -0
- package/dist/src/knowledge/skill.js +106 -0
- package/dist/src/knowledge/synthesis.d.ts +7 -0
- package/dist/src/knowledge/synthesis.js +315 -0
- package/dist/src/knowledge/types.d.ts +49 -0
- package/dist/src/knowledge/types.js +1 -0
- package/dist/src/knowledge/validate.d.ts +6 -0
- package/dist/src/knowledge/validate.js +134 -0
- package/dist/src/local/carry_over_store.d.ts +18 -0
- package/dist/src/local/carry_over_store.js +40 -0
- package/dist/src/local/codegraph_exclude.d.ts +3 -0
- package/dist/src/local/codegraph_exclude.js +26 -0
- package/dist/src/local/codegraph_prepare.d.ts +17 -0
- package/dist/src/local/codegraph_prepare.js +51 -0
- package/dist/src/local/git_ops.d.ts +12 -0
- package/dist/src/local/git_ops.js +98 -0
- package/dist/src/local/git_parse.d.ts +27 -0
- package/dist/src/local/git_parse.js +131 -0
- package/dist/src/local/git_revision.d.ts +15 -0
- package/dist/src/local/git_revision.js +141 -0
- package/dist/src/local/git_untracked.d.ts +21 -0
- package/dist/src/local/git_untracked.js +79 -0
- package/dist/src/local/review_local.d.ts +4 -0
- package/dist/src/local/review_local.js +273 -0
- package/dist/src/local/review_lock.d.ts +7 -0
- package/dist/src/local/review_lock.js +45 -0
- package/dist/src/pr/checkout.d.ts +25 -0
- package/dist/src/pr/checkout.js +136 -0
- package/dist/src/pr/codegraph_tool_args.d.ts +8 -0
- package/dist/src/pr/codegraph_tool_args.js +34 -0
- package/dist/src/pr/codegraph_tools.d.ts +15 -0
- package/dist/src/pr/codegraph_tools.js +408 -0
- package/dist/src/pr/diff_summary.d.ts +6 -0
- package/dist/src/pr/diff_summary.js +52 -0
- package/dist/src/pr/findings.d.ts +14 -0
- package/dist/src/pr/findings.js +94 -0
- package/dist/src/pr/github_revision.d.ts +4 -0
- package/dist/src/pr/github_revision.js +39 -0
- package/dist/src/pr/hunks.d.ts +18 -0
- package/dist/src/pr/hunks.js +73 -0
- package/dist/src/pr/reviewer.d.ts +40 -0
- package/dist/src/pr/reviewer.js +582 -0
- package/dist/src/pr/rounds.d.ts +17 -0
- package/dist/src/pr/rounds.js +43 -0
- package/dist/src/pr/scope.d.ts +35 -0
- package/dist/src/pr/scope.js +84 -0
- package/dist/src/pr/snapshot.d.ts +11 -0
- package/dist/src/pr/snapshot.js +1 -0
- package/dist/src/pr/suggestion.d.ts +17 -0
- package/dist/src/pr/suggestion.js +70 -0
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +199 -0
- package/dist/src/remote/client_tools.d.ts +13 -0
- package/dist/src/remote/client_tools.js +30 -0
- package/dist/src/remote/fixtures.d.ts +1 -0
- package/dist/src/remote/fixtures.js +30 -0
- package/dist/src/remote/revision_from_submit.d.ts +2 -0
- package/dist/src/remote/revision_from_submit.js +26 -0
- package/dist/src/remote/schema.d.ts +14 -0
- package/dist/src/remote/schema.js +30 -0
- package/dist/src/remote/server/body.d.ts +18 -0
- package/dist/src/remote/server/body.js +73 -0
- package/dist/src/remote/server/routes.d.ts +1 -0
- package/dist/src/remote/server/routes.js +150 -0
- package/dist/src/remote/server/sessions.d.ts +16 -0
- package/dist/src/remote/server/sessions.js +48 -0
- package/dist/src/remote/server/sync.d.ts +3 -0
- package/dist/src/remote/server/sync.js +90 -0
- package/dist/src/remote/tool_bridge.d.ts +15 -0
- package/dist/src/remote/tool_bridge.js +84 -0
- package/dist/src/remote/validate.d.ts +8 -0
- package/dist/src/remote/validate.js +248 -0
- package/dist/src/review/carry_over.d.ts +63 -0
- package/dist/src/review/carry_over.js +372 -0
- package/dist/src/review/engine.d.ts +13 -0
- package/dist/src/review/engine.js +4 -0
- package/dist/src/review/guides.d.ts +12 -0
- package/dist/src/review/guides.js +44 -0
- package/dist/src/review/revision.d.ts +26 -0
- package/dist/src/review/revision.js +57 -0
- package/dist/src/server/api/activity.d.ts +1 -0
- package/dist/src/server/api/activity.js +12 -0
- package/dist/src/server/api/analytics.d.ts +1 -0
- package/dist/src/server/api/analytics.js +9 -0
- package/dist/src/server/api/installations.d.ts +4 -0
- package/dist/src/server/api/installations.js +24 -0
- package/dist/src/server/api/jobs.d.ts +1 -0
- package/dist/src/server/api/jobs.js +86 -0
- package/dist/src/server/api/json_body.d.ts +1 -0
- package/dist/src/server/api/json_body.js +14 -0
- package/dist/src/server/api/remote_tokens.d.ts +1 -0
- package/dist/src/server/api/remote_tokens.js +71 -0
- package/dist/src/server/api/repos.d.ts +4 -0
- package/dist/src/server/api/repos.js +136 -0
- package/dist/src/server/api/settings.d.ts +1 -0
- package/dist/src/server/api/settings.js +210 -0
- package/dist/src/server/app.d.ts +27 -0
- package/dist/src/server/app.js +147 -0
- package/dist/src/server/assets.d.ts +3 -0
- package/dist/src/server/assets.js +21 -0
- package/dist/src/server/auth.d.ts +44 -0
- package/dist/src/server/auth.js +188 -0
- package/dist/src/server/errors.d.ts +8 -0
- package/dist/src/server/errors.js +10 -0
- package/dist/src/server/http.d.ts +8 -0
- package/dist/src/server/http.js +117 -0
- package/dist/src/server/logo.d.ts +1 -0
- package/dist/src/server/logo.js +2 -0
- package/dist/src/server/pages/activity.d.ts +4 -0
- package/dist/src/server/pages/activity.js +200 -0
- package/dist/src/server/pages/add_repo.d.ts +9 -0
- package/dist/src/server/pages/add_repo.js +56 -0
- package/dist/src/server/pages/analytics.d.ts +2 -0
- package/dist/src/server/pages/analytics.js +193 -0
- package/dist/src/server/pages/client.d.ts +2 -0
- package/dist/src/server/pages/client.js +2 -0
- package/dist/src/server/pages/home.d.ts +16 -0
- package/dist/src/server/pages/home.js +87 -0
- package/dist/src/server/pages/knowledge.d.ts +2 -0
- package/dist/src/server/pages/knowledge.js +64 -0
- package/dist/src/server/pages/layout.d.ts +32 -0
- package/dist/src/server/pages/layout.js +230 -0
- package/dist/src/server/pages/pr.d.ts +2 -0
- package/dist/src/server/pages/pr.js +109 -0
- package/dist/src/server/pages/repo.d.ts +2 -0
- package/dist/src/server/pages/repo.js +92 -0
- package/dist/src/server/pages/repo_prs.d.ts +2 -0
- package/dist/src/server/pages/repo_prs.js +75 -0
- package/dist/src/server/pages/repo_remote.d.ts +2 -0
- package/dist/src/server/pages/repo_remote.js +38 -0
- package/dist/src/server/pages/repo_settings.d.ts +3 -0
- package/dist/src/server/pages/repo_settings.js +127 -0
- package/dist/src/server/pages/router.d.ts +17 -0
- package/dist/src/server/pages/router.js +312 -0
- package/dist/src/server/pages/settings.d.ts +2 -0
- package/dist/src/server/pages/settings.js +337 -0
- package/dist/src/server/pages/setup.d.ts +5 -0
- package/dist/src/server/pages/setup.js +38 -0
- package/dist/src/server/pages/styles.d.ts +2 -0
- package/dist/src/server/pages/styles.js +2 -0
- package/dist/src/server/webhook/index.d.ts +1 -0
- package/dist/src/server/webhook/index.js +64 -0
- package/dist/src/server/webhook/signature.d.ts +7 -0
- package/dist/src/server/webhook/signature.js +41 -0
- package/dist/src/services/credentials.d.ts +19 -0
- package/dist/src/services/credentials.js +146 -0
- package/dist/src/services/dashboard.d.ts +165 -0
- package/dist/src/services/dashboard.js +200 -0
- package/dist/src/services/drift.d.ts +13 -0
- package/dist/src/services/drift.js +76 -0
- package/dist/src/services/jobs.d.ts +34 -0
- package/dist/src/services/jobs.js +222 -0
- package/dist/src/services/remote_review.d.ts +10 -0
- package/dist/src/services/remote_review.js +274 -0
- package/dist/src/services/remote_review_abort.d.ts +3 -0
- package/dist/src/services/remote_review_abort.js +11 -0
- package/dist/src/services/remote_token_jobs.d.ts +1 -0
- package/dist/src/services/remote_token_jobs.js +20 -0
- package/dist/src/services/remote_tokens.d.ts +13 -0
- package/dist/src/services/remote_tokens.js +57 -0
- package/dist/src/services/replies.d.ts +7 -0
- package/dist/src/services/replies.js +248 -0
- package/dist/src/services/review.d.ts +35 -0
- package/dist/src/services/review.js +671 -0
- package/dist/src/services/setup.d.ts +29 -0
- package/dist/src/services/setup.js +364 -0
- package/dist/src/services/webhook.d.ts +22 -0
- package/dist/src/services/webhook.js +401 -0
- package/dist/src/store/app_db.d.ts +10 -0
- package/dist/src/store/app_db.js +85 -0
- package/dist/src/store/cache_db.d.ts +4 -0
- package/dist/src/store/cache_db.js +63 -0
- package/dist/src/store/deliveries.d.ts +12 -0
- package/dist/src/store/deliveries.js +23 -0
- package/dist/src/store/drift.d.ts +4 -0
- package/dist/src/store/drift.js +22 -0
- package/dist/src/store/findings.d.ts +31 -0
- package/dist/src/store/findings.js +64 -0
- package/dist/src/store/installations.d.ts +9 -0
- package/dist/src/store/installations.js +35 -0
- package/dist/src/store/job_logs.d.ts +5 -0
- package/dist/src/store/job_logs.js +17 -0
- package/dist/src/store/jobs.d.ts +27 -0
- package/dist/src/store/jobs.js +86 -0
- package/dist/src/store/migrations.d.ts +5 -0
- package/dist/src/store/migrations.js +258 -0
- package/dist/src/store/remote_review_inputs.d.ts +11 -0
- package/dist/src/store/remote_review_inputs.js +25 -0
- package/dist/src/store/remote_tokens.d.ts +10 -0
- package/dist/src/store/remote_tokens.js +45 -0
- package/dist/src/store/replies.d.ts +24 -0
- package/dist/src/store/replies.js +79 -0
- package/dist/src/store/repos.d.ts +11 -0
- package/dist/src/store/repos.js +59 -0
- package/dist/src/store/reviews.d.ts +86 -0
- package/dist/src/store/reviews.js +220 -0
- package/dist/src/store/rows.d.ts +179 -0
- package/dist/src/store/rows.js +4 -0
- package/dist/src/store/sessions.d.ts +8 -0
- package/dist/src/store/sessions.js +34 -0
- package/dist/src/store/skill_state.d.ts +3 -0
- package/dist/src/store/skill_state.js +24 -0
- package/dist/src/store/sqlite.d.ts +39 -0
- package/dist/src/store/sqlite.js +77 -0
- package/dist/src/store/subjects.d.ts +15 -0
- package/dist/src/store/subjects.js +78 -0
- package/dist/src/tools/codegraph.d.ts +58 -0
- package/dist/src/tools/codegraph.js +165 -0
- package/dist/src/tools/codegraph_exec.d.ts +13 -0
- package/dist/src/tools/codegraph_exec.js +66 -0
- package/dist/src/types.d.ts +89 -0
- package/dist/src/types.js +1 -0
- package/dist/src/util/atomic.d.ts +4 -0
- package/dist/src/util/atomic.js +78 -0
- package/dist/src/util/keyed_lock.d.ts +3 -0
- package/dist/src/util/keyed_lock.js +33 -0
- package/dist/src/util/log.d.ts +11 -0
- package/dist/src/util/log.js +42 -0
- package/dist/src/util/redact.d.ts +5 -0
- package/dist/src/util/redact.js +30 -0
- package/dist/src/util/runtime.d.ts +90 -0
- package/dist/src/util/runtime.js +206 -0
- package/dist/src/util/time.d.ts +9 -0
- package/dist/src/util/time.js +36 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +5 -0
- package/package.json +54 -0
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
import { OpenRouterProvider } from "../ai/openrouter.js";
|
|
2
|
+
import { completeWithMermaidTools, } from "../ai/mermaid_loop.js";
|
|
3
|
+
import { loadGuides } from "../review/guides.js";
|
|
4
|
+
import { computeScope } from "./scope.js";
|
|
5
|
+
import { prepareCodegraphTools } from "./codegraph_tools.js";
|
|
6
|
+
import { needsSummary, READ_FULL_DIFF_TOOL, readFullDiff, summarizeDiff, } from "./diff_summary.js";
|
|
7
|
+
import { numberPatch } from "./hunks.js";
|
|
8
|
+
const MAX_REVIEW_DIFF_CHARS = 240_000;
|
|
9
|
+
export const MERMAID_GUIDANCE = `Mermaid selection and minimal syntax:
|
|
10
|
+
flowchart = decisions, branches, pipelines, and fallback paths;
|
|
11
|
+
swimlane-beta = work crossing owners, actors, services, teams, or layers;
|
|
12
|
+
sequenceDiagram = ordered calls, webhooks, retries, responses, and timing;
|
|
13
|
+
classDiagram = class, interface, type, inheritance, or composition relationships;
|
|
14
|
+
stateDiagram-v2 = lifecycle and state transitions;
|
|
15
|
+
erDiagram = database entities, keys, and cardinality;
|
|
16
|
+
requirementDiagram = requirements linked to tests or implementation;
|
|
17
|
+
usecase-beta = actors and system capabilities;
|
|
18
|
+
C4Context = users, systems, boundaries, and integrations;
|
|
19
|
+
zenuml = compact nested call sequences;
|
|
20
|
+
packet = binary fields, bit ranges, and protocol layout;
|
|
21
|
+
architecture-beta = services, containers, storage, and deployment topology;
|
|
22
|
+
eventmodeling = commands, events, processors, read models, and timelines;
|
|
23
|
+
treeView-beta = directory, file, module, or dependency hierarchy.
|
|
24
|
+
If a diagram is useful, call read-mermaid-syntaxes before writing any Mermaid.
|
|
25
|
+
Request every type you need in a single call, because repeated tool rounds are
|
|
26
|
+
capped. Use the smallest type that matches the evidence.`;
|
|
27
|
+
const REVIEW_ROLE = `You are a precise open-source code reviewer.
|
|
28
|
+
Evidence must come from the supplied diff and review guide. Keep findings
|
|
29
|
+
concise by default.`;
|
|
30
|
+
const REVIEW_DIAGRAM_RULES = `People generally find it easier to understand the
|
|
31
|
+
problem you've identified when it's presented in diagrams. When a multi-step
|
|
32
|
+
flow, lifecycle, dependency, data model, protocol, or architecture change is
|
|
33
|
+
part of the finding, you are expected to draw it — do not skip the diagram
|
|
34
|
+
just to avoid the extra tool call. During the initial review, use at most one
|
|
35
|
+
diagram, so spend it on the finding that benefits most. If a user later asks
|
|
36
|
+
for detailed reasoning in a reply, that reply may use up to five diagrams, but
|
|
37
|
+
only when each adds a distinct useful view.
|
|
38
|
+
${MERMAID_GUIDANCE}
|
|
39
|
+
Do not invent nodes, actors, states, services, tables, or events. Omit the
|
|
40
|
+
diagram only when no finding actually fits one of the categories above, or
|
|
41
|
+
when the syntax is genuinely uncertain.`;
|
|
42
|
+
/** Without tool support the model cannot read the Mermaid syntax docs, so
|
|
43
|
+
* asking for a diagram only invites invented syntax. */
|
|
44
|
+
export const NO_DIAGRAM_RULES = `Do not use Mermaid or any other diagram. Explain with prose only.`;
|
|
45
|
+
const DIAGRAM_PROMPT_RULES = `People generally find it easier to understand the
|
|
46
|
+
problem you've identified when it's presented in diagrams. When a finding
|
|
47
|
+
involves a multi-step flow, lifecycle, dependency, data model, protocol, or
|
|
48
|
+
architecture change, you are expected to include a Mermaid fenced code block
|
|
49
|
+
for it — reading the syntax with read-mermaid-syntaxes first is a small cost,
|
|
50
|
+
not a reason to skip the diagram. This review may contain at most one diagram
|
|
51
|
+
in total, so if more than one finding qualifies, pick the one the diagram
|
|
52
|
+
clarifies most. Its type must be one of the types named in the system
|
|
53
|
+
instructions. Keep labels short and grounded in the supplied evidence. A
|
|
54
|
+
one-line fix or an obvious, single-step cause and effect genuinely needs no
|
|
55
|
+
diagram — that is the only reason to omit one. Close every fenced block.`;
|
|
56
|
+
export function reviewSystemPrompt(diagrams) {
|
|
57
|
+
return `${REVIEW_ROLE}
|
|
58
|
+
${diagrams ? REVIEW_DIAGRAM_RULES : NO_DIAGRAM_RULES}`;
|
|
59
|
+
}
|
|
60
|
+
/** Shared PR and local/remote workspace review instructions: confirm claims
|
|
61
|
+
* against the indexed graph, not only the diff slice. */
|
|
62
|
+
export const CODEGRAPH_DIFF_VERIFICATION = `Examine the changes line by line, not just file by file — a single file can
|
|
63
|
+
contain more than one independent defect, and a change that looks fine in
|
|
64
|
+
isolation can be wrong once you trace what calls it or what else it affects.
|
|
65
|
+
When a finding depends on behavior outside the changed lines, confirm it with
|
|
66
|
+
codegraph-node, codegraph-callers, codegraph-callees, codegraph-impact, and
|
|
67
|
+
codegraph-affected before reporting it. Drop or correct findings that only seem
|
|
68
|
+
plausible from the diff but contradict unchanged callers, callees, or the same
|
|
69
|
+
pattern elsewhere in the repo. Use those tools to check blast radius and whether
|
|
70
|
+
a test reaches the path — do not guess coverage or impact from the diff alone
|
|
71
|
+
when a tool can answer. A missing regression test is not a substitute for
|
|
72
|
+
identifying the concrete input or code path that misbehaves when you can.`;
|
|
73
|
+
function text(value, limit = 20_000) {
|
|
74
|
+
const result = String(value ?? "");
|
|
75
|
+
return result.length > limit
|
|
76
|
+
? `${result.slice(0, limit)}\n[truncated]`
|
|
77
|
+
: result;
|
|
78
|
+
}
|
|
79
|
+
const MAX_FILE_PATCH_CHARS = 12_000;
|
|
80
|
+
/** GitHub omits `patch` entirely for files it considers too large, and the file
|
|
81
|
+
* still appears in the compare response with only its counts. Rendering that as
|
|
82
|
+
* an empty body reads as "this file did not change", and a reviewer then
|
|
83
|
+
* reports the absence as a finding (a lockfile that was in fact regenerated,
|
|
84
|
+
* say). Say plainly that the hunks are missing, and mark a per-file truncation
|
|
85
|
+
* for the same reason. */
|
|
86
|
+
export function filePatch(file) {
|
|
87
|
+
const patch = String(file.patch ?? "");
|
|
88
|
+
const status = String(file.status ?? "modified");
|
|
89
|
+
const changes = Number(file.changes ?? 0);
|
|
90
|
+
const additions = Number(file.additions ?? 0);
|
|
91
|
+
const deletions = Number(file.deletions ?? 0);
|
|
92
|
+
if (patch === "") {
|
|
93
|
+
const counts = Number.isFinite(changes) && changes > 0
|
|
94
|
+
? `${changes} changed lines (+${additions} -${deletions})`
|
|
95
|
+
: "an unreported number of changed lines";
|
|
96
|
+
return (`[${status}; ${counts}; diff withheld by GitHub, not shown here. ` +
|
|
97
|
+
`Do not treat this file as unchanged and do not report its contents.]`);
|
|
98
|
+
}
|
|
99
|
+
if (patch.length > MAX_FILE_PATCH_CHARS) {
|
|
100
|
+
return (`${numberPatch(patch.slice(0, MAX_FILE_PATCH_CHARS))}\n[${status}; ${changes} ` +
|
|
101
|
+
`changed lines total; this file's diff is cut off here, later hunks are ` +
|
|
102
|
+
`not shown.]`);
|
|
103
|
+
}
|
|
104
|
+
return numberPatch(patch);
|
|
105
|
+
}
|
|
106
|
+
// Uncapped, a 100,000-line diff would ask for 1,000+ tool-loop rounds, each
|
|
107
|
+
// able to spend multiple external calls.
|
|
108
|
+
export function clampToolRounds(totalDiffLines) {
|
|
109
|
+
return Math.min(Math.round(4 + totalDiffLines / 100), 8);
|
|
110
|
+
}
|
|
111
|
+
// Uncapped, --improve-matrix scales maxTokens past what most providers'
|
|
112
|
+
// 128k-token context can hold on its own, before the prompt even counts.
|
|
113
|
+
export function clampImproveMatrix(matrix) {
|
|
114
|
+
return Math.min(Math.max(1, matrix), 4);
|
|
115
|
+
}
|
|
116
|
+
export async function readGuide(repo, name) {
|
|
117
|
+
const guides = await loadGuides(repo);
|
|
118
|
+
switch (name) {
|
|
119
|
+
case "PR_REVIEW_GUIDE.md":
|
|
120
|
+
return guides.shortGuide;
|
|
121
|
+
case "PR_REVIEW_DETAILED_GUIDE.md":
|
|
122
|
+
return guides.detailed;
|
|
123
|
+
case "CODEBASE.md":
|
|
124
|
+
return guides.codebase;
|
|
125
|
+
case "SKILL.md":
|
|
126
|
+
return guides.skill;
|
|
127
|
+
default:
|
|
128
|
+
return "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
export async function reviewPullRequest(client, options, usage, snapshot, ai, progress, extras) {
|
|
132
|
+
if (!options.prNumber)
|
|
133
|
+
throw new Error("review requires a PR number");
|
|
134
|
+
const number = options.prNumber;
|
|
135
|
+
const report = progress ?? (() => { });
|
|
136
|
+
report(`loading PR context for ${options.repo}#${number}`);
|
|
137
|
+
const [pr, allComments, allReviews, guides] = await Promise.all([
|
|
138
|
+
client.request(`repos/${options.repo}/pulls/${number}`),
|
|
139
|
+
client.pages(`repos/${options.repo}/issues/${number}/comments`),
|
|
140
|
+
client.pages(`repos/${options.repo}/pulls/${number}/reviews`),
|
|
141
|
+
loadGuides(options.repo),
|
|
142
|
+
]);
|
|
143
|
+
const { shortGuide, detailed, codebase, skill } = guides;
|
|
144
|
+
report(`context loaded · comments=${allComments.length} · reviews=${allReviews.length} · ` +
|
|
145
|
+
`guide=${shortGuide.length || skill.length} chars · codebase=${codebase.length} chars`);
|
|
146
|
+
const guide = shortGuide || skill;
|
|
147
|
+
const before = (item) => !snapshot || String(item.created_at ?? "") < snapshot.before;
|
|
148
|
+
const comments = allComments.filter(before);
|
|
149
|
+
const reviews = allReviews.filter(before);
|
|
150
|
+
const files = snapshot
|
|
151
|
+
? ((await client.request(`repos/${options.repo}/compare/${snapshot.base ??
|
|
152
|
+
String(pr.base?.ref ?? "main")}...${snapshot.commit}`)).files ?? [])
|
|
153
|
+
: await client.pages(`repos/${options.repo}/pulls/${number}/files`);
|
|
154
|
+
report(`diff files loaded · ${files.length} files`);
|
|
155
|
+
if (!guide) {
|
|
156
|
+
throw new Error(`repos/${options.repo}/PR_REVIEW_GUIDE.md was not found; run init first`);
|
|
157
|
+
}
|
|
158
|
+
if (options.debug) {
|
|
159
|
+
console.log(`[debug] guides · short=${guide.length} chars · detailed=${detailed.length} chars · codebase=${codebase.length} chars`);
|
|
160
|
+
console.log(`[debug] PR #${number} · comments=${comments.length} · reviews=${reviews.length} · files=${files.length}`);
|
|
161
|
+
}
|
|
162
|
+
// What the author actually wrote this round, as opposed to code that
|
|
163
|
+
// arrived by merging the default branch in — see scope.ts. A re-review
|
|
164
|
+
// round's diff regularly carries a `merge main` that dwarfs the PR's own
|
|
165
|
+
// change and that no human reviewer reads either; --review-upstream turns
|
|
166
|
+
// this off and reviews everything, matching the pre-scope behavior.
|
|
167
|
+
const headSha = snapshot?.commit ?? String(pr.head?.sha ?? "");
|
|
168
|
+
const baseRevision = snapshot?.base ?? String(pr.base?.ref ?? "main");
|
|
169
|
+
if (options.reviewUpstream) {
|
|
170
|
+
report("scope skipped · --review-upstream");
|
|
171
|
+
}
|
|
172
|
+
else if (!headSha) {
|
|
173
|
+
report("scope skipped · no head commit for this pull request");
|
|
174
|
+
}
|
|
175
|
+
const scope = options.reviewUpstream || !headSha
|
|
176
|
+
? undefined
|
|
177
|
+
: await computeScope(options.repo, baseRevision, headSha);
|
|
178
|
+
if (!options.reviewUpstream && headSha && !scope) {
|
|
179
|
+
report("scope unavailable · reviewing every changed file");
|
|
180
|
+
}
|
|
181
|
+
const named = files.map((file) => {
|
|
182
|
+
const value = file;
|
|
183
|
+
return { file: value, path: String(value.filename ?? "") };
|
|
184
|
+
});
|
|
185
|
+
const unchanged = new Set(extras?.unchangedPaths ?? []);
|
|
186
|
+
const ownFiles = (scope ? named.filter(({ path }) => !scope.upstreamFiles.has(path)) : named).filter(({ path }) => !unchanged.has(path));
|
|
187
|
+
const upstreamFiles = scope
|
|
188
|
+
? named.filter(({ path }) => scope.upstreamFiles.has(path))
|
|
189
|
+
: [];
|
|
190
|
+
if (scope) {
|
|
191
|
+
report(`scope resolved · own=${ownFiles.length} files · upstream=${upstreamFiles.length} files`);
|
|
192
|
+
}
|
|
193
|
+
// Built only if needed: OpenRouterProvider's constructor requires a real
|
|
194
|
+
// API key, which a fake-AI test run never has.
|
|
195
|
+
const filesNeedSummary = ownFiles.some(({ file }) => needsSummary(Number(file.changes ?? 0), String(file.patch ?? "")));
|
|
196
|
+
const lowProvider = filesNeedSummary
|
|
197
|
+
? (ai ??
|
|
198
|
+
new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
|
|
199
|
+
: undefined;
|
|
200
|
+
const patchByPath = new Map();
|
|
201
|
+
const [ownSections, codegraphTools] = await Promise.all([
|
|
202
|
+
Promise.all(ownFiles.map(async ({ file, path }) => {
|
|
203
|
+
const changes = Number(file.changes ?? 0);
|
|
204
|
+
const patch = String(file.patch ?? "");
|
|
205
|
+
if (!needsSummary(changes, patch)) {
|
|
206
|
+
return `FILE: ${path}\n${filePatch(file)}`;
|
|
207
|
+
}
|
|
208
|
+
patchByPath.set(path, patch);
|
|
209
|
+
const description = await summarizeDiff(path, patch, lowProvider, usage);
|
|
210
|
+
report(`summarized large diff · ${path} · ${changes} changed lines`);
|
|
211
|
+
return (`FILE: ${path}\n[${changes} changed lines — summarized below; ` +
|
|
212
|
+
`call read-full-diff("${path}") for the complete diff if this is not ` +
|
|
213
|
+
`enough]\n${description}`);
|
|
214
|
+
})),
|
|
215
|
+
options.useCodegraph && headSha
|
|
216
|
+
? prepareCodegraphTools(options.repo, number, headSha)
|
|
217
|
+
: Promise.resolve([]),
|
|
218
|
+
]);
|
|
219
|
+
if (options.useCodegraph) {
|
|
220
|
+
report(`codegraph tools · ${codegraphTools.length > 0 ? "ready" : "unavailable"}`);
|
|
221
|
+
}
|
|
222
|
+
const extraTools = [
|
|
223
|
+
...codegraphTools,
|
|
224
|
+
...(patchByPath.size > 0
|
|
225
|
+
? [
|
|
226
|
+
{
|
|
227
|
+
name: "read-full-diff",
|
|
228
|
+
tool: READ_FULL_DIFF_TOOL,
|
|
229
|
+
run: (args) => readFullDiff(patchByPath, args),
|
|
230
|
+
},
|
|
231
|
+
]
|
|
232
|
+
: []),
|
|
233
|
+
];
|
|
234
|
+
// Bigger own-scope diffs need more codegraph round-trips to trace — fixed
|
|
235
|
+
// at 3 rounds regardless of size caused the model to run out mid-review on
|
|
236
|
+
// large PRs (verified via a tokio benchmark run: it hallucinated fake
|
|
237
|
+
// tool-call text instead of a finding once tools were dropped).
|
|
238
|
+
const totalDiffLines = ownFiles.reduce((sum, { file }) => sum + Number(file.changes ?? 0), 0);
|
|
239
|
+
const maxToolRounds = clampToolRounds(totalDiffLines);
|
|
240
|
+
report(`tool rounds · ${maxToolRounds} (own diff ${totalDiffLines} changed lines)`);
|
|
241
|
+
const ownPatch = ownSections.join("\n\n");
|
|
242
|
+
const diffWasTruncated = ownPatch.length > MAX_REVIEW_DIFF_CHARS;
|
|
243
|
+
const ownDiff = text(ownPatch, MAX_REVIEW_DIFF_CHARS);
|
|
244
|
+
const unchangedListing = extras?.unchangedPaths?.length
|
|
245
|
+
? `\nUNCHANGED SINCE LAST REVIEW (paths only — do not re-report findings here):\n${extras.unchangedPaths
|
|
246
|
+
.map((path) => `- ${path}`)
|
|
247
|
+
.join("\n")}`
|
|
248
|
+
: "";
|
|
249
|
+
const upstreamListing = upstreamFiles
|
|
250
|
+
.map(({ file, path }) => `- ${path} (+${Number(file.additions ?? 0)} -${Number(file.deletions ?? 0)})`)
|
|
251
|
+
.join("\n");
|
|
252
|
+
const diff = upstreamFiles.length === 0
|
|
253
|
+
? `${ownDiff}${unchangedListing}`
|
|
254
|
+
: `${ownDiff}
|
|
255
|
+
|
|
256
|
+
UPSTREAM CONTEXT — arrived via a merge this round, not authored by this pull
|
|
257
|
+
request. Do not raise a finding located only in this code; only note an
|
|
258
|
+
interaction if the pull request's own change above relies on or conflicts with
|
|
259
|
+
one of these files, and never mark that finding blocking:
|
|
260
|
+
${upstreamListing}${unchangedListing}`;
|
|
261
|
+
report(`diff prepared · ${ownPatch.length} chars${diffWasTruncated ? " · truncated for model context" : ""}`);
|
|
262
|
+
const provider = ai ??
|
|
263
|
+
new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
|
|
264
|
+
const diagrams = provider.supportsTools !== false;
|
|
265
|
+
const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
|
|
266
|
+
const prompt = `Review this pull request against the repository's review guide and
|
|
267
|
+
codebase conventions. Find only actionable code-level violations supported by
|
|
268
|
+
the diff and either the guide or the codebase conventions — a pull request
|
|
269
|
+
that departs from how this repository's own code is actually written is a
|
|
270
|
+
valid finding even when the review guide has no matching rule.
|
|
271
|
+
Do not repeat existing review comments unless the diff still contains the issue.
|
|
272
|
+
Do not invent requirements. Ignore bot noise and historical PR identities.
|
|
273
|
+
Reason thoroughly, then return concise Markdown only with either:
|
|
274
|
+
"## Findings" followed by findings, or "## Findings\\n\\nNo actionable findings."
|
|
275
|
+
There is no fixed number of findings. Return every independently actionable
|
|
276
|
+
finding supported by the diff and guide, including zero findings when appropriate.
|
|
277
|
+
Do not stop early; inspect all supplied diff text first and return the natural
|
|
278
|
+
count. If the diff contains a truncation marker, limit claims to the supplied
|
|
279
|
+
text and do not imply that omitted files were reviewed.
|
|
280
|
+
Do not invent low-value findings.
|
|
281
|
+
When the DIFF section below has an UPSTREAM CONTEXT part, that code arrived
|
|
282
|
+
through a merge and was not authored by this pull request; do not raise a
|
|
283
|
+
finding located only there, and never mark blocking a finding whose only
|
|
284
|
+
support is upstream context.
|
|
285
|
+
${CODEGRAPH_DIFF_VERIFICATION}
|
|
286
|
+
Each finding must use this exact structure, keeping the default finding under
|
|
287
|
+
120 words excluding an optional diagram and an optional suggestion:
|
|
288
|
+
|
|
289
|
+
### [P1 · blocking] \`path/to/file.ts\` — \`symbol()\`
|
|
290
|
+
Location: \`path/to/file.ts:42\`
|
|
291
|
+
|
|
292
|
+
One sentence describing what is wrong and its impact.
|
|
293
|
+
|
|
294
|
+
Add one short evidence paragraph explaining the mechanism or reproduction.
|
|
295
|
+
Do not add labels such as Mechanism, Symptom, Scenario, Verified, Repro,
|
|
296
|
+
Options, or Scope unless that detail is necessary to understand a complex
|
|
297
|
+
finding. End every finding with this exact sentence on its own line:
|
|
298
|
+
"If you'd like me to explain it in more detail, please ask." No finding may
|
|
299
|
+
omit it and nothing may follow it.
|
|
300
|
+
|
|
301
|
+
Use P0-P3 severity and exactly either "blocking" or "non-blocking".
|
|
302
|
+
Keep the Location line machine-readable; it is removed from user-facing
|
|
303
|
+
review copies. Use Markdown backticks around paths and symbols.
|
|
304
|
+
Every diff line in the DIFF section starts with its line number in the new
|
|
305
|
+
file. Copy Location numbers from that column instead of counting from the @@
|
|
306
|
+
header, and use \`path:from-to\` when the finding spans several lines. Removed
|
|
307
|
+
lines have no number, so anchor a finding about removed code to the nearest
|
|
308
|
+
numbered line.
|
|
309
|
+
|
|
310
|
+
Include one GitHub suggestion when the fix is a direct replacement of
|
|
311
|
+
consecutive numbered lines from a single hunk of the same file and you are
|
|
312
|
+
confident in the exact replacement text — this is the common case for
|
|
313
|
+
single-line and small multi-line fixes. Keep Location as the full span of
|
|
314
|
+
the problem, and put the suggestion right before the closing sentence:
|
|
315
|
+
|
|
316
|
+
Suggestion: \`path/to/file.ts:42-43\`
|
|
317
|
+
\`\`\`suggestion
|
|
318
|
+
every line of 42-43 as it should read, with its original indentation
|
|
319
|
+
\`\`\`
|
|
320
|
+
|
|
321
|
+
The Suggestion range must sit inside the Location range and cover only the
|
|
322
|
+
lines the fix changes. The block replaces that whole range, so write every
|
|
323
|
+
line of it, not just the edited part, and write nothing else inside the block.
|
|
324
|
+
When the replacement itself contains three backticks, open and close the block
|
|
325
|
+
with four. Leave the suggestion out when the fix needs removed lines, another
|
|
326
|
+
file, or more than one hunk, or when you are not sure of the exact code.
|
|
327
|
+
${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
|
|
328
|
+
|
|
329
|
+
REVIEW GUIDE:
|
|
330
|
+
${guide}
|
|
331
|
+
|
|
332
|
+
DETAILED GUIDE:
|
|
333
|
+
${detailed}
|
|
334
|
+
|
|
335
|
+
CODEBASE CONVENTIONS:
|
|
336
|
+
${codebase || "None recorded."}
|
|
337
|
+
|
|
338
|
+
PULL REQUEST:
|
|
339
|
+
${JSON.stringify({
|
|
340
|
+
number,
|
|
341
|
+
title: String(pr.title ?? ""),
|
|
342
|
+
body: String(pr.body ?? ""),
|
|
343
|
+
state: pr.state,
|
|
344
|
+
changedFiles: files.map((file) => String(file.filename ?? "")),
|
|
345
|
+
existingComments: comments.map((comment) => String(comment.body ?? "")),
|
|
346
|
+
existingReviews: reviews.map((review) => String(review.body ?? "")),
|
|
347
|
+
})}
|
|
348
|
+
|
|
349
|
+
${carryBlock}DIFF:
|
|
350
|
+
${diff}`;
|
|
351
|
+
const matrix = clampImproveMatrix(options.improveMatrix);
|
|
352
|
+
const request = {
|
|
353
|
+
job: "review_pull_request",
|
|
354
|
+
system: reviewSystemPrompt(diagrams),
|
|
355
|
+
prompt,
|
|
356
|
+
maxTokens: 24_000 * matrix,
|
|
357
|
+
reasoningEffort: "high",
|
|
358
|
+
};
|
|
359
|
+
report(`AI request · model=${options.highModel ?? "openrouter default"} · ` +
|
|
360
|
+
`prompt=${prompt.length} chars · maxTokens=${request.maxTokens}`);
|
|
361
|
+
if (options.debug) {
|
|
362
|
+
console.log(`[debug] review prompt · ${prompt.length} chars · diff=${diff.length} chars`);
|
|
363
|
+
console.log(`[debug] openrouter request · model=${options.highModel ?? "openai/gpt-5.6-luna"} · maxTokens=${request.maxTokens}`);
|
|
364
|
+
}
|
|
365
|
+
let response = await completeWithMermaidTools(provider, request, 1, extraTools, maxToolRounds);
|
|
366
|
+
if (usage)
|
|
367
|
+
await usage(response);
|
|
368
|
+
report(`AI response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
|
|
369
|
+
if (options.debug) {
|
|
370
|
+
console.log(`[debug] initial review response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
|
|
371
|
+
console.log(`\n----- INITIAL REVIEW -----\n${response.text}\n`);
|
|
372
|
+
}
|
|
373
|
+
if (!response.text.trim()) {
|
|
374
|
+
throw new Error("OpenRouter returned an empty review; the reasoning budget may have been exhausted");
|
|
375
|
+
}
|
|
376
|
+
let reviewText = response.text.trim();
|
|
377
|
+
for (let pass = 2; pass <= matrix; pass++) {
|
|
378
|
+
const improvementRequest = {
|
|
379
|
+
...request,
|
|
380
|
+
job: "improve_review",
|
|
381
|
+
prompt: `Audit the draft review below against the complete pull-request diff
|
|
382
|
+
and the supplied review guides. Preserve valid findings, correct inaccurate ones,
|
|
383
|
+
remove duplicate or unsupported ones, and add every missing actionable finding.
|
|
384
|
+
Do not stop early and do not invent requirements. Keep the existing finding
|
|
385
|
+
structure unchanged. Return only the complete revised review in the same format.
|
|
386
|
+
|
|
387
|
+
ORIGINAL REVIEW CONTEXT:
|
|
388
|
+
${prompt}
|
|
389
|
+
|
|
390
|
+
DRAFT REVIEW:
|
|
391
|
+
${reviewText}`,
|
|
392
|
+
};
|
|
393
|
+
if (options.debug) {
|
|
394
|
+
console.log(`[debug] improvement ${pass - 1} request · prompt=${improvementRequest.prompt.length} chars · maxTokens=${improvementRequest.maxTokens}`);
|
|
395
|
+
}
|
|
396
|
+
report(`AI improvement pass ${pass - 1} of ${matrix - 1}`);
|
|
397
|
+
response = await completeWithMermaidTools(provider, improvementRequest, 1, extraTools, maxToolRounds);
|
|
398
|
+
if (usage)
|
|
399
|
+
await usage(response);
|
|
400
|
+
report(`AI improvement response · input=${response.tokensIn} tokens · ` +
|
|
401
|
+
`output=${response.tokensOut} tokens`);
|
|
402
|
+
if (!response.text.trim()) {
|
|
403
|
+
throw new Error(`OpenRouter returned an empty review improvement at pass ${pass - 1}`);
|
|
404
|
+
}
|
|
405
|
+
reviewText = response.text.trim();
|
|
406
|
+
if (options.debug) {
|
|
407
|
+
console.log(`[debug] improvement ${pass - 1} response · input=${response.tokensIn} tokens · output=${response.tokensOut} tokens`);
|
|
408
|
+
console.log(`\n----- IMPROVED REVIEW ${pass - 1} -----\n${reviewText}\n`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
const visiblePaths = ownFiles.map(({ path }) => path);
|
|
412
|
+
return {
|
|
413
|
+
...response,
|
|
414
|
+
text: `## Severity
|
|
415
|
+
|
|
416
|
+
- P0 — Critical: production outage, data loss, or security issue.
|
|
417
|
+
- P1 — High: major behavior is broken and should be fixed before merge.
|
|
418
|
+
- P2 — Medium: important correctness or maintainability issue.
|
|
419
|
+
- P3 — Low: minor, non-blocking improvement or edge case.
|
|
420
|
+
|
|
421
|
+
${reviewText}`,
|
|
422
|
+
visiblePaths,
|
|
423
|
+
guideBuiltAt: guides.guideBuiltAt,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
function revisionToGithubFiles(revision) {
|
|
427
|
+
return revision.files.map((file) => ({
|
|
428
|
+
filename: file.path,
|
|
429
|
+
status: file.status === "renamed" ? "renamed" : file.status,
|
|
430
|
+
previous_filename: file.previousPath,
|
|
431
|
+
additions: file.additions,
|
|
432
|
+
deletions: file.deletions,
|
|
433
|
+
changes: file.additions + file.deletions,
|
|
434
|
+
patch: file.patch,
|
|
435
|
+
}));
|
|
436
|
+
}
|
|
437
|
+
/** Local workspace review — same prompt loop as PR review without GitHub. */
|
|
438
|
+
export async function reviewWorkspaceRevision(revision, options, headSha, usage, ai, progress, extras) {
|
|
439
|
+
const report = progress ?? (() => { });
|
|
440
|
+
const guides = await loadGuides(options.repo);
|
|
441
|
+
const { shortGuide, detailed, codebase, skill } = guides;
|
|
442
|
+
const guide = shortGuide || skill;
|
|
443
|
+
if (!guide) {
|
|
444
|
+
throw new Error(`repos/${options.repo}/PR_REVIEW_GUIDE.md was not found; run init first`);
|
|
445
|
+
}
|
|
446
|
+
const files = revisionToGithubFiles(revision);
|
|
447
|
+
const unchanged = new Set(extras?.unchangedPaths ?? []);
|
|
448
|
+
const ownFiles = files
|
|
449
|
+
.map((file) => ({
|
|
450
|
+
file,
|
|
451
|
+
path: String(file.filename ?? ""),
|
|
452
|
+
}))
|
|
453
|
+
.filter(({ path }) => !unchanged.has(path));
|
|
454
|
+
const patchByPath = new Map();
|
|
455
|
+
const ownSections = await Promise.all(ownFiles.map(async ({ file, path }) => {
|
|
456
|
+
const changes = Number(file.changes ?? 0);
|
|
457
|
+
const patch = String(file.patch ?? "");
|
|
458
|
+
const lowProvider = needsSummary(changes, patch)
|
|
459
|
+
? (ai ??
|
|
460
|
+
new OpenRouterProvider(options.aiToken ?? "", options.lowModel ?? "openai/gpt-oss-120b"))
|
|
461
|
+
: undefined;
|
|
462
|
+
if (!lowProvider)
|
|
463
|
+
return `FILE: ${path}\n${filePatch(file)}`;
|
|
464
|
+
patchByPath.set(path, patch);
|
|
465
|
+
const description = await summarizeDiff(path, patch, lowProvider, usage);
|
|
466
|
+
return `FILE: ${path}\n[${changes} changed lines — summarized below]\n${description}`;
|
|
467
|
+
}));
|
|
468
|
+
const codegraphTools = options.useCodegraph
|
|
469
|
+
? extras?.prepareCodegraphTools
|
|
470
|
+
? await extras.prepareCodegraphTools()
|
|
471
|
+
: await prepareCodegraphTools(options.repo, 0, headSha)
|
|
472
|
+
: [];
|
|
473
|
+
const extraTools = [
|
|
474
|
+
...codegraphTools,
|
|
475
|
+
...(patchByPath.size > 0
|
|
476
|
+
? [
|
|
477
|
+
{
|
|
478
|
+
name: "read-full-diff",
|
|
479
|
+
tool: READ_FULL_DIFF_TOOL,
|
|
480
|
+
run: (args) => readFullDiff(patchByPath, args),
|
|
481
|
+
},
|
|
482
|
+
]
|
|
483
|
+
: []),
|
|
484
|
+
];
|
|
485
|
+
const totalDiffLines = ownFiles.reduce((sum, { file }) => sum + Number(file.changes ?? 0), 0);
|
|
486
|
+
const maxToolRounds = clampToolRounds(totalDiffLines);
|
|
487
|
+
const ownDiff = text(ownSections.join("\n\n"), MAX_REVIEW_DIFF_CHARS);
|
|
488
|
+
const unchangedListing = extras?.unchangedPaths?.length
|
|
489
|
+
? `\nUNCHANGED SINCE LAST REVIEW (paths only):\n${extras.unchangedPaths
|
|
490
|
+
.map((path) => `- ${path}`)
|
|
491
|
+
.join("\n")}`
|
|
492
|
+
: "";
|
|
493
|
+
const carryBlock = extras?.carryPrompt ? `${extras.carryPrompt}\n` : "";
|
|
494
|
+
const provider = ai ??
|
|
495
|
+
new OpenRouterProvider(options.aiToken ?? "", options.highModel ?? "openai/gpt-5.6-luna");
|
|
496
|
+
const diagrams = provider.supportsTools !== false;
|
|
497
|
+
const prompt = `Review these local changes against the repository's review guide and
|
|
498
|
+
codebase conventions. Find only actionable code-level violations supported by
|
|
499
|
+
the diff and either the guide or the codebase conventions.
|
|
500
|
+
Return concise Markdown with either "## Findings" and findings, or
|
|
501
|
+
"## Findings\\n\\nNo actionable findings."
|
|
502
|
+
Do not invent low-value findings. If the diff contains a truncation marker or a
|
|
503
|
+
file is summarized, limit claims to the supplied text and use read-full-diff or
|
|
504
|
+
codegraph tools before asserting behavior outside what was shown.
|
|
505
|
+
${CODEGRAPH_DIFF_VERIFICATION}
|
|
506
|
+
${diagrams ? DIAGRAM_PROMPT_RULES : NO_DIAGRAM_RULES}
|
|
507
|
+
|
|
508
|
+
REVIEW GUIDE:
|
|
509
|
+
${guide}
|
|
510
|
+
|
|
511
|
+
DETAILED GUIDE:
|
|
512
|
+
${detailed}
|
|
513
|
+
|
|
514
|
+
CODEBASE CONVENTIONS:
|
|
515
|
+
${codebase || "None recorded."}
|
|
516
|
+
|
|
517
|
+
WORKSPACE:
|
|
518
|
+
${JSON.stringify({
|
|
519
|
+
branch: revision.baseLabel,
|
|
520
|
+
changedFiles: ownFiles.map(({ path }) => path),
|
|
521
|
+
})}
|
|
522
|
+
|
|
523
|
+
${carryBlock}DIFF:
|
|
524
|
+
${ownDiff}${unchangedListing}`;
|
|
525
|
+
const matrix = clampImproveMatrix(options.improveMatrix);
|
|
526
|
+
const request = {
|
|
527
|
+
job: "review_local",
|
|
528
|
+
system: reviewSystemPrompt(diagrams),
|
|
529
|
+
prompt,
|
|
530
|
+
maxTokens: 24_000 * matrix,
|
|
531
|
+
reasoningEffort: "high",
|
|
532
|
+
};
|
|
533
|
+
report(`AI request · prompt=${prompt.length} chars`);
|
|
534
|
+
let response = await completeWithMermaidTools(provider, request, 1, extraTools, maxToolRounds);
|
|
535
|
+
if (usage)
|
|
536
|
+
await usage(response);
|
|
537
|
+
if (!response.text.trim()) {
|
|
538
|
+
throw new Error("OpenRouter returned an empty review");
|
|
539
|
+
}
|
|
540
|
+
let reviewText = response.text.trim();
|
|
541
|
+
for (let pass = 2; pass <= matrix; pass++) {
|
|
542
|
+
const improvementRequest = {
|
|
543
|
+
...request,
|
|
544
|
+
job: "improve_review",
|
|
545
|
+
prompt: `Audit the draft review below against the complete diff and guides.
|
|
546
|
+
Preserve valid findings, correct inaccurate ones, remove duplicate or
|
|
547
|
+
unsupported ones, and add every missing actionable finding. Re-check each
|
|
548
|
+
finding with codegraph when it depends on behavior outside the diff; remove
|
|
549
|
+
findings that only looked plausible from the diff slice.
|
|
550
|
+
Return only the complete revised review in the same format.
|
|
551
|
+
|
|
552
|
+
ORIGINAL REVIEW CONTEXT:
|
|
553
|
+
${prompt}
|
|
554
|
+
|
|
555
|
+
DRAFT REVIEW:
|
|
556
|
+
${reviewText}`,
|
|
557
|
+
};
|
|
558
|
+
response = await completeWithMermaidTools(provider, improvementRequest, 1, extraTools, maxToolRounds);
|
|
559
|
+
if (usage)
|
|
560
|
+
await usage(response);
|
|
561
|
+
reviewText = response.text.trim();
|
|
562
|
+
}
|
|
563
|
+
const codegraphState = !options.useCodegraph
|
|
564
|
+
? "disabled"
|
|
565
|
+
: codegraphTools.length > 0
|
|
566
|
+
? "used"
|
|
567
|
+
: "unavailable";
|
|
568
|
+
return {
|
|
569
|
+
...response,
|
|
570
|
+
text: `## Severity
|
|
571
|
+
|
|
572
|
+
- P0 — Critical: production outage, data loss, or security issue.
|
|
573
|
+
- P1 — High: major behavior is broken and should be fixed before merge.
|
|
574
|
+
- P2 — Medium: important correctness or maintainability issue.
|
|
575
|
+
- P3 — Low: minor, non-blocking improvement or edge case.
|
|
576
|
+
|
|
577
|
+
${reviewText}`,
|
|
578
|
+
visiblePaths: ownFiles.map(({ path }) => path),
|
|
579
|
+
guideBuiltAt: guides.guideBuiltAt,
|
|
580
|
+
codegraphState,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Round detection and repeat matching for P11 re-reviews.
|
|
2
|
+
* Rounds are clusters of review comments that share `original_commit_id`,
|
|
3
|
+
* oldest first, the same grouping benchmark/core_v2/README.md describes. */
|
|
4
|
+
import type { FindingRow } from "../store/rows.ts";
|
|
5
|
+
import type { Span } from "./findings.ts";
|
|
6
|
+
export type RoundComment = {
|
|
7
|
+
original_commit_id?: string;
|
|
8
|
+
commit_id?: string;
|
|
9
|
+
created_at?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function reviewRounds(comments: RoundComment[]): string[];
|
|
12
|
+
/** Previous round's commit: the last clustered original_commit_id that is
|
|
13
|
+
* not the current head. Undefined when there is no earlier round. */
|
|
14
|
+
export declare function incrementalBase(rounds: string[], headSha: string): string | undefined;
|
|
15
|
+
export declare function spansOverlap(a: Span, b: Span): boolean;
|
|
16
|
+
/** First previous finding that covers the same path and line. */
|
|
17
|
+
export declare function matchRepeat(finding: Span, previous: FindingRow[]): FindingRow | undefined;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function reviewRounds(comments) {
|
|
2
|
+
const sorted = [...comments].sort((a, b) => String(a.created_at ?? "").localeCompare(String(b.created_at ?? "")));
|
|
3
|
+
const rounds = [];
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
for (const comment of sorted) {
|
|
6
|
+
const id = comment.original_commit_id || comment.commit_id;
|
|
7
|
+
if (!id || seen.has(id))
|
|
8
|
+
continue;
|
|
9
|
+
seen.add(id);
|
|
10
|
+
rounds.push(id);
|
|
11
|
+
}
|
|
12
|
+
return rounds;
|
|
13
|
+
}
|
|
14
|
+
/** Previous round's commit: the last clustered original_commit_id that is
|
|
15
|
+
* not the current head. Undefined when there is no earlier round. */
|
|
16
|
+
export function incrementalBase(rounds, headSha) {
|
|
17
|
+
const previous = rounds.filter((sha) => sha !== headSha);
|
|
18
|
+
return previous.at(-1);
|
|
19
|
+
}
|
|
20
|
+
function normalizePath(path) {
|
|
21
|
+
return path.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
22
|
+
}
|
|
23
|
+
function asSpan(row) {
|
|
24
|
+
if (!row.path || row.line_from == null)
|
|
25
|
+
return undefined;
|
|
26
|
+
return {
|
|
27
|
+
path: row.path,
|
|
28
|
+
from: row.line_from,
|
|
29
|
+
to: row.line_to ?? row.line_from,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function spansOverlap(a, b) {
|
|
33
|
+
if (normalizePath(a.path) !== normalizePath(b.path))
|
|
34
|
+
return false;
|
|
35
|
+
return a.from <= b.to && b.from <= a.to;
|
|
36
|
+
}
|
|
37
|
+
/** First previous finding that covers the same path and line. */
|
|
38
|
+
export function matchRepeat(finding, previous) {
|
|
39
|
+
return previous.find((row) => {
|
|
40
|
+
const span = asSpan(row);
|
|
41
|
+
return span !== undefined && spansOverlap(finding, span);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type Run } from "./checkout.ts";
|
|
2
|
+
/** What a pull request's author actually wrote this round, as opposed to code
|
|
3
|
+
* that arrived by merging the default branch in. A re-review round's diff
|
|
4
|
+
* regularly includes a `merge main` a human reviewer never reads and a bot
|
|
5
|
+
* should not either — one measured case in this repo's own benchmark had a
|
|
6
|
+
* round diff of 48 files where the author's own commits touched 5. Findings
|
|
7
|
+
* belong in the own set; upstream code is context, shown but not policed,
|
|
8
|
+
* because the author's own change can still interact badly with it. */
|
|
9
|
+
export type ScopeResult = {
|
|
10
|
+
/** Files touched by a commit the author made this round: their own
|
|
11
|
+
* non-merge commits, plus the conflict-resolution hunks of any merge they
|
|
12
|
+
* made. A clean "merge main in" contributes nothing here — `--cc` only
|
|
13
|
+
* surfaces lines that differ from every parent, which is exactly the part
|
|
14
|
+
* a human resolved by hand. */
|
|
15
|
+
ownFiles: Set<string>;
|
|
16
|
+
/** Everything else touched between the round's boundary and its head —
|
|
17
|
+
* code that arrived via a merge and was not authored this round. */
|
|
18
|
+
upstreamFiles: Set<string>;
|
|
19
|
+
defaultBranch: string;
|
|
20
|
+
ownCommits: number;
|
|
21
|
+
mergeCommits: number;
|
|
22
|
+
};
|
|
23
|
+
/** The scope algorithm against a clone that is already on disk. Split out from
|
|
24
|
+
* `computeScope` so a caller that already manages its own clone — the
|
|
25
|
+
* benchmark's OCR runner has its own, separate from co-maintainer's — can run
|
|
26
|
+
* the identical algorithm without a second clone of the same repository. */
|
|
27
|
+
export declare function scopeInClone(clone: string, base: string, head: string, run?: Run, remote?: string): Promise<ScopeResult | undefined>;
|
|
28
|
+
/** Clones (or reuses) `repo` under co-maintainer's own cache and runs the
|
|
29
|
+
* scope algorithm against it. Returns `undefined` on any failure — no clone,
|
|
30
|
+
* an unresolvable commit, no default branch to compare against — so the
|
|
31
|
+
* caller's fallback is always "review everything, unscoped", never a broken
|
|
32
|
+
* review. */
|
|
33
|
+
export declare function computeScope(repo: string, base: string, head: string, options?: {
|
|
34
|
+
run?: Run;
|
|
35
|
+
}): Promise<ScopeResult | undefined>;
|