@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,8 +1,6 @@
1
- import { type JsonValue, isStringValue } from "@dev.fast/json";
2
1
  import {
3
2
  type Anchor,
4
3
  type LensSource,
5
- commitLinkTarget,
6
4
  parseAnchor,
7
5
  selectionProblem,
8
6
  sourceAnchors,
@@ -146,11 +144,6 @@ const targets = <Repository extends Record<string, z.ZodType>>(
146
144
  repository: Repository,
147
145
  ) =>
148
146
  z.discriminatedUnion("kind", [
149
- /** The project of the repository, no change: a plan. */
150
- z.strictObject({
151
- kind: z.literal("project"),
152
- ...repository,
153
- }),
154
147
  z.strictObject({
155
148
  kind: z.literal("worktree"),
156
149
  ...repository,
@@ -166,16 +159,8 @@ const targets = <Repository extends Record<string, z.ZodType>>(
166
159
 
167
160
  export const reviewTargetSchema = targets({ repositoryId: label });
168
161
 
169
- /** Targets as agents write them: a checkout path instead of a repository id,
170
- * plus a PR by URL or number. */
171
- export const pathTargetSchema = z.discriminatedUnion("kind", [
172
- ...targets({ repositoryPath: label }).options,
173
- z.strictObject({
174
- kind: z.literal("pullRequest"),
175
- repositoryPath: label,
176
- pullRequest: z.union([label, z.number().int().positive()]),
177
- }),
178
- ]);
162
+ /** How agents name a target: by the checkout's path, registered on acceptance. */
163
+ export const pathTargetSchema = targets({ repositoryPath: label });
179
164
 
180
165
  export type ReviewTarget = z.infer<typeof reviewTargetSchema>;
181
166
 
@@ -190,99 +175,11 @@ const unitTypes = new Set(["step", "flow_node", "flow_edge"]);
190
175
  export const isUnit = (element: Element): element is Unit =>
191
176
  unitTypes.has(element.type);
192
177
 
193
- /** Rewrites old review-source: and review-trace: links to their whiteboard: form. */
194
- export function canonicalLink(href: string): string {
195
- return href.replace(
196
- /^review-(source|trace):/i,
197
- (_, kind: string) => `whiteboard:${kind.toLowerCase()}/`,
198
- );
199
- }
200
-
201
- /** Markdown link destinations and definitions that begin with `target`, a
202
- * regex source; the match's first group is what comes before it. */
203
- const linkDestination = (target: string) =>
204
- new RegExp(String.raw`(\]\(\s*<?|^\s*\[[^\]\n]+\]:\s*<?)` + target, "gim");
205
-
206
- /** Every Markdown text in `value`, rewritten. */
207
- function rewriteMarkdown<T>(
208
- value: T,
209
- rewrite: (markdown: string) => string,
210
- ): T {
211
- // SAFETY: a JSON round trip of stored or parsed JSON, rewriting only
212
- // Markdown text.
213
- return JSON.parse(JSON.stringify(value), (key, field: JsonValue) =>
214
- key === "markdown" && isStringValue(field) ? rewrite(field) : field,
215
- ) as T;
216
- }
217
-
218
- const retiredLinks = linkDestination("review-(source|trace):");
219
-
220
- /** Rewrites old link prefixes in Markdown link destinations and definitions. */
221
- export function respellLinks(markdown: string): string {
222
- return markdown.replace(
223
- retiredLinks,
224
- (_, before: string, kind: string) =>
225
- `${before}whiteboard:${kind.toLowerCase()}/`,
226
- );
227
- }
228
-
229
- /** Every Markdown text in `document` with its links spelled the current
230
- * way. */
231
- export function respellDocumentLinks<T>(document: T): T {
232
- return rewriteMarkdown(document, respellLinks);
233
- }
234
-
235
- export function respellEditLinks(edit: Edit): Edit {
236
- return rewriteMarkdown(edit, respellLinks);
237
- }
238
-
239
- /** Names a block from outside its board. Block ids are unique only within a board. */
240
- export type BlockRef = { board: string; block: string };
241
-
242
- export type BoardRef = BlockRef | { board: string; block?: undefined };
243
-
244
- /** Parses whiteboard:board/<id> or whiteboard:board/<id>#<blockId>. */
245
- export function boardLink(href: string): BoardRef | undefined {
246
- const match = /^whiteboard:board\/([^#/]+)(?:#(.+))?$/.exec(href);
247
-
248
- if (!match) return undefined;
249
-
250
- try {
251
- const board = decodeURIComponent(match[1]!);
252
-
253
- return match[2] === undefined
254
- ? { board }
255
- : { board, block: decodeURIComponent(match[2]) };
256
- } catch {
257
- return undefined;
258
- }
259
- }
260
-
261
- /** A link from a block to a board or one of its blocks. */
262
- export interface BoardLinkRef {
263
- from: BlockRef;
264
- to: BoardRef;
265
- }
266
-
267
- /** Every board link in a document. Malformed links are skipped; writes reject them. */
268
- export function boardLinks(board: string, document: Block[]): BoardLinkRef[] {
269
- return elements(document).flatMap((element) =>
270
- element.type === "markdown"
271
- ? [...markdownNodes(parseMarkdown(element.markdown))].flatMap((node) => {
272
- const to =
273
- node.type === "link" ? boardLink(node.url ?? "") : undefined;
274
-
275
- return to ? [{ from: { board, block: element.id! }, to }] : [];
276
- })
277
- : [],
278
- );
279
- }
280
-
281
178
  /** Inline trace quotes keep paragraph/list layout while using retained resources. */
282
179
  export function traceQuoteLink(
283
180
  href: string,
284
181
  ): { traceId: string; eventId: string } | undefined {
285
- const match = /^whiteboard:trace\/([^#]+)#(.+)$/.exec(canonicalLink(href));
182
+ const match = /^review-trace:([^#]+)#(.+)$/.exec(href);
286
183
 
287
184
  return match
288
185
  ? {
@@ -318,12 +215,9 @@ export function resourceReferences(document: Block[]): Block[] {
318
215
 
319
216
  return [...markdownNodes(parseMarkdown(block.markdown))].flatMap(
320
217
  (node): Block[] => {
321
- if (
322
- node.type !== "link" ||
323
- !canonicalLink(node.url ?? "").startsWith("whiteboard:trace/")
324
- )
218
+ if (node.type !== "link" || !node.url?.startsWith("review-trace:"))
325
219
  return [];
326
- const quote = traceQuoteLink(node.url ?? "");
220
+ const quote = traceQuoteLink(node.url);
327
221
 
328
222
  if (!quote) throw new ReviewInputError("Invalid trace quote link.");
329
223
 
@@ -343,13 +237,7 @@ function documentReferences(
343
237
  {
344
238
  tolerant = false,
345
239
  markdown = true,
346
- repositoryId,
347
- }: {
348
- tolerant?: boolean;
349
- markdown?: boolean;
350
- /** The board's repository, which commit links read from. */
351
- repositoryId?: string;
352
- } = {},
240
+ }: { tolerant?: boolean; markdown?: boolean } = {},
353
241
  ): {
354
242
  id: string;
355
243
  source: LensSource;
@@ -386,53 +274,27 @@ function documentReferences(
386
274
  return [...markdownNodes(parseMarkdown(element.markdown))].flatMap(
387
275
  (node) => {
388
276
  if (node.type !== "link") return [];
389
- const href = canonicalLink(node.url ?? "");
277
+ const href = node.url ?? "";
390
278
 
391
- if (!href.startsWith("whiteboard:source/")) {
279
+ if (!/^review-source:/i.test(href)) {
392
280
  // Trace links are checked by resourceReferences.
393
- if (href.startsWith("whiteboard:trace/")) return [];
394
-
395
- // Board links are checked against the store on write.
396
- if (href.startsWith("whiteboard:board/"))
397
- return boardLink(href)
398
- ? []
399
- : reject(
400
- `Use whiteboard:board/<sessionId> or whiteboard:board/<sessionId>#<blockId> to link a board, in block ${element.id}.`,
401
- );
281
+ if (href.startsWith("review-trace:")) return [];
402
282
 
403
283
  if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
404
284
 
405
285
  return reject(
406
- `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.`,
286
+ `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.`,
407
287
  );
408
288
  }
409
289
 
410
290
  let target: string;
411
291
 
412
292
  try {
413
- target = decodeURIComponent(
414
- href.slice("whiteboard:source/".length),
415
- );
293
+ target = decodeURIComponent(href.slice("review-source:".length));
416
294
  } catch {
417
295
  return reject("Invalid URL encoding in source link.");
418
296
  }
419
297
 
420
- // A commit of the board's repository: the commit is the link's pins.
421
- const commit = commitLinkTarget(target);
422
-
423
- if (commit)
424
- return repositoryId
425
- ? select(commit.anchor, {
426
- repositoryId,
427
- head: commit.commit,
428
- }).map((source) => ({
429
- id: `${element.id}:${node.url}`,
430
- source,
431
- }))
432
- : reject(
433
- `A commit link names a commit of the board's repository, and this board has none, in block ${element.id}.`,
434
- );
435
-
436
298
  // Links share the anchor grammar blocks use.
437
299
  const anchor = target.replace(/^(head|base|diff)\//i, (side) =>
438
300
  side.toLowerCase(),
@@ -440,7 +302,7 @@ function documentReferences(
440
302
 
441
303
  if (!parseAnchor(anchor))
442
304
  return reject(
443
- "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.",
305
+ "Use review-source:head/path#L10-L24 (or base, or diff/path#L84-R90 across sides) for a source link.",
444
306
  );
445
307
 
446
308
  return select(anchor, element.pins).map((source) => ({
@@ -552,108 +414,10 @@ function documentReferences(
552
414
  /** All authored attachments, including code peeks, select the aligned diff. */
553
415
  export const selectionReferences = documentReferences;
554
416
 
555
- const commitLinkDestination = linkDestination(
556
- String.raw`whiteboard:source\/([0-9a-f]{7,40})\/`,
557
- );
558
-
559
- /** The commits a document's Markdown commit links name, as written. */
560
- export function commitLinkCommits(document: Block[]): string[] {
561
- const commits = new Set<string>();
562
-
563
- JSON.stringify(document, (key, value: JsonValue) => {
564
- if (key === "markdown" && isStringValue(value))
565
- for (const match of value.matchAll(commitLinkDestination))
566
- commits.add(match[2]!);
567
-
568
- return value;
569
- });
570
-
571
- return [...commits];
572
- }
573
-
574
- /** Rewrites the commits Markdown commit links name, as a short commit
575
- * becomes the full one on save. */
576
- export function respellCommitLinks(
577
- document: Block[],
578
- commits: ReadonlyMap<string, string>,
579
- ): Block[] {
580
- return rewriteMarkdown(document, (markdown) =>
581
- markdown.replace(
582
- commitLinkDestination,
583
- (link, before: string, commit: string) =>
584
- `${before}whiteboard:source/${commits.get(commit) ?? commit}/`,
585
- ),
586
- );
587
- }
588
-
589
- /** Blocks that carry pins of their own for the references in them. */
590
- const pinnedBlockTypes = new Set([
591
- "markdown",
592
- "code_peek",
593
- "sequence",
594
- "flow_diagram",
595
- "call_stack_diff",
596
- "database_lens",
597
- ]);
598
-
599
- const sideLinkDestination = linkDestination(
600
- String.raw`whiteboard:source\/(head|base)\/`,
601
- );
602
-
603
- /**
604
- * Makes every reference that reads at the document's pins carry them
605
- * itself, for a board about to lose its pins: Markdown head/ and base/
606
- * links become commit links, and a block whose references still lean on
607
- * the document's pins (any anchor, or a diff/ link) takes them as its own.
608
- * Nothing changes what it points at.
609
- */
610
- export function pinDocumentReferences(document: Block[], pins: Pins): Block[] {
611
- const own: SourcePins = {
612
- repositoryId: pins.repositoryId,
613
- base: pins.base,
614
- head: pins.head,
615
- };
616
-
617
- const pinned = structuredClone(document);
618
-
619
- for (const element of elements(pinned)) {
620
- if (!pinnedBlockTypes.has(element.type)) continue;
621
-
622
- // SAFETY: every pinned block type has optional source pins of its own.
623
- const block = element as Block & { pins?: SourcePins };
624
-
625
- if (block.pins) continue;
626
-
627
- if (block.type === "markdown")
628
- block.markdown = respellLinks(block.markdown).replace(
629
- sideLinkDestination,
630
- (_, before: string, side: string) =>
631
- `${before}whiteboard:source/${side.toLowerCase() === "base" ? pins.base : pins.head}/`,
632
- );
633
-
634
- if (
635
- documentReferences([block], { tolerant: true }).some(
636
- (reference) => !reference.source.pins,
637
- )
638
- )
639
- block.pins = own;
640
- }
641
-
642
- return pinned;
643
- }
644
-
645
- /** The repository a board's commit links read from: its target's. */
646
- export function boardRepositoryId(board: {
647
- target?: { repositoryId: string };
648
- pins?: { repositoryId: string };
649
- }): string | undefined {
650
- return board.target?.repositoryId ?? board.pins?.repositoryId;
651
- }
652
-
653
417
  /** Per-side read coordinates for endpoint validation and retained source quotes. */
654
418
  export function sourceReferences(
655
419
  document: Block[],
656
- options: { tolerant?: boolean; repositoryId?: string } = {},
420
+ options: { tolerant?: boolean } = {},
657
421
  ) {
658
422
  return selectionReferences(document, options).flatMap((ref) =>
659
423
  sourceAnchors(ref.source).map((source) => ({ ...ref, source })),
@@ -661,16 +425,12 @@ export function sourceReferences(
661
425
  }
662
426
 
663
427
  /** Includes lenses and maps without inline ranges. */
664
- /** Whether a board reads code at its own pins: a commit link or a block
665
- * with pins of its own names its commits itself. */
666
- export function readsAtBoardPins(review: {
428
+ export function hasCodeReferences(review: {
667
429
  document: Block[];
668
430
  lenses?: readonly Lens[];
669
431
  }): boolean {
670
432
  return Boolean(
671
- sourceReferences(review.document, { tolerant: true }).some(
672
- (reference) => !reference.source.pins,
673
- ) ||
433
+ sourceReferences(review.document).length ||
674
434
  review.lenses?.length ||
675
435
  resourceReferences(review.document).some(
676
436
  (block) => block.type === "software_map",
@@ -754,8 +514,6 @@ export interface EditSummary {
754
514
  units?: string[];
755
515
  /** The agent whose courier draws this edit, when the host could tell. */
756
516
  activityId?: string;
757
- /** The Ask thread that made this edit, if any. */
758
- threadId?: string;
759
517
  }
760
518
 
761
519
  /** A component an edit wrote, named so the author can address it. */
@@ -953,33 +711,18 @@ export function assignFreshIds(
953
711
  element: Element,
954
712
  allocate: (prefix: string) => string,
955
713
  ): void {
956
- assignIds(element, allocate, true);
957
- }
714
+ if (element.id !== undefined)
715
+ throw new ReviewInputError("IDs are assigned by the server.");
716
+ element.id = allocate(idPrefix(element));
958
717
 
959
- /** Give ids to the parts of an element tree that have none, keeping the
960
- * rest: a person's save names the blocks it kept and leaves new ones bare. */
961
- export function assignMissingIds(
962
- element: Element,
963
- allocate: (prefix: string) => string,
964
- ): void {
965
- assignIds(element, allocate, false);
966
- }
718
+ for (const child of children(element)) assignFreshIds(child, allocate);
967
719
 
968
- function assignIds(
969
- element: Element,
970
- allocate: (prefix: string) => string,
971
- fresh: boolean,
972
- ): void {
973
720
  const assign = (item: { id?: string }, prefix: string) => {
974
- if (item.id === undefined) item.id = allocate(prefix);
975
- else if (fresh)
721
+ if (item.id !== undefined)
976
722
  throw new ReviewInputError("IDs are assigned by the server.");
723
+ item.id = allocate(prefix);
977
724
  };
978
725
 
979
- assign(element, idPrefix(element));
980
-
981
- for (const child of children(element)) assignIds(child, allocate, fresh);
982
-
983
726
  if (element.type === "call_stack_diff")
984
727
  for (const frame of [...element.base, ...element.head])
985
728
  assign(frame, "frame");
@@ -992,10 +735,16 @@ function assignIds(
992
735
  }
993
736
  }
994
737
 
738
+ export interface ApplyEditOptions {
739
+ /** Where a root-level insert with no placement lands: the end by default. */
740
+ placement?: "first" | "last";
741
+ }
742
+
995
743
  export function applyEdit(
996
744
  document: Block[],
997
745
  edit: Edit,
998
746
  allocate: (prefix: string) => string,
747
+ options: ApplyEditOptions = {},
999
748
  ): Applied {
1000
749
  adoptFlowUnits(document, allocate);
1001
750
 
@@ -1059,10 +808,14 @@ export function applyEdit(
1059
808
  // Resolve the destination before detaching, then compute its final position.
1060
809
  if (from) from.splice(from.indexOf(element), 1);
1061
810
 
811
+ // A running log reads newest first, so a root insert with no placement
812
+ // may lead the document; a move or a unit inside a diagram never does.
1062
813
  const index =
1063
- afterId === undefined
1064
- ? siblings.length
1065
- : siblings.findIndex((s) => s.id === afterId) + 1;
814
+ afterId !== undefined
815
+ ? siblings.findIndex((s) => s.id === afterId) + 1
816
+ : !parent && !from && options.placement === "first"
817
+ ? 0
818
+ : siblings.length;
1066
819
 
1067
820
  siblings.splice(index, 0, element);
1068
821
  };
@@ -1,11 +1,10 @@
1
1
  import { parseGitRemote } from "@dev.fast/local-vcs";
2
- import type { PullRequestState } from "@dev.fast/review-protocol";
3
2
  import { errorMessage } from "@dev.fast/trace-core";
4
3
  import { z } from "zod";
5
4
 
6
5
  import { ReviewInputError } from "./document.js";
7
6
  import type { PullRequestProvider } from "./provider.js";
8
- import { GitHubRefusedRead, firstLine } from "./pull-request.js";
7
+ import { firstLine } from "./pull-request.js";
9
8
 
10
9
  const GH_TIMEOUT_MS = 20_000;
11
10
 
@@ -21,24 +20,8 @@ const metadataSchema = z.object({
21
20
  baseRefOid: z.string().optional(),
22
21
  headRefName: z.string().min(1),
23
22
  isCrossRepository: z.boolean(),
24
- state: z.enum(["OPEN", "CLOSED", "MERGED"]).optional(),
25
- isDraft: z.boolean().optional(),
26
23
  });
27
24
 
28
- /** gh's state and draft flag as one state. */
29
- function ghState(
30
- state: "OPEN" | "CLOSED" | "MERGED" | undefined,
31
- isDraft: boolean | undefined,
32
- ): PullRequestState | undefined {
33
- if (state === "MERGED") return "merged";
34
-
35
- if (state === "CLOSED") return "closed";
36
-
37
- if (state === "OPEN") return isDraft ? "draft" : "open";
38
-
39
- return undefined;
40
- }
41
-
42
25
  function unreadable(url: string, ghFailure: string, fallback: string) {
43
26
  return new ReviewInputError(
44
27
  `Could not read ${url}: gh failed (${ghFailure}) and ${fallback}. Run \`gh auth status\` and sign in with an account that can read the repository, then retry.`,
@@ -71,27 +54,24 @@ export const githubProvider: PullRequestProvider = {
71
54
  [
72
55
  "pr",
73
56
  "view",
74
- { value: String(number) },
57
+ String(number),
75
58
  "--repo",
76
- { value: `${host}/${slug}` },
59
+ `${host}/${slug}`,
77
60
  "--json",
78
- "number,title,baseRefName,baseRefOid,headRefName,isCrossRepository,state,isDraft",
61
+ "number,title,baseRefName,baseRefOid,headRefName,isCrossRepository",
79
62
  ],
80
63
  { timeoutMs: GH_TIMEOUT_MS },
81
64
  );
82
65
 
83
- const { headRefName, isCrossRepository, state, isDraft, ...metadata } =
66
+ const { headRefName, isCrossRepository, ...metadata } =
84
67
  metadataSchema.parse(JSON.parse(stdout));
85
68
 
86
- const pullRequestState = ghState(state, isDraft);
87
-
88
69
  return {
89
70
  host,
90
71
  slug,
91
72
  headRef,
92
73
  ...metadata,
93
74
  headBranch: isCrossRepository ? undefined : headRefName,
94
- ...(pullRequestState && { state: pullRequestState }),
95
75
  };
96
76
  } catch (error) {
97
77
  ghFailure = firstLine(errorMessage(error));
@@ -130,21 +110,15 @@ export const githubProvider: PullRequestProvider = {
130
110
  404,
131
111
  );
132
112
 
133
- if (response.status === 403 || response.status === 429) {
134
- const reset = Number(response.headers.get("x-ratelimit-reset"));
135
-
136
- throw new GitHubRefusedRead(
137
- unreadable(
138
- url,
139
- ghFailure,
140
- response.headers.get("x-ratelimit-remaining") === "0" ||
141
- response.status === 429
142
- ? "the unauthenticated GitHub API rate limit is exhausted"
143
- : "the GitHub API refused the request",
144
- ).message,
145
- reset > 0 ? reset * 1000 : Date.now() + 15 * 60_000,
113
+ if (response.status === 403 || response.status === 429)
114
+ throw unreadable(
115
+ url,
116
+ ghFailure,
117
+ response.headers.get("x-ratelimit-remaining") === "0" ||
118
+ response.status === 429
119
+ ? "the unauthenticated GitHub API rate limit is exhausted"
120
+ : "the GitHub API refused the request",
146
121
  );
147
- }
148
122
 
149
123
  if (!response.ok)
150
124
  throw unreadable(
@@ -166,9 +140,6 @@ export const githubProvider: PullRequestProvider = {
166
140
  ref: z.string().min(1),
167
141
  repo: z.object({ id: z.number() }).nullable(),
168
142
  }),
169
- state: z.enum(["open", "closed"]).optional(),
170
- draft: z.boolean().optional(),
171
- merged_at: z.string().nullable().optional(),
172
143
  })
173
144
  .safeParse(await response.json().catch(() => undefined));
174
145
 
@@ -187,15 +158,6 @@ export const githubProvider: PullRequestProvider = {
187
158
  parsed.data.head.repo?.id === parsed.data.base.repo.id
188
159
  ? parsed.data.head.ref
189
160
  : undefined,
190
- ...(parsed.data.state && {
191
- state: parsed.data.merged_at
192
- ? "merged"
193
- : parsed.data.state === "closed"
194
- ? "closed"
195
- : parsed.data.draft
196
- ? "draft"
197
- : "open",
198
- }),
199
161
  };
200
162
  },
201
163