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,84 @@
|
|
|
1
|
+
import { ensureCloneUnlocked, ensureCommit, runCommand, withCloneLock, } from "./checkout.js";
|
|
2
|
+
import { detectDefaultBranchRef } from "../git/default_branch.js";
|
|
3
|
+
import { log } from "../util/log.js";
|
|
4
|
+
function lines(output) {
|
|
5
|
+
return output
|
|
6
|
+
.split("\n")
|
|
7
|
+
.map((line) => line.trim())
|
|
8
|
+
.filter(Boolean);
|
|
9
|
+
}
|
|
10
|
+
/** The scope algorithm against a clone that is already on disk. Split out from
|
|
11
|
+
* `computeScope` so a caller that already manages its own clone — the
|
|
12
|
+
* benchmark's OCR runner has its own, separate from co-maintainer's — can run
|
|
13
|
+
* the identical algorithm without a second clone of the same repository. */
|
|
14
|
+
export async function scopeInClone(clone, base, head, run = runCommand, remote = "origin") {
|
|
15
|
+
let baseSha;
|
|
16
|
+
let headSha;
|
|
17
|
+
try {
|
|
18
|
+
baseSha = await ensureCommit(clone, base, run);
|
|
19
|
+
headSha = await ensureCommit(clone, head, run);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
log("scope", `unavailable · ${String(error)}`);
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const defaultBranch = await detectDefaultBranchRef(clone, remote, run);
|
|
26
|
+
if (!defaultBranch) {
|
|
27
|
+
log("scope", "unavailable · could not determine the default branch");
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
// Mirrors GitHub's three-dot compare: the round started at the point where
|
|
31
|
+
// the PR's branch diverged, not at whatever `base` happens to be today.
|
|
32
|
+
const mergeBase = await run("git", ["merge-base", baseSha, headSha], clone);
|
|
33
|
+
const boundary = mergeBase.code === 0 ? mergeBase.stdout.trim() : baseSha;
|
|
34
|
+
const range = [`${boundary}..${headSha}`, "--not", defaultBranch];
|
|
35
|
+
const [ownCommits, mergeCommits, allFiles] = await Promise.all([
|
|
36
|
+
run("git", ["rev-list", "--no-merges", ...range], clone),
|
|
37
|
+
run("git", ["rev-list", "--merges", ...range], clone),
|
|
38
|
+
run("git", ["diff", "--name-only", `${boundary}..${headSha}`], clone),
|
|
39
|
+
]);
|
|
40
|
+
if (ownCommits.code !== 0 || mergeCommits.code !== 0 || allFiles.code !== 0) {
|
|
41
|
+
log("scope", "unavailable · git failed to list the round's commits");
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const ownFiles = new Set();
|
|
45
|
+
for (const sha of lines(ownCommits.stdout)) {
|
|
46
|
+
const shown = await run("git", ["show", "--name-only", "--format=", sha], clone);
|
|
47
|
+
for (const path of lines(shown.stdout))
|
|
48
|
+
ownFiles.add(path);
|
|
49
|
+
}
|
|
50
|
+
for (const sha of lines(mergeCommits.stdout)) {
|
|
51
|
+
const resolved = await run("git", ["diff-tree", "--cc", "-r", "--no-commit-id", "--name-only", sha], clone);
|
|
52
|
+
for (const path of lines(resolved.stdout))
|
|
53
|
+
ownFiles.add(path);
|
|
54
|
+
}
|
|
55
|
+
const upstreamFiles = new Set(lines(allFiles.stdout).filter((path) => !ownFiles.has(path)));
|
|
56
|
+
log("scope", `${ownFiles.size} own files · ${upstreamFiles.size} upstream files · ` +
|
|
57
|
+
`${lines(ownCommits.stdout).length} commits · ${lines(mergeCommits.stdout).length} merges · default=${defaultBranch}`);
|
|
58
|
+
return {
|
|
59
|
+
ownFiles,
|
|
60
|
+
upstreamFiles,
|
|
61
|
+
defaultBranch,
|
|
62
|
+
ownCommits: lines(ownCommits.stdout).length,
|
|
63
|
+
mergeCommits: lines(mergeCommits.stdout).length,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/** Clones (or reuses) `repo` under co-maintainer's own cache and runs the
|
|
67
|
+
* scope algorithm against it. Returns `undefined` on any failure — no clone,
|
|
68
|
+
* an unresolvable commit, no default branch to compare against — so the
|
|
69
|
+
* caller's fallback is always "review everything, unscoped", never a broken
|
|
70
|
+
* review. */
|
|
71
|
+
export async function computeScope(repo, base, head, options = {}) {
|
|
72
|
+
const run = options.run ?? runCommand;
|
|
73
|
+
return withCloneLock(repo, async () => {
|
|
74
|
+
let clone;
|
|
75
|
+
try {
|
|
76
|
+
clone = await ensureCloneUnlocked(repo, run);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
log("scope", `unavailable · ${String(error)}`);
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return await scopeInClone(clone, base, head, run);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Pins a review to a historical diff/discussion state instead of the PR's
|
|
2
|
+
* current (latest) state — used by the benchmarks to show the model exactly
|
|
3
|
+
* what a human reviewer saw, without leaking later comments as hints.
|
|
4
|
+
* `base` defaults to the PR's merge-base with its base branch (the whole PR
|
|
5
|
+
* so far); pass an earlier review round's commit as `base` to show only the
|
|
6
|
+
* incremental diff a re-reviewer would see since that round (P11). */
|
|
7
|
+
export type Snapshot = {
|
|
8
|
+
base?: string;
|
|
9
|
+
commit: string;
|
|
10
|
+
before: string;
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ParsedFinding, Span } from "./findings.ts";
|
|
2
|
+
import { type Anchor } from "./hunks.ts";
|
|
3
|
+
export type Suggestion = Span & {
|
|
4
|
+
code: string;
|
|
5
|
+
};
|
|
6
|
+
/** The one suggestion a finding carries, or undefined when there is none or
|
|
7
|
+
* the model wrote more than one. */
|
|
8
|
+
export declare function readSuggestion(body: string): Suggestion | undefined;
|
|
9
|
+
export declare function withoutSuggestionLine(body: string): string;
|
|
10
|
+
export declare function stripSuggestion(body: string): string;
|
|
11
|
+
/** Replies explain a fix in a normal code block. A suggestion there would
|
|
12
|
+
* anchor to whatever lines the thread sits on, which may have moved. */
|
|
13
|
+
export declare function plainSuggestionFences(text: string): string;
|
|
14
|
+
/** The anchor for a suggestion GitHub can apply as written: same file,
|
|
15
|
+
* inside the finding's span, one hunk holding the whole range, and an actual
|
|
16
|
+
* change to those lines. */
|
|
17
|
+
export declare function suggestionAnchor(finding: ParsedFinding, patch: string | undefined): Anchor | undefined;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { anchorFor, rightLines } from "./hunks.js";
|
|
2
|
+
const LINE = /^[ \t]*Suggestion:[ \t]*`?(?<path>[^`\s]+?):(?<from>\d+)(?:-(?<to>\d+))?`?[ \t]*\r?$/gm;
|
|
3
|
+
const BLOCK = /^(?<fence>`{3,})suggestion[ \t]*\r?\n(?<code>[\s\S]*?)^\k<fence>[ \t]*\r?$/gm;
|
|
4
|
+
const OPENING_FENCE = /^(`{3,})suggestion\b/gm;
|
|
5
|
+
/** The one suggestion a finding carries, or undefined when there is none or
|
|
6
|
+
* the model wrote more than one. */
|
|
7
|
+
export function readSuggestion(body) {
|
|
8
|
+
const lines = [...body.matchAll(LINE)];
|
|
9
|
+
const blocks = [...body.matchAll(BLOCK)];
|
|
10
|
+
if (lines.length !== 1 || blocks.length !== 1)
|
|
11
|
+
return undefined;
|
|
12
|
+
const { path, from, to } = lines[0].groups;
|
|
13
|
+
return {
|
|
14
|
+
path,
|
|
15
|
+
from: Number(from),
|
|
16
|
+
to: Number(to ?? from),
|
|
17
|
+
code: blocks[0].groups.code.replace(/\r?\n$/, ""),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function withoutSuggestionLine(body) {
|
|
21
|
+
return body
|
|
22
|
+
.replace(LINE, "")
|
|
23
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
24
|
+
.trim();
|
|
25
|
+
}
|
|
26
|
+
export function stripSuggestion(body) {
|
|
27
|
+
return withoutSuggestionLine(body.replace(BLOCK, ""));
|
|
28
|
+
}
|
|
29
|
+
/** Replies explain a fix in a normal code block. A suggestion there would
|
|
30
|
+
* anchor to whatever lines the thread sits on, which may have moved. */
|
|
31
|
+
export function plainSuggestionFences(text) {
|
|
32
|
+
return text.replace(OPENING_FENCE, "$1");
|
|
33
|
+
}
|
|
34
|
+
function sameCode(a, b) {
|
|
35
|
+
const clean = (value) => value
|
|
36
|
+
.split("\n")
|
|
37
|
+
.map((line) => line.trimEnd())
|
|
38
|
+
.join("\n");
|
|
39
|
+
return clean(a) === clean(b);
|
|
40
|
+
}
|
|
41
|
+
/** The anchor for a suggestion GitHub can apply as written: same file,
|
|
42
|
+
* inside the finding's span, one hunk holding the whole range, and an actual
|
|
43
|
+
* change to those lines. */
|
|
44
|
+
export function suggestionAnchor(finding, patch) {
|
|
45
|
+
const suggestion = readSuggestion(finding.excerpt);
|
|
46
|
+
if (!suggestion || !patch || suggestion.path !== finding.path) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
const { from, to } = suggestion;
|
|
50
|
+
const low = Math.min(finding.from, finding.to);
|
|
51
|
+
const high = Math.max(finding.from, finding.to);
|
|
52
|
+
if (from > to || from < low || to > high)
|
|
53
|
+
return undefined;
|
|
54
|
+
const anchor = anchorFor(patch, from, to);
|
|
55
|
+
const whole = anchor?.line === to &&
|
|
56
|
+
anchor.start_line === (from === to ? undefined : from);
|
|
57
|
+
if (!whole)
|
|
58
|
+
return undefined;
|
|
59
|
+
const lines = rightLines(patch);
|
|
60
|
+
const original = [];
|
|
61
|
+
for (let line = from; line <= to; line++) {
|
|
62
|
+
const text = lines.get(line);
|
|
63
|
+
if (text === undefined)
|
|
64
|
+
return undefined;
|
|
65
|
+
original.push(text);
|
|
66
|
+
}
|
|
67
|
+
if (sameCode(original.join("\n"), suggestion.code))
|
|
68
|
+
return undefined;
|
|
69
|
+
return anchor;
|
|
70
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { VERSION } from "../version.js";
|
|
2
|
+
import { printLocalReview } from "../cli/review_output.js";
|
|
3
|
+
import { formatHumanJsonFindings, reviewExitCodeFromJsonFindings, } from "../cli/review_result.js";
|
|
4
|
+
import { readConfig, writeUserConfig } from "../config.js";
|
|
5
|
+
import { prepareLocalCodegraph } from "../local/codegraph_prepare.js";
|
|
6
|
+
import { runRemoteToolCalls, toolHandlerMap, } from "./client_tools.js";
|
|
7
|
+
import { REMOTE_KNOWN_TOOL_NAMES } from "./schema.js";
|
|
8
|
+
import { runCommand } from "../pr/checkout.js";
|
|
9
|
+
import { assertGitQuiet, currentBranch, detectRemoteRepo, gitRoot, ReviewCliError, } from "../local/git_ops.js";
|
|
10
|
+
import { buildLocalRevision, mergeBase, resolveBaseRef, } from "../local/git_revision.js";
|
|
11
|
+
import { withCliLogsToStderr } from "../util/log.js";
|
|
12
|
+
import { setCliInteractive } from "../cli/args.js";
|
|
13
|
+
import { MIN_SERVER_SCHEMA, REMOTE_CLI_UPGRADE_COMMAND, REMOTE_SCHEMA_VERSION, } from "./schema.js";
|
|
14
|
+
function die(code, message, hint, exitCode = 2) {
|
|
15
|
+
throw new ReviewCliError(code, message, hint, exitCode);
|
|
16
|
+
}
|
|
17
|
+
function baseUrl(host) {
|
|
18
|
+
const trimmed = host.trim().replace(/\/+$/, "");
|
|
19
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
20
|
+
die("remote_not_configured", "remoteHost must be an absolute http(s) URL.", "co-maintainer set --remote-host=https://your-server");
|
|
21
|
+
}
|
|
22
|
+
return trimmed;
|
|
23
|
+
}
|
|
24
|
+
async function remoteFetch(host, token, path, init) {
|
|
25
|
+
const url = `${baseUrl(host)}${path}`;
|
|
26
|
+
const headers = new Headers(init.headers);
|
|
27
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
28
|
+
if (init.body && !headers.has("content-type")) {
|
|
29
|
+
headers.set("content-type", "application/json");
|
|
30
|
+
}
|
|
31
|
+
return await fetch(url, { ...init, headers });
|
|
32
|
+
}
|
|
33
|
+
async function readApiError(response) {
|
|
34
|
+
try {
|
|
35
|
+
const body = await response.json();
|
|
36
|
+
return String(body.error?.message ?? response.statusText);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return response.statusText;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function runRemoteReview(cli) {
|
|
43
|
+
const config = readConfig();
|
|
44
|
+
const host = config.remoteHost;
|
|
45
|
+
const token = config.remoteToken;
|
|
46
|
+
if (!host || !token) {
|
|
47
|
+
die("remote_not_configured", "Remote review is not configured.", "co-maintainer set --remote-host=... --remote-token=...");
|
|
48
|
+
}
|
|
49
|
+
setCliInteractive(!cli.json);
|
|
50
|
+
await withCliLogsToStderr(async () => {
|
|
51
|
+
const root = await gitRoot(process.cwd());
|
|
52
|
+
await assertGitQuiet(root);
|
|
53
|
+
const repo = await detectRemoteRepo(root, cli.repoOverride);
|
|
54
|
+
const branch = await currentBranch(root, cli.branch);
|
|
55
|
+
const remotes = await runCommand("git", ["remote"], root);
|
|
56
|
+
const remoteName = ["upstream", "origin"].find((n) => remotes.stdout
|
|
57
|
+
.split("\n")
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.includes(n)) ??
|
|
60
|
+
remotes.stdout
|
|
61
|
+
.split("\n")
|
|
62
|
+
.map((l) => l.trim())
|
|
63
|
+
.filter(Boolean)[0];
|
|
64
|
+
const base = await resolveBaseRef(root, remoteName, cli.toBranch, runCommand);
|
|
65
|
+
const baseSha = await mergeBase(root, base.ref, remoteName, runCommand);
|
|
66
|
+
const built = await buildLocalRevision(root, baseSha, base.label, runCommand);
|
|
67
|
+
if (built.revision.files.length === 0) {
|
|
68
|
+
if (cli.json) {
|
|
69
|
+
console.log(JSON.stringify({
|
|
70
|
+
schemaVersion: 1,
|
|
71
|
+
ok: true,
|
|
72
|
+
mode: "remote",
|
|
73
|
+
message: "No changes to review.",
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
console.log("No changes to review.");
|
|
78
|
+
}
|
|
79
|
+
process.exit(0);
|
|
80
|
+
}
|
|
81
|
+
const handshake = await remoteFetch(host, token, "/api/remote/handshake", {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: JSON.stringify({
|
|
84
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
85
|
+
clientVersion: VERSION,
|
|
86
|
+
repo,
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
if (!handshake.ok) {
|
|
90
|
+
die("remote_handshake_failed", await readApiError(handshake));
|
|
91
|
+
}
|
|
92
|
+
const hs = (await handshake.json());
|
|
93
|
+
if (REMOTE_SCHEMA_VERSION < hs.minClientSchema) {
|
|
94
|
+
die("upgrade_required", "This co-maintainer CLI is too old for the server.", REMOTE_CLI_UPGRADE_COMMAND, 4);
|
|
95
|
+
}
|
|
96
|
+
if (hs.schemaVersion < MIN_SERVER_SCHEMA) {
|
|
97
|
+
die("server_too_old", "The dashboard server is too old for this CLI.", undefined, 4);
|
|
98
|
+
}
|
|
99
|
+
const hostKey = baseUrl(host);
|
|
100
|
+
const shown = config.remoteNoticeShownFor ?? [];
|
|
101
|
+
if (!shown.includes(hostKey) && !cli.json) {
|
|
102
|
+
console.error("Remote review sends your unpublished diff to the server you configured. " +
|
|
103
|
+
"Use a host you trust.");
|
|
104
|
+
await writeUserConfig({
|
|
105
|
+
remoteNoticeShownFor: [...shown, hostKey],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
const codegraphPrep = await prepareLocalCodegraph({
|
|
109
|
+
gitRoot: root,
|
|
110
|
+
enabled: !cli.disableCodegraph,
|
|
111
|
+
allowInstall: cli.allowToolInstall,
|
|
112
|
+
interactive: !cli.json,
|
|
113
|
+
});
|
|
114
|
+
const localTools = toolHandlerMap(codegraphPrep.tools);
|
|
115
|
+
const capabilityTools = [...localTools.keys()]
|
|
116
|
+
.filter((name) => REMOTE_KNOWN_TOOL_NAMES.has(name))
|
|
117
|
+
.map((name) => ({ name }));
|
|
118
|
+
const requestId = crypto.randomUUID();
|
|
119
|
+
const submitBody = {
|
|
120
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
121
|
+
requestId,
|
|
122
|
+
repo: hs.repo.fullName,
|
|
123
|
+
branch,
|
|
124
|
+
toBranch: cli.toBranch ?? hs.repo.defaultBranch ?? undefined,
|
|
125
|
+
fresh: cli.fresh,
|
|
126
|
+
revision: {
|
|
127
|
+
title: built.revision.title,
|
|
128
|
+
description: built.revision.description,
|
|
129
|
+
baseLabel: built.revision.baseLabel,
|
|
130
|
+
files: built.revision.files,
|
|
131
|
+
},
|
|
132
|
+
capabilities: { tools: capabilityTools },
|
|
133
|
+
};
|
|
134
|
+
const serialized = JSON.stringify(submitBody);
|
|
135
|
+
if (serialized.length > hs.limits.maxBodyBytes) {
|
|
136
|
+
die("payload_too_large", "Diff is too large for the remote server limit.");
|
|
137
|
+
}
|
|
138
|
+
const submit = await remoteFetch(host, token, "/api/remote/reviews", {
|
|
139
|
+
method: "POST",
|
|
140
|
+
body: serialized,
|
|
141
|
+
});
|
|
142
|
+
if (submit.status !== 202) {
|
|
143
|
+
die("remote_submit_failed", await readApiError(submit));
|
|
144
|
+
}
|
|
145
|
+
const { jobId } = (await submit.json());
|
|
146
|
+
let afterLogSeq = 0;
|
|
147
|
+
let toolResults = [];
|
|
148
|
+
const intervalMs = hs.sync.intervalSeconds * 1000;
|
|
149
|
+
while (true) {
|
|
150
|
+
const sync = await remoteFetch(host, token, `/api/remote/reviews/${jobId}/sync`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
schemaVersion: REMOTE_SCHEMA_VERSION,
|
|
154
|
+
afterLogSeq,
|
|
155
|
+
toolResults,
|
|
156
|
+
}),
|
|
157
|
+
});
|
|
158
|
+
toolResults = [];
|
|
159
|
+
if (!sync.ok) {
|
|
160
|
+
die("remote_sync_failed", await readApiError(sync));
|
|
161
|
+
}
|
|
162
|
+
const payload = (await sync.json());
|
|
163
|
+
for (const line of payload.logs ?? []) {
|
|
164
|
+
afterLogSeq = Math.max(afterLogSeq, line.seq);
|
|
165
|
+
if (!cli.json)
|
|
166
|
+
console.error(line.message);
|
|
167
|
+
}
|
|
168
|
+
if (payload.toolCalls?.length) {
|
|
169
|
+
toolResults = await runRemoteToolCalls(payload.toolCalls, localTools);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (payload.status === "done" && payload.result) {
|
|
173
|
+
if (cli.json) {
|
|
174
|
+
console.log(JSON.stringify({
|
|
175
|
+
schemaVersion: 1,
|
|
176
|
+
ok: true,
|
|
177
|
+
mode: "remote",
|
|
178
|
+
...payload.result,
|
|
179
|
+
}));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
const findings = (payload.result.findings ??
|
|
183
|
+
[]);
|
|
184
|
+
const summary = payload.result.summary;
|
|
185
|
+
const summaryLine = summary
|
|
186
|
+
? `${summary.new ?? 0} new · ${summary.open ?? 0} open · ${summary.closed ?? 0} closed · ${summary.blocking ?? 0} blocking`
|
|
187
|
+
: "";
|
|
188
|
+
printLocalReview(`co-maintainer review · ${repo} · ${branch} (remote)\n${summaryLine}`, formatHumanJsonFindings(findings));
|
|
189
|
+
}
|
|
190
|
+
const findings = (payload.result.findings ?? []);
|
|
191
|
+
process.exit(reviewExitCodeFromJsonFindings(findings));
|
|
192
|
+
}
|
|
193
|
+
if (payload.status === "failed" || payload.status === "canceled") {
|
|
194
|
+
die(payload.abort?.reason ?? "remote_failed", payload.abort?.message ?? "Remote review did not complete.");
|
|
195
|
+
}
|
|
196
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ToolHandler } from "../ai/mermaid_loop.ts";
|
|
2
|
+
export type RemoteToolCall = {
|
|
3
|
+
callId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
arguments: Record<string, unknown>;
|
|
6
|
+
};
|
|
7
|
+
export type RemoteToolResult = {
|
|
8
|
+
callId: string;
|
|
9
|
+
output?: string;
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function toolHandlerMap(handlers: ToolHandler[]): Map<string, ToolHandler>;
|
|
13
|
+
export declare function runRemoteToolCalls(calls: RemoteToolCall[], handlers: Map<string, ToolHandler>): Promise<RemoteToolResult[]>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function toolHandlerMap(handlers) {
|
|
2
|
+
return new Map(handlers.map((handler) => [handler.name, handler]));
|
|
3
|
+
}
|
|
4
|
+
export async function runRemoteToolCalls(calls, handlers) {
|
|
5
|
+
const results = [];
|
|
6
|
+
for (const call of calls) {
|
|
7
|
+
const handler = handlers.get(call.name);
|
|
8
|
+
if (!handler) {
|
|
9
|
+
results.push({
|
|
10
|
+
callId: call.callId,
|
|
11
|
+
error: `unknown tool: ${call.name}`,
|
|
12
|
+
});
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const output = await handler.run(call.arguments);
|
|
17
|
+
results.push({
|
|
18
|
+
callId: call.callId,
|
|
19
|
+
output: typeof output === "string" ? output : String(output),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
results.push({
|
|
24
|
+
callId: call.callId,
|
|
25
|
+
error: error instanceof Error ? error.message : String(error),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function hashRemoteFixtures(version: number): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Fixture directory hashing for plan §15.3. */
|
|
2
|
+
import { readDir, readFile } from "../util/runtime.js";
|
|
3
|
+
export async function hashRemoteFixtures(version) {
|
|
4
|
+
const dir = new URL(`./fixtures/v${version}/`, import.meta.url);
|
|
5
|
+
const names = [];
|
|
6
|
+
for await (const entry of readDir(dir)) {
|
|
7
|
+
if (entry.isFile && entry.name.endsWith(".json")) {
|
|
8
|
+
names.push(entry.name);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
names.sort();
|
|
12
|
+
const chunks = [];
|
|
13
|
+
const enc = new TextEncoder();
|
|
14
|
+
for (const name of names) {
|
|
15
|
+
chunks.push(enc.encode(`${name}\n`));
|
|
16
|
+
chunks.push(await readFile(new URL(name, dir)));
|
|
17
|
+
chunks.push(enc.encode("\n"));
|
|
18
|
+
}
|
|
19
|
+
const total = chunks.reduce((n, c) => n + c.length, 0);
|
|
20
|
+
const merged = new Uint8Array(total);
|
|
21
|
+
let offset = 0;
|
|
22
|
+
for (const chunk of chunks) {
|
|
23
|
+
merged.set(chunk, offset);
|
|
24
|
+
offset += chunk.length;
|
|
25
|
+
}
|
|
26
|
+
const digest = await crypto.subtle.digest("SHA-256", merged);
|
|
27
|
+
return [...new Uint8Array(digest)]
|
|
28
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { validateRevisionPayload } from "./validate.js";
|
|
2
|
+
export function revisionFromSubmitJson(raw) {
|
|
3
|
+
const err = validateRevisionPayload(raw);
|
|
4
|
+
if (err)
|
|
5
|
+
throw new Error(err);
|
|
6
|
+
const record = raw;
|
|
7
|
+
const files = record.files.map((file) => {
|
|
8
|
+
const f = file;
|
|
9
|
+
return {
|
|
10
|
+
path: f.path,
|
|
11
|
+
previousPath: f.previousPath == null ? null : f.previousPath,
|
|
12
|
+
status: f.status,
|
|
13
|
+
binary: f.binary,
|
|
14
|
+
additions: f.additions,
|
|
15
|
+
deletions: f.deletions,
|
|
16
|
+
patch: f.patch,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
return {
|
|
20
|
+
files,
|
|
21
|
+
title: typeof record.title === "string" ? record.title : "",
|
|
22
|
+
description: typeof record.description === "string" ? record.description : "",
|
|
23
|
+
baseLabel: typeof record.baseLabel === "string" ? record.baseLabel : "",
|
|
24
|
+
producer: "remote",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Remote review wire protocol — plan §15.1–15.2. */
|
|
2
|
+
export declare const REMOTE_SCHEMA_VERSION = 1;
|
|
3
|
+
export declare const MIN_CLIENT_SCHEMA = 1;
|
|
4
|
+
export declare const MIN_SERVER_SCHEMA = 1;
|
|
5
|
+
/** README install line for 426 responses (plan §14.4). */
|
|
6
|
+
export declare const REMOTE_CLI_UPGRADE_COMMAND = "npm install -g co-maintainer@latest";
|
|
7
|
+
export declare const REMOTE_MAX_BODY_BYTES = 52428800;
|
|
8
|
+
export declare const REMOTE_SYNC_INTERVAL_SECONDS = 3;
|
|
9
|
+
/** SHA-256 of sorted `fixtures/v<N>/*.json` (name + contents); bump with schema version. */
|
|
10
|
+
export declare const REMOTE_FIXTURE_HASH: Record<number, string>;
|
|
11
|
+
export declare const REMOTE_SYNC_STATUSES: readonly ["queued", "running", "done", "failed", "canceled"];
|
|
12
|
+
export type RemoteSyncStatus = (typeof REMOTE_SYNC_STATUSES)[number];
|
|
13
|
+
/** Tool names the server may invoke via the bridge (plan §13.6, §14.5). */
|
|
14
|
+
export declare const REMOTE_KNOWN_TOOL_NAMES: Set<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Remote review wire protocol — plan §15.1–15.2. */
|
|
2
|
+
export const REMOTE_SCHEMA_VERSION = 1;
|
|
3
|
+
export const MIN_CLIENT_SCHEMA = 1;
|
|
4
|
+
export const MIN_SERVER_SCHEMA = 1;
|
|
5
|
+
/** README install line for 426 responses (plan §14.4). */
|
|
6
|
+
export const REMOTE_CLI_UPGRADE_COMMAND = "npm install -g co-maintainer@latest";
|
|
7
|
+
export const REMOTE_MAX_BODY_BYTES = 52_428_800;
|
|
8
|
+
export const REMOTE_SYNC_INTERVAL_SECONDS = 3;
|
|
9
|
+
/** SHA-256 of sorted `fixtures/v<N>/*.json` (name + contents); bump with schema version. */
|
|
10
|
+
export const REMOTE_FIXTURE_HASH = {
|
|
11
|
+
1: "1cbb11016227fcc7fc58b692ed5a2b72dc9b0e23b4cf68858e2d7facc98efd79",
|
|
12
|
+
};
|
|
13
|
+
export const REMOTE_SYNC_STATUSES = [
|
|
14
|
+
"queued",
|
|
15
|
+
"running",
|
|
16
|
+
"done",
|
|
17
|
+
"failed",
|
|
18
|
+
"canceled",
|
|
19
|
+
];
|
|
20
|
+
/** Tool names the server may invoke via the bridge (plan §13.6, §14.5). */
|
|
21
|
+
export const REMOTE_KNOWN_TOOL_NAMES = new Set([
|
|
22
|
+
"codegraph-query",
|
|
23
|
+
"codegraph-node",
|
|
24
|
+
"codegraph-explore",
|
|
25
|
+
"codegraph-callers",
|
|
26
|
+
"codegraph-callees",
|
|
27
|
+
"codegraph-impact",
|
|
28
|
+
"codegraph-affected",
|
|
29
|
+
"read-full-diff",
|
|
30
|
+
]);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Bounded request body read — plan §14.5 / G6 (bytes, streaming). */
|
|
2
|
+
export type BoundedBodyResult = {
|
|
3
|
+
ok: true;
|
|
4
|
+
text: string;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
reason: "too_large" | "empty";
|
|
8
|
+
};
|
|
9
|
+
export declare function readBoundedUtf8(request: Request, maxBytes: number): Promise<BoundedBodyResult>;
|
|
10
|
+
export declare function readBoundedJson(request: Request, maxBytes: number): Promise<{
|
|
11
|
+
ok: true;
|
|
12
|
+
value: unknown;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
status: number;
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Bounded request body read — plan §14.5 / G6 (bytes, streaming). */
|
|
2
|
+
export async function readBoundedUtf8(request, maxBytes) {
|
|
3
|
+
const lengthHeader = request.headers.get("content-length");
|
|
4
|
+
if (lengthHeader !== null) {
|
|
5
|
+
const declared = Number(lengthHeader);
|
|
6
|
+
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
7
|
+
return { ok: false, reason: "too_large" };
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const stream = request.body;
|
|
11
|
+
if (!stream)
|
|
12
|
+
return { ok: true, text: "" };
|
|
13
|
+
const reader = stream.getReader();
|
|
14
|
+
const chunks = [];
|
|
15
|
+
let total = 0;
|
|
16
|
+
try {
|
|
17
|
+
while (true) {
|
|
18
|
+
const { done, value } = await reader.read();
|
|
19
|
+
if (done)
|
|
20
|
+
break;
|
|
21
|
+
if (!value?.length)
|
|
22
|
+
continue;
|
|
23
|
+
total += value.length;
|
|
24
|
+
if (total > maxBytes) {
|
|
25
|
+
await reader.cancel();
|
|
26
|
+
return { ok: false, reason: "too_large" };
|
|
27
|
+
}
|
|
28
|
+
chunks.push(value);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
reader.releaseLock();
|
|
33
|
+
}
|
|
34
|
+
if (total === 0)
|
|
35
|
+
return { ok: true, text: "" };
|
|
36
|
+
const merged = new Uint8Array(total);
|
|
37
|
+
let offset = 0;
|
|
38
|
+
for (const chunk of chunks) {
|
|
39
|
+
merged.set(chunk, offset);
|
|
40
|
+
offset += chunk.length;
|
|
41
|
+
}
|
|
42
|
+
return { ok: true, text: new TextDecoder().decode(merged) };
|
|
43
|
+
}
|
|
44
|
+
export async function readBoundedJson(request, maxBytes) {
|
|
45
|
+
const body = await readBoundedUtf8(request, maxBytes);
|
|
46
|
+
if (!body.ok) {
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
status: 413,
|
|
50
|
+
code: "payload_too_large",
|
|
51
|
+
message: "request body too large",
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
if (!body.text.trim()) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
status: 400,
|
|
58
|
+
code: "bad_request",
|
|
59
|
+
message: "expected a JSON body",
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
return { ok: true, value: JSON.parse(body.text) };
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
status: 400,
|
|
69
|
+
code: "bad_request",
|
|
70
|
+
message: "expected a JSON body",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function handleRemoteRoute(request: Request, url: URL, remoteAddr: string): Promise<Response>;
|