@dev.fast/whiteboard 0.3.3 → 0.3.4-preview.20261019.125

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-xOdmMnDH.js → account-alias-ClRtWF_-.js} +8 -6
  2. package/dist/{agent-cli-DT7MdaqL.js → agent-cli-C6AM9dml.js} +15 -10
  3. package/dist/agent-cli-DKpWo-4F.js +2 -0
  4. package/dist/{agent-client-BRBzv3AV.js → agent-client-G4KxB8zB.js} +13 -3
  5. package/dist/{authoring-tools-DGSEag8y.js → authoring-tools-Dfo0uOSj.js} +34 -27
  6. package/dist/{background-server-WKzI_a98.js → background-server-BF_lLFxY.js} +5 -5
  7. package/dist/{background-server-C_REWFkG.js → background-server-DJfYWRoN.js} +1 -1
  8. package/dist/build-info.json +1 -1
  9. package/dist/{cli-BBSDi_dX.js → cli-CqOiiTW_.js} +1 -1
  10. package/dist/{cli-runner-DshTBske.js → cli-runner-D8CsXff4.js} +26 -26
  11. package/dist/cli.js +2 -2
  12. package/dist/{client-BN_FDxFr.js → client-lKVF_Elo.js} +253 -47
  13. package/dist/{fuzzy-match-bFX6wm8j.js → fuzzy-match-DH9lZivo.js} +42 -14
  14. package/dist/{headless-host-CS4Gv0c5.js → headless-host-C4bhVPZx.js} +513 -140
  15. package/dist/{headless-host-CzvYpKOX.js → headless-host-CeNnXxiL.js} +1 -1
  16. package/dist/lens-selection-CyJapcsl.js +214 -0
  17. package/dist/{local-data-DtQTIzHX.js → local-data-BdqqCYUD.js} +2434 -328
  18. package/dist/{mcp-DpKnnmHH.js → mcp-B2mvDEhC.js} +5 -9
  19. package/dist/{package-paths-jMg6JxDP.js → package-paths-Cm1tR8c4.js} +1 -1
  20. package/dist/{profile-DaqmODhc.js → profile-DcigfSms.js} +20 -8
  21. package/dist/profile-DthRnZ7x.js +2 -0
  22. package/dist/{remote-attach-DCngg8cl.js → remote-attach-DGwVRmY5.js} +4 -4
  23. package/dist/{remote-extensions-BCAbQZTv.js → remote-extensions-CferWwM9.js} +3 -3
  24. package/dist/{remote-extensions-Cf3L-ZGG.js → remote-extensions-D8W2IgKH.js} +1 -1
  25. package/dist/{remote-language-server-UWQgpoh-.js → remote-language-server-By-rEosC.js} +5 -5
  26. package/dist/{remote-uninstall-CcDOcGyi.js → remote-uninstall-DgHnDAFH.js} +6 -6
  27. package/dist/{request-origin-Zcn_CjX4.js → request-origin-DDmfnytX.js} +38 -10
  28. package/dist/{review-app-launcher-tqSQPlbu.js → review-app-launcher-CG3TFZEx.js} +2 -2
  29. package/dist/{review-home-paths-Lc02WFuX.js → review-home-paths-D1JVitqN.js} +19 -17
  30. package/dist/{review-telemetry-BHWJsnoD.js → review-telemetry-Myzm7o6-.js} +5 -5
  31. package/dist/runtime-Bj2tD33R.js +27 -0
  32. package/dist/runtime.d.ts +1 -1
  33. package/dist/runtime.js +1 -1
  34. package/dist/{s3-77-qkEhs.js → s3-DtJFWC0G.js} +1 -1
  35. package/dist/{s3-config-DDWWMh5x.js → s3-config-BTO12tCC.js} +1 -1
  36. package/dist/{s3-config-0xuCh-F_.js → s3-config-CSy3fx8H.js} +1 -1
  37. package/dist/server/desktop-host.js +29 -76
  38. package/dist/{server-discovery-BVqrE1_-.js → server-discovery-Bwa_6Tsj.js} +3 -3
  39. package/dist/sharing/index.d.ts +486 -70
  40. package/dist/sharing/index.js +1 -1
  41. package/dist/{src-DsM3jO8G.js → src-BiWNbcF_.js} +89 -12
  42. package/dist/{src-BMPuxWv4.js → src-WPaQn75m.js} +208 -73
  43. package/dist/{stored-document-migration-CChxtFAy.js → stored-document-migration-dFbewRTG.js} +270 -269
  44. package/dist/{thread-state-5pYr6_zM.js → thread-state-Do5LX10I.js} +7 -2
  45. package/dist/{tool-failure-BFt2xj9i.js → tool-failure-C9J3r7KD.js} +10 -3
  46. package/instructions/authoring.md +3 -2
  47. package/package.json +4 -4
  48. package/src/agent-connect.ts +8 -2
  49. package/src/ask/agents.ts +51 -9
  50. package/src/ask/checkout-files.ts +2 -5
  51. package/src/ask/thread-state.ts +4 -0
  52. package/src/ask/thread.ts +16 -0
  53. package/src/ask/threads.ts +24 -3
  54. package/src/cli-runner.ts +4 -1
  55. package/src/diff-selection-migration.ts +48 -34
  56. package/src/heading-anchors.ts +42 -0
  57. package/src/lens-selection.ts +38 -2
  58. package/src/review-api/README.md +52 -13
  59. package/src/review-api/activity.ts +1 -4
  60. package/src/review-api/agent-cli.ts +9 -1
  61. package/src/review-api/anchor-quotes.ts +14 -7
  62. package/src/review-api/ask-history.ts +137 -51
  63. package/src/review-api/authoring-tools.ts +45 -30
  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/board-order.ts +144 -0
  67. package/src/review-api/checkout-groups.ts +560 -0
  68. package/src/review-api/document-headings.ts +58 -27
  69. package/src/review-api/document-text.ts +31 -2
  70. package/src/review-api/document.ts +281 -34
  71. package/src/review-api/foreign-references.ts +328 -0
  72. package/src/review-api/github-provider.ts +51 -13
  73. package/src/review-api/http.ts +702 -123
  74. package/src/review-api/instructions.ts +0 -19
  75. package/src/review-api/link-preview.ts +113 -0
  76. package/src/review-api/local-data.ts +310 -46
  77. package/src/review-api/mcp.ts +3 -11
  78. package/src/review-api/onboarding.ts +1 -1
  79. package/src/review-api/profile.ts +37 -6
  80. package/src/review-api/public-tools.ts +24 -2
  81. package/src/review-api/pull-request.ts +82 -49
  82. package/src/review-api/request-origin.ts +10 -0
  83. package/src/review-api/review-progress.ts +61 -27
  84. package/src/review-api/scratchpad-plan-upgrade.ts +250 -0
  85. package/src/review-api/store-migrations.ts +357 -0
  86. package/src/review-api/store-schema.ts +38 -29
  87. package/src/review-api/store.ts +2144 -480
  88. package/src/review-api/workspaces.ts +52 -13
  89. package/src/review-api/worktree-events.ts +3 -5
  90. package/src/review-api/worktree-source.ts +3 -7
  91. package/src/review-api/worktree-structural.integration.ts +72 -0
  92. package/src/review-stack.ts +15 -23
  93. package/src/runtime.ts +4 -30
  94. package/src/server/desktop-host.ts +6 -1
  95. package/src/server/desktop-server.ts +1 -31
  96. package/src/server/headless-host.ts +0 -2
  97. package/src/server/hono-http.ts +2 -1
  98. package/src/server/legacy-tutorial-cleanup.ts +2 -3
  99. package/src/server/review-gateway-list.ts +3 -5
  100. package/src/server/review-gateway-memory.ts +1 -1
  101. package/src/server/review-lifecycle-telemetry.ts +2 -2
  102. package/src/server/review-server-core.ts +1 -5
  103. package/src/server/structural-comparisons.ts +47 -0
  104. package/src/sharing/export.ts +36 -7
  105. package/src/sharing/host.ts +34 -14
  106. package/src/sharing/import.ts +156 -24
  107. package/src/sharing/repository.ts +128 -20
  108. package/src/startup-trace.ts +3 -3
  109. package/src/stored-document-migration.ts +110 -19
  110. package/src/ui-telemetry-events.ts +25 -9
  111. package/dist/agent-cli-DeCrUC2s.js +0 -2
  112. package/dist/profile-Cz7L781D.js +0 -2
  113. package/dist/runtime-CKiWhGrf.js +0 -52
  114. package/instructions/scratchpad.md +0 -23
  115. package/src/review-preferences.ts +0 -88
@@ -1,196 +1,12 @@
1
- import { f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
- import "./src-DsM3jO8G.js";
1
+ import { d as isBooleanValue, f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
+ import "./src-BiWNbcF_.js";
3
+ import { a as commitLinkTarget, c as parseAnchor, d as selectionProblem, f as sourceAnchors, i as anchorSelection, r as anchorSchema, s as formatAnchor } from "./lens-selection-CyJapcsl.js";
3
4
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
4
5
  import { z } from "zod";
5
6
  import { fromMarkdown } from "mdast-util-from-markdown";
6
7
  import { gfmFromMarkdown } from "mdast-util-gfm";
7
8
  import { mathFromMarkdown } from "mdast-util-math";
8
9
  import { gfm } from "micromark-extension-gfm";
9
- //#region src/viewed-coverage.ts
10
- const coverageSchema = z.object({
11
- base: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()])),
12
- head: z.array(z.tuple([z.number().int().nonnegative(), z.number().int().nonnegative()]))
13
- });
14
- const emptyCoverage = () => ({
15
- base: [],
16
- head: []
17
- });
18
- function unionIntervals(ranges) {
19
- const result = [];
20
- for (const [start, end] of [...ranges].sort((a, b) => a[0] - b[0])) {
21
- if (end <= start) continue;
22
- const last = result.at(-1);
23
- if (last && start <= last[1]) last[1] = Math.max(last[1], end);
24
- else result.push([start, end]);
25
- }
26
- return result;
27
- }
28
- function intersectIntervals(left, right) {
29
- const result = [];
30
- const a = unionIntervals(left), b = unionIntervals(right);
31
- let i = 0, j = 0;
32
- while (i < a.length && j < b.length) {
33
- const start = Math.max(a[i][0], b[j][0]), end = Math.min(a[i][1], b[j][1]);
34
- if (start < end) result.push([start, end]);
35
- if (a[i][1] < b[j][1]) i++;
36
- else j++;
37
- }
38
- return result;
39
- }
40
- function subtractIntervals(left, right) {
41
- const result = [];
42
- const removed = unionIntervals(right);
43
- for (const [start, end] of unionIntervals(left)) {
44
- let cursor = start;
45
- for (const [from, to] of removed) {
46
- if (to <= cursor) continue;
47
- if (from >= end) break;
48
- if (from > cursor) result.push([cursor, from]);
49
- cursor = Math.min(end, Math.max(cursor, to));
50
- }
51
- if (cursor < end) result.push([cursor, end]);
52
- }
53
- return result;
54
- }
55
- function updateCoverage(current, scope, viewed) {
56
- const apply = (side) => viewed ? unionIntervals([...current[side], ...scope[side]]) : subtractIntervals(current[side], scope[side]);
57
- return {
58
- base: apply("base"),
59
- head: apply("head")
60
- };
61
- }
62
- function scopedCoverage(file, sources) {
63
- if (!sources) return file.changed;
64
- const side = (side) => intersectIntervals(file.changed[side], sources.flatMap((source) => source.side === side && source.file === (side === "base" ? file.previousPath ?? file.path : file.path) ? [[source.fromLine - 1, source.toLine]] : []));
65
- return {
66
- base: side("base"),
67
- head: side("head")
68
- };
69
- }
70
- function coverageSources(file, coverage = file.viewed) {
71
- return ["base", "head"].flatMap((side) => coverage[side].map(([start, end]) => ({
72
- side,
73
- file: side === "base" ? file.previousPath ?? file.path : file.path,
74
- fromLine: start + 1,
75
- toLine: end
76
- })));
77
- }
78
- //#endregion
79
- //#region src/lens-selection.ts
80
- const ANCHOR_FORMS = "\"head/path#L10-L24\" or \"base/path#L7\" (one side, lines 10–24 or line 7); across sides, GitHub diff style \"diff/path#L84-R90\" (L = base line, R = head line)";
81
- const sameSideAnchor = /^(head|base)\/(.+)#L(\d+)(?:-L(\d+))?$/;
82
- const crossSideAnchor = /^diff\/(.+)#([LR])(\d+)-([LR])(\d+)$/;
83
- /** An anchor's selection, as markdown source links write it too (without
84
- * their `review-source:` scheme). Undefined when it isn't one. */
85
- function parseAnchor(text) {
86
- const same = sameSideAnchor.exec(text);
87
- if (same) {
88
- const side = same[1] === "base" ? "base" : "head";
89
- return {
90
- file: same[2],
91
- start: {
92
- side,
93
- line: Number(same[3])
94
- },
95
- end: {
96
- side,
97
- line: Number(same[4] ?? same[3])
98
- }
99
- };
100
- }
101
- const cross = crossSideAnchor.exec(text);
102
- if (!cross || cross[2] === cross[4]) return void 0;
103
- const side = (letter) => letter === "L" ? "base" : "head";
104
- return {
105
- file: cross[1],
106
- start: {
107
- side: side(cross[2]),
108
- line: Number(cross[3])
109
- },
110
- end: {
111
- side: side(cross[4]),
112
- line: Number(cross[5])
113
- }
114
- };
115
- }
116
- /** Why a parsed selection can't be quoted, or undefined when it can. */
117
- function selectionProblem(selection) {
118
- if (selection.start.line < 1 || selection.end.line < 1) return "Source lines count from 1.";
119
- if (selection.start.side === selection.end.side && selection.start.line > selection.end.line) return "Source range ends before it starts.";
120
- if (selection.pins && selection.pins.base === void 0 && (selection.start.side === "base" || selection.end.side === "base")) return "A base-side anchor needs base pins.";
121
- }
122
- const anchorSchema = z.string({ error: `Expected a source anchor: ${ANCHOR_FORMS}.` }).superRefine((text, context) => {
123
- const selection = parseAnchor(text);
124
- const problem = selection ? selectionProblem(selection) : `Use ${ANCHOR_FORMS} for a source anchor.`;
125
- if (problem) context.addIssue({
126
- code: "custom",
127
- message: problem
128
- });
129
- }).describe("head/path#L10-L24, base/path#L7, or diff/path#L84-R90");
130
- /** A stored anchor's selection, read at the pins of whatever holds it. */
131
- function anchorSelection(anchor, pins) {
132
- const selection = parseAnchor(anchor);
133
- if (!selection) throw new Error(`Not a source anchor: ${anchor}`);
134
- return pins ? {
135
- ...selection,
136
- pins
137
- } : selection;
138
- }
139
- /** The anchor that writes a selection; its pins belong to its holder. */
140
- function formatAnchor(selection) {
141
- const { file, start, end } = selection;
142
- if (start.side === end.side) return start.line === end.line ? `${start.side}/${file}#L${start.line}` : `${start.side}/${file}#L${start.line}-L${end.line}`;
143
- const letter = (side) => side === "base" ? "L" : "R";
144
- return `diff/${file}#${letter(start.side)}${start.line}-${letter(end.side)}${end.line}`;
145
- }
146
- /** Pins-less selections keep the key they always had. */
147
- function selectionKey(source) {
148
- return JSON.stringify([
149
- source.file,
150
- source.start.side,
151
- source.start.line,
152
- source.end.side,
153
- source.end.line,
154
- ...source.pins ? [sourcePinsKey(source.pins)] : []
155
- ]);
156
- }
157
- function sourcePinsKey(pins) {
158
- return `${pins.repositoryId}:${pins.base ?? ""}:${pins.head}`;
159
- }
160
- /** The comparison a source's pins name: head alone means head against
161
- * itself. Reference coverage is grouped under this key on both sides. */
162
- function comparisonKey(pins) {
163
- return `${pins.repositoryId}:${pins.base ?? pins.head}:${pins.head}`;
164
- }
165
- /** Navigation/quote anchors only; never use these as lens coverage. */
166
- function sourceAnchors(source) {
167
- return ["head", "base"].flatMap((side) => {
168
- const lines = [source.start, source.end].flatMap((endpoint) => endpoint.side === side ? [endpoint.line] : []);
169
- if (!lines.length) return [];
170
- const anchor = {
171
- file: source.file,
172
- side,
173
- fromLine: Math.min(...lines),
174
- toLine: Math.max(...lines)
175
- };
176
- if (source.pins) anchor.pins = source.pins;
177
- return [anchor];
178
- });
179
- }
180
- /** Resolve once against the renderer's rows. No matching or context expansion. */
181
- function resolveDiffSelection(source, rows, file) {
182
- const locate = (endpoint) => rows.findIndex((row) => row[endpoint.side === "base" ? 0 : 1] === endpoint.line - 1);
183
- const start = locate(source.start);
184
- const end = locate(source.end);
185
- if (start < 0 || end < start) throw new Error("Lens endpoints do not identify an ordered interval in this diff.");
186
- const selected = rows.slice(start, end + 1);
187
- return ["base", "head"].flatMap((side, index) => unionIntervals(selected.flatMap((row) => row[index] === null ? [] : [[row[index], row[index] + 1]])).map(([from, to]) => ({
188
- file: side === "base" ? file.previousPath ?? file.path : file.path,
189
- side,
190
- fromLine: from + 1,
191
- toLine: to
192
- })));
193
- }
194
10
  /**
195
11
  * Check whether a character code is a markdown line ending.
196
12
  *
@@ -1036,7 +852,7 @@ const identity = { id: text$1.optional() };
1036
852
  /** The repository and commits an element's anchors quote. A step, frame,
1037
853
  * attachment or operation without them reads its block's; a block without
1038
854
  * them reads its document's. */
1039
- const elementPins = sourcePinsSchema.optional().describe("Pins for the anchors here; needed on the scratchpad.");
855
+ const elementPins = sourcePinsSchema.optional().describe("Pins for the anchors here.");
1040
856
  /** The strict schema for a block's own fields; id and type are added here. */
1041
857
  function defineBlock(type, props) {
1042
858
  return z.strictObject({
@@ -1376,8 +1192,8 @@ const image = {
1376
1192
  const markdown = {
1377
1193
  type: "markdown",
1378
1194
  schema: defineBlock("markdown", {
1379
- markdown: text$1.describe("Safe Markdown. Repository file links must use [label](review-source:head/path#L10-L24) or review-source:base/path#L10-L24, with a repository-relative path and verified line numbers. Relative paths, absolute filesystem paths, and file/editor URLs are rejected. External links use https://, http://, or mailto:; document anchors use #heading."),
1380
- pins: sourcePinsSchema.optional().describe("Repository and commits this block's review-source links resolve against, instead of the document's pins.")
1195
+ markdown: text$1.describe("Safe Markdown. Repository file links must use [label](whiteboard:source/head/path#L10-L24) or whiteboard:source/base/path#L10-L24, or whiteboard:source/<commit>/path#L10-L24 at any commit of the board's repository, with a repository-relative path and verified line numbers. Link another board as [label](whiteboard:board/<sessionId>) or one of its blocks as whiteboard:board/<sessionId>#<blockId>; both must exist. Relative paths, absolute filesystem paths, and file/editor URLs are rejected. External links use https://, http://, or mailto:; document anchors use #heading."),
1196
+ pins: sourcePinsSchema.optional().describe("Repository and commits this block's whiteboard:source/ links resolve against, instead of the document's pins.")
1381
1197
  })
1382
1198
  };
1383
1199
  //#endregion
@@ -1581,28 +1397,91 @@ function pinnedRepositoryIds(document) {
1581
1397
  })) if (source.pins) ids.add(source.pins.repositoryId);
1582
1398
  return ids;
1583
1399
  }
1584
- const targets = (repository) => z.discriminatedUnion("kind", [z.strictObject({
1585
- kind: z.literal("worktree"),
1586
- ...repository,
1587
- base: label.optional()
1588
- }), z.strictObject({
1589
- kind: z.literal("commits"),
1590
- ...repository,
1591
- head: label,
1592
- base: label.optional()
1593
- })]);
1400
+ const targets = (repository) => z.discriminatedUnion("kind", [
1401
+ z.strictObject({
1402
+ kind: z.literal("project"),
1403
+ ...repository
1404
+ }),
1405
+ z.strictObject({
1406
+ kind: z.literal("worktree"),
1407
+ ...repository,
1408
+ base: label.optional()
1409
+ }),
1410
+ z.strictObject({
1411
+ kind: z.literal("commits"),
1412
+ ...repository,
1413
+ head: label,
1414
+ base: label.optional()
1415
+ })
1416
+ ]);
1594
1417
  const reviewTargetSchema = targets({ repositoryId: label });
1595
- /** How agents name a target: by the checkout's path, registered on acceptance. */
1596
- const pathTargetSchema = targets({ repositoryPath: label });
1418
+ /** Targets as agents write them: a checkout path instead of a repository id,
1419
+ * plus a PR by URL or number. */
1420
+ const pathTargetSchema = z.discriminatedUnion("kind", [...targets({ repositoryPath: label }).options, z.strictObject({
1421
+ kind: z.literal("pullRequest"),
1422
+ repositoryPath: label,
1423
+ pullRequest: z.union([label, z.number().int().positive()])
1424
+ })]);
1597
1425
  const unitTypes = /* @__PURE__ */ new Set([
1598
1426
  "step",
1599
1427
  "flow_node",
1600
1428
  "flow_edge"
1601
1429
  ]);
1602
1430
  const isUnit = (element) => unitTypes.has(element.type);
1431
+ /** Rewrites old review-source: and review-trace: links to their whiteboard: form. */
1432
+ function canonicalLink(href) {
1433
+ return href.replace(/^review-(source|trace):/i, (_, kind) => `whiteboard:${kind.toLowerCase()}/`);
1434
+ }
1435
+ /** Markdown link destinations and definitions that begin with `target`, a
1436
+ * regex source; the match's first group is what comes before it. */
1437
+ const linkDestination = (target) => new RegExp(String.raw`(\]\(\s*<?|^\s*\[[^\]\n]+\]:\s*<?)` + target, "gim");
1438
+ /** Every Markdown text in `value`, rewritten. */
1439
+ function rewriteMarkdown(value, rewrite) {
1440
+ return JSON.parse(JSON.stringify(value), (key, field) => key === "markdown" && isStringValue(field) ? rewrite(field) : field);
1441
+ }
1442
+ const retiredLinks = linkDestination("review-(source|trace):");
1443
+ /** Rewrites old link prefixes in Markdown link destinations and definitions. */
1444
+ function respellLinks(markdown) {
1445
+ return markdown.replace(retiredLinks, (_, before, kind) => `${before}whiteboard:${kind.toLowerCase()}/`);
1446
+ }
1447
+ /** Every Markdown text in `document` with its links spelled the current
1448
+ * way. */
1449
+ function respellDocumentLinks(document) {
1450
+ return rewriteMarkdown(document, respellLinks);
1451
+ }
1452
+ function respellEditLinks(edit) {
1453
+ return rewriteMarkdown(edit, respellLinks);
1454
+ }
1455
+ /** Parses whiteboard:board/<id> or whiteboard:board/<id>#<blockId>. */
1456
+ function boardLink(href) {
1457
+ const match = /^whiteboard:board\/([^#/]+)(?:#(.+))?$/.exec(href);
1458
+ if (!match) return void 0;
1459
+ try {
1460
+ const board = decodeURIComponent(match[1]);
1461
+ return match[2] === void 0 ? { board } : {
1462
+ board,
1463
+ block: decodeURIComponent(match[2])
1464
+ };
1465
+ } catch {
1466
+ return;
1467
+ }
1468
+ }
1469
+ /** Every board link in a document. Malformed links are skipped; writes reject them. */
1470
+ function boardLinks(board, document) {
1471
+ return elements(document).flatMap((element) => element.type === "markdown" ? [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
1472
+ const to = node.type === "link" ? boardLink(node.url ?? "") : void 0;
1473
+ return to ? [{
1474
+ from: {
1475
+ board,
1476
+ block: element.id
1477
+ },
1478
+ to
1479
+ }] : [];
1480
+ }) : []);
1481
+ }
1603
1482
  /** Inline trace quotes keep paragraph/list layout while using retained resources. */
1604
1483
  function traceQuoteLink(href) {
1605
- const match = /^review-trace:([^#]+)#(.+)$/.exec(href);
1484
+ const match = /^whiteboard:trace\/([^#]+)#(.+)$/.exec(canonicalLink(href));
1606
1485
  return match ? {
1607
1486
  traceId: decodeURIComponent(match[1]),
1608
1487
  eventId: decodeURIComponent(match[2])
@@ -1629,8 +1508,8 @@ function resourceReferences(document) {
1629
1508
  return elements(document).flatMap((block) => {
1630
1509
  if (block.type !== "markdown") return block.type === "image" || block.type === "trace_quote" || block.type === "software_map" ? [block] : [];
1631
1510
  return [...markdownNodes(parseMarkdown(block.markdown))].flatMap((node) => {
1632
- if (node.type !== "link" || !node.url?.startsWith("review-trace:")) return [];
1633
- const quote = traceQuoteLink(node.url);
1511
+ if (node.type !== "link" || !canonicalLink(node.url ?? "").startsWith("whiteboard:trace/")) return [];
1512
+ const quote = traceQuoteLink(node.url ?? "");
1634
1513
  if (!quote) throw new ReviewInputError("Invalid trace quote link.");
1635
1514
  return [{
1636
1515
  type: "trace_quote",
@@ -1645,7 +1524,7 @@ function resourceReferences(document) {
1645
1524
  * `tolerant` skips malformed Markdown source links instead of rejecting, for
1646
1525
  * content that is already stored.
1647
1526
  */
1648
- function documentReferences(document, { tolerant = false, markdown = true } = {}) {
1527
+ function documentReferences(document, { tolerant = false, markdown = true, repositoryId } = {}) {
1649
1528
  const reject = (message) => {
1650
1529
  if (tolerant) return [];
1651
1530
  throw new ReviewInputError(message);
@@ -1664,20 +1543,29 @@ function documentReferences(document, { tolerant = false, markdown = true } = {}
1664
1543
  if (element.type === "markdown" && !markdown) return [];
1665
1544
  if (element.type === "markdown") return [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
1666
1545
  if (node.type !== "link") return [];
1667
- const href = node.url ?? "";
1668
- if (!/^review-source:/i.test(href)) {
1669
- if (href.startsWith("review-trace:")) return [];
1546
+ const href = canonicalLink(node.url ?? "");
1547
+ if (!href.startsWith("whiteboard:source/")) {
1548
+ if (href.startsWith("whiteboard:trace/")) return [];
1549
+ if (href.startsWith("whiteboard:board/")) return boardLink(href) ? [] : reject(`Use whiteboard:board/<sessionId> or whiteboard:board/<sessionId>#<blockId> to link a board, in block ${element.id}.`);
1670
1550
  if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
1671
- return reject(`Unsupported Markdown link ${JSON.stringify(href)} in block ${element.id}. Use [label](review-source:head/path#L10-L24) or review-source:base/path#L10-L24 for repository files, or review-source:diff/path#L84-R90 across sides, with a repository-relative path and verified line numbers. External links must use https://, http://, or mailto:; document anchors use #heading.`);
1551
+ return reject(`Unsupported Markdown link ${JSON.stringify(href)} in block ${element.id}. Use [label](whiteboard:source/head/path#L10-L24) or whiteboard:source/base/path#L10-L24 for repository files, whiteboard:source/diff/path#L84-R90 across sides, or whiteboard:source/<commit>/path#L10-L24 at a commit of the board's repository, with a repository-relative path and verified line numbers. Link another board as whiteboard:board/<sessionId>, or one of its blocks as whiteboard:board/<sessionId>#<blockId>. External links must use https://, http://, or mailto:; document anchors use #heading.`);
1672
1552
  }
1673
1553
  let target;
1674
1554
  try {
1675
- target = decodeURIComponent(href.slice(14));
1555
+ target = decodeURIComponent(href.slice(18));
1676
1556
  } catch {
1677
1557
  return reject("Invalid URL encoding in source link.");
1678
1558
  }
1559
+ const commit = commitLinkTarget(target);
1560
+ if (commit) return repositoryId ? select(commit.anchor, {
1561
+ repositoryId,
1562
+ head: commit.commit
1563
+ }).map((source) => ({
1564
+ id: `${element.id}:${node.url}`,
1565
+ source
1566
+ })) : reject(`A commit link names a commit of the board's repository, and this board has none, in block ${element.id}.`);
1679
1567
  const anchor = target.replace(/^(head|base|diff)\//i, (side) => side.toLowerCase());
1680
- if (!parseAnchor(anchor)) return reject("Use review-source:head/path#L10-L24 (or base, or diff/path#L84-R90 across sides) for a source link.");
1568
+ if (!parseAnchor(anchor)) return reject("Use whiteboard:source/head/path#L10-L24 (or base, diff/path#L84-R90 across sides, or <commit>/path#L10-L24 at a commit of the board's repository) for a source link.");
1681
1569
  return select(anchor, element.pins).map((source) => ({
1682
1570
  id: `${element.id}:${node.url}`,
1683
1571
  source
@@ -1738,6 +1626,58 @@ function documentReferences(document, { tolerant = false, markdown = true } = {}
1738
1626
  }
1739
1627
  /** All authored attachments, including code peeks, select the aligned diff. */
1740
1628
  const selectionReferences = documentReferences;
1629
+ const commitLinkDestination = linkDestination(String.raw`whiteboard:source\/([0-9a-f]{7,40})\/`);
1630
+ /** The commits a document's Markdown commit links name, as written. */
1631
+ function commitLinkCommits(document) {
1632
+ const commits = /* @__PURE__ */ new Set();
1633
+ JSON.stringify(document, (key, value) => {
1634
+ if (key === "markdown" && isStringValue(value)) for (const match of value.matchAll(commitLinkDestination)) commits.add(match[2]);
1635
+ return value;
1636
+ });
1637
+ return [...commits];
1638
+ }
1639
+ /** Rewrites the commits Markdown commit links name, as a short commit
1640
+ * becomes the full one on save. */
1641
+ function respellCommitLinks(document, commits) {
1642
+ return rewriteMarkdown(document, (markdown) => markdown.replace(commitLinkDestination, (link, before, commit) => `${before}whiteboard:source/${commits.get(commit) ?? commit}/`));
1643
+ }
1644
+ /** Blocks that carry pins of their own for the references in them. */
1645
+ const pinnedBlockTypes = /* @__PURE__ */ new Set([
1646
+ "markdown",
1647
+ "code_peek",
1648
+ "sequence",
1649
+ "flow_diagram",
1650
+ "call_stack_diff",
1651
+ "database_lens"
1652
+ ]);
1653
+ const sideLinkDestination = linkDestination(String.raw`whiteboard:source\/(head|base)\/`);
1654
+ /**
1655
+ * Makes every reference that reads at the document's pins carry them
1656
+ * itself, for a board about to lose its pins: Markdown head/ and base/
1657
+ * links become commit links, and a block whose references still lean on
1658
+ * the document's pins (any anchor, or a diff/ link) takes them as its own.
1659
+ * Nothing changes what it points at.
1660
+ */
1661
+ function pinDocumentReferences(document, pins) {
1662
+ const own = {
1663
+ repositoryId: pins.repositoryId,
1664
+ base: pins.base,
1665
+ head: pins.head
1666
+ };
1667
+ const pinned = structuredClone(document);
1668
+ for (const element of elements(pinned)) {
1669
+ if (!pinnedBlockTypes.has(element.type)) continue;
1670
+ const block = element;
1671
+ if (block.pins) continue;
1672
+ if (block.type === "markdown") block.markdown = respellLinks(block.markdown).replace(sideLinkDestination, (_, before, side) => `${before}whiteboard:source/${side.toLowerCase() === "base" ? pins.base : pins.head}/`);
1673
+ if (documentReferences([block], { tolerant: true }).some((reference) => !reference.source.pins)) block.pins = own;
1674
+ }
1675
+ return pinned;
1676
+ }
1677
+ /** The repository a board's commit links read from: its target's. */
1678
+ function boardRepositoryId(board) {
1679
+ return board.target?.repositoryId ?? board.pins?.repositoryId;
1680
+ }
1741
1681
  /** Per-side read coordinates for endpoint validation and retained source quotes. */
1742
1682
  function sourceReferences(document, options = {}) {
1743
1683
  return selectionReferences(document, options).flatMap((ref) => sourceAnchors(ref.source).map((source) => ({
@@ -1746,8 +1686,10 @@ function sourceReferences(document, options = {}) {
1746
1686
  })));
1747
1687
  }
1748
1688
  /** Includes lenses and maps without inline ranges. */
1749
- function hasCodeReferences(review) {
1750
- return Boolean(sourceReferences(review.document).length || review.lenses?.length || resourceReferences(review.document).some((block) => block.type === "software_map"));
1689
+ /** Whether a board reads code at its own pins: a commit link or a block
1690
+ * with pins of its own names its commits itself. */
1691
+ function readsAtBoardPins(review) {
1692
+ return Boolean(sourceReferences(review.document, { tolerant: true }).some((reference) => !reference.source.pins) || review.lenses?.length || resourceReferences(review.document).some((block) => block.type === "software_map"));
1751
1693
  }
1752
1694
  const documentSchema = z.array(blockSchema);
1753
1695
  /** Discriminated by type, like blockSchema, so a malformed insert reports
@@ -1910,20 +1852,27 @@ function adoptFlowUnits(document, allocate) {
1910
1852
  /** Mutate a private candidate. Only the store owns allocation and commits. */
1911
1853
  /** Assign server ids to an element tree that arrives without any. */
1912
1854
  function assignFreshIds(element, allocate) {
1913
- if (element.id !== void 0) throw new ReviewInputError("IDs are assigned by the server.");
1914
- element.id = allocate(idPrefix(element));
1915
- for (const child of children(element)) assignFreshIds(child, allocate);
1855
+ assignIds(element, allocate, true);
1856
+ }
1857
+ /** Give ids to the parts of an element tree that have none, keeping the
1858
+ * rest: a person's save names the blocks it kept and leaves new ones bare. */
1859
+ function assignMissingIds(element, allocate) {
1860
+ assignIds(element, allocate, false);
1861
+ }
1862
+ function assignIds(element, allocate, fresh) {
1916
1863
  const assign = (item, prefix) => {
1917
- if (item.id !== void 0) throw new ReviewInputError("IDs are assigned by the server.");
1918
- item.id = allocate(prefix);
1864
+ if (item.id === void 0) item.id = allocate(prefix);
1865
+ else if (fresh) throw new ReviewInputError("IDs are assigned by the server.");
1919
1866
  };
1867
+ assign(element, idPrefix(element));
1868
+ for (const child of children(element)) assignIds(child, allocate, fresh);
1920
1869
  if (element.type === "call_stack_diff") for (const frame of [...element.base, ...element.head]) assign(frame, "frame");
1921
1870
  if (element.type === "database_lens") for (const useCase of element.useCases) {
1922
1871
  assign(useCase, "case");
1923
1872
  for (const op of useCase.operations) assign(op, "operation");
1924
1873
  }
1925
1874
  }
1926
- function applyEdit(document, edit, allocate, options = {}) {
1875
+ function applyEdit(document, edit, allocate) {
1927
1876
  adoptFlowUnits(document, allocate);
1928
1877
  const locate = (id) => {
1929
1878
  const find = (siblings) => {
@@ -1952,7 +1901,7 @@ function applyEdit(document, edit, allocate, options = {}) {
1952
1901
  if (afterId === element.id) throw new ReviewInputError("An element cannot follow itself.");
1953
1902
  if (afterId !== void 0 && !siblings.some((s) => s.id === afterId)) throw new ReviewInputError("afterId must identify a sibling.");
1954
1903
  if (from) from.splice(from.indexOf(element), 1);
1955
- const index = afterId !== void 0 ? siblings.findIndex((s) => s.id === afterId) + 1 : !parent && !from && options.placement === "first" ? 0 : siblings.length;
1904
+ const index = afterId === void 0 ? siblings.length : siblings.findIndex((s) => s.id === afterId) + 1;
1956
1905
  siblings.splice(index, 0, element);
1957
1906
  };
1958
1907
  if (edit.type === "insert") {
@@ -2546,54 +2495,114 @@ z.strictObject({
2546
2495
  function migrateDiffSelections(input) {
2547
2496
  return migrateNode(jsonValueSchema.parse(input));
2548
2497
  }
2498
+ /** Keys whose values held retired attachments. */
2499
+ const ATTACHMENT_KEYS = [
2500
+ "source",
2501
+ "peek",
2502
+ "sources",
2503
+ "contextSources",
2504
+ "callSite"
2505
+ ];
2506
+ /** A retired attachment as a selection, or undefined for anything else. */
2507
+ function retiredSelection(value) {
2508
+ if (!isStringValue(value.file)) return void 0;
2509
+ if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
2510
+ const side = value.side ?? value.graph ?? "head";
2511
+ return {
2512
+ file: value.file,
2513
+ start: {
2514
+ side,
2515
+ line: value.fromLine
2516
+ },
2517
+ end: {
2518
+ side,
2519
+ line: value.toLine
2520
+ }
2521
+ };
2522
+ }
2523
+ if (isJsonObject(value.start) && isJsonObject(value.end) && "baseLine" in value.start) {
2524
+ const endpoint = (row) => isNumberValue(row.headLine) ? {
2525
+ side: "head",
2526
+ line: row.headLine
2527
+ } : {
2528
+ side: "base",
2529
+ line: row.baseLine ?? null
2530
+ };
2531
+ return {
2532
+ file: value.file,
2533
+ start: endpoint(value.start),
2534
+ end: endpoint(value.end)
2535
+ };
2536
+ }
2537
+ }
2549
2538
  function migrateNode(value, attachment = false) {
2550
2539
  if (Array.isArray(value)) return value.map((child) => migrateNode(child, attachment));
2551
2540
  if (!isJsonObject(value)) return value;
2552
- if (attachment && isStringValue(value.file)) {
2553
- if (isNumberValue(value.fromLine) && isNumberValue(value.toLine)) {
2554
- const side = value.side ?? value.graph ?? "head";
2555
- return {
2556
- file: value.file,
2557
- start: {
2558
- side,
2559
- line: value.fromLine
2560
- },
2561
- end: {
2562
- side,
2563
- line: value.toLine
2564
- }
2565
- };
2566
- }
2567
- if (isJsonObject(value.start) && isJsonObject(value.end) && "baseLine" in value.start) {
2568
- const endpoint = (row) => isNumberValue(row.headLine) ? {
2569
- side: "head",
2570
- line: row.headLine
2571
- } : {
2572
- side: "base",
2573
- line: row.baseLine
2574
- };
2575
- return {
2576
- file: value.file,
2577
- start: endpoint(value.start),
2578
- end: endpoint(value.end)
2579
- };
2580
- }
2581
- }
2582
- return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, migrateNode(child, [
2583
- "source",
2584
- "peek",
2585
- "sources",
2586
- "contextSources",
2587
- "callSite"
2588
- ].includes(key))]));
2541
+ const selection = attachment ? retiredSelection(value) : void 0;
2542
+ if (selection) return selection;
2543
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, migrateNode(child, ATTACHMENT_KEYS.includes(key))]));
2589
2544
  }
2590
2545
  //#endregion
2591
2546
  //#region src/stored-document-migration.ts
2592
2547
  /** Upgrade a saved or shared Review document to the current block schema at the
2593
2548
  * read/import boundary. New edits use the strict schema and never accept these
2594
- * retired forms. */
2549
+ * retired forms. One walk makes a fresh copy: retired attachments become
2550
+ * selections, anchors become strings, sections lose their status, and
2551
+ * Markdown links are spelled the current way. */
2595
2552
  function migrateStoredDocument(input) {
2596
- return anchorStrings(dropSectionStatus(migrateDiffSelections(input)));
2553
+ return upgradeNode(input, {
2554
+ attachment: false,
2555
+ anchors: true
2556
+ });
2557
+ }
2558
+ function upgradeNode(value, place) {
2559
+ if (isStringValue(value) || isBooleanValue(value) || value === null) return value;
2560
+ if (isNumberValue(value) && Number.isFinite(value)) return value;
2561
+ if (Array.isArray(value)) return value.map((child) => upgradeNode(child, place));
2562
+ if (!isJsonObject(value)) throw new TypeError("A document must be JSON.");
2563
+ const selection = place.attachment ? retiredSelection(value) : void 0;
2564
+ if (selection) return upgradeNode(selection, {
2565
+ attachment: false,
2566
+ anchors: false
2567
+ });
2568
+ const inherited = place.anchors ? value.pins ?? place.blockPins : void 0;
2569
+ let pins;
2570
+ const convert = (anchor) => {
2571
+ if (isStringValue(anchor)) return anchor;
2572
+ const parsed = selectionObjectSchema.safeParse(anchor);
2573
+ if (!parsed.success) return anchor;
2574
+ const { pins: own, ...anchorAt } = parsed.data;
2575
+ if (own) pins = own;
2576
+ return formatAnchor(anchorAt);
2577
+ };
2578
+ const entries = [];
2579
+ for (const [key, child] of Object.entries(value)) {
2580
+ if (key === "status" && value.type === "section") continue;
2581
+ if (key === "markdown" && isStringValue(child)) {
2582
+ entries.push([key, respellLinks(child)]);
2583
+ continue;
2584
+ }
2585
+ const attachment = ATTACHMENT_KEYS.includes(key);
2586
+ if (place.anchors && ANCHOR_KEYS.includes(key)) {
2587
+ const anchors = upgradeNode(child, {
2588
+ attachment,
2589
+ anchors: false
2590
+ });
2591
+ entries.push([key, Array.isArray(anchors) ? anchors.map(convert) : convert(anchors)]);
2592
+ } else entries.push([key, upgradeNode(child, {
2593
+ attachment,
2594
+ anchors: place.anchors,
2595
+ blockPins: inherited
2596
+ })]);
2597
+ }
2598
+ const upgraded = Object.fromEntries(entries);
2599
+ if (pins !== void 0 && value.kind !== "ranges" && JSON.stringify(pins) !== JSON.stringify(place.blockPins)) upgraded.pins = pins;
2600
+ return upgraded;
2601
+ }
2602
+ /** A stored document as the store reads it: retired forms upgraded, and
2603
+ * lens blocks lifted out to the snapshot's own lenses. */
2604
+ function readStoredDocument(input) {
2605
+ return liftFileLenses(migrateStoredDocument(input));
2597
2606
  }
2598
2607
  /** Lenses saved before ranges were anchor strings. */
2599
2608
  function migrateStoredLenses(input) {
@@ -2640,14 +2649,6 @@ function anchorStrings(value, blockPins) {
2640
2649
  if (pins !== void 0 && value.kind !== "ranges" && JSON.stringify(pins) !== JSON.stringify(blockPins)) migrated.pins = pins;
2641
2650
  return migrated;
2642
2651
  }
2643
- /** Sections once carried an optional `status` (pending, in_progress or
2644
- * complete). The authoring lease is now the only signal of work in progress,
2645
- * so versions saved before it was retired drop the field when read. */
2646
- function dropSectionStatus(value) {
2647
- if (Array.isArray(value)) return value.map(dropSectionStatus);
2648
- if (!isJsonObject(value)) return value;
2649
- return Object.fromEntries(Object.entries(value).filter(([key]) => !(value.type === "section" && key === "status")).map(([key, child]) => [key, dropSectionStatus(child)]));
2650
- }
2651
2652
  /** A saved `file_lens` block, from before lenses left the document. */
2652
2653
  const legacyFileLensSchema = z.object({
2653
2654
  type: z.literal("file_lens"),
@@ -2686,4 +2687,4 @@ function liftFileLenses(document) {
2686
2687
  };
2687
2688
  }
2688
2689
  //#endregion
2689
- export { intersectIntervals as $, documentAdvice as A, requireVisibleSource as B, resourceReference as C, sourceReferences as D, selectionReferences as E, lensSchema as F, comparisonKey as G, markdownNodes as H, lensSelections as I, selectionKey as J, formatAnchor as K, SourceRangeError as L, UNCATEGORIZED_LENS_ID as M, applyLensEdit as N, summarizeEdit as O, lensEditSchema as P, emptyCoverage as Q, checkSourcePath as R, publishedEditSchema as S, reviewTargetSchema as T, parseMarkdown as U, sliceSourceRange as V, anchorSelection as W, coverageSchema as X, sourceAnchors as Y, coverageSources as Z, hasCodeReferences as _, defineSoftwareMap as a, pinnedRepositoryIds as b, traceSchema as c, applyEdit as d, scopedCoverage as et, assignFreshIds as f, explicitPins as g, elements as h, SoftwareModelValidationError as i, stepSources as j, checkReferences as k, textIncludesQuote as l, editSchema as m, migrateStoredDocument as n, unionIntervals as nt, normalizedSoftwareElementSchema as o, documentSchema as p, resolveDiffSelection as q, migrateStoredLenses as r, updateCoverage as rt, normalizedSoftwareRelationshipSchema as s, liftFileLenses as t, subtractIntervals as tt, anchorPins as u, isUnit as v, resourceReferences as w, pinsSchema as x, pathTargetSchema as y, fileLineRangeSchema as z };
2690
+ export { parseMarkdown as $, resourceReferences as A, stepSources as B, pathTargetSchema as C, publishedEditSchema as D, pinsSchema as E, selectionReferences as F, lensSelections as G, applyLensEdit as H, sourceReferences as I, fileLineRangeSchema as J, SourceRangeError as K, summarizeEdit as L, respellDocumentLinks as M, respellEditLinks as N, readsAtBoardPins as O, reviewTargetSchema as P, markdownText as Q, checkReferences as R, explicitPins as S, pinnedRepositoryIds as T, lensEditSchema as U, UNCATEGORIZED_LENS_ID as V, lensSchema as W, sliceSourceRange as X, requireVisibleSource as Y, markdownNodes as Z, boardRepositoryId as _, SoftwareModelValidationError as a, editSchema as b, normalizedSoftwareRelationshipSchema as c, adoptFlowUnits as d, anchorPins as f, boardLinks as g, assignMissingIds as h, readStoredDocument as i, respellCommitLinks as j, resourceReference as k, traceSchema as l, assignFreshIds as m, migrateStoredDocument as n, defineSoftwareMap as o, applyEdit as p, checkSourcePath as q, migrateStoredLenses as r, normalizedSoftwareElementSchema as s, liftFileLenses as t, textIncludesQuote as u, commitLinkCommits as v, pinDocumentReferences as w, elements as x, documentSchema as y, documentAdvice as z };