@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,12 +1,196 @@
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";
1
+ import { f as isNumberValue, l as jsonValueSchema, m as isStringValue, n as isJsonObject } from "./src-X9phtB2j.js";
2
+ import "./src-DsM3jO8G.js";
4
3
  import { t as ReviewInputError } from "./input-error-OLgB_h31.js";
5
4
  import { z } from "zod";
6
5
  import { fromMarkdown } from "mdast-util-from-markdown";
7
6
  import { gfmFromMarkdown } from "mdast-util-gfm";
8
7
  import { mathFromMarkdown } from "mdast-util-math";
9
8
  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
+ }
10
194
  /**
11
195
  * Check whether a character code is a markdown line ending.
12
196
  *
@@ -852,7 +1036,7 @@ const identity = { id: text$1.optional() };
852
1036
  /** The repository and commits an element's anchors quote. A step, frame,
853
1037
  * attachment or operation without them reads its block's; a block without
854
1038
  * them reads its document's. */
855
- const elementPins = sourcePinsSchema.optional().describe("Pins for the anchors here.");
1039
+ const elementPins = sourcePinsSchema.optional().describe("Pins for the anchors here; needed on the scratchpad.");
856
1040
  /** The strict schema for a block's own fields; id and type are added here. */
857
1041
  function defineBlock(type, props) {
858
1042
  return z.strictObject({
@@ -1192,8 +1376,8 @@ const image = {
1192
1376
  const markdown = {
1193
1377
  type: "markdown",
1194
1378
  schema: defineBlock("markdown", {
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.")
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.")
1197
1381
  })
1198
1382
  };
1199
1383
  //#endregion
@@ -1397,91 +1581,28 @@ function pinnedRepositoryIds(document) {
1397
1581
  })) if (source.pins) ids.add(source.pins.repositoryId);
1398
1582
  return ids;
1399
1583
  }
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
- ]);
1417
- const reviewTargetSchema = targets({ repositoryId: 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()])
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()
1424
1593
  })]);
1594
+ 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 });
1425
1597
  const unitTypes = /* @__PURE__ */ new Set([
1426
1598
  "step",
1427
1599
  "flow_node",
1428
1600
  "flow_edge"
1429
1601
  ]);
1430
1602
  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
- }
1482
1603
  /** Inline trace quotes keep paragraph/list layout while using retained resources. */
1483
1604
  function traceQuoteLink(href) {
1484
- const match = /^whiteboard:trace\/([^#]+)#(.+)$/.exec(canonicalLink(href));
1605
+ const match = /^review-trace:([^#]+)#(.+)$/.exec(href);
1485
1606
  return match ? {
1486
1607
  traceId: decodeURIComponent(match[1]),
1487
1608
  eventId: decodeURIComponent(match[2])
@@ -1508,8 +1629,8 @@ function resourceReferences(document) {
1508
1629
  return elements(document).flatMap((block) => {
1509
1630
  if (block.type !== "markdown") return block.type === "image" || block.type === "trace_quote" || block.type === "software_map" ? [block] : [];
1510
1631
  return [...markdownNodes(parseMarkdown(block.markdown))].flatMap((node) => {
1511
- if (node.type !== "link" || !canonicalLink(node.url ?? "").startsWith("whiteboard:trace/")) return [];
1512
- const quote = traceQuoteLink(node.url ?? "");
1632
+ if (node.type !== "link" || !node.url?.startsWith("review-trace:")) return [];
1633
+ const quote = traceQuoteLink(node.url);
1513
1634
  if (!quote) throw new ReviewInputError("Invalid trace quote link.");
1514
1635
  return [{
1515
1636
  type: "trace_quote",
@@ -1524,7 +1645,7 @@ function resourceReferences(document) {
1524
1645
  * `tolerant` skips malformed Markdown source links instead of rejecting, for
1525
1646
  * content that is already stored.
1526
1647
  */
1527
- function documentReferences(document, { tolerant = false, markdown = true, repositoryId } = {}) {
1648
+ function documentReferences(document, { tolerant = false, markdown = true } = {}) {
1528
1649
  const reject = (message) => {
1529
1650
  if (tolerant) return [];
1530
1651
  throw new ReviewInputError(message);
@@ -1543,29 +1664,20 @@ function documentReferences(document, { tolerant = false, markdown = true, repos
1543
1664
  if (element.type === "markdown" && !markdown) return [];
1544
1665
  if (element.type === "markdown") return [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
1545
1666
  if (node.type !== "link") 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}.`);
1667
+ const href = node.url ?? "";
1668
+ if (!/^review-source:/i.test(href)) {
1669
+ if (href.startsWith("review-trace:")) return [];
1550
1670
  if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
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.`);
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.`);
1552
1672
  }
1553
1673
  let target;
1554
1674
  try {
1555
- target = decodeURIComponent(href.slice(18));
1675
+ target = decodeURIComponent(href.slice(14));
1556
1676
  } catch {
1557
1677
  return reject("Invalid URL encoding in source link.");
1558
1678
  }
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}.`);
1567
1679
  const anchor = target.replace(/^(head|base|diff)\//i, (side) => side.toLowerCase());
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.");
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.");
1569
1681
  return select(anchor, element.pins).map((source) => ({
1570
1682
  id: `${element.id}:${node.url}`,
1571
1683
  source
@@ -1626,58 +1738,6 @@ function documentReferences(document, { tolerant = false, markdown = true, repos
1626
1738
  }
1627
1739
  /** All authored attachments, including code peeks, select the aligned diff. */
1628
1740
  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
- }
1681
1741
  /** Per-side read coordinates for endpoint validation and retained source quotes. */
1682
1742
  function sourceReferences(document, options = {}) {
1683
1743
  return selectionReferences(document, options).flatMap((ref) => sourceAnchors(ref.source).map((source) => ({
@@ -1686,10 +1746,8 @@ function sourceReferences(document, options = {}) {
1686
1746
  })));
1687
1747
  }
1688
1748
  /** Includes lenses and maps without inline ranges. */
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"));
1749
+ function hasCodeReferences(review) {
1750
+ return Boolean(sourceReferences(review.document).length || review.lenses?.length || resourceReferences(review.document).some((block) => block.type === "software_map"));
1693
1751
  }
1694
1752
  const documentSchema = z.array(blockSchema);
1695
1753
  /** Discriminated by type, like blockSchema, so a malformed insert reports
@@ -1852,27 +1910,20 @@ function adoptFlowUnits(document, allocate) {
1852
1910
  /** Mutate a private candidate. Only the store owns allocation and commits. */
1853
1911
  /** Assign server ids to an element tree that arrives without any. */
1854
1912
  function assignFreshIds(element, 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) {
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);
1863
1916
  const assign = (item, prefix) => {
1864
- if (item.id === void 0) item.id = allocate(prefix);
1865
- else if (fresh) throw new ReviewInputError("IDs are assigned by the server.");
1917
+ if (item.id !== void 0) throw new ReviewInputError("IDs are assigned by the server.");
1918
+ item.id = allocate(prefix);
1866
1919
  };
1867
- assign(element, idPrefix(element));
1868
- for (const child of children(element)) assignIds(child, allocate, fresh);
1869
1920
  if (element.type === "call_stack_diff") for (const frame of [...element.base, ...element.head]) assign(frame, "frame");
1870
1921
  if (element.type === "database_lens") for (const useCase of element.useCases) {
1871
1922
  assign(useCase, "case");
1872
1923
  for (const op of useCase.operations) assign(op, "operation");
1873
1924
  }
1874
1925
  }
1875
- function applyEdit(document, edit, allocate) {
1926
+ function applyEdit(document, edit, allocate, options = {}) {
1876
1927
  adoptFlowUnits(document, allocate);
1877
1928
  const locate = (id) => {
1878
1929
  const find = (siblings) => {
@@ -1901,7 +1952,7 @@ function applyEdit(document, edit, allocate) {
1901
1952
  if (afterId === element.id) throw new ReviewInputError("An element cannot follow itself.");
1902
1953
  if (afterId !== void 0 && !siblings.some((s) => s.id === afterId)) throw new ReviewInputError("afterId must identify a sibling.");
1903
1954
  if (from) from.splice(from.indexOf(element), 1);
1904
- const index = afterId === void 0 ? siblings.length : siblings.findIndex((s) => s.id === afterId) + 1;
1955
+ const index = afterId !== void 0 ? siblings.findIndex((s) => s.id === afterId) + 1 : !parent && !from && options.placement === "first" ? 0 : siblings.length;
1905
1956
  siblings.splice(index, 0, element);
1906
1957
  };
1907
1958
  if (edit.type === "insert") {
@@ -2495,114 +2546,54 @@ z.strictObject({
2495
2546
  function migrateDiffSelections(input) {
2496
2547
  return migrateNode(jsonValueSchema.parse(input));
2497
2548
  }
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
- }
2538
2549
  function migrateNode(value, attachment = false) {
2539
2550
  if (Array.isArray(value)) return value.map((child) => migrateNode(child, attachment));
2540
2551
  if (!isJsonObject(value)) return value;
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))]));
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))]));
2544
2589
  }
2545
2590
  //#endregion
2546
2591
  //#region src/stored-document-migration.ts
2547
2592
  /** Upgrade a saved or shared Review document to the current block schema at the
2548
2593
  * read/import boundary. New edits use the strict schema and never accept these
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. */
2594
+ * retired forms. */
2552
2595
  function migrateStoredDocument(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));
2596
+ return anchorStrings(dropSectionStatus(migrateDiffSelections(input)));
2606
2597
  }
2607
2598
  /** Lenses saved before ranges were anchor strings. */
2608
2599
  function migrateStoredLenses(input) {
@@ -2649,6 +2640,14 @@ function anchorStrings(value, blockPins) {
2649
2640
  if (pins !== void 0 && value.kind !== "ranges" && JSON.stringify(pins) !== JSON.stringify(blockPins)) migrated.pins = pins;
2650
2641
  return migrated;
2651
2642
  }
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
+ }
2652
2651
  /** A saved `file_lens` block, from before lenses left the document. */
2653
2652
  const legacyFileLensSchema = z.object({
2654
2653
  type: z.literal("file_lens"),
@@ -2687,4 +2686,4 @@ function liftFileLenses(document) {
2687
2686
  };
2688
2687
  }
2689
2688
  //#endregion
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 };
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 };