@dev.fast/whiteboard 0.0.0-stage → 0.2.1-preview.20261005.98

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 (247) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -2
  3. package/THIRD_PARTY_NOTICES.md +93 -0
  4. package/dist/account-alias-DOHN1RSH.js +973 -0
  5. package/dist/agent-cli-ChOq0ZuO.js +117 -0
  6. package/dist/agent-cli-DzWfZr6G.js +2 -0
  7. package/dist/agent-client-CPQo7iTI.js +305 -0
  8. package/dist/authoring-tools-Cr6Kpjsx.js +234 -0
  9. package/dist/build-info.json +1 -0
  10. package/dist/cli-hVgHcdsv.js +47 -0
  11. package/dist/cli-runner-D8y2_luU.js +5095 -0
  12. package/dist/cli.d.ts +1 -0
  13. package/dist/cli.js +128 -0
  14. package/dist/client-DFoh50W3.js +950 -0
  15. package/dist/desktop-discovery-DBO-PM2V.js +164 -0
  16. package/dist/error-message-OtiDonty.js +6 -0
  17. package/dist/fs-utils-BMPLt0cr.js +22 -0
  18. package/dist/fuzzy-match-BoAmcyak.js +30 -0
  19. package/dist/headless-host-BJxf4dUU.js +94 -0
  20. package/dist/input-error-OLgB_h31.js +11 -0
  21. package/dist/local-data-CxLk75rx.js +4491 -0
  22. package/dist/mcp-QeF8vkdf.js +140 -0
  23. package/dist/package-paths-B6-zxvIO.js +47 -0
  24. package/dist/process-error-telemetry-v9e5h6D7.js +3952 -0
  25. package/dist/profile-4ry2f3AM.js +76 -0
  26. package/dist/profile-Bh5FIrmf.js +2 -0
  27. package/dist/request-origin-D_4QIdoN.js +811 -0
  28. package/dist/review-agent-traces-DNJFYzc8.js +3866 -0
  29. package/dist/review-home-paths-6zZH-1c9.js +2560 -0
  30. package/dist/review-telemetry-DEFnQsQ2.js +1195 -0
  31. package/dist/runtime-CKiWhGrf.js +52 -0
  32. package/dist/runtime.d.ts +9 -0
  33. package/dist/runtime.js +2 -0
  34. package/dist/s3-SzLpeCG6.js +345 -0
  35. package/dist/s3-config-BgxkSoOy.js +2 -0
  36. package/dist/s3-config-CAxhO9u_.js +676 -0
  37. package/dist/server/desktop-host.d.ts +4 -0
  38. package/dist/server/desktop-host.js +1702 -0
  39. package/dist/server-discovery-Dbzn3w6Z.js +66 -0
  40. package/dist/sharing/index.d.ts +1977 -0
  41. package/dist/sharing/index.js +2 -0
  42. package/dist/src-CGP5ytbV.js +283 -0
  43. package/dist/src-CmdiBL20.js +3676 -0
  44. package/dist/src-DnwdaQ2r.js +865 -0
  45. package/dist/src-X9phtB2j.js +68 -0
  46. package/dist/stored-document-migration-DHeMpLMb.js +2671 -0
  47. package/dist/tool-failure-C8zB73HV.js +98 -0
  48. package/dist/tutorial-trace-DrBvkaeL.js +43 -0
  49. package/instructions/authoring.md +36 -0
  50. package/instructions/file-lenses.md +13 -0
  51. package/instructions/scratchpad.md +23 -0
  52. package/instructions/trace-archaeology.md +113 -0
  53. package/onboarding.md +8 -0
  54. package/package.json +106 -3
  55. package/src/agent-selection.ts +100 -0
  56. package/src/agent-session-ref.ts +80 -0
  57. package/src/ask/agents.ts +351 -0
  58. package/src/ask/checkout-files.ts +47 -0
  59. package/src/ask/file-refs.ts +111 -0
  60. package/src/ask/pi-mcp.ts +154 -0
  61. package/src/ask/protocol.ts +244 -0
  62. package/src/ask/thread-state.ts +350 -0
  63. package/src/ask/thread.ts +1400 -0
  64. package/src/ask/threads.ts +219 -0
  65. package/src/ask/watch.ts +79 -0
  66. package/src/cli-install.ts +1041 -0
  67. package/src/cli-runner.ts +1461 -0
  68. package/src/cli-runtime-info.ts +35 -0
  69. package/src/cli.ts +222 -0
  70. package/src/connect-prompts.ts +248 -0
  71. package/src/cursor-deeplink.ts +11 -0
  72. package/src/desktop-discovery.ts +355 -0
  73. package/src/diff-selection-migration.ts +63 -0
  74. package/src/embedded-posthog-key.ts +6 -0
  75. package/src/error-telemetry.ts +247 -0
  76. package/src/evidence.ts +14 -0
  77. package/src/exception-telemetry.ts +126 -0
  78. package/src/fixtures/blocks/call_stack_diff.json +27 -0
  79. package/src/fixtures/blocks/callout.json +11 -0
  80. package/src/fixtures/blocks/code.json +9 -0
  81. package/src/fixtures/blocks/code_peek.json +8 -0
  82. package/src/fixtures/blocks/database_lens.json +51 -0
  83. package/src/fixtures/blocks/divider.json +1 -0
  84. package/src/fixtures/blocks/fixtures.ts +33 -0
  85. package/src/fixtures/blocks/flow_diagram.json +36 -0
  86. package/src/fixtures/blocks/ids.ts +8 -0
  87. package/src/fixtures/blocks/image.json +9 -0
  88. package/src/fixtures/blocks/markdown.json +7 -0
  89. package/src/fixtures/blocks/section.json +11 -0
  90. package/src/fixtures/blocks/sequence.json +31 -0
  91. package/src/fixtures/blocks/software_map.json +7 -0
  92. package/src/fixtures/blocks/trace_quote.json +9 -0
  93. package/src/fixtures/blocks/tutorial.json +51 -0
  94. package/src/fs-utils.ts +32 -0
  95. package/src/fuzzy-match.ts +94 -0
  96. package/src/install.ts +29 -0
  97. package/src/legacy-skills.ts +133 -0
  98. package/src/lens-selection.ts +230 -0
  99. package/src/markdown-latex-math.ts +230 -0
  100. package/src/markdown.ts +81 -0
  101. package/src/package-paths.ts +53 -0
  102. package/src/posthog-capture-client.ts +610 -0
  103. package/src/review-api/README.md +295 -0
  104. package/src/review-api/activity.ts +340 -0
  105. package/src/review-api/agent-cli.ts +219 -0
  106. package/src/review-api/agent-client.ts +188 -0
  107. package/src/review-api/anchor-quotes.ts +88 -0
  108. package/src/review-api/ask-history.ts +242 -0
  109. package/src/review-api/authoring-tools.ts +211 -0
  110. package/src/review-api/blocks/call_stack_diff.ts +75 -0
  111. package/src/review-api/blocks/callout.ts +24 -0
  112. package/src/review-api/blocks/code.ts +9 -0
  113. package/src/review-api/blocks/code_peek.ts +13 -0
  114. package/src/review-api/blocks/database_lens.ts +150 -0
  115. package/src/review-api/blocks/definition.ts +40 -0
  116. package/src/review-api/blocks/divider.ts +6 -0
  117. package/src/review-api/blocks/flow_diagram.ts +111 -0
  118. package/src/review-api/blocks/image.ts +10 -0
  119. package/src/review-api/blocks/index.ts +90 -0
  120. package/src/review-api/blocks/markdown.ts +17 -0
  121. package/src/review-api/blocks/section.ts +24 -0
  122. package/src/review-api/blocks/sequence.ts +56 -0
  123. package/src/review-api/blocks/software_map.ts +9 -0
  124. package/src/review-api/blocks/trace_quote.ts +10 -0
  125. package/src/review-api/blocks/tutorial.ts +39 -0
  126. package/src/review-api/checkout-fs.ts +14 -0
  127. package/src/review-api/client.ts +1 -0
  128. package/src/review-api/comparison-coverage.ts +304 -0
  129. package/src/review-api/component-reference.ts +30 -0
  130. package/src/review-api/diff-lenses.ts +175 -0
  131. package/src/review-api/document-headings.ts +51 -0
  132. package/src/review-api/document-text.ts +200 -0
  133. package/src/review-api/document.ts +906 -0
  134. package/src/review-api/file-lenses.ts +100 -0
  135. package/src/review-api/http.ts +1877 -0
  136. package/src/review-api/image-decode.ts +29 -0
  137. package/src/review-api/input-error.ts +9 -0
  138. package/src/review-api/instructions.ts +89 -0
  139. package/src/review-api/lens-alignment.ts +52 -0
  140. package/src/review-api/local-data.ts +1837 -0
  141. package/src/review-api/map-input.ts +154 -0
  142. package/src/review-api/mcp-client-agent.ts +32 -0
  143. package/src/review-api/mcp.ts +240 -0
  144. package/src/review-api/origin.ts +43 -0
  145. package/src/review-api/profile.ts +146 -0
  146. package/src/review-api/public-tools.ts +102 -0
  147. package/src/review-api/pull-request.ts +389 -0
  148. package/src/review-api/read-schemas.ts +85 -0
  149. package/src/review-api/recovery.ts +2 -0
  150. package/src/review-api/request-origin.ts +33 -0
  151. package/src/review-api/review-progress.ts +382 -0
  152. package/src/review-api/status-tool.ts +10 -0
  153. package/src/review-api/store-schema.ts +22 -0
  154. package/src/review-api/store.ts +1647 -0
  155. package/src/review-api/tool-failure.ts +64 -0
  156. package/src/review-api/trace-schema.ts +13 -0
  157. package/src/review-api/traces.ts +119 -0
  158. package/src/review-api/unsupported-files.integration.ts +91 -0
  159. package/src/review-api/workspaces.ts +692 -0
  160. package/src/review-api/worktree-source.ts +170 -0
  161. package/src/review-api/worktree-structural.integration.ts +320 -0
  162. package/src/review-app-launcher.ts +432 -0
  163. package/src/review-app-picker.ts +168 -0
  164. package/src/review-app.ts +134 -0
  165. package/src/review-bundled-tools.ts +249 -0
  166. package/src/review-checkout-paths.ts +37 -0
  167. package/src/review-diff-files.ts +89 -0
  168. package/src/review-head-checkout.ts +300 -0
  169. package/src/review-home-paths.ts +73 -0
  170. package/src/review-info.ts +59 -0
  171. package/src/review-instances.ts +113 -0
  172. package/src/review-logger.ts +183 -0
  173. package/src/review-preferences.ts +88 -0
  174. package/src/review-prepare.ts +291 -0
  175. package/src/review-stack.ts +112 -0
  176. package/src/review-telemetry.ts +1066 -0
  177. package/src/runtime.ts +74 -0
  178. package/src/server/account-alias.ts +30 -0
  179. package/src/server/bounded-stream.ts +34 -0
  180. package/src/server/bug-report.ts +301 -0
  181. package/src/server/client-error-budget.ts +45 -0
  182. package/src/server/crash-report.ts +273 -0
  183. package/src/server/desktop-host-shutdown.ts +64 -0
  184. package/src/server/desktop-host.ts +190 -0
  185. package/src/server/desktop-server.ts +703 -0
  186. package/src/server/diffr-config.ts +509 -0
  187. package/src/server/diffr-languages.ts +97 -0
  188. package/src/server/global-verb-relay.ts +190 -0
  189. package/src/server/headless-host.ts +149 -0
  190. package/src/server/hono-http.ts +162 -0
  191. package/src/server/http-json.ts +24 -0
  192. package/src/server/json-review-reporting.ts +174 -0
  193. package/src/server/process-error-telemetry.ts +174 -0
  194. package/src/server/review-api-parsers.ts +90 -0
  195. package/src/server/review-lifecycle-telemetry.ts +73 -0
  196. package/src/server/review-open-watchdog.ts +46 -0
  197. package/src/server/review-server-core.ts +290 -0
  198. package/src/server/structural-comparisons.ts +153 -0
  199. package/src/server/structural-diff.ts +186 -0
  200. package/src/server/tutorial-service.ts +241 -0
  201. package/src/server/ui-telemetry.ts +198 -0
  202. package/src/server-discovery.ts +95 -0
  203. package/src/session-markers.ts +132 -0
  204. package/src/sharing/auth.ts +41 -0
  205. package/src/sharing/cli.ts +83 -0
  206. package/src/sharing/client.ts +334 -0
  207. package/src/sharing/export.ts +195 -0
  208. package/src/sharing/host.ts +316 -0
  209. package/src/sharing/import.ts +787 -0
  210. package/src/sharing/index.ts +16 -0
  211. package/src/sharing/repository.ts +145 -0
  212. package/src/sharing/routes.ts +34 -0
  213. package/src/slug.ts +23 -0
  214. package/src/software-map-diff-counts.ts +517 -0
  215. package/src/software-map-model.ts +1147 -0
  216. package/src/software-map-topology-diff.ts +260 -0
  217. package/src/source.ts +92 -0
  218. package/src/startup-trace.ts +232 -0
  219. package/src/stored-document-migration.ts +151 -0
  220. package/src/telemetry-clean-text.ts +257 -0
  221. package/src/telemetry-config.ts +298 -0
  222. package/src/telemetry-debug-sink.ts +38 -0
  223. package/src/telemetry.ts +13 -0
  224. package/src/trace-cli.ts +156 -0
  225. package/src/trace-storage-cli.ts +509 -0
  226. package/src/tutorial-conversation.ts +18 -0
  227. package/src/ui-telemetry-events.ts +765 -0
  228. package/src/unified-diff.ts +71 -0
  229. package/src/viewed-coverage.ts +259 -0
  230. package/src/windows-cli.ts +125 -0
  231. package/tutorial/document.json +275 -0
  232. package/tutorial/runtime-manifest.json +12 -0
  233. package/tutorial/sample-service/package.json +9 -0
  234. package/tutorial/sample-service/src/api/checkout-api.ts +17 -0
  235. package/tutorial/sample-service/src/app.ts +26 -0
  236. package/tutorial/sample-service/src/database/schema.ts +10 -0
  237. package/tutorial/sample-service/src/fulfillment/fulfillment-queue.ts +15 -0
  238. package/tutorial/sample-service/src/fulfillment/fulfillment-worker.ts +24 -0
  239. package/tutorial/sample-service/src/inventory/inventory-service.ts +11 -0
  240. package/tutorial/sample-service/src/orders/order-service.ts +34 -0
  241. package/tutorial/sample-service/src/orders/order.ts +21 -0
  242. package/tutorial/sample-service/src/orders/orders-repository.ts +24 -0
  243. package/tutorial/sample-service/src/payments/payment-gateway.ts +12 -0
  244. package/tutorial/sample-service/src/shipping/shipping-gateway.ts +15 -0
  245. package/tutorial/sample-service/tsconfig.json +12 -0
  246. package/tutorial/software-map.json +139 -0
  247. package/tutorial/trace.json +20 -0
@@ -0,0 +1,906 @@
1
+ import {
2
+ type Anchor,
3
+ type LensSource,
4
+ parseAnchor,
5
+ selectionProblem,
6
+ sourceAnchors,
7
+ } from "@review/lens-selection.js";
8
+ import {
9
+ markdownNodes,
10
+ markdownText,
11
+ parseMarkdown,
12
+ } from "@review/markdown.js";
13
+ import {
14
+ type FileLineRange,
15
+ type SourcePins,
16
+ fileLineRangeSchema,
17
+ sourcePinsSchema,
18
+ } from "@review/source.js";
19
+ import { z } from "zod";
20
+
21
+ import {
22
+ type FlowDiagramEdge,
23
+ type FlowDiagramNode,
24
+ flowEdgeSchema,
25
+ flowNodeInsertSchema,
26
+ flowNodeSchema,
27
+ } from "./blocks/flow_diagram.js";
28
+ import {
29
+ type Block,
30
+ blockKindSchemas,
31
+ blockSchema,
32
+ blocks,
33
+ } from "./blocks/index.js";
34
+ import { type Step, stepSchema } from "./blocks/sequence.js";
35
+ import type { Lens } from "./diff-lenses.js";
36
+ import { ReviewInputError } from "./input-error.js";
37
+
38
+ export { ReviewInputError } from "./input-error.js";
39
+
40
+ export { type FileLineRange, type SourcePins, fileLineRangeSchema };
41
+
42
+ export {
43
+ type Block,
44
+ type BlockType,
45
+ blockSchema,
46
+ blocks,
47
+ checkReferences,
48
+ } from "./blocks/index.js";
49
+
50
+ export { type Frame, frameSchema } from "./blocks/call_stack_diff.js";
51
+
52
+ export {
53
+ type DatabaseActor,
54
+ type DatabaseField,
55
+ type DatabaseLensBlock,
56
+ type DatabaseOperation,
57
+ type DatabaseStore,
58
+ databaseActorSchema,
59
+ databaseLensSchema,
60
+ fieldSchema,
61
+ operationSchema,
62
+ storeSchema,
63
+ } from "./blocks/database_lens.js";
64
+
65
+ export {
66
+ type SequenceBlock,
67
+ type Step,
68
+ sequenceSchema,
69
+ stepSchema,
70
+ } from "./blocks/sequence.js";
71
+
72
+ const text = z.string();
73
+
74
+ const label = text.trim().min(1);
75
+
76
+ export const pinsSchema = z.strictObject({
77
+ repositoryId: label,
78
+ base: label,
79
+ head: label,
80
+ });
81
+
82
+ /** Source identity retained internally for a saved worktree generation. */
83
+ /** worktreeRevision is a refresh token, never an address for stored source. */
84
+ export type Pins = z.infer<typeof pinsSchema> & { worktreeRevision?: string };
85
+
86
+ export { sourcePinsSchema };
87
+
88
+ /** The pins one source reference reads at: its own when it names them,
89
+ * otherwise its document's. A reference with base-less pins reads base at
90
+ * head, like a commits target without a base. */
91
+ export function anchorPins(
92
+ source: { pins?: SourcePins },
93
+ documentPins: Pins | undefined,
94
+ ): Pins {
95
+ if (source.pins)
96
+ return {
97
+ repositoryId: source.pins.repositoryId,
98
+ base: source.pins.base ?? source.pins.head,
99
+ head: source.pins.head,
100
+ };
101
+
102
+ if (!documentPins)
103
+ throw new ReviewInputError(
104
+ "This source names no repository or commit, and the document has no pins.",
105
+ );
106
+
107
+ return documentPins;
108
+ }
109
+
110
+ /** Distinct explicit pins named by a document's references, for validation. */
111
+ export function explicitPins(references: { source: { pins?: SourcePins } }[]) {
112
+ const seen = new Map<string, Pins>();
113
+
114
+ for (const { source } of references)
115
+ if (source.pins) {
116
+ const pins = anchorPins(source, undefined);
117
+ seen.set(JSON.stringify(pins), pins);
118
+ }
119
+
120
+ return [...seen.values()];
121
+ }
122
+
123
+ const targets = <Repository extends Record<string, z.ZodType>>(
124
+ repository: Repository,
125
+ ) =>
126
+ z.discriminatedUnion("kind", [
127
+ z.strictObject({
128
+ kind: z.literal("worktree"),
129
+ ...repository,
130
+ base: label.optional(),
131
+ }),
132
+ z.strictObject({
133
+ kind: z.literal("commits"),
134
+ ...repository,
135
+ head: label,
136
+ base: label.optional(),
137
+ }),
138
+ ]);
139
+
140
+ export const reviewTargetSchema = targets({ repositoryId: label });
141
+
142
+ /** How agents name a target: by the checkout's path, registered on acceptance. */
143
+ export const pathTargetSchema = targets({ repositoryPath: label });
144
+
145
+ export type ReviewTarget = z.infer<typeof reviewTargetSchema>;
146
+
147
+ /** Everything with an id: blocks, and the units diagrams are drawn from. */
148
+ export type Element = Block | Step | FlowDiagramNode | FlowDiagramEdge;
149
+
150
+ /** A diagram unit: addressed on its own, but only ever inside its diagram. */
151
+ export type Unit = Step | FlowDiagramNode | FlowDiagramEdge;
152
+
153
+ const unitTypes = new Set(["step", "flow_node", "flow_edge"]);
154
+
155
+ export const isUnit = (element: Element): element is Unit =>
156
+ unitTypes.has(element.type);
157
+
158
+ /** Inline trace quotes keep paragraph/list layout while using retained resources. */
159
+ export function traceQuoteLink(
160
+ href: string,
161
+ ): { traceId: string; eventId: string } | undefined {
162
+ const match = /^review-trace:([^#]+)#(.+)$/.exec(href);
163
+
164
+ return match
165
+ ? {
166
+ traceId: decodeURIComponent(match[1]!),
167
+ eventId: decodeURIComponent(match[2]!),
168
+ }
169
+ : undefined;
170
+ }
171
+
172
+ export function resourceReference(
173
+ block: Block,
174
+ ): { id: string; kind: "image" | "trace" | "map" } | undefined {
175
+ switch (block.type) {
176
+ case "image":
177
+ return { id: block.assetId, kind: "image" };
178
+ case "trace_quote":
179
+ return { id: block.traceId, kind: "trace" };
180
+ case "software_map":
181
+ return { id: block.mapVersionId, kind: "map" };
182
+ default:
183
+ return undefined;
184
+ }
185
+ }
186
+
187
+ export function resourceReferences(document: Block[]): Block[] {
188
+ return elements(document).flatMap((block): Block[] => {
189
+ if (block.type !== "markdown")
190
+ return block.type === "image" ||
191
+ block.type === "trace_quote" ||
192
+ block.type === "software_map"
193
+ ? [block]
194
+ : [];
195
+
196
+ return [...markdownNodes(parseMarkdown(block.markdown))].flatMap(
197
+ (node): Block[] => {
198
+ if (node.type !== "link" || !node.url?.startsWith("review-trace:"))
199
+ return [];
200
+ const quote = traceQuoteLink(node.url);
201
+
202
+ if (!quote) throw new ReviewInputError("Invalid trace quote link.");
203
+
204
+ return [{ type: "trace_quote", ...quote, text: markdownText(node) }];
205
+ },
206
+ );
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Source-bearing items share their owning element's stable identity.
212
+ * `tolerant` skips malformed Markdown source links instead of rejecting, for
213
+ * content that is already stored.
214
+ */
215
+ function documentReferences(
216
+ document: Block[],
217
+ { tolerant = false }: { tolerant?: boolean } = {},
218
+ ): {
219
+ id: string;
220
+ source: LensSource;
221
+ label?: string;
222
+ peek?: boolean;
223
+ }[] {
224
+ const reject = (message: string): [] => {
225
+ if (tolerant) return [];
226
+ throw new ReviewInputError(message);
227
+ };
228
+
229
+ type Reference = {
230
+ id: string;
231
+ source: LensSource;
232
+ label?: string;
233
+ peek?: boolean;
234
+ };
235
+
236
+ // An anchor reads at its holder's pins, else its block's.
237
+ const select = (anchor: Anchor, pins?: SourcePins): LensSource[] => {
238
+ const parsed = parseAnchor(anchor);
239
+
240
+ if (!parsed) return reject(`Not a source anchor: ${anchor}`);
241
+ const source = pins ? { ...parsed, pins } : parsed;
242
+ const problem = selectionProblem(source);
243
+
244
+ return problem ? reject(problem) : [source];
245
+ };
246
+
247
+ return elements(document).flatMap<Reference>((element) => {
248
+ if (element.type === "markdown")
249
+ return [...markdownNodes(parseMarkdown(element.markdown))].flatMap(
250
+ (node) => {
251
+ if (node.type !== "link") return [];
252
+ const href = node.url ?? "";
253
+
254
+ if (!/^review-source:/i.test(href)) {
255
+ // Trace links are checked by resourceReferences.
256
+ if (href.startsWith("review-trace:")) return [];
257
+
258
+ if (/^(?:https?:\/\/|mailto:|#)/i.test(href)) return [];
259
+
260
+ return reject(
261
+ `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.`,
262
+ );
263
+ }
264
+
265
+ let target: string;
266
+
267
+ try {
268
+ target = decodeURIComponent(href.slice("review-source:".length));
269
+ } catch {
270
+ return reject("Invalid URL encoding in source link.");
271
+ }
272
+
273
+ // Links share the anchor grammar blocks use.
274
+ const anchor = target.replace(/^(head|base|diff)\//i, (side) =>
275
+ side.toLowerCase(),
276
+ );
277
+
278
+ if (!parseAnchor(anchor))
279
+ return reject(
280
+ "Use review-source:head/path#L10-L24 (or base, or diff/path#L84-R90 across sides) for a source link.",
281
+ );
282
+
283
+ return select(anchor, element.pins).map((source) => ({
284
+ id: `${element.id}:${node.url}`,
285
+ source,
286
+ }));
287
+ },
288
+ );
289
+
290
+ if (element.type === "code_peek")
291
+ return select(element.source, element.pins).map((source) => ({
292
+ id: element.id!,
293
+ source,
294
+ label: element.caption,
295
+ peek: true,
296
+ }));
297
+
298
+ // A step, frame and operation render the range as a peek, so a
299
+ // whitespace-only range is an authoring mistake for each of them; prose
300
+ // links and context sources only need the range to exist.
301
+ if (element.type === "sequence")
302
+ return element.steps.flatMap((step) =>
303
+ step.source
304
+ ? select(step.source, step.pins ?? element.pins).map((source) => ({
305
+ id: step.id!,
306
+ source,
307
+ label: step.label,
308
+ peek: true,
309
+ }))
310
+ : [],
311
+ );
312
+
313
+ if (element.type === "flow_diagram")
314
+ return element.nodes.flatMap((node) =>
315
+ node.attachments.flatMap((attachment, index) =>
316
+ attachment.sources.flatMap((anchor, sourceIndex) =>
317
+ select(anchor, attachment.pins ?? element.pins).map((source) => ({
318
+ id: `${element.id}:${node.key}:${index}:${sourceIndex}`,
319
+ source,
320
+ label: attachment.label,
321
+ peek: true,
322
+ })),
323
+ ),
324
+ ),
325
+ );
326
+
327
+ if (element.type === "call_stack_diff")
328
+ return [...element.base, ...element.head].flatMap((frame) => {
329
+ const pins = frame.pins ?? element.pins;
330
+
331
+ return [
332
+ ...select(frame.source, pins).map((source) => ({
333
+ id: frame.id!,
334
+ source,
335
+ label: frame.label,
336
+ peek: true,
337
+ })),
338
+ ...(frame.contextSources ?? []).flatMap((anchor, index) =>
339
+ select(anchor, pins).map((source) => ({
340
+ id: `${frame.id}:context:${index}`,
341
+ source,
342
+ })),
343
+ ),
344
+ ...(frame.callSite
345
+ ? select(frame.callSite, pins).map((source) => ({
346
+ id: `${frame.id}:call-site`,
347
+ source,
348
+ label: frame.label,
349
+ peek: true,
350
+ }))
351
+ : []),
352
+ ];
353
+ });
354
+
355
+ if (element.type === "database_lens")
356
+ return element.useCases.flatMap((useCase) =>
357
+ useCase.operations.flatMap((operation) =>
358
+ select(operation.source, operation.pins ?? element.pins).map(
359
+ (source) => ({
360
+ id: operation.id!,
361
+ source,
362
+ label: operation.label,
363
+ peek: true,
364
+ }),
365
+ ),
366
+ ),
367
+ );
368
+
369
+ return [];
370
+ });
371
+ }
372
+
373
+ /** All authored attachments, including code peeks, select the aligned diff. */
374
+ export const selectionReferences = documentReferences;
375
+
376
+ /** Per-side read coordinates for endpoint validation and retained source quotes. */
377
+ export function sourceReferences(
378
+ document: Block[],
379
+ options: { tolerant?: boolean } = {},
380
+ ) {
381
+ return selectionReferences(document, options).flatMap((ref) =>
382
+ sourceAnchors(ref.source).map((source) => ({ ...ref, source })),
383
+ );
384
+ }
385
+
386
+ /** Includes lenses and maps without inline ranges. */
387
+ export function hasCodeReferences(review: {
388
+ document: Block[];
389
+ lenses?: readonly Lens[];
390
+ }): boolean {
391
+ return Boolean(
392
+ sourceReferences(review.document).length ||
393
+ review.lenses?.length ||
394
+ resourceReferences(review.document).some(
395
+ (block) => block.type === "software_map",
396
+ ),
397
+ );
398
+ }
399
+
400
+ export const documentSchema = z.array(blockSchema);
401
+
402
+ /** Discriminated by type, like blockSchema, so a malformed insert reports
403
+ * its own kind's issues. */
404
+ export const contentSchema = z.discriminatedUnion("type", [
405
+ ...blockKindSchemas(),
406
+ stepSchema,
407
+ flowNodeInsertSchema,
408
+ flowEdgeSchema,
409
+ ]);
410
+
411
+ const placement = { parentId: label.optional(), afterId: label.optional() };
412
+
413
+ function edits<
414
+ Content extends z.ZodType,
415
+ Replacement extends z.ZodType,
416
+ Value extends z.ZodType,
417
+ >(content: Content, replacement: Replacement, value: Value) {
418
+ return z.discriminatedUnion("type", [
419
+ z.strictObject({ type: z.literal("insert"), content, ...placement }),
420
+ z.strictObject({
421
+ type: z.literal("replace"),
422
+ targetId: label,
423
+ content: replacement,
424
+ }),
425
+ z.strictObject({
426
+ type: z.literal("update"),
427
+ targetId: label,
428
+ changes: z.record(text, value),
429
+ }),
430
+ z.strictObject({ type: z.literal("move"), targetId: label, ...placement }),
431
+ z.strictObject({ type: z.literal("remove"), targetId: label }),
432
+ ]);
433
+ }
434
+
435
+ export const editSchema = edits(contentSchema, blockSchema, z.json());
436
+
437
+ export type Edit = z.infer<typeof editSchema>;
438
+
439
+ // Tutorial blocks are the built-in tutorial's, not for agents to write.
440
+ const writableBlockTypes = Object.keys(blocks).filter(
441
+ (type) => type !== "tutorial",
442
+ );
443
+
444
+ const namedByType = (types: string[]) =>
445
+ z
446
+ .looseObject({ type: z.enum(types) })
447
+ .describe("Fields depend on type; see the tool description.");
448
+
449
+ /** What agents are shown: content names only its type. The host parses
450
+ * editSchema, reporting the named kind's issues. */
451
+ export const publishedEditSchema = edits(
452
+ namedByType([...writableBlockTypes, "step", "flow_node", "flow_edge"]),
453
+ namedByType(writableBlockTypes),
454
+ z.unknown(),
455
+ );
456
+
457
+ /**
458
+ * What one saved version did, for a canvas drawing the document as the
459
+ * agent writes it: the edit's kind, the element it landed on, and the block
460
+ * that element belongs to (itself, for a block; its diagram, for a unit).
461
+ */
462
+ export interface EditSummary {
463
+ type: Edit["type"];
464
+ targetId: string;
465
+ /** For a lens, the lens itself. */
466
+ blockId: string;
467
+ /** What the target is: a block type, a unit type, or a Diff-view lens. */
468
+ kind: Element["type"] | "lens";
469
+ unit?: Unit["type"];
470
+ /** The edge a new flow node arrived with, drawn right after the node. */
471
+ linkId?: string;
472
+ /** An update's patched fields, so the canvas can draw only what changed. */
473
+ fields?: string[];
474
+ /** A diagram written whole: its units in the order a hand would draw
475
+ * them, so the canvas can trace the whole diagram in one quick pass. */
476
+ units?: string[];
477
+ /** The agent whose courier draws this edit, when the host could tell. */
478
+ activityId?: string;
479
+ }
480
+
481
+ /** A component an edit wrote, named so the author can address it. */
482
+ export interface WrittenComponent {
483
+ id: string;
484
+ type: Element["type"];
485
+ }
486
+
487
+ /** What applying an edit produced: the target, what it is, the first-level
488
+ * children an insert or replace gave it fresh IDs, and the edge a node came
489
+ * with. */
490
+ export interface Applied {
491
+ targetId: string;
492
+ type: Element["type"];
493
+ children?: WrittenComponent[];
494
+ linkId?: string;
495
+ }
496
+
497
+ /** The block an element belongs to: itself, or the diagram around a unit. */
498
+ export function enclosingBlock(
499
+ document: Element[],
500
+ id: string,
501
+ ): { block: Element; element: Element } | undefined {
502
+ for (const block of elements(document)) {
503
+ if (block.id === id) return { block, element: block };
504
+
505
+ for (const list of childLists(block))
506
+ for (const element of list)
507
+ if (element.id === id && isUnit(element)) return { block, element };
508
+ }
509
+
510
+ return undefined;
511
+ }
512
+
513
+ /** Summarize an edit against the document it was applied to. A removed
514
+ * element is found in the document before the edit; everything else after. */
515
+ export function summarizeEdit(
516
+ edit: Edit,
517
+ applied: Applied,
518
+ before: Element[],
519
+ after: Element[],
520
+ ): EditSummary | undefined {
521
+ const found = enclosingBlock(
522
+ edit.type === "remove" ? before : after,
523
+ applied.targetId,
524
+ );
525
+
526
+ if (!found?.block.id) return undefined;
527
+
528
+ const summary: EditSummary = {
529
+ type: edit.type,
530
+ targetId: applied.targetId,
531
+ blockId: found.block.id,
532
+ kind: found.element.type,
533
+ };
534
+
535
+ if (isUnit(found.element)) summary.unit = found.element.type;
536
+
537
+ if (applied.linkId) summary.linkId = applied.linkId;
538
+
539
+ if (edit.type === "update") summary.fields = Object.keys(edit.changes);
540
+
541
+ if (edit.type === "insert" || edit.type === "replace") {
542
+ const units = drawOrder(found.element);
543
+
544
+ if (units.length) summary.units = units;
545
+ }
546
+
547
+ return summary;
548
+ }
549
+
550
+ /** The order a hand draws a diagram: a sequence step by step; a flow node by
551
+ * node, each edge as soon as both of its ends are on the board. */
552
+ export function drawOrder(element: Element): string[] {
553
+ if (element.type === "sequence")
554
+ return element.steps.flatMap((step) => (step.id ? [step.id] : []));
555
+
556
+ if (element.type !== "flow_diagram") return [];
557
+
558
+ const order: string[] = [];
559
+ const drawn = new Set<string>();
560
+ const waiting = [...element.edges];
561
+
562
+ for (const node of element.nodes) {
563
+ if (node.id) order.push(node.id);
564
+
565
+ drawn.add(node.key);
566
+
567
+ for (let i = 0; i < waiting.length; ) {
568
+ const edge = waiting[i]!;
569
+
570
+ if (drawn.has(edge.from) && drawn.has(edge.to)) {
571
+ if (edge.id) order.push(edge.id);
572
+
573
+ waiting.splice(i, 1);
574
+ } else i++;
575
+ }
576
+ }
577
+
578
+ return order;
579
+ }
580
+
581
+ /** The arrays an element's children live in, so an edit can splice the
582
+ * real list. A flow diagram keeps its nodes and edges apart. */
583
+ export function childLists(element: Element): Element[][] {
584
+ if ("children" in element) return [element.children];
585
+
586
+ if (element.type === "sequence") return [element.steps];
587
+
588
+ if (element.type === "flow_diagram") return [element.nodes, element.edges];
589
+
590
+ return [];
591
+ }
592
+
593
+ export function children(element: Element): Element[] {
594
+ return childLists(element).flat();
595
+ }
596
+
597
+ export function elements(document: Element[]): Element[] {
598
+ return document.flatMap((element) => [
599
+ element,
600
+ ...elements(children(element)),
601
+ ]);
602
+ }
603
+
604
+ const unitParent = {
605
+ step: "sequence",
606
+ flow_node: "flow_diagram",
607
+ flow_edge: "flow_diagram",
608
+ } as const;
609
+
610
+ /** What to do instead of patching a field an update can't reach. */
611
+ function patchRefusal(key: string, element: Element): string {
612
+ if (key === "base" || key === "head")
613
+ return `A call stack's frames change only by replacing it: send {type:"replace", targetId:"${element.id}", content} with the whole call_stack_diff.`;
614
+
615
+ if (key === "nodes" || key === "edges" || key === "steps")
616
+ return `Edit a diagram's ${key} one at a time by their own IDs (session_get lists them), or replace the diagram.`;
617
+
618
+ if (key === "children")
619
+ return `Edit a ${element.type}'s children by their own IDs, or insert into it with parentId "${element.id}".`;
620
+
621
+ if (key === "id" || key === "type")
622
+ return `A component's ${key} can't change; replace it instead.`;
623
+
624
+ return `Cannot patch ${key}; replace the ${element.type} instead.`;
625
+ }
626
+
627
+ const structural = new Set([
628
+ "nodes",
629
+ "edges",
630
+ "id",
631
+ "type",
632
+ "children",
633
+ "steps",
634
+ "actors",
635
+ "stores",
636
+ "useCases",
637
+ "base",
638
+ "head",
639
+ ]);
640
+
641
+ const idPrefix = (element: Element) =>
642
+ element.type === "sequence" || element.type === "flow_diagram"
643
+ ? "diagram"
644
+ : element.type === "step"
645
+ ? "step"
646
+ : element.type === "flow_node"
647
+ ? "node"
648
+ : element.type === "flow_edge"
649
+ ? "edge"
650
+ : "block";
651
+
652
+ /** Flow units saved before they had ids or types get both on the next
653
+ * write, so a stored diagram can be drawn on unit by unit. */
654
+ export function adoptFlowUnits(
655
+ document: Element[],
656
+ allocate: (prefix: string) => string,
657
+ ): void {
658
+ for (const element of elements(document)) {
659
+ if (element.type === "flow_diagram") {
660
+ for (const node of element.nodes) node.type ??= "flow_node";
661
+
662
+ for (const edge of element.edges) edge.type ??= "flow_edge";
663
+ }
664
+
665
+ if (isUnit(element) && element.id === undefined)
666
+ element.id = allocate(idPrefix(element));
667
+ }
668
+ }
669
+
670
+ /** Mutate a private candidate. Only the store owns allocation and commits. */
671
+ /** Assign server ids to an element tree that arrives without any. */
672
+ export function assignFreshIds(
673
+ element: Element,
674
+ allocate: (prefix: string) => string,
675
+ ): void {
676
+ if (element.id !== undefined)
677
+ throw new ReviewInputError("IDs are assigned by the server.");
678
+ element.id = allocate(idPrefix(element));
679
+
680
+ for (const child of children(element)) assignFreshIds(child, allocate);
681
+
682
+ const assign = (item: { id?: string }, prefix: string) => {
683
+ if (item.id !== undefined)
684
+ throw new ReviewInputError("IDs are assigned by the server.");
685
+ item.id = allocate(prefix);
686
+ };
687
+
688
+ if (element.type === "call_stack_diff")
689
+ for (const frame of [...element.base, ...element.head])
690
+ assign(frame, "frame");
691
+
692
+ if (element.type === "database_lens")
693
+ for (const useCase of element.useCases) {
694
+ assign(useCase, "case");
695
+
696
+ for (const op of useCase.operations) assign(op, "operation");
697
+ }
698
+ }
699
+
700
+ export interface ApplyEditOptions {
701
+ /** Where a root-level insert with no placement lands: the end by default. */
702
+ placement?: "first" | "last";
703
+ }
704
+
705
+ export function applyEdit(
706
+ document: Block[],
707
+ edit: Edit,
708
+ allocate: (prefix: string) => string,
709
+ options: ApplyEditOptions = {},
710
+ ): Applied {
711
+ adoptFlowUnits(document, allocate);
712
+
713
+ const locate = (id: string): { element: Element; siblings: Element[] } => {
714
+ const find = (
715
+ siblings: Element[],
716
+ ): ReturnType<typeof locate> | undefined => {
717
+ for (const element of siblings) {
718
+ if (element.id === id) return { element, siblings };
719
+
720
+ for (const list of childLists(element)) {
721
+ const found = find(list);
722
+
723
+ if (found) return found;
724
+ }
725
+ }
726
+ };
727
+
728
+ const found = find(document);
729
+
730
+ if (!found) throw new ReviewInputError(`Target ${id} does not exist.`);
731
+
732
+ return found;
733
+ };
734
+
735
+ const fresh = (element: Element) => assignFreshIds(element, allocate);
736
+
737
+ const place = (
738
+ element: Element,
739
+ parentId?: string,
740
+ afterId?: string,
741
+ from?: Element[],
742
+ ) => {
743
+ const parent = parentId ? locate(parentId).element : undefined;
744
+
745
+ if (parent && elements([element]).includes(parent))
746
+ throw new ReviewInputError("Cannot move a block inside itself.");
747
+
748
+ if (!isUnit(element) && parent && !("children" in parent))
749
+ throw new ReviewInputError(
750
+ `${parentId} is a ${parent.type}, which holds no components: insert into a section or callout, or at the top level.`,
751
+ );
752
+
753
+ if (isUnit(element) && parent?.type !== unitParent[element.type])
754
+ throw new ReviewInputError(
755
+ `A ${element.type} belongs inside a ${unitParent[element.type]}.`,
756
+ );
757
+
758
+ // A unit's parent type was checked just above: a flow diagram's lists
759
+ // are [nodes, edges]; every other container has one list.
760
+ const siblings: Element[] = parent
761
+ ? (childLists(parent)[element.type === "flow_edge" ? 1 : 0] ?? document)
762
+ : document;
763
+
764
+ if (afterId === element.id)
765
+ throw new ReviewInputError("An element cannot follow itself.");
766
+
767
+ if (afterId !== undefined && !siblings.some((s) => s.id === afterId))
768
+ throw new ReviewInputError("afterId must identify a sibling.");
769
+
770
+ // Resolve the destination before detaching, then compute its final position.
771
+ if (from) from.splice(from.indexOf(element), 1);
772
+
773
+ // A running log reads newest first, so a root insert with no placement
774
+ // may lead the document; a move or a unit inside a diagram never does.
775
+ const index =
776
+ afterId !== undefined
777
+ ? siblings.findIndex((s) => s.id === afterId) + 1
778
+ : !parent && !from && options.placement === "first"
779
+ ? 0
780
+ : siblings.length;
781
+
782
+ siblings.splice(index, 0, element);
783
+ };
784
+
785
+ if (edit.type === "insert") {
786
+ // A node that arrives with its edge: the node is placed first, then the
787
+ // edge joins it to the board, so the layout has both from the start.
788
+ if (edit.content.type === "flow_node" && edit.content.link) {
789
+ const { link, ...node } = edit.content;
790
+
791
+ if ((link.from === undefined) === (link.to === undefined))
792
+ throw new ReviewInputError(
793
+ "A link names exactly one of from or to: the node already on the board.",
794
+ );
795
+ fresh(node);
796
+ place(node, edit.parentId, edit.afterId);
797
+
798
+ const edge: FlowDiagramEdge = {
799
+ type: "flow_edge",
800
+ from: link.from ?? node.key,
801
+ to: link.to ?? node.key,
802
+ };
803
+
804
+ if (link.label !== undefined) edge.label = link.label;
805
+
806
+ if (link.style !== undefined) edge.style = link.style;
807
+ fresh(edge);
808
+ place(edge, edit.parentId);
809
+
810
+ return { targetId: node.id!, type: node.type, linkId: edge.id! };
811
+ }
812
+
813
+ fresh(edit.content);
814
+ place(edit.content, edit.parentId, edit.afterId);
815
+
816
+ return written(edit.content);
817
+ }
818
+
819
+ const { element, siblings } = locate(edit.targetId);
820
+ const index = siblings.indexOf(element);
821
+
822
+ switch (edit.type) {
823
+ case "update": {
824
+ if (!Object.keys(edit.changes).length)
825
+ throw new ReviewInputError("Supply at least one field to update.");
826
+
827
+ for (const key of Object.keys(edit.changes))
828
+ if (
829
+ structural.has(key) ||
830
+ ["__proto__", "constructor", "prototype"].includes(key)
831
+ )
832
+ throw new ReviewInputError(patchRefusal(key, element));
833
+
834
+ if ("link" in edit.changes)
835
+ throw new ReviewInputError(
836
+ "A link only comes with a new node; insert a flow_edge instead.",
837
+ );
838
+
839
+ const merged = Object.fromEntries(
840
+ Object.entries({ ...element, ...edit.changes }).filter(
841
+ ([, value]) => value !== null,
842
+ ),
843
+ );
844
+
845
+ siblings[index] = contentSchema.parse(merged);
846
+ break;
847
+ }
848
+
849
+ case "remove":
850
+ siblings.splice(index, 1);
851
+
852
+ // A node takes its edges with it: an edge with a missing end is not a
853
+ // state the diagram can be in, and the agent asked for the node to go.
854
+ if (element.type === "flow_node") {
855
+ const diagram = elements(document).find(
856
+ (candidate) =>
857
+ candidate.type === "flow_diagram" && candidate.nodes === siblings,
858
+ );
859
+
860
+ if (diagram?.type === "flow_diagram")
861
+ diagram.edges = diagram.edges.filter(
862
+ (edge) => edge.from !== element.key && edge.to !== element.key,
863
+ );
864
+ }
865
+
866
+ break;
867
+ case "replace":
868
+ if (isUnit(element))
869
+ throw new ReviewInputError(
870
+ `Patch the ${element.type} or replace its diagram.`,
871
+ );
872
+ fresh(edit.content);
873
+ edit.content.id = element.id;
874
+ siblings[index] = edit.content;
875
+
876
+ return written(edit.content);
877
+ case "move":
878
+ // Names are diagram-local: moving a unit between diagrams is an explicit replacement, not a move.
879
+ if (
880
+ isUnit(element) &&
881
+ !childLists(locate(edit.parentId ?? "").element).includes(siblings)
882
+ )
883
+ throw new ReviewInputError(
884
+ `Move a ${element.type} within its own diagram.`,
885
+ );
886
+ place(element, edit.parentId, edit.afterId, siblings);
887
+ break;
888
+ }
889
+
890
+ return { targetId: edit.targetId, type: element.type };
891
+ }
892
+
893
+ /** An inserted or replaced component and its first-level children: blocks
894
+ * in a container, or a diagram's steps, or its nodes then its edges. */
895
+ function written(element: Element): Applied {
896
+ const applied: Applied = { targetId: element.id!, type: element.type };
897
+
898
+ const list = children(element).map((child) => ({
899
+ id: child.id!,
900
+ type: child.type,
901
+ }));
902
+
903
+ if (list.length) applied.children = list;
904
+
905
+ return applied;
906
+ }