@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.
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
@@ -1,328 +0,0 @@
1
- import path from "node:path";
2
-
3
- import { type JsonValue, isJsonObject, isStringValue } from "@dev.fast/json";
4
- import { parseAnchor } from "@review/lens-selection.js";
5
- import {
6
- markdownNodes,
7
- markdownText,
8
- parseMarkdown,
9
- } from "@review/markdown.js";
10
-
11
- /** A file's text at a commit; undefined when it can't be read. */
12
- export type ReadAtCommit = (
13
- repositoryId: string,
14
- commit: string,
15
- file: string,
16
- ) => string | undefined;
17
-
18
- const LANGUAGES = new Map([
19
- [".ts", "typescript"],
20
- [".tsx", "tsx"],
21
- [".js", "javascript"],
22
- [".jsx", "jsx"],
23
- [".mjs", "javascript"],
24
- [".rs", "rust"],
25
- [".go", "go"],
26
- [".py", "python"],
27
- [".rb", "ruby"],
28
- [".java", "java"],
29
- [".kt", "kotlin"],
30
- [".swift", "swift"],
31
- [".c", "c"],
32
- [".h", "c"],
33
- [".cc", "cpp"],
34
- [".cpp", "cpp"],
35
- [".css", "css"],
36
- [".html", "html"],
37
- [".json", "json"],
38
- [".md", "markdown"],
39
- [".sh", "bash"],
40
- [".sql", "sql"],
41
- [".toml", "toml"],
42
- [".yaml", "yaml"],
43
- [".yml", "yaml"],
44
- ]);
45
-
46
- /** A source link in Markdown, in either spelling. */
47
- const SOURCE_LINK = /^(?:whiteboard:source\/|review-source:)/i;
48
-
49
- /**
50
- * Migrates the old global scratchpad, which quoted several repositories.
51
- * A board may only quote its own, so each quote of another repository is
52
- * replaced:
53
- * - a code peek becomes a `code` block of those lines;
54
- * - a source link becomes its text, then what it pointed at in code;
55
- * - a diagram attachment leaves the diagram and its lines follow it as
56
- * a `code` block;
57
- * - a step keeps its label and takes the lines as its code;
58
- * - a call stack or database lens becomes a line saying what it was,
59
- * then a `code` block for each range it quoted, in any repository; the
60
- * first takes its id. A lens that quotes only its own repository is
61
- * kept.
62
- * New blocks take ids from `allocate`.
63
- */
64
- export function snapshotForeignReferences(
65
- document: JsonValue[],
66
- own: string,
67
- read: ReadAtCommit,
68
- repositoryName: (repositoryId: string) => string,
69
- allocate: (prefix: string) => string,
70
- ): JsonValue[] {
71
- const foreign = (pins: JsonValue | undefined) =>
72
- isJsonObject(pins) &&
73
- isStringValue(pins.repositoryId) &&
74
- pins.repositoryId !== own
75
- ? pins
76
- : undefined;
77
-
78
- const caption = (
79
- pins: { repositoryId: string; head: string; base?: string },
80
- anchor: string,
81
- ) => {
82
- const selection = parseAnchor(anchor);
83
-
84
- const commit =
85
- selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
86
-
87
- const range = selection
88
- ? `${selection.file}:${selection.start.line}-${selection.end.line}`
89
- : anchor;
90
-
91
- return `${repositoryName(pins.repositoryId)} · ${range} @ ${commit.slice(0, 7)}`;
92
- };
93
-
94
- const snapshot = (
95
- id: JsonValue | undefined,
96
- pins: { repositoryId: string; head: string; base?: string },
97
- anchor: string,
98
- ): JsonValue => {
99
- const selection = parseAnchor(anchor);
100
-
101
- const commit =
102
- selection?.start.side === "base" ? (pins.base ?? pins.head) : pins.head;
103
-
104
- const file = selection
105
- ? read(pins.repositoryId, commit, selection.file)
106
- : undefined;
107
-
108
- const label = caption(pins, anchor);
109
-
110
- const text =
111
- file !== undefined && selection
112
- ? file
113
- .split("\n")
114
- .slice(selection.start.line - 1, selection.end.line)
115
- .join("\n")
116
- : `${label}\n(source unavailable)`;
117
-
118
- return {
119
- type: "code",
120
- ...(isStringValue(id) && { id }),
121
- language: selection
122
- ? (LANGUAGES.get(path.extname(selection.file).toLowerCase()) ?? "text")
123
- : "text",
124
- text,
125
- caption: label,
126
- };
127
- };
128
-
129
- const pinsIn = (pins: JsonValue | undefined) => {
130
- return isJsonObject(pins) &&
131
- isStringValue(pins.repositoryId) &&
132
- isStringValue(pins.head)
133
- ? {
134
- repositoryId: pins.repositoryId,
135
- head: pins.head,
136
- ...(isStringValue(pins.base) && { base: pins.base }),
137
- }
138
- : undefined;
139
- };
140
-
141
- const pinsOf = (value: JsonValue | undefined) => pinsIn(foreign(value));
142
-
143
- const peeks = (blocks: JsonValue[]): JsonValue[] =>
144
- blocks.map((block) => {
145
- if (!isJsonObject(block)) return block;
146
-
147
- if (Array.isArray(block.children))
148
- return { ...block, children: peeks(block.children) };
149
-
150
- const pins = pinsOf(block.pins);
151
-
152
- if (block.type === "code_peek" && pins && isStringValue(block.source))
153
- return snapshot(block.id, pins, block.source);
154
-
155
- return block;
156
- });
157
-
158
- const rest = (blocks: JsonValue[]): JsonValue[] =>
159
- blocks.flatMap((block): JsonValue | JsonValue[] => {
160
- if (!isJsonObject(block)) return block;
161
-
162
- if (Array.isArray(block.children))
163
- return { ...block, children: rest(block.children) };
164
-
165
- const blockPins = pinsOf(block.pins);
166
-
167
- if (
168
- block.type === "markdown" &&
169
- blockPins &&
170
- isStringValue(block.markdown)
171
- ) {
172
- const { pins: _pins, ...kept } = block;
173
-
174
- return {
175
- ...kept,
176
- markdown: unlinkSourceLinks(block.markdown, blockPins),
177
- };
178
- }
179
-
180
- if (block.type === "sequence" && Array.isArray(block.steps))
181
- return {
182
- ...block,
183
- steps: block.steps.map((step) => {
184
- if (!isJsonObject(step) || !isStringValue(step.source)) return step;
185
- const pins = pinsOf(step.pins ?? block.pins);
186
-
187
- if (!pins) return step;
188
- const { source, pins: _pins, ...kept } = step;
189
- const quoted = snapshot(undefined, pins, source);
190
-
191
- return isJsonObject(quoted)
192
- ? {
193
- ...kept,
194
- code: { language: quoted.language, text: quoted.text },
195
- }
196
- : kept;
197
- }),
198
- };
199
-
200
- if (block.type === "flow_diagram" && Array.isArray(block.nodes)) {
201
- const moved: JsonValue[] = [];
202
-
203
- const diagram = {
204
- ...block,
205
- nodes: block.nodes.map((node) =>
206
- isJsonObject(node) && Array.isArray(node.attachments)
207
- ? {
208
- ...node,
209
- attachments: node.attachments.filter((attachment) => {
210
- if (
211
- !isJsonObject(attachment) ||
212
- !pinsOf(attachment.pins ?? block.pins)
213
- )
214
- return true;
215
-
216
- // Its lines stay on the board, after the diagram.
217
- for (const [pins, anchor] of quotes(
218
- attachment,
219
- pinsIn,
220
- block.pins,
221
- ))
222
- moved.push(snapshot(allocate("block"), pins, anchor));
223
-
224
- return false;
225
- }),
226
- }
227
- : node,
228
- ),
229
- };
230
-
231
- return [diagram, ...moved];
232
- }
233
-
234
- if (block.type === "call_stack_diff" || block.type === "database_lens") {
235
- const foreignQuote = quotes(block, pinsOf)[0];
236
-
237
- if (!foreignQuote) return block;
238
-
239
- const what =
240
- block.type === "call_stack_diff" ? "Call stack" : "Database lens";
241
-
242
- const title = isStringValue(block.title) ? ` “${block.title}”` : "";
243
-
244
- // Its own repository's ranges too, so none of it is lost.
245
- return [
246
- {
247
- type: "markdown",
248
- id: allocate("block"),
249
- markdown: `${what}${title} (from ${repositoryName(foreignQuote[0].repositoryId)}), kept as snapshots below.`,
250
- },
251
- ...quotes(block, pinsIn).map(([pins, anchor], index) =>
252
- snapshot(index === 0 ? block.id : allocate("block"), pins, anchor),
253
- ),
254
- ];
255
- }
256
-
257
- return block;
258
- });
259
-
260
- /** The ranges a block's frames or operations quote at the pins `read`
261
- * accepts. */
262
- function quotes(
263
- block: JsonValue,
264
- read: typeof pinsIn,
265
- inherited?: JsonValue,
266
- ): [{ repositoryId: string; head: string; base?: string }, string][] {
267
- if (Array.isArray(block))
268
- return block.flatMap((item) => quotes(item, read, inherited));
269
-
270
- if (!isJsonObject(block)) return [];
271
- const scope = block.pins ?? inherited;
272
- const pins = read(scope);
273
-
274
- const own: [
275
- { repositoryId: string; head: string; base?: string },
276
- string,
277
- ][] = pins
278
- ? [
279
- block.source,
280
- block.callSite,
281
- ...(Array.isArray(block.contextSources) ? block.contextSources : []),
282
- ...(Array.isArray(block.sources) ? block.sources : []),
283
- ].flatMap((anchor): [typeof pins, string][] =>
284
- isStringValue(anchor) ? [[pins, anchor]] : [],
285
- )
286
- : [];
287
-
288
- return [
289
- ...own,
290
- ...Object.entries(block)
291
- .filter(([key]) => key !== "pins")
292
- .flatMap(([, value]) =>
293
- Array.isArray(value) || isJsonObject(value)
294
- ? quotes(value, read, scope)
295
- : [],
296
- ),
297
- ];
298
- }
299
-
300
- function unlinkSourceLinks(
301
- markdown: string,
302
- pins: { repositoryId: string; head: string; base?: string },
303
- ) {
304
- let result = markdown;
305
-
306
- // Rewrite from the end so earlier offsets stay valid.
307
- const links = [...markdownNodes(parseMarkdown(markdown))]
308
- .filter(
309
- (node) =>
310
- node.type === "link" &&
311
- SOURCE_LINK.test(node.url ?? "") &&
312
- node.position?.start.offset !== undefined &&
313
- node.position.end.offset !== undefined,
314
- )
315
- .reverse();
316
-
317
- // Its text, then where it pointed, so neither is lost.
318
- for (const node of links)
319
- result =
320
- result.slice(0, node.position!.start.offset) +
321
- `${markdownText(node)} (\`${caption(pins, node.url!.replace(SOURCE_LINK, ""))}\`)` +
322
- result.slice(node.position!.end.offset);
323
-
324
- return result;
325
- }
326
-
327
- return rest(peeks(document));
328
- }
@@ -1,113 +0,0 @@
1
- import type { Block } from "./blocks/index.js";
2
-
3
- /** What a board link's hover card shows of the board or block it links to. */
4
- export interface BoardLinkPreview {
5
- /** A block link: the title of the section the block is in. */
6
- section?: string;
7
- /** Markdown paragraphs, opening the board or block. */
8
- prose?: string[];
9
- /** A block link to code: its language and first lines. */
10
- code?: { language: string; lines: string[] };
11
- /** A block link to a diagram or other figure: its kind and title. */
12
- figure?: { type: string; title: string };
13
- }
14
-
15
- /** About what the card shows before it fades; more is cut, not sent. */
16
- const PROSE_CHARACTERS = 700;
17
-
18
- const CODE_LINES = 6;
19
-
20
- function childBlocks(block: Block): Block[] {
21
- return "children" in block ? block.children : [];
22
- }
23
-
24
- /** Paragraphs of markdown, without headings. */
25
- function paragraphs(markdown: string): string[] {
26
- return markdown
27
- .split(/\n\s*\n/)
28
- .map((paragraph) => paragraph.trim())
29
- .filter((paragraph) => paragraph && !paragraph.startsWith("#"));
30
- }
31
-
32
- /** The prose from the first markdown block on, in reading order, until the
33
- * card has enough. Diagrams and code before it are skipped. */
34
- function openingProse(blocks: Block[]): string[] {
35
- const prose: string[] = [];
36
- let length = 0;
37
-
38
- const walk = (list: Block[]): boolean => {
39
- for (const block of list) {
40
- if (block.type === "markdown")
41
- for (const paragraph of paragraphs(block.markdown)) {
42
- prose.push(paragraph);
43
- length += paragraph.length;
44
-
45
- if (length >= PROSE_CHARACTERS) return true;
46
- }
47
- else if (walk(childBlocks(block))) return true;
48
- }
49
-
50
- return false;
51
- };
52
-
53
- walk(blocks);
54
-
55
- return prose;
56
- }
57
-
58
- /** The block and the title of the section it is in. */
59
- function find(
60
- blocks: Block[],
61
- id: string,
62
- section?: string,
63
- ): { block: Block; section?: string } | undefined {
64
- for (const block of blocks) {
65
- if (block.id === id) return { block, section };
66
-
67
- const found = find(
68
- childBlocks(block),
69
- id,
70
- block.type === "section" ? block.title : section,
71
- );
72
-
73
- if (found) return found;
74
- }
75
-
76
- return undefined;
77
- }
78
-
79
- /** The opening prose of a board, or one block of it. */
80
- export function boardLinkPreview(
81
- document: Block[],
82
- blockId?: string,
83
- ): BoardLinkPreview {
84
- if (blockId === undefined) return { prose: openingProse(document) };
85
-
86
- const found = find(document, blockId);
87
-
88
- if (!found) return {};
89
-
90
- const { block } = found;
91
- // A section link is about the section itself.
92
- const section = block.type === "section" ? block.title : found.section;
93
-
94
- if (block.type === "code")
95
- return {
96
- ...(section && { section }),
97
- code: {
98
- language: block.language,
99
- lines: block.text.split("\n").slice(0, CODE_LINES),
100
- },
101
- };
102
-
103
- const prose = openingProse([block]);
104
-
105
- if (prose.length) return { ...(section && { section }), prose };
106
-
107
- const title = "title" in block ? block.title : undefined;
108
-
109
- return {
110
- ...(section && { section }),
111
- figure: { type: block.type, title: title ?? "" },
112
- };
113
- }
@@ -1,250 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
-
4
- import type { JsonValue } from "@dev.fast/json";
5
- import { migrateStoredDocument } from "@review/stored-document-migration.js";
6
-
7
- import { type Block, sourceReferences } from "./document.js";
8
- import { snapshotForeignReferences } from "./foreign-references.js";
9
- import type { StoreUpgradeContext } from "./store-migrations.js";
10
- import { BOARD_REPOSITORY_ID_SQL, columnsOf } from "./store-schema.js";
11
-
12
- // The old global scratchpad becomes a plan titled "Scratchpad", history
13
- // kept, in the project of the repository its blocks quote most, else of the
14
- // repository opened last. A board quotes only its own repository, so its
15
- // quotes of others become snapshots.
16
-
17
- const OLD_SCRATCHPAD_ID = "scratchpad";
18
-
19
- const hasScratchpadVersions = ({ db }: StoreUpgradeContext) =>
20
- Boolean(
21
- db
22
- .prepare("SELECT 1 FROM versions WHERE review_id=? LIMIT 1")
23
- .get(OLD_SCRATCHPAD_ID),
24
- );
25
-
26
- type PadSnapshot = { document: JsonValue[]; [key: string]: JsonValue };
27
-
28
- /** A pad version's snapshot with its document in today's form (old pads
29
- * held anchors as objects); undefined for one that won't parse. */
30
- function parsePadVersion(text: string): PadSnapshot | undefined {
31
- try {
32
- // SAFETY: versions holds snapshots validated when they were saved.
33
- const snapshot = JSON.parse(text) as PadSnapshot;
34
- const document = migrateStoredDocument(snapshot.document);
35
-
36
- snapshot.document = Array.isArray(document) ? document : [];
37
-
38
- return snapshot;
39
- } catch {
40
- return undefined;
41
- }
42
- }
43
-
44
- /** Each pad version as parsed, by version, for one open. The run, inside
45
- * the write lock, reads the rows again: it reuses a parse only of text that
46
- * hasn't changed since. */
47
- const parsedPads = new WeakMap<
48
- StoreUpgradeContext,
49
- Map<number, { text: string; snapshot: PadSnapshot | undefined }>
50
- >();
51
-
52
- function padVersion(
53
- context: StoreUpgradeContext,
54
- version: number,
55
- text: string,
56
- ) {
57
- let parsed = parsedPads.get(context);
58
-
59
- if (!parsed) parsedPads.set(context, (parsed = new Map()));
60
- const known = parsed.get(version);
61
-
62
- if (known?.text === text) return known.snapshot;
63
-
64
- const snapshot = parsePadVersion(text);
65
-
66
- parsed.set(version, { text, snapshot });
67
-
68
- return snapshot;
69
- }
70
-
71
- /** The pad's versions, oldest first, each parsed once per open. */
72
- const scratchpadVersions = (context: StoreUpgradeContext) =>
73
- context.db
74
- .prepare(
75
- "SELECT version,snapshot FROM versions WHERE review_id=? ORDER BY version",
76
- )
77
- .all(OLD_SCRATCHPAD_ID)
78
- .map((row) => ({
79
- version: Number(row.version),
80
- snapshot: padVersion(context, Number(row.version), String(row.snapshot)),
81
- }));
82
-
83
- /** The pad's newest version that parses, reading no further back. */
84
- function latestScratchpadDocument(context: StoreUpgradeContext) {
85
- for (const row of context.db
86
- .prepare(
87
- "SELECT version,snapshot FROM versions WHERE review_id=? ORDER BY version DESC",
88
- )
89
- .iterate(OLD_SCRATCHPAD_ID)) {
90
- const snapshot = padVersion(
91
- context,
92
- Number(row.version),
93
- String(row.snapshot),
94
- );
95
-
96
- if (snapshot) return snapshot.document;
97
- }
98
-
99
- return undefined;
100
- }
101
-
102
- /** The checkout the pad quotes most, else the one opened last. Only a
103
- * checkout that still exists: a plan opens on it. */
104
- function scratchpadRepository(context: StoreUpgradeContext) {
105
- const exists = (repositoryId: string) => {
106
- try {
107
- return existsSync(context.repositoryPath(repositoryId));
108
- } catch {
109
- return false;
110
- }
111
- };
112
-
113
- const counts = new Map<string, number>();
114
-
115
- // SAFETY: a stored pad's blocks, in today's form.
116
- const latest = latestScratchpadDocument(context) as Block[] | undefined;
117
-
118
- for (const { source } of sourceReferences(latest ?? [], {
119
- tolerant: true,
120
- })) {
121
- const repositoryId = source.pins?.repositoryId;
122
-
123
- if (repositoryId && exists(repositoryId))
124
- counts.set(repositoryId, (counts.get(repositoryId) ?? 0) + 1);
125
- }
126
-
127
- const [quoted] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [];
128
-
129
- if (quoted) return quoted;
130
-
131
- const viewed = context.db
132
- .prepare(
133
- `SELECT ${BOARD_REPOSITORY_ID_SQL} AS repository
134
- FROM review_attention
135
- JOIN reviews ON reviews.id=review_attention.review_id
136
- JOIN versions ON versions.review_id=reviews.id AND versions.version=reviews.version
137
- WHERE review_attention.viewed_at IS NOT NULL AND repository IS NOT NULL
138
- ORDER BY review_attention.viewed_at DESC`,
139
- )
140
- .all()
141
- .map(({ repository }) => String(repository));
142
-
143
- return [...viewed, ...context.repositories().map(({ id }) => id)].find(
144
- exists,
145
- );
146
- }
147
-
148
- const snapshots = (
149
- context: StoreUpgradeContext,
150
- own: string,
151
- document: JsonValue[],
152
- allocate: (prefix: string) => string,
153
- ) =>
154
- snapshotForeignReferences(
155
- document,
156
- own,
157
- context.fileAtCommit,
158
- context.repositoryName,
159
- allocate,
160
- );
161
-
162
- /** Reads the files the pad quotes, which the context keeps, before the
163
- * write lock other hosts on this home wait on. */
164
- export function readScratchpadQuotes(context: StoreUpgradeContext) {
165
- if (!hasScratchpadVersions(context)) return;
166
-
167
- const quoting = scratchpadRepository(context);
168
-
169
- if (quoting)
170
- for (const { snapshot } of scratchpadVersions(context))
171
- if (snapshot)
172
- snapshots(context, quoting, snapshot.document, (prefix) => prefix);
173
- }
174
-
175
- export function planFromScratchpad(context: StoreUpgradeContext) {
176
- const { db } = context;
177
-
178
- // A pad row with no versions has nothing to keep; it stays as it is.
179
- if (!hasScratchpadVersions(context)) return;
180
-
181
- const own = scratchpadRepository(context);
182
- const id = randomUUID();
183
-
184
- db.prepare(
185
- "INSERT INTO reviews(id,version,next_id,project_id) SELECT ?,version,next_id,? FROM reviews WHERE id=?",
186
- ).run(
187
- id,
188
- own ? context.projectOfRepository(own) : context.fallbackProject(),
189
- OLD_SCRATCHPAD_ID,
190
- );
191
-
192
- let nextId = Number(
193
- db.prepare("SELECT next_id FROM reviews WHERE id=?").get(id)!.next_id,
194
- );
195
-
196
- const old = scratchpadVersions(context);
197
-
198
- // The run changes the snapshots it took; nothing parses them again.
199
- parsedPads.delete(context);
200
-
201
- // Every version moves over; one that no longer parses, as it is.
202
- db.prepare("UPDATE versions SET review_id=? WHERE review_id=?").run(
203
- id,
204
- OLD_SCRATCHPAD_ID,
205
- );
206
-
207
- for (const { version, snapshot } of old) {
208
- if (!snapshot) continue;
209
-
210
- delete snapshot.kind;
211
-
212
- snapshot.reviewId = id;
213
- snapshot.title = "Scratchpad";
214
-
215
- if (own) {
216
- snapshot.target = { kind: "project", repositoryId: own };
217
- snapshot.document = snapshots(
218
- context,
219
- own,
220
- snapshot.document,
221
- (prefix) => `${prefix}-${++nextId}`,
222
- );
223
- }
224
-
225
- context.rewriteVersion(id, version, snapshot);
226
- }
227
-
228
- db.prepare("UPDATE reviews SET next_id=? WHERE id=?").run(nextId, id);
229
-
230
- // Tables a store makes after its upgrades may not exist yet. A thread's
231
- // board is its review_id until ask-threads-by-project renames it, which
232
- // a development build may have done already.
233
- for (const [table, ...names] of [
234
- ["ask_conversations", "review_id", "anchor_board"],
235
- ["authoring_presences", "review_id"],
236
- ["review_coverage", "review_id"],
237
- ["review_attention", "review_id"],
238
- ]) {
239
- const columns = columnsOf(db, table!);
240
- const column = names.find((name) => columns.has(name));
241
-
242
- if (column)
243
- db.prepare(`UPDATE ${table} SET ${column}=? WHERE ${column}=?`).run(
244
- id,
245
- OLD_SCRATCHPAD_ID,
246
- );
247
- }
248
-
249
- db.prepare("DELETE FROM reviews WHERE id=?").run(OLD_SCRATCHPAD_ID);
250
- }