@dev.fast/whiteboard 0.3.4-preview.20261019.127 → 0.3.4
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/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
- package/dist/agent-cli-C6SPSII0.js +2 -0
- package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
- package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
- package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
- package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
- package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
- package/dist/build-info.json +1 -1
- package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
- package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
- package/dist/cli.js +3 -4
- package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
- package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
- package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
- package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
- package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
- package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
- package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
- package/dist/profile-CYPmvBoA.js +2 -0
- package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
- package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
- package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
- package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
- package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
- package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
- package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
- package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
- package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
- package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
- package/dist/runtime-CKiWhGrf.js +52 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +1 -1
- package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
- package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
- package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
- package/dist/server/desktop-host.js +76 -29
- package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
- package/dist/sharing/index.d.ts +70 -484
- package/dist/sharing/index.js +1 -1
- package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
- package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
- package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
- package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
- package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
- package/instructions/authoring.md +2 -3
- package/instructions/scratchpad.md +23 -0
- package/package.json +4 -4
- package/src/ask/agents.ts +1 -13
- package/src/ask/checkout-files.ts +5 -2
- package/src/ask/thread-state.ts +0 -4
- package/src/ask/thread.ts +0 -16
- package/src/ask/threads.ts +3 -24
- package/src/cli-install.ts +2 -13
- package/src/cli-runner.ts +1 -4
- package/src/cli.ts +1 -4
- package/src/diff-selection-migration.ts +34 -48
- package/src/lens-selection.ts +2 -38
- package/src/review-api/README.md +13 -52
- package/src/review-api/activity.ts +4 -1
- package/src/review-api/agent-cli.ts +1 -9
- package/src/review-api/anchor-quotes.ts +7 -14
- package/src/review-api/ask-history.ts +51 -137
- package/src/review-api/authoring-tools.ts +30 -45
- package/src/review-api/blocks/definition.ts +1 -1
- package/src/review-api/blocks/markdown.ts +2 -2
- package/src/review-api/document-headings.ts +27 -58
- package/src/review-api/document-text.ts +2 -31
- package/src/review-api/document.ts +34 -281
- package/src/review-api/github-provider.ts +13 -51
- package/src/review-api/http.ts +123 -702
- package/src/review-api/instructions.ts +19 -0
- package/src/review-api/local-data.ts +46 -310
- package/src/review-api/mcp.ts +11 -3
- package/src/review-api/onboarding.ts +1 -1
- package/src/review-api/profile.ts +6 -37
- package/src/review-api/public-tools.ts +2 -24
- package/src/review-api/pull-request.ts +49 -82
- package/src/review-api/request-origin.ts +0 -10
- package/src/review-api/review-progress.ts +27 -61
- package/src/review-api/store-schema.ts +29 -38
- package/src/review-api/store.ts +478 -2142
- package/src/review-api/worktree-events.ts +5 -3
- package/src/review-api/worktree-source.ts +7 -3
- package/src/review-api/worktree-structural.integration.ts +0 -72
- package/src/review-preferences.ts +88 -0
- package/src/review-stack.ts +23 -15
- package/src/runtime.ts +30 -4
- package/src/server/desktop-host.ts +1 -6
- package/src/server/desktop-server.ts +31 -1
- package/src/server/headless-host.ts +2 -0
- package/src/server/hono-http.ts +1 -2
- package/src/server/legacy-tutorial-cleanup.ts +3 -2
- package/src/server/review-gateway-list.ts +5 -3
- package/src/server/review-gateway-memory.ts +1 -1
- package/src/server/review-lifecycle-telemetry.ts +2 -2
- package/src/server/review-server-core.ts +5 -1
- package/src/server/structural-comparisons.ts +0 -47
- package/src/sharing/export.ts +7 -36
- package/src/sharing/host.ts +14 -34
- package/src/sharing/import.ts +24 -156
- package/src/sharing/repository.ts +20 -128
- package/src/startup-trace.ts +3 -3
- package/src/stored-document-migration.ts +19 -110
- package/src/ui-telemetry-events.ts +11 -33
- package/dist/agent-cli-_YyMIMRY.js +0 -2
- package/dist/lens-selection-CyJapcsl.js +0 -214
- package/dist/profile-DJvkBJNQ.js +0 -2
- package/dist/runtime-Bj2tD33R.js +0 -27
- package/src/heading-anchors.ts +0 -42
- package/src/review-api/board-order.ts +0 -144
- package/src/review-api/checkout-groups.ts +0 -560
- package/src/review-api/foreign-references.ts +0 -328
- package/src/review-api/link-preview.ts +0 -113
- package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
- package/src/review-api/store-migrations.ts +0 -357
|
@@ -4,11 +4,8 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
4
4
|
|
|
5
5
|
import { withFileLock } from "@dev.fast/trace-core";
|
|
6
6
|
|
|
7
|
-
import { CheckoutGroups } from "./checkout-groups.js";
|
|
8
7
|
import { openLocalReviewStore } from "./local-data.js";
|
|
9
|
-
import { groupCheckoutsAhead } from "./store-migrations.js";
|
|
10
8
|
import { initializeReviewStoreSchema } from "./store-schema.js";
|
|
11
|
-
import { projectOfCheckout } from "./store.js";
|
|
12
9
|
|
|
13
10
|
const lockOptions = {
|
|
14
11
|
retryMs: 50,
|
|
@@ -19,7 +16,7 @@ const lockOptions = {
|
|
|
19
16
|
|
|
20
17
|
export async function openReviewProfile(
|
|
21
18
|
home: string,
|
|
22
|
-
options: { manageWorkspaces: boolean
|
|
19
|
+
options: { manageWorkspaces: boolean },
|
|
23
20
|
) {
|
|
24
21
|
await mkdir(home, { recursive: true, mode: 0o700 });
|
|
25
22
|
|
|
@@ -33,12 +30,7 @@ export async function openReviewProfile(
|
|
|
33
30
|
])
|
|
34
31
|
await importHeadlessStore(home, source);
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
return openLocalReviewStore(database, {
|
|
39
|
-
...options,
|
|
40
|
-
checkoutOwners: await groupCheckoutsAhead(database),
|
|
41
|
-
});
|
|
33
|
+
return openLocalReviewStore(path.join(home, "review-api.db"), options);
|
|
42
34
|
},
|
|
43
35
|
);
|
|
44
36
|
|
|
@@ -97,7 +89,7 @@ async function importHeadlessStore(home: string, source: string) {
|
|
|
97
89
|
`Cannot merge ${source}: the shared store already contains one of its review IDs. Both databases are unchanged.`,
|
|
98
90
|
);
|
|
99
91
|
database.exec(`
|
|
100
|
-
INSERT INTO repositories
|
|
92
|
+
INSERT INTO repositories SELECT s.* FROM headless.repositories s WHERE NOT EXISTS (SELECT 1 FROM repositories t WHERE t.path=s.path);
|
|
101
93
|
CREATE TEMP TABLE repository_ids AS SELECT s.id old_id,t.id new_id FROM headless.repositories s JOIN repositories t ON s.path=t.path;
|
|
102
94
|
`);
|
|
103
95
|
|
|
@@ -122,36 +114,13 @@ async function importHeadlessStore(home: string, source: string) {
|
|
|
122
114
|
`Cannot merge ${source}: a resource ID has different content. Both databases are unchanged.`,
|
|
123
115
|
);
|
|
124
116
|
|
|
125
|
-
if (
|
|
126
|
-
database
|
|
127
|
-
.prepare(
|
|
128
|
-
"SELECT 1 FROM pragma_table_info('reviews') WHERE name='project_id'",
|
|
129
|
-
)
|
|
130
|
-
.get()
|
|
131
|
-
) {
|
|
132
|
-
// A store with projects: file each board under its repository's,
|
|
133
|
-
// grouping the checkouts once for all of them.
|
|
134
|
-
const groups = new CheckoutGroups(database);
|
|
135
|
-
|
|
136
|
-
for (const { new_id: repositoryId } of database
|
|
137
|
-
.prepare("SELECT new_id FROM repository_ids")
|
|
138
|
-
.all())
|
|
139
|
-
projectOfCheckout(database, String(repositoryId), groups);
|
|
140
|
-
database.exec(`INSERT INTO reviews(id,version,next_id,project_id) SELECT s.id,s.version,s.next_id,p.project_id
|
|
141
|
-
FROM headless.reviews s JOIN headless.versions v ON v.review_id=s.id AND v.version=s.version
|
|
142
|
-
JOIN repository_ids r ON json_extract(v.snapshot,'$.pins.repositoryId')=r.old_id
|
|
143
|
-
JOIN repositories p ON p.id=r.new_id;`);
|
|
144
|
-
} else
|
|
145
|
-
database.exec(
|
|
146
|
-
"INSERT INTO reviews(id,version,next_id) SELECT id,version,next_id FROM headless.reviews;",
|
|
147
|
-
);
|
|
148
117
|
database.exec(`
|
|
149
118
|
INSERT OR IGNORE INTO resources SELECT s.id,r.new_id,s.kind,s.mime_type,s.data FROM headless.resources s JOIN repository_ids r ON s.repository_id=r.old_id;
|
|
150
|
-
INSERT INTO
|
|
119
|
+
INSERT INTO reviews SELECT * FROM headless.reviews;
|
|
120
|
+
INSERT INTO versions SELECT s.review_id,s.version,
|
|
151
121
|
CASE WHEN json_type(s.snapshot,'$.target')='object'
|
|
152
122
|
THEN json_set(s.snapshot,'$.pins.repositoryId',r.new_id,'$.target.repositoryId',r.new_id)
|
|
153
|
-
ELSE json_set(s.snapshot,'$.pins.repositoryId',r.new_id) END
|
|
154
|
-
NULL, NULL
|
|
123
|
+
ELSE json_set(s.snapshot,'$.pins.repositoryId',r.new_id) END
|
|
155
124
|
FROM headless.versions s JOIN repository_ids r ON json_extract(s.snapshot,'$.pins.repositoryId')=r.old_id;
|
|
156
125
|
INSERT INTO review_attention SELECT * FROM headless.review_attention;
|
|
157
126
|
`);
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
|
|
2
|
-
import { boardTitle } from "@dev.fast/review-protocol";
|
|
3
2
|
|
|
4
3
|
import {
|
|
5
4
|
type AuthoringTool,
|
|
@@ -79,13 +78,7 @@ export function publicResult(value: JsonValue): JsonValue {
|
|
|
79
78
|
|
|
80
79
|
return [
|
|
81
80
|
name,
|
|
82
|
-
key === "review" || key === "reviews"
|
|
83
|
-
? publicResult(item)
|
|
84
|
-
: // A board's title as every surface shows it: an empty one is
|
|
85
|
-
// "Untitled".
|
|
86
|
-
key === "title" && "reviewId" in value && isStringValue(item)
|
|
87
|
-
? boardTitle(item)
|
|
88
|
-
: item,
|
|
81
|
+
key === "review" || key === "reviews" ? publicResult(item) : item,
|
|
89
82
|
];
|
|
90
83
|
}),
|
|
91
84
|
);
|
|
@@ -105,20 +98,5 @@ export async function callPublicTool(
|
|
|
105
98
|
if (sessionId !== undefined) fields.reviewId = sessionId;
|
|
106
99
|
const result = await callAuthoringTool(client, tool, fields, signal);
|
|
107
100
|
|
|
108
|
-
return result instanceof ToolText
|
|
109
|
-
? result
|
|
110
|
-
: withoutRepositoryIds(publicResult(result));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Agents never see repository ids. */
|
|
114
|
-
function withoutRepositoryIds(value: JsonValue): JsonValue {
|
|
115
|
-
if (Array.isArray(value)) return value.map(withoutRepositoryIds);
|
|
116
|
-
|
|
117
|
-
if (!isJsonObject(value)) return value;
|
|
118
|
-
|
|
119
|
-
return Object.fromEntries(
|
|
120
|
-
Object.entries(value)
|
|
121
|
-
.filter(([key]) => key !== "repositoryId")
|
|
122
|
-
.map(([key, item]) => [key, withoutRepositoryIds(item)]),
|
|
123
|
-
);
|
|
101
|
+
return result instanceof ToolText ? result : publicResult(result);
|
|
124
102
|
}
|
|
@@ -1,29 +1,18 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
type LocalVcsKind,
|
|
7
|
-
type VcsArg,
|
|
8
|
-
renderVcsArgs,
|
|
9
|
-
runVcs,
|
|
10
|
-
} from "@dev.fast/local-vcs";
|
|
11
|
-
import type { PullRequestState } from "@dev.fast/review-protocol";
|
|
3
|
+
|
|
4
|
+
import type { LocalVcsKind } from "@dev.fast/local-vcs";
|
|
12
5
|
import { errorMessage } from "@dev.fast/trace-core";
|
|
13
6
|
|
|
14
7
|
import { ReviewInputError } from "./document.js";
|
|
15
8
|
|
|
16
|
-
/** Runs one subprocess and resolves its stdout; rejects on failure or
|
|
17
|
-
* timeout. git, jj and gh go through the VCS runner; Azure DevOps's az (by
|
|
18
|
-
* way of cmd.exe on Windows) gets its operands checked the same way. */
|
|
9
|
+
/** Runs one subprocess and resolves its stdout; rejects on failure or timeout. */
|
|
19
10
|
export type RunCommand = (
|
|
20
|
-
file:
|
|
21
|
-
args:
|
|
11
|
+
file: string,
|
|
12
|
+
args: string[],
|
|
22
13
|
options: { cwd?: string; timeoutMs: number },
|
|
23
14
|
) => Promise<string>;
|
|
24
15
|
|
|
25
|
-
const execFileAsync = promisify(execFile);
|
|
26
|
-
|
|
27
16
|
export interface PullRequestDeps {
|
|
28
17
|
run: RunCommand;
|
|
29
18
|
fetch: typeof fetch;
|
|
@@ -42,7 +31,6 @@ export interface PullRequestRecord {
|
|
|
42
31
|
headBranch?: string;
|
|
43
32
|
/** GitHub's base commit, frozen at the PR's last update. */
|
|
44
33
|
baseRefOid?: string;
|
|
45
|
-
state?: PullRequestState;
|
|
46
34
|
}
|
|
47
35
|
|
|
48
36
|
const FETCH_TIMEOUT_MS = 120_000;
|
|
@@ -50,38 +38,35 @@ const FETCH_TIMEOUT_MS = 120_000;
|
|
|
50
38
|
const LOCAL_TIMEOUT_MS = 30_000;
|
|
51
39
|
|
|
52
40
|
export const defaultPullRequestDeps: PullRequestDeps = {
|
|
53
|
-
run:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
throw new Error(`${file} ${String(args[0])}: ${reason}`);
|
|
83
|
-
}
|
|
84
|
-
},
|
|
41
|
+
run: (file, args, options) =>
|
|
42
|
+
new Promise((resolve, reject) => {
|
|
43
|
+
execFile(
|
|
44
|
+
file,
|
|
45
|
+
args,
|
|
46
|
+
{
|
|
47
|
+
cwd: options.cwd,
|
|
48
|
+
timeout: options.timeoutMs,
|
|
49
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
50
|
+
// Never wait on a credential or confirmation prompt nobody can see.
|
|
51
|
+
env: {
|
|
52
|
+
...process.env,
|
|
53
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
54
|
+
GH_PROMPT_DISABLED: "1",
|
|
55
|
+
GIT_SSH_COMMAND:
|
|
56
|
+
process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
(error, stdout, stderr) => {
|
|
60
|
+
if (!error) return resolve(stdout);
|
|
61
|
+
|
|
62
|
+
const reason = error.killed
|
|
63
|
+
? `timed out after ${options.timeoutMs / 1000}s`
|
|
64
|
+
: String(stderr).trim() || error.message;
|
|
65
|
+
|
|
66
|
+
reject(new Error(`${file} ${args[0]}: ${reason}`));
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
}),
|
|
85
70
|
fetch: (input, init) => fetch(input, init),
|
|
86
71
|
};
|
|
87
72
|
|
|
@@ -150,62 +135,55 @@ export async function fetchPullRequest(
|
|
|
150
135
|
const { pullRequest: pr } = input;
|
|
151
136
|
const refs = pullRequestRefs(input.provider, pr);
|
|
152
137
|
|
|
153
|
-
const git = (args:
|
|
138
|
+
const git = (args: string[], timeoutMs = LOCAL_TIMEOUT_MS) =>
|
|
154
139
|
deps
|
|
155
140
|
.run("git", ["--git-dir", input.gitDir, ...args], { timeoutMs })
|
|
156
141
|
.then((stdout) => stdout.trim());
|
|
157
142
|
|
|
158
|
-
const fetchRefs = (...refspecs:
|
|
143
|
+
const fetchRefs = (...refspecs: string[]) =>
|
|
159
144
|
git(
|
|
160
145
|
[
|
|
161
146
|
"fetch",
|
|
162
147
|
"--no-tags",
|
|
163
148
|
"--no-write-fetch-head",
|
|
164
149
|
"--quiet",
|
|
165
|
-
|
|
150
|
+
input.remote,
|
|
166
151
|
...refspecs,
|
|
167
152
|
],
|
|
168
153
|
FETCH_TIMEOUT_MS,
|
|
169
154
|
);
|
|
170
155
|
|
|
171
156
|
const commit = (ref: string) =>
|
|
172
|
-
git(["rev-parse", "--verify", "--quiet",
|
|
157
|
+
git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]).catch(
|
|
173
158
|
() => undefined,
|
|
174
159
|
);
|
|
175
160
|
|
|
176
161
|
const mergeBase = (left: string, right: string) =>
|
|
177
|
-
git(["merge-base",
|
|
162
|
+
git(["merge-base", left, right]).catch(() => undefined);
|
|
178
163
|
|
|
179
164
|
const frozenBase = async () => {
|
|
180
165
|
if (!pr.baseRefOid) return undefined;
|
|
181
166
|
|
|
182
167
|
if (!(await commit(pr.baseRefOid)))
|
|
183
|
-
await fetchRefs({
|
|
184
|
-
|
|
185
|
-
|
|
168
|
+
await fetchRefs(`+${pr.baseRefOid}:${refs.frozenBase}`).catch(
|
|
169
|
+
() => undefined,
|
|
170
|
+
);
|
|
186
171
|
|
|
187
172
|
return commit(pr.baseRefOid);
|
|
188
173
|
};
|
|
189
174
|
|
|
190
175
|
let baseTip: string | undefined;
|
|
191
176
|
|
|
192
|
-
const pullHead: VcsArg = {
|
|
193
|
-
refspec: { from: pr.headRef, to: refs.head, force: true },
|
|
194
|
-
};
|
|
195
|
-
|
|
196
177
|
try {
|
|
197
|
-
await fetchRefs(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
force: true,
|
|
202
|
-
},
|
|
203
|
-
});
|
|
178
|
+
await fetchRefs(
|
|
179
|
+
`+${pr.headRef}:${refs.head}`,
|
|
180
|
+
`+refs/heads/${pr.baseRefName}:${refs.base}`,
|
|
181
|
+
);
|
|
204
182
|
baseTip = await commit(refs.base);
|
|
205
183
|
} catch (error) {
|
|
206
184
|
// The base branch may be gone; the head and the frozen base suffice.
|
|
207
185
|
try {
|
|
208
|
-
await fetchRefs(
|
|
186
|
+
await fetchRefs(`+${pr.headRef}:${refs.head}`);
|
|
209
187
|
} catch {
|
|
210
188
|
throw new ReviewInputError(
|
|
211
189
|
`Could not fetch PR #${pr.number} from remote "${input.remote}" (${pr.slug}). Check \`git fetch ${input.remote}\` works with your Git credentials, then retry. Git said: ${firstLine(errorMessage(error))}`,
|
|
@@ -254,7 +232,7 @@ export async function fetchPullRequest(
|
|
|
254
232
|
async function indexForJj(
|
|
255
233
|
rootPath: string,
|
|
256
234
|
commits: string[],
|
|
257
|
-
git: (args:
|
|
235
|
+
git: (args: string[]) => Promise<string>,
|
|
258
236
|
deps: PullRequestDeps,
|
|
259
237
|
) {
|
|
260
238
|
const tags = commits.map(() => `refs/tags/review-index-${randomUUID()}`);
|
|
@@ -268,7 +246,7 @@ async function indexForJj(
|
|
|
268
246
|
|
|
269
247
|
try {
|
|
270
248
|
for (const [index, tag] of tags.entries())
|
|
271
|
-
await git(["update-ref",
|
|
249
|
+
await git(["update-ref", tag, commits[index]!]);
|
|
272
250
|
await jjImport();
|
|
273
251
|
} finally {
|
|
274
252
|
for (const tag of tags)
|
|
@@ -277,17 +255,6 @@ async function indexForJj(
|
|
|
277
255
|
}
|
|
278
256
|
}
|
|
279
257
|
|
|
280
|
-
/** GitHub refused a read, as it does past a rate limit; it takes reads
|
|
281
|
-
* again at `retryAt`. */
|
|
282
|
-
export class GitHubRefusedRead extends ReviewInputError {
|
|
283
|
-
constructor(
|
|
284
|
-
message: string,
|
|
285
|
-
readonly retryAt: number,
|
|
286
|
-
) {
|
|
287
|
-
super(message, 409);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
258
|
export function firstLine(text: string) {
|
|
292
259
|
return text.split("\n")[0]!.slice(0, 300);
|
|
293
260
|
}
|
|
@@ -13,16 +13,6 @@ export const REVIEW_VIA_HEADER = "x-review-via";
|
|
|
13
13
|
|
|
14
14
|
export const REVIEW_AGENT_HEADER = "x-review-agent";
|
|
15
15
|
|
|
16
|
-
/** The Ask thread a request comes from. The CLI reads it from ASK_THREAD_ENV,
|
|
17
|
-
* which Desktop sets for the thread's agent. */
|
|
18
|
-
export const REVIEW_THREAD_HEADER = "x-review-thread";
|
|
19
|
-
|
|
20
|
-
export const ASK_THREAD_ENV = "DEV_REVIEW_ASK_THREAD";
|
|
21
|
-
|
|
22
|
-
/** Set by Desktop and the canvas on a command a person gave, such as a new
|
|
23
|
-
* board from the sidebar; its version is credited to them. */
|
|
24
|
-
export const REVIEW_PERSON_HEADER = "x-review-person";
|
|
25
|
-
|
|
26
16
|
export type ReviewRequestVia = "api" | "mcp" | "other";
|
|
27
17
|
|
|
28
18
|
const viaSchema = z.enum(["api", "mcp", "other"]).catch("other");
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type LensSource,
|
|
3
|
-
SelectionUnavailableError,
|
|
4
|
-
type SelectionUnavailableReason,
|
|
5
3
|
comparisonKey,
|
|
6
4
|
resolveDiffSelection,
|
|
7
5
|
selectionKey,
|
|
@@ -17,15 +15,8 @@ import {
|
|
|
17
15
|
UNCATEGORIZED_LENS_ID,
|
|
18
16
|
lensSelections,
|
|
19
17
|
} from "./diff-lenses.js";
|
|
20
|
-
import {
|
|
21
|
-
type Pins,
|
|
22
|
-
anchorPins,
|
|
23
|
-
boardRepositoryId,
|
|
24
|
-
selectionReferences,
|
|
25
|
-
} from "./document.js";
|
|
18
|
+
import { type Pins, anchorPins, selectionReferences } from "./document.js";
|
|
26
19
|
import { resolveFileLens, uncategorizedSources } from "./file-lenses.js";
|
|
27
|
-
import { ReviewInputError } from "./input-error.js";
|
|
28
|
-
import { textualRows } from "./lens-alignment.js";
|
|
29
20
|
import type { LocalReviewData } from "./local-data.js";
|
|
30
21
|
import type { ReviewStore, Snapshot } from "./store.js";
|
|
31
22
|
|
|
@@ -49,7 +40,7 @@ export const progressUpdateSchema = z.strictObject({
|
|
|
49
40
|
|
|
50
41
|
export interface ReviewProgress {
|
|
51
42
|
complete?: boolean;
|
|
52
|
-
unavailableSelections?: Record<string,
|
|
43
|
+
unavailableSelections?: Record<string, string>;
|
|
53
44
|
files: CoverageFile[];
|
|
54
45
|
/** Changed files of the comparisons that references with their own pins
|
|
55
46
|
* named, by `comparisonKey` and then path. They are not the document's
|
|
@@ -139,7 +130,7 @@ export async function reviewProgress(
|
|
|
139
130
|
const alignments = new Map(comparison.alignments);
|
|
140
131
|
const resolvedSelections: Record<string, FileLineRange[]> = {};
|
|
141
132
|
|
|
142
|
-
const unavailableSelections: Record<string,
|
|
133
|
+
const unavailableSelections: Record<string, string> = {};
|
|
143
134
|
const pendingSources = new Set<string>();
|
|
144
135
|
|
|
145
136
|
const resolve = async (source: LensSource): Promise<FileLineRange[]> => {
|
|
@@ -178,8 +169,7 @@ export async function reviewProgress(
|
|
|
178
169
|
let rows = file && ownAlignments.get(file.path);
|
|
179
170
|
|
|
180
171
|
// Context-only files are absent from the changed-file stream. Read both pins
|
|
181
|
-
// and require equality rather than inventing correspondence for missing
|
|
182
|
-
// diffs; a file on one side only, such as an untracked one, is that side.
|
|
172
|
+
// and require equality rather than inventing correspondence for missing diffs.
|
|
183
173
|
if (!file && counting && !own) {
|
|
184
174
|
pendingSources.add(key);
|
|
185
175
|
|
|
@@ -187,43 +177,20 @@ export async function reviewProgress(
|
|
|
187
177
|
}
|
|
188
178
|
|
|
189
179
|
if (!file) {
|
|
190
|
-
const [base, head] = await Promise.all(
|
|
191
|
-
(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
),
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
if (base === null && head === null)
|
|
204
|
-
throw new SelectionUnavailableError(
|
|
205
|
-
"missing_file",
|
|
206
|
-
"File is unavailable at these pins.",
|
|
207
|
-
);
|
|
208
|
-
|
|
209
|
-
if (base !== null && head !== null && base !== head)
|
|
210
|
-
throw new SelectionUnavailableError(
|
|
211
|
-
"no_alignment",
|
|
212
|
-
"Changed context file has no alignment.",
|
|
213
|
-
);
|
|
214
|
-
rows = textualRows(
|
|
215
|
-
source.file,
|
|
216
|
-
"",
|
|
217
|
-
base?.split("\n").length ?? 0,
|
|
218
|
-
head?.split("\n").length ?? 0,
|
|
180
|
+
const [base, head] = await Promise.all([
|
|
181
|
+
data.file(sourcePins, "base", source.file),
|
|
182
|
+
data.file(sourcePins, "head", source.file),
|
|
183
|
+
]);
|
|
184
|
+
|
|
185
|
+
if (base.text !== head.text)
|
|
186
|
+
throw new Error("Changed context file has no alignment.");
|
|
187
|
+
rows = Array.from(
|
|
188
|
+
{ length: base.text.split("\n").length },
|
|
189
|
+
(_, line) => [line, line] as const,
|
|
219
190
|
);
|
|
220
191
|
}
|
|
221
192
|
|
|
222
|
-
if (!rows)
|
|
223
|
-
throw new SelectionUnavailableError(
|
|
224
|
-
"no_alignment",
|
|
225
|
-
"File has no text alignment.",
|
|
226
|
-
);
|
|
193
|
+
if (!rows) throw new Error("File has no text alignment.");
|
|
227
194
|
|
|
228
195
|
const result = resolveDiffSelection(
|
|
229
196
|
source,
|
|
@@ -239,9 +206,9 @@ export async function reviewProgress(
|
|
|
239
206
|
const resolveAvailable = async (source: LensSource) => {
|
|
240
207
|
try {
|
|
241
208
|
return await resolve(source);
|
|
242
|
-
} catch
|
|
209
|
+
} catch {
|
|
243
210
|
unavailableSelections[selectionKey(source)] =
|
|
244
|
-
|
|
211
|
+
"Source links need updating at these pins";
|
|
245
212
|
|
|
246
213
|
return [];
|
|
247
214
|
}
|
|
@@ -287,16 +254,15 @@ export async function reviewProgress(
|
|
|
287
254
|
|
|
288
255
|
// Peeks share alignment resolution without claiming lens coverage.
|
|
289
256
|
await Promise.all(
|
|
290
|
-
selectionReferences(snapshot.document, {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}),
|
|
257
|
+
selectionReferences(snapshot.document, { tolerant: true }).map(
|
|
258
|
+
async ({ source }) => {
|
|
259
|
+
try {
|
|
260
|
+
await resolveAvailable(source);
|
|
261
|
+
} catch {
|
|
262
|
+
/* An unavailable peek does not hide other content. */
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
),
|
|
300
266
|
);
|
|
301
267
|
|
|
302
268
|
const uncategorized = counting
|
|
@@ -333,7 +299,7 @@ export async function reviewProgress(
|
|
|
333
299
|
});
|
|
334
300
|
|
|
335
301
|
const progress: ReviewProgress = {
|
|
336
|
-
complete: !counting
|
|
302
|
+
complete: !counting,
|
|
337
303
|
files,
|
|
338
304
|
referenceFiles,
|
|
339
305
|
lenses,
|
|
@@ -1,55 +1,46 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
2
|
|
|
3
|
-
/** A board version's repository in SQL over `versions.snapshot`, as
|
|
4
|
-
* `boardRepositoryId` reads it: its target's, else its pins'. */
|
|
5
|
-
export const BOARD_REPOSITORY_ID_SQL =
|
|
6
|
-
"CASE WHEN json_valid(versions.snapshot) THEN COALESCE(json_extract(versions.snapshot,'$.target.repositoryId'),json_extract(versions.snapshot,'$.pins.repositoryId')) END";
|
|
7
|
-
|
|
8
|
-
export const columnsOf = (db: DatabaseSync, table: string) =>
|
|
9
|
-
new Set(
|
|
10
|
-
db
|
|
11
|
-
.prepare("SELECT name FROM pragma_table_info(?)")
|
|
12
|
-
.all(table)
|
|
13
|
-
.map(({ name }) => String(name)),
|
|
14
|
-
);
|
|
15
|
-
|
|
16
|
-
export const hasTable = (db: DatabaseSync, table: string) =>
|
|
17
|
-
Boolean(
|
|
18
|
-
db
|
|
19
|
-
.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?")
|
|
20
|
-
.get(table),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
3
|
export function initializeReviewStoreSchema(db: DatabaseSync): void {
|
|
24
4
|
db.exec(`
|
|
25
5
|
PRAGMA journal_mode=WAL;
|
|
26
6
|
PRAGMA foreign_keys=ON;
|
|
27
|
-
CREATE TABLE IF NOT EXISTS reviews(id TEXT PRIMARY KEY, version INTEGER NOT NULL, next_id INTEGER NOT NULL
|
|
28
|
-
project_id TEXT NOT NULL REFERENCES projects(id), parent_id TEXT REFERENCES reviews(id) ON DELETE SET NULL);
|
|
7
|
+
CREATE TABLE IF NOT EXISTS reviews(id TEXT PRIMARY KEY, version INTEGER NOT NULL, next_id INTEGER NOT NULL);
|
|
29
8
|
CREATE TABLE IF NOT EXISTS versions(review_id TEXT REFERENCES reviews(id), version INTEGER, snapshot TEXT NOT NULL,
|
|
30
|
-
thread_id TEXT, author_kind TEXT,
|
|
31
9
|
PRIMARY KEY(review_id,version));
|
|
10
|
+
DROP TABLE IF EXISTS receipts;
|
|
32
11
|
CREATE TABLE IF NOT EXISTS review_attention(review_id TEXT PRIMARY KEY REFERENCES reviews(id), viewed_at TEXT, deleted_at TEXT);
|
|
33
|
-
CREATE TABLE IF NOT EXISTS
|
|
34
|
-
CREATE TABLE IF NOT EXISTS repositories(id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, name TEXT NOT NULL,
|
|
35
|
-
project_id TEXT REFERENCES projects(id));
|
|
12
|
+
CREATE TABLE IF NOT EXISTS repositories(id TEXT PRIMARY KEY, path TEXT NOT NULL UNIQUE, name TEXT NOT NULL);
|
|
36
13
|
CREATE TABLE IF NOT EXISTS resources(id TEXT PRIMARY KEY, repository_id TEXT NOT NULL REFERENCES repositories(id),
|
|
37
14
|
kind TEXT NOT NULL, mime_type TEXT NOT NULL, data BLOB NOT NULL);
|
|
38
15
|
CREATE TABLE IF NOT EXISTS review_coverage(review_id TEXT REFERENCES reviews(id), file TEXT, fingerprint TEXT NOT NULL, coverage TEXT NOT NULL,
|
|
39
16
|
PRIMARY KEY(review_id,file));
|
|
17
|
+
DROP TABLE IF EXISTS review_viewed;
|
|
40
18
|
CREATE TABLE IF NOT EXISTS comparison_stats(identity TEXT PRIMARY KEY, stats TEXT NOT NULL);
|
|
41
19
|
CREATE TABLE IF NOT EXISTS server_identity(one INTEGER PRIMARY KEY CHECK(one=1), id TEXT NOT NULL);
|
|
42
|
-
|
|
43
|
-
depends_on TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, PRIMARY KEY(review_id,depends_on));
|
|
44
|
-
CREATE INDEX IF NOT EXISTS board_dependencies_depends_on ON board_dependencies(depends_on);
|
|
45
|
-
-- A link goes with the board it is on. Where it points stays plain
|
|
46
|
-
-- text, no foreign key: a link to a deleted board stays, and reads
|
|
47
|
-
-- "Board deleted" from the deleted_boards tombstone.
|
|
48
|
-
CREATE TABLE IF NOT EXISTS board_links(from_review TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, from_block TEXT NOT NULL,
|
|
49
|
-
to_review TEXT NOT NULL, to_block TEXT);
|
|
50
|
-
CREATE INDEX IF NOT EXISTS board_links_from ON board_links(from_review);
|
|
51
|
-
CREATE INDEX IF NOT EXISTS board_links_to ON board_links(to_review);
|
|
52
|
-
CREATE TABLE IF NOT EXISTS deleted_boards(id TEXT PRIMARY KEY, title TEXT NOT NULL, deleted_at TEXT NOT NULL,
|
|
53
|
-
project_id TEXT);
|
|
20
|
+
DROP TABLE IF EXISTS authoring_drafts;
|
|
54
21
|
`);
|
|
22
|
+
|
|
23
|
+
db.exec("BEGIN IMMEDIATE");
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
// Migrate the former Archive timestamp without losing recoverable boards.
|
|
27
|
+
const attentionColumns = db
|
|
28
|
+
.prepare("PRAGMA table_info(review_attention)")
|
|
29
|
+
.all();
|
|
30
|
+
|
|
31
|
+
if (attentionColumns.some((column) => column.name === "dismissed_at")) {
|
|
32
|
+
db.exec(
|
|
33
|
+
"ALTER TABLE review_attention RENAME COLUMN dismissed_at TO deleted_at",
|
|
34
|
+
);
|
|
35
|
+
// Archive had no retention limit. Give migrated boards the full recovery window.
|
|
36
|
+
db.prepare(
|
|
37
|
+
"UPDATE review_attention SET deleted_at=? WHERE deleted_at IS NOT NULL",
|
|
38
|
+
).run(new Date().toISOString());
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
db.exec("COMMIT");
|
|
42
|
+
} catch (error) {
|
|
43
|
+
db.exec("ROLLBACK");
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
55
46
|
}
|