thurview 0.8.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,226 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { runJson, ok } from "./run.js";
3
+ import { parseRemote } from "./github.js";
4
+ /**
5
+ * GitLab, through `glab api`. Every parameter goes through `--raw-field`, the
6
+ * literal-string flag, so a comment body starting with `@` is sent as text
7
+ * rather than read as a file path.
8
+ * Three things differ from GitHub beyond the
9
+ * nouns, and each one is answered here rather than papered over:
10
+ *
11
+ * - There is no atomic review. A pass is N discussions plus one note, so a
12
+ * failure part-way through leaves the comments already posted. `submit`
13
+ * posts the inline comments first and the summary last, so a partial pass
14
+ * is one the author can still read.
15
+ * - There is no CHANGES_REQUESTED. `request-changes` removes this account's
16
+ * approval and says so in the summary note; it cannot block a merge the
17
+ * way GitHub's state does, and `notes` says that out loud.
18
+ * - A comment anchors to one line. A range is anchored at its last line and
19
+ * reported in `notes`; the range itself belongs in the comment's own
20
+ * permalink.
21
+ */
22
+ /**
23
+ * Job statuses, mapped once and here. `manual` is a job that exists and has
24
+ * not run, which is the exact case a "no failures" reading gets wrong.
25
+ */
26
+ const JOB = {
27
+ success: "passed",
28
+ failed: "failed",
29
+ canceled: "cancelled",
30
+ cancelled: "cancelled",
31
+ skipped: "skipped",
32
+ manual: "skipped",
33
+ running: "running",
34
+ pending: "running",
35
+ created: "running",
36
+ preparing: "running",
37
+ waiting_for_resource: "running",
38
+ scheduled: "running",
39
+ };
40
+ function iidOf(ref) {
41
+ const m = /^(?:.*\/merge_requests\/)?!?(\d+)\/?$/.exec(ref.trim());
42
+ if (!m)
43
+ throw new AxiError(`${ref} is not a merge request number or URL`, "VALIDATION_ERROR", [
44
+ "Pass --change 123 or the full merge request URL",
45
+ ]);
46
+ return m[1];
47
+ }
48
+ export class GitLabForge {
49
+ id = "gitlab";
50
+ cli = "glab";
51
+ api(repo, endpoint, args = []) {
52
+ return ["api", "--hostname", repo.host, endpoint, ...args];
53
+ }
54
+ project(repo) {
55
+ return encodeURIComponent(repo.path);
56
+ }
57
+ async owns(host) {
58
+ if (host === "gitlab.com" || host === process.env["GITLAB_HOST"])
59
+ return true;
60
+ return ok("glab", ["auth", "status", "--hostname", host]);
61
+ }
62
+ async whoami(repo) {
63
+ const me = await runJson("glab", this.api(repo, "user"));
64
+ return me.username;
65
+ }
66
+ repoFromRemote(url) {
67
+ return parseRemote(url);
68
+ }
69
+ async get(repo, ref) {
70
+ const iid = iidOf(ref);
71
+ const mr = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${iid}`));
72
+ return {
73
+ number: String(mr.iid),
74
+ title: mr.title,
75
+ url: mr.web_url,
76
+ state: mr.state === "merged" ? "merged" : mr.state === "opened" ? "open" : "closed",
77
+ author: mr.author?.username ?? "unknown",
78
+ head: mr.diff_refs?.head_sha ?? mr.sha,
79
+ headBranch: mr.source_branch,
80
+ base: mr.diff_refs?.base_sha ?? "",
81
+ baseBranch: mr.target_branch,
82
+ fromFork: mr.source_project_id !== mr.target_project_id,
83
+ draft: Boolean(mr.draft ?? mr.work_in_progress),
84
+ body: mr.description ?? "",
85
+ };
86
+ }
87
+ fetchRef(cr) {
88
+ return `refs/merge-requests/${cr.number}/head`;
89
+ }
90
+ async checks(repo, cr) {
91
+ const pipelines = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/pipelines`));
92
+ return this.jobsOf(repo, pipelines[0]);
93
+ }
94
+ async baseline(repo, branch) {
95
+ const pipelines = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/pipelines?ref=${encodeURIComponent(branch)}&per_page=1`));
96
+ return this.jobsOf(repo, pipelines[0]);
97
+ }
98
+ /** Jobs live in the pipeline's own project, which for a fork is not `repo`. */
99
+ async jobsOf(repo, pipeline) {
100
+ if (!pipeline)
101
+ return [];
102
+ const jobs = await runJson("glab", this.api(repo, `projects/${pipeline.project_id}/pipelines/${pipeline.id}/jobs?per_page=100`));
103
+ return jobs.map((j) => ({
104
+ name: j.name,
105
+ state: JOB[j.status] ?? "unknown",
106
+ raw: j.status,
107
+ ...(j.web_url ? { url: j.web_url } : {}),
108
+ }));
109
+ }
110
+ async prior(repo, cr) {
111
+ const discussions = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/discussions?per_page=100`));
112
+ const threads = [];
113
+ const passes = [];
114
+ for (const d of discussions) {
115
+ const notes = d.notes.filter((n) => !n.system);
116
+ const first = notes[0];
117
+ if (!first)
118
+ continue;
119
+ // A discussion with no position and no resolvable flag is a plain note
120
+ // on the merge request, which is what a summary pass looks like here.
121
+ if (!first.position && !first.resolvable) {
122
+ passes.push({
123
+ id: d.id,
124
+ author: first.author?.username ?? "unknown",
125
+ verdict: "comment",
126
+ at: "",
127
+ body: first.body,
128
+ });
129
+ continue;
130
+ }
131
+ threads.push({
132
+ id: d.id,
133
+ author: first.author?.username ?? "unknown",
134
+ ...(first.position?.new_path ? { path: first.position.new_path } : {}),
135
+ ...(first.position?.new_line ? { line: first.position.new_line } : {}),
136
+ side: first.position?.new_line ? "head" : "base",
137
+ resolved: notes.some((n) => n.resolvable) && notes.every((n) => !n.resolvable || n.resolved),
138
+ // GitLab reports no per-comment staleness; the head it was written
139
+ // against is the only signal, and `prior` compares it in the CLI.
140
+ outdated: false,
141
+ ...(first.position?.head_sha ? { commit: first.position.head_sha } : {}),
142
+ messages: notes.map((n) => ({ author: n.author?.username ?? "unknown", body: n.body })),
143
+ });
144
+ }
145
+ const approvals = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/approvals`));
146
+ for (const a of approvals.approved_by ?? [])
147
+ passes.push({
148
+ id: `approval:${a.user.username}`,
149
+ author: a.user.username,
150
+ verdict: "approve",
151
+ at: "",
152
+ body: "",
153
+ });
154
+ return { passes, threads };
155
+ }
156
+ async submit(repo, cr, s) {
157
+ const mr = await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}`));
158
+ const refs = mr.diff_refs;
159
+ if (!refs && s.comments.length)
160
+ throw new AxiError("this merge request reports no diff refs", "FORGE_ERROR", [
161
+ "Post the summary alone with an empty comments array, or retry once the merge request has a diff",
162
+ ]);
163
+ const notes = [];
164
+ let posted = 0;
165
+ for (const c of s.comments) {
166
+ if (c.startLine && c.startLine < c.line)
167
+ notes.push(`${c.path}:${c.startLine}-${c.line} anchored at line ${c.line}`);
168
+ await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/discussions`, [
169
+ "--method",
170
+ "POST",
171
+ "--raw-field",
172
+ `body=${c.body}`,
173
+ "--raw-field",
174
+ "position[position_type]=text",
175
+ "--raw-field",
176
+ `position[base_sha]=${refs.base_sha}`,
177
+ "--raw-field",
178
+ `position[start_sha]=${refs.start_sha}`,
179
+ "--raw-field",
180
+ `position[head_sha]=${refs.head_sha}`,
181
+ "--raw-field",
182
+ `position[new_path]=${c.path}`,
183
+ "--raw-field",
184
+ `position[old_path]=${c.path}`,
185
+ "--raw-field",
186
+ c.side === "base" ? `position[old_line]=${c.line}` : `position[new_line]=${c.line}`,
187
+ ]));
188
+ posted++;
189
+ }
190
+ if (s.verdict === "approve") {
191
+ await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/approve`, [
192
+ "--method",
193
+ "POST",
194
+ ]));
195
+ }
196
+ else if (s.verdict === "request-changes") {
197
+ notes.push("GitLab has no changes-requested state; this account's approval was removed and the summary says changes are requested");
198
+ await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/unapprove`, [
199
+ "--method",
200
+ "POST",
201
+ ])).catch(() => notes.push("no approval by this account to remove"));
202
+ }
203
+ await runJson("glab", this.api(repo, `projects/${this.project(repo)}/merge_requests/${cr.number}/notes`, [
204
+ "--method",
205
+ "POST",
206
+ "--raw-field",
207
+ `body=${s.body}`,
208
+ ]));
209
+ return { verdict: s.verdict, posted, url: cr.url, notes };
210
+ }
211
+ async reply(repo, cr, threadId, body, resolve) {
212
+ const base = `projects/${this.project(repo)}/merge_requests/${cr.number}/discussions/${threadId}`;
213
+ if (body !== undefined)
214
+ await runJson("glab", this.api(repo, `${base}/notes`, ["--method", "POST", "--raw-field", `body=${body}`]));
215
+ if (resolve)
216
+ await runJson("glab", this.api(repo, base, ["--method", "PUT", "--raw-field", "resolved=true"]));
217
+ return { replied: body !== undefined, resolved: resolve, notes: [] };
218
+ }
219
+ permalink(repo, sha, path, from, to) {
220
+ const base = `https://${repo.host}/${repo.path}/-/blob/${sha}/${path}`;
221
+ if (!from)
222
+ return base;
223
+ return to && to > from ? `${base}#L${from}-${to}` : `${base}#L${from}`;
224
+ }
225
+ }
226
+ //# sourceMappingURL=gitlab.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gitlab.js","sourceRoot":"","sources":["../../src/forge/gitlab.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import * as g from "../git.js";
3
+ import { GitHubForge, parseRemote } from "./github.js";
4
+ import { GitLabForge } from "./gitlab.js";
5
+ export * from "./types.js";
6
+ export { parseRemote };
7
+ const BUILTIN = [new GitHubForge(), new GitLabForge()];
8
+ export function forgeById(id) {
9
+ const f = BUILTIN.find((x) => x.id === id);
10
+ if (!f)
11
+ throw new AxiError(`unknown forge ${id}`, "VALIDATION_ERROR", [
12
+ `--forge takes ${BUILTIN.map((x) => x.id).join(" or ")}`,
13
+ ]);
14
+ return f;
15
+ }
16
+ /**
17
+ * Which adapter owns a host. `github.com` and `gitlab.com` are decided by
18
+ * name; every other host is asked of the machine, because a self-hosted
19
+ * instance is the normal case and its name says nothing. A host no adapter
20
+ * claims is REFUSED rather than guessed at - guessing wrong means posting a
21
+ * review on somebody else's repository.
22
+ */
23
+ export async function forgeFor(host, explicit) {
24
+ if (explicit)
25
+ return forgeById(explicit);
26
+ for (const f of BUILTIN)
27
+ if (await f.owns(host))
28
+ return f;
29
+ throw new AxiError(`no forge configured for ${host}`, "FORGE_ERROR", [
30
+ `Authenticate the host (\`gh auth login --hostname ${host}\` or \`glab auth login --hostname ${host}\`)`,
31
+ `Or pass --forge ${BUILTIN.map((x) => x.id).join("|")} to name it`,
32
+ ]);
33
+ }
34
+ /** The repository this worktree's `origin` points at. */
35
+ export async function repoOf(worktree, explicit) {
36
+ if (explicit) {
37
+ const parts = explicit.split("/").filter(Boolean);
38
+ if (parts.length < 2)
39
+ throw new AxiError(`${explicit} is not a host-qualified repository`, "VALIDATION_ERROR", [
40
+ "--repo takes host/path, as in github.com/owner/repo",
41
+ ]);
42
+ return { host: parts[0], path: parts.slice(1).join("/") };
43
+ }
44
+ const url = (await g.git(worktree, ["remote", "get-url", "origin"]).catch(() => "")).trim();
45
+ const repo = url ? parseRemote(url) : null;
46
+ if (!repo)
47
+ throw new AxiError("could not read a forge repository from `origin`", "FORGE_ERROR", [
48
+ "Pass --repo host/path, as in github.com/owner/repo",
49
+ ]);
50
+ return repo;
51
+ }
52
+ function count(checks, state) {
53
+ return checks.filter((c) => c.state === state).length;
54
+ }
55
+ /**
56
+ * What CI actually established, in one sentence the review can quote.
57
+ *
58
+ * Two failures this answers, both of which shipped bugs before it existed: a
59
+ * change request from a fork runs a fraction of the checks an internal branch
60
+ * runs, and a cancelled job reads as "no failures" in every summary view. So
61
+ * the count that matters is not "did anything fail" but "how much ran here
62
+ * against how much runs on the branch this would merge into".
63
+ */
64
+ export function summariseCi(checks, baseline, baselineBranch) {
65
+ const s = {
66
+ trustworthy: false,
67
+ verdict: "",
68
+ ran: checks.length,
69
+ passed: count(checks, "passed"),
70
+ failed: count(checks, "failed"),
71
+ cancelled: count(checks, "cancelled"),
72
+ skipped: count(checks, "skipped"),
73
+ running: count(checks, "running"),
74
+ unknown: count(checks, "unknown"),
75
+ baselineRan: baseline ? baseline.length : null,
76
+ baselineBranch,
77
+ };
78
+ const missing = s.baselineRan === null ? null : s.baselineRan - s.ran;
79
+ const parts = [];
80
+ if (!s.ran)
81
+ parts.push("No check ran on this head");
82
+ else
83
+ parts.push(`${s.passed} of ${s.ran} checks passed`);
84
+ if (s.failed)
85
+ parts.push(`${s.failed} failed`);
86
+ if (s.cancelled)
87
+ parts.push(`${s.cancelled} were cancelled, so they asserted nothing`);
88
+ if (s.skipped)
89
+ parts.push(`${s.skipped} never ran`);
90
+ if (s.running)
91
+ parts.push(`${s.running} are still running`);
92
+ if (s.unknown)
93
+ parts.push(`${s.unknown} could not be read`);
94
+ if (missing === null)
95
+ parts.push(`what ${baselineBranch} itself runs could not be read, so nothing here says how much CI is missing`);
96
+ else if (missing > 0)
97
+ parts.push(`${baselineBranch} runs ${s.baselineRan}, so ${missing} of them are absent here`);
98
+ s.trustworthy =
99
+ s.ran > 0 &&
100
+ s.failed === 0 &&
101
+ s.cancelled === 0 &&
102
+ s.running === 0 &&
103
+ s.unknown === 0 &&
104
+ missing !== null &&
105
+ missing <= 0;
106
+ parts.push(s.trustworthy ? "CI is a real gate on this change" : "CI is not a gate here; the review is");
107
+ s.verdict = parts.join("; ") + ".";
108
+ return s;
109
+ }
110
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/forge/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { AxiError } from "axi-sdk-js";
2
+ import { execFile } from "node:child_process";
3
+ import { promisify } from "node:util";
4
+ const execFileP = promisify(execFile);
5
+ /** 8 MB: a busy change request's discussions exceed the 1 MB default. */
6
+ const MAX_OUTPUT = 8 * 1024 * 1024;
7
+ /**
8
+ * Drive a forge CLI. A missing binary and a failed call both become an
9
+ * AxiError naming the command, so the agent is told which tool to install or
10
+ * authenticate rather than seeing a decoded empty answer.
11
+ */
12
+ export async function run(cli, args, opts = {}) {
13
+ try {
14
+ const child = execFileP(cli, args, { cwd: opts.cwd, maxBuffer: MAX_OUTPUT });
15
+ if (opts.input !== undefined) {
16
+ child.child.stdin?.end(opts.input);
17
+ }
18
+ const { stdout } = await child;
19
+ return stdout;
20
+ }
21
+ catch (e) {
22
+ const err = e;
23
+ if (err.code === "ENOENT")
24
+ throw new AxiError(`${cli} is not on PATH`, "FORGE_ERROR", [
25
+ `Install ${cli} and authenticate it, or pass --forge to name the other forge`,
26
+ ]);
27
+ const detail = (err.stderr || err.message || "").trim().split("\n").slice(0, 3).join("; ");
28
+ throw new AxiError(`${cli} ${args[0] ?? ""} failed: ${detail}`, "FORGE_ERROR", [
29
+ `Check \`${cli} auth status\``,
30
+ "Check the change request number and that the account can read the repository",
31
+ ]);
32
+ }
33
+ }
34
+ export async function runJson(cli, args, opts = {}) {
35
+ const out = await run(cli, args, opts);
36
+ try {
37
+ return JSON.parse(out);
38
+ }
39
+ catch {
40
+ throw new AxiError(`${cli} returned output that is not JSON`, "FORGE_ERROR", [
41
+ `Run \`${cli} ${args.join(" ")}\` by hand to see what it answered`,
42
+ ]);
43
+ }
44
+ }
45
+ /** True when the CLI exists and exits 0 for `args`; used for host discovery. */
46
+ export async function ok(cli, args) {
47
+ try {
48
+ await execFileP(cli, args, { maxBuffer: MAX_OUTPUT });
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ //# sourceMappingURL=run.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run.js","sourceRoot":"","sources":["../../src/forge/run.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC,MAAM,SAAS,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;AAEtC,yEAAyE;AACzE,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;AAQnC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,GAAG,CAAC,GAAW,EAAE,IAAc,EAAE,IAAI,GAAe,EAAE;IAC1E,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QAC7E,IAAI,IAAI,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;YAC7B,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACrC,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC;QAC/B,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,GAAG,GAAG,CAAgD,CAAC;QAC7D,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ;YACvB,MAAM,IAAI,QAAQ,CAAC,GAAG,GAAG,iBAAiB,EAAE,aAAa,EAAE;gBACzD,WAAW,GAAG,+DAA+D;aAC9E,CAAC,CAAC;QACL,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QAC3F,MAAM,IAAI,QAAQ,CAAC,GAAG,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,YAAY,MAAM,EAAE,EAAE,aAAa,EAAE;YAC7E,WAAW,GAAG,gBAAgB;YAC9B,8EAA8E;SAC/E,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAAI,GAAW,EAAE,IAAc,EAAE,IAAI,GAAe,EAAE;IACjF,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAM,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,QAAQ,CAAC,GAAG,GAAG,mCAAmC,EAAE,aAAa,EAAE;YAC3E,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,oCAAoC;SACnE,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,gFAAgF;AAChF,MAAM,CAAC,KAAK,UAAU,EAAE,CAAC,GAAW,EAAE,IAAc;IAClD,IAAI,CAAC;QACH,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,CAAC,CAAC;QACtD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { AxiError } from "axi-sdk-js";
3
+ /**
4
+ * The file `thurview forge submit` takes. One pass of a review: a summary, the
5
+ * inline comments, and a verdict. It is a file rather than flags because a
6
+ * review comment is prose with newlines in it, and because the file is the
7
+ * thing a human can read before it is posted on their behalf.
8
+ */
9
+ const Schema = z.object({
10
+ verdict: z.enum(["comment", "approve", "request-changes"]),
11
+ body: z.string().min(1),
12
+ comments: z
13
+ .array(z.object({
14
+ path: z.string().min(1),
15
+ line: z.number().int().positive(),
16
+ startLine: z.number().int().positive().optional(),
17
+ side: z.enum(["head", "base"]).optional(),
18
+ body: z.string().min(1),
19
+ }))
20
+ .default([]),
21
+ });
22
+ export function parseSubmission(text, file) {
23
+ let raw;
24
+ try {
25
+ raw = JSON.parse(text);
26
+ }
27
+ catch (e) {
28
+ throw new AxiError(`${file} is not valid JSON: ${e.message}`, "VALIDATION_ERROR", [
29
+ 'The file holds {"verdict": "comment", "body": "...", "comments": []}',
30
+ ]);
31
+ }
32
+ const parsed = Schema.safeParse(raw);
33
+ if (!parsed.success)
34
+ throw new AxiError(`${file} is not a submission: ${parsed.error.issues
35
+ .map((i) => `${i.path.join(".") || "(root)"} ${i.message}`)
36
+ .join("; ")}`, "VALIDATION_ERROR", [
37
+ "verdict is comment, approve or request-changes; body is the summary; comments[] each need path, line and body",
38
+ ]);
39
+ for (const c of parsed.data.comments)
40
+ if (c.startLine && c.startLine > c.line)
41
+ throw new AxiError(`${c.path}: startLine ${c.startLine} is after line ${c.line}`, "VALIDATION_ERROR", ["line is the LAST line of the range and startLine the first"]);
42
+ return parsed.data;
43
+ }
44
+ /**
45
+ * Comments long enough that the author will skim them. A finding that does
46
+ * not fit is two findings, or a claim plus one suggestion with the evidence
47
+ * behind a permalink - never a wall of prose on a line of someone's diff.
48
+ */
49
+ export function longComments(s, max) {
50
+ return s.comments
51
+ .map((c, i) => ({ c, i, lines: c.body.trimEnd().split("\n").length }))
52
+ .filter((x) => x.lines > max)
53
+ .map((x) => `comments[${x.i}] on ${x.c.path}:${x.c.line} is ${x.lines} lines (max ${max})`);
54
+ }
55
+ //# sourceMappingURL=submission.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"submission.js","sourceRoot":"","sources":["../../src/forge/submission.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The FORGE seam - what a review needs to know about a proposed change, and
3
+ * nothing about which forge answers it.
4
+ *
5
+ * THE WORD. GitHub calls it a pull request, GitLab a merge request. They are
6
+ * the same thing to this code, which says CHANGE REQUEST throughout and never
7
+ * picks a side. Prose that is genuinely about GitHub still says "pull
8
+ * request", because there it is describing GitHub.
9
+ *
10
+ * THE QUESTIONS. Each method exists because a review step needs it, not
11
+ * because a forge happens to offer it:
12
+ *
13
+ * get the change request itself - the head a review is written
14
+ * against, the branch it is open from, whether it comes from a
15
+ * fork
16
+ * checks what CI did on that head, one row per job
17
+ * baseline what CI does on the target branch's own tip, which is the only
18
+ * way to see that a fork change request ran almost nothing
19
+ * prior every earlier review pass and every review thread, with who
20
+ * wrote it, whether it is resolved and which commit it was
21
+ * written against
22
+ * submit one pass - a summary, inline comments, a verdict
23
+ * reply answer one thread, and resolve it when its point is verified
24
+ * permalink a file and line range at a pinned commit, as a URL the author
25
+ * can open
26
+ *
27
+ * WHAT IS NOT HERE, deliberately: merge, close, push. A review comments; the
28
+ * maintainer merges. A seam with no merge method cannot be talked into one.
29
+ *
30
+ * A CHECK THAT COULD NOT BE READ IS `unknown`, NEVER `passed`. Every mapping
31
+ * below is explicit about which forge state becomes which verdict, because a
32
+ * cancelled job that reads as a pass is how two real bugs shipped.
33
+ */
34
+ export function qualified(repo) {
35
+ return `${repo.host}/${repo.path}`;
36
+ }
37
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/forge/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AASH,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;AACrC,CAAC"}
@@ -0,0 +1,34 @@
1
+ import { rm } from "node:fs/promises";
2
+ import { agentFile, readJson, writeJson, now } from "./store.js";
3
+ /** A heartbeat older than this is a dead `wait`, not a listening agent. */
4
+ const TTL_MS = 15_000;
5
+ const BEAT_MS = 3_000;
6
+ export const NOBODY = { attached: false, lastSeen: null };
7
+ export async function presenceOf(reviewId) {
8
+ const rec = await readJson(agentFile(reviewId));
9
+ if (!rec)
10
+ return NOBODY;
11
+ const at = Date.parse(rec.at);
12
+ if (!Number.isFinite(at))
13
+ return NOBODY;
14
+ return { attached: Date.now() - at < TTL_MS, lastSeen: rec.at };
15
+ }
16
+ /** Announce that this process is waiting on the review until `stop()` is called. */
17
+ export function attach(reviewId) {
18
+ let stopped = false;
19
+ const beat = () => writeJson(agentFile(reviewId), { at: now(), pid: process.pid }).catch(() => { });
20
+ void beat();
21
+ const timer = setInterval(() => {
22
+ if (!stopped)
23
+ void beat();
24
+ }, BEAT_MS);
25
+ timer.unref();
26
+ return {
27
+ stop: async () => {
28
+ stopped = true;
29
+ clearInterval(timer);
30
+ await rm(agentFile(reviewId), { force: true });
31
+ },
32
+ };
33
+ }
34
+ //# sourceMappingURL=presence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"presence.js","sourceRoot":"","sources":["../src/presence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,YAAY,CAAC;AAcjE,2EAA2E;AAC3E,MAAM,MAAM,GAAG,MAAM,CAAC;AACtB,MAAM,OAAO,GAAG,KAAK,CAAC;AAEtB,MAAM,CAAC,MAAM,MAAM,GAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;AAEpE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,QAAgB;IAC/C,MAAM,GAAG,GAAG,MAAM,QAAQ,CAA8B,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IAC7E,IAAI,CAAC,GAAG;QAAE,OAAO,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC9B,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,MAAM,CAAC;IACxC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,MAAM,EAAE,QAAQ,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC;AAClE,CAAC;AAED,oFAAoF;AACpF,MAAM,UAAU,MAAM,CAAC,QAAgB;IACrC,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,MAAM,IAAI,GAAG,GAAG,EAAE,CAChB,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IAClF,KAAK,IAAI,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,CAAC,OAAO;YAAE,KAAK,IAAI,EAAE,CAAC;IAC5B,CAAC,EAAE,OAAO,CAAC,CAAC;IACZ,KAAK,CAAC,KAAK,EAAE,CAAC;IACd,OAAO;QACL,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,OAAO,GAAG,IAAI,CAAC;YACf,aAAa,CAAC,KAAK,CAAC,CAAC;YACrB,MAAM,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QACjD,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -11,6 +11,7 @@ import { symbolIndex } from "../symbols.js";
11
11
  import { registerTheme } from "../highlight.js";
12
12
  import { listReviews, readReview, writeReview, readThreads, readJson, reviewDir, revisionDir, deleteReview, writeJson, serverStateFile, } from "../store.js";
13
13
  import { createThread, replyThread, setThreadStatus, submitReview, deleteThread, } from "../threads.js";
14
+ import { presenceOf } from "../presence.js";
14
15
  const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "ui");
15
16
  class HttpError extends Error {
16
17
  status;
@@ -167,11 +168,14 @@ export async function startServer(opts = {}) {
167
168
  ...data,
168
169
  threads: threads.threads,
169
170
  decisions: threads.decisions,
171
+ agent: await presenceOf(id),
170
172
  };
171
173
  }
172
174
  if (sub === "events") {
173
175
  throw new HttpError(500, "handled elsewhere");
174
176
  }
177
+ if (sub === "presence")
178
+ return (await presenceOf(id));
175
179
  if (sub === "revisions") {
176
180
  const out = [];
177
181
  for (let n = 1; n <= review.revision; n++)