@dev.fast/whiteboard 0.3.4-preview.20261019.126 → 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.
Files changed (115) hide show
  1. package/dist/{account-alias-B5-HYi1D.js → account-alias-CprZojXd.js} +8 -13
  2. package/dist/agent-cli-C6SPSII0.js +2 -0
  3. package/dist/{agent-cli-CohHf6Ua.js → agent-cli-CThN7Q59.js} +10 -15
  4. package/dist/{agent-client-Dtc-Kn_w.js → agent-client-BOOi7EUR.js} +3 -13
  5. package/dist/{authoring-tools-B7KU04f7.js → authoring-tools-BNwuMnz6.js} +27 -34
  6. package/dist/{background-server-C03RKxKS.js → background-server-CkvxA-6O.js} +5 -5
  7. package/dist/{background-server-CB9C4uoc.js → background-server-DLSDogkI.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-CJaEb7zC.js → cli-R4yfa5DT.js} +1 -1
  10. package/dist/{cli-runner-DGDIqh3V.js → cli-runner-Cuc9FSzu.js} +26 -26
  11. package/dist/cli.js +3 -4
  12. package/dist/{client-lKVF_Elo.js → client-BN_FDxFr.js} +47 -253
  13. package/dist/{fuzzy-match-BmVJJm9I.js → fuzzy-match-BO9bK37N.js} +4 -6
  14. package/dist/{headless-host-DRtjf9WA.js → headless-host-CVBOQyRS.js} +1 -1
  15. package/dist/{headless-host-XhL1X4vX.js → headless-host-Cog6SpS9.js} +140 -513
  16. package/dist/{local-data-Cpl4ENbJ.js → local-data-D_9tvdsp.js} +323 -2419
  17. package/dist/{mcp-ovmiUE_x.js → mcp-D1hziANa.js} +9 -5
  18. package/dist/{package-paths-BWxO-UEL.js → package-paths-C--__jEv.js} +1 -1
  19. package/dist/profile-CYPmvBoA.js +2 -0
  20. package/dist/{profile-BadSkCkI.js → profile-DokFw782.js} +8 -20
  21. package/dist/{remote-attach-Dd-YobM6.js → remote-attach-CEJ-kvDY.js} +4 -4
  22. package/dist/{remote-extensions-CNXwqK0D.js → remote-extensions-Blcjr9Ze.js} +3 -3
  23. package/dist/{remote-extensions-VP-kfq0C.js → remote-extensions-GN8Gf_qG.js} +1 -1
  24. package/dist/{remote-language-server-BXk87KCr.js → remote-language-server-BTKBQ7x4.js} +5 -5
  25. package/dist/{remote-uninstall-DuOzsvaD.js → remote-uninstall-DW7Gdryh.js} +6 -6
  26. package/dist/{request-origin-B9qCPiVB.js → request-origin-Zcn_CjX4.js} +12 -45
  27. package/dist/{review-app-launcher-C7el2Uoy.js → review-app-launcher-CkwLclzR.js} +2 -2
  28. package/dist/{review-home-paths-D8AoCdd2.js → review-home-paths-j2BRAaNv.js} +17 -19
  29. package/dist/{review-telemetry-BsoRMC2w.js → review-telemetry-BLNwYnLV.js} +5 -5
  30. package/dist/runtime-CKiWhGrf.js +52 -0
  31. package/dist/runtime.d.ts +1 -1
  32. package/dist/runtime.js +1 -1
  33. package/dist/{s3-CsG2EWED.js → s3-DtJFWC0G.js} +1 -1
  34. package/dist/{s3-config-CjcLyFV5.js → s3-config-BTO12tCC.js} +2 -4
  35. package/dist/{s3-config-zz4GWh2T.js → s3-config-CSy3fx8H.js} +1 -1
  36. package/dist/server/desktop-host.js +76 -29
  37. package/dist/{server-discovery-CKjw3-6h.js → server-discovery-Bey8IqXH.js} +3 -3
  38. package/dist/sharing/index.d.ts +70 -484
  39. package/dist/sharing/index.js +1 -1
  40. package/dist/{src-WPaQn75m.js → src-BMPuxWv4.js} +73 -208
  41. package/dist/{src-BiWNbcF_.js → src-DsM3jO8G.js} +12 -89
  42. package/dist/{stored-document-migration-dFbewRTG.js → stored-document-migration-CChxtFAy.js} +269 -270
  43. package/dist/{thread-state-Do5LX10I.js → thread-state-5pYr6_zM.js} +2 -7
  44. package/dist/{tool-failure-DvI-gys2.js → tool-failure-C9MpbXJJ.js} +3 -10
  45. package/instructions/authoring.md +2 -3
  46. package/instructions/scratchpad.md +23 -0
  47. package/package.json +4 -4
  48. package/src/ask/agents.ts +1 -13
  49. package/src/ask/checkout-files.ts +5 -2
  50. package/src/ask/thread-state.ts +0 -4
  51. package/src/ask/thread.ts +0 -16
  52. package/src/ask/threads.ts +3 -24
  53. package/src/cli-install.ts +2 -13
  54. package/src/cli-runner.ts +1 -4
  55. package/src/cli.ts +1 -4
  56. package/src/diff-selection-migration.ts +34 -48
  57. package/src/lens-selection.ts +2 -38
  58. package/src/review-api/README.md +13 -52
  59. package/src/review-api/activity.ts +4 -1
  60. package/src/review-api/agent-cli.ts +1 -9
  61. package/src/review-api/anchor-quotes.ts +7 -14
  62. package/src/review-api/ask-history.ts +51 -137
  63. package/src/review-api/authoring-tools.ts +30 -45
  64. package/src/review-api/blocks/definition.ts +1 -1
  65. package/src/review-api/blocks/markdown.ts +2 -2
  66. package/src/review-api/document-headings.ts +27 -58
  67. package/src/review-api/document-text.ts +2 -31
  68. package/src/review-api/document.ts +34 -281
  69. package/src/review-api/github-provider.ts +13 -51
  70. package/src/review-api/http.ts +123 -702
  71. package/src/review-api/instructions.ts +19 -0
  72. package/src/review-api/local-data.ts +46 -310
  73. package/src/review-api/mcp.ts +11 -3
  74. package/src/review-api/onboarding.ts +1 -1
  75. package/src/review-api/profile.ts +6 -37
  76. package/src/review-api/public-tools.ts +2 -24
  77. package/src/review-api/pull-request.ts +49 -82
  78. package/src/review-api/request-origin.ts +0 -10
  79. package/src/review-api/review-progress.ts +27 -61
  80. package/src/review-api/store-schema.ts +29 -38
  81. package/src/review-api/store.ts +478 -2142
  82. package/src/review-api/worktree-events.ts +5 -3
  83. package/src/review-api/worktree-source.ts +7 -3
  84. package/src/review-api/worktree-structural.integration.ts +0 -72
  85. package/src/review-preferences.ts +88 -0
  86. package/src/review-stack.ts +23 -15
  87. package/src/runtime.ts +30 -4
  88. package/src/server/desktop-host.ts +1 -6
  89. package/src/server/desktop-server.ts +31 -1
  90. package/src/server/headless-host.ts +2 -0
  91. package/src/server/hono-http.ts +1 -2
  92. package/src/server/legacy-tutorial-cleanup.ts +3 -2
  93. package/src/server/review-gateway-list.ts +5 -3
  94. package/src/server/review-gateway-memory.ts +1 -1
  95. package/src/server/review-lifecycle-telemetry.ts +2 -2
  96. package/src/server/review-server-core.ts +5 -1
  97. package/src/server/structural-comparisons.ts +0 -47
  98. package/src/sharing/export.ts +7 -36
  99. package/src/sharing/host.ts +14 -34
  100. package/src/sharing/import.ts +24 -156
  101. package/src/sharing/repository.ts +20 -128
  102. package/src/startup-trace.ts +3 -3
  103. package/src/stored-document-migration.ts +19 -110
  104. package/src/ui-telemetry-events.ts +11 -33
  105. package/dist/agent-cli-_YyMIMRY.js +0 -2
  106. package/dist/lens-selection-CyJapcsl.js +0 -214
  107. package/dist/profile-DJvkBJNQ.js +0 -2
  108. package/dist/runtime-Bj2tD33R.js +0 -27
  109. package/src/heading-anchors.ts +0 -42
  110. package/src/review-api/board-order.ts +0 -144
  111. package/src/review-api/checkout-groups.ts +0 -560
  112. package/src/review-api/foreign-references.ts +0 -328
  113. package/src/review-api/link-preview.ts +0 -113
  114. package/src/review-api/scratchpad-plan-upgrade.ts +0 -250
  115. 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; pullRequestStates?: 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
- const database = path.join(home, "review-api.db");
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(id,path,name) SELECT s.id,s.path,s.name FROM headless.repositories s WHERE NOT EXISTS (SELECT 1 FROM repositories t WHERE t.path=s.path);
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 versions(review_id,version,snapshot,thread_id,author_kind) SELECT s.review_id,s.version,
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
- import { promisify } from "node:util";
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: "git" | "jj" | "gh" | "az" | "cmd.exe",
21
- args: VcsArg[],
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: async (file, args, options) => {
54
- const spawnOptions = {
55
- cwd: options.cwd,
56
- timeout: options.timeoutMs,
57
- maxBuffer: 16 * 1024 * 1024,
58
- // Never wait on ssh asking for a passphrase nobody can see.
59
- env: {
60
- ...process.env,
61
- GIT_SSH_COMMAND: process.env.GIT_SSH_COMMAND ?? "ssh -o BatchMode=yes",
62
- },
63
- };
64
-
65
- try {
66
- return (
67
- file === "az" || file === "cmd.exe"
68
- ? await execFileAsync(file, renderVcsArgs(args), {
69
- ...spawnOptions,
70
- encoding: "utf8",
71
- })
72
- : await runVcs(file, args, spawnOptions)
73
- ).stdout;
74
- } catch (error) {
75
- // SAFETY: execFile rejects with an ExecFileException carrying stderr.
76
- const failure = error as { killed?: boolean; stderr?: string };
77
-
78
- const reason = failure.killed
79
- ? `timed out after ${options.timeoutMs / 1000}s`
80
- : String(failure.stderr ?? "").trim() || errorMessage(error);
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: VcsArg[], timeoutMs = LOCAL_TIMEOUT_MS) =>
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: VcsArg[]) =>
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
- { remote: input.remote },
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", { rev: `${ref}^{commit}` }]).catch(
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", { rev: left }, { rev: right }]).catch(() => undefined);
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
- refspec: { from: pr.baseRefOid, to: refs.frozenBase, force: true },
185
- }).catch(() => undefined);
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(pullHead, {
198
- refspec: {
199
- from: `refs/heads/${pr.baseRefName}`,
200
- to: refs.base,
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(pullHead);
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: VcsArg[]) => Promise<string>,
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", { ref: tag }, { commit: commits[index]! }]);
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, SelectionUnavailableReason>;
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, SelectionUnavailableReason> = {};
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
- (["base", "head"] as const).map((side) =>
192
- data.file(sourcePins, side, source.file).then(
193
- (value) => value.text,
194
- (error) => {
195
- if (error instanceof ReviewInputError && error.status === 404)
196
- return null;
197
- throw error;
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 (error) {
209
+ } catch {
243
210
  unavailableSelections[selectionKey(source)] =
244
- error instanceof SelectionUnavailableError ? error.reason : "other";
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
- tolerant: true,
292
- repositoryId: boardRepositoryId(snapshot),
293
- }).map(async ({ source }) => {
294
- try {
295
- await resolveAvailable(source);
296
- } catch {
297
- /* An unavailable peek does not hide other content. */
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 && !pendingSources.size,
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 projects(id TEXT PRIMARY KEY, name TEXT NOT NULL);
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
- CREATE TABLE IF NOT EXISTS board_dependencies(review_id TEXT NOT NULL REFERENCES reviews(id) ON DELETE CASCADE,
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
  }