@dev.fast/whiteboard 0.0.0-stage → 0.2.0

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,260 @@
1
+ import type {
2
+ NormalizedSoftwareElement,
3
+ NormalizedSoftwareModel,
4
+ NormalizedSoftwareRelationship,
5
+ SoftwareChangeStatus,
6
+ } from "./software-map-model";
7
+
8
+ export interface SoftwareMapElementTopologyChange {
9
+ path: string;
10
+ status: Exclude<SoftwareChangeStatus, "unchanged">;
11
+ label: string;
12
+ type: NormalizedSoftwareElement["type"];
13
+ beforeParentPath?: string;
14
+ afterParentPath?: string;
15
+ }
16
+
17
+ export interface SoftwareMapRelationshipTopologyChange {
18
+ key: string;
19
+ status: Exclude<SoftwareChangeStatus, "unchanged">;
20
+ from: string;
21
+ to: string;
22
+ kind: NormalizedSoftwareRelationship["kind"];
23
+ label?: string;
24
+ }
25
+
26
+ export interface SoftwareMapTopologyDiff {
27
+ elementChanges: SoftwareMapElementTopologyChange[];
28
+ relationshipChanges: SoftwareMapRelationshipTopologyChange[];
29
+ elementStatusByPath: Record<
30
+ string,
31
+ Exclude<SoftwareChangeStatus, "unchanged">
32
+ >;
33
+ counts: {
34
+ addedElements: number;
35
+ removedElements: number;
36
+ modifiedElements: number;
37
+ addedRelationships: number;
38
+ removedRelationships: number;
39
+ modifiedRelationships: number;
40
+ };
41
+ }
42
+
43
+ export function diffSoftwareMaps(
44
+ base: NormalizedSoftwareModel | null | undefined,
45
+ head: NormalizedSoftwareModel | null | undefined,
46
+ ): SoftwareMapTopologyDiff | null {
47
+ if (!base || !head) return null;
48
+
49
+ const elementChanges = diffElements(base, head);
50
+ const relationshipChanges = diffRelationships(base, head);
51
+
52
+ const elementStatusByPath = Object.fromEntries(
53
+ elementChanges
54
+ .filter((change) => change.status !== "removed")
55
+ .map((change) => [change.path, change.status] as const),
56
+ );
57
+
58
+ return {
59
+ elementChanges,
60
+ relationshipChanges,
61
+ elementStatusByPath,
62
+ counts: {
63
+ addedElements: elementChanges.filter(
64
+ (change) => change.status === "added",
65
+ ).length,
66
+ removedElements: elementChanges.filter(
67
+ (change) => change.status === "removed",
68
+ ).length,
69
+ modifiedElements: elementChanges.filter(
70
+ (change) => change.status === "modified",
71
+ ).length,
72
+ addedRelationships: relationshipChanges.filter(
73
+ (change) => change.status === "added",
74
+ ).length,
75
+ removedRelationships: relationshipChanges.filter(
76
+ (change) => change.status === "removed",
77
+ ).length,
78
+ modifiedRelationships: relationshipChanges.filter(
79
+ (change) => change.status === "modified",
80
+ ).length,
81
+ },
82
+ };
83
+ }
84
+
85
+ function diffElements(
86
+ base: NormalizedSoftwareModel,
87
+ head: NormalizedSoftwareModel,
88
+ ): SoftwareMapElementTopologyChange[] {
89
+ const changes: SoftwareMapElementTopologyChange[] = [];
90
+ const baseByPath = base.elementsByPath;
91
+ const headByPath = head.elementsByPath;
92
+
93
+ for (const headElement of head.elements) {
94
+ const baseElement = baseByPath.get(headElement.path);
95
+
96
+ if (!baseElement) {
97
+ changes.push(elementChange(headElement, "added"));
98
+ continue;
99
+ }
100
+
101
+ if (
102
+ elementTopologySignature(baseElement) !==
103
+ elementTopologySignature(headElement)
104
+ ) {
105
+ changes.push(
106
+ elementChange(headElement, "modified", {
107
+ beforeParentPath: baseElement.parentPath,
108
+ afterParentPath: headElement.parentPath,
109
+ }),
110
+ );
111
+ }
112
+ }
113
+
114
+ for (const baseElement of base.elements) {
115
+ if (!headByPath.has(baseElement.path)) {
116
+ changes.push(elementChange(baseElement, "removed"));
117
+ }
118
+ }
119
+
120
+ return changes.sort(compareElementChanges);
121
+ }
122
+
123
+ function elementChange(
124
+ element: NormalizedSoftwareElement,
125
+ status: Exclude<SoftwareChangeStatus, "unchanged">,
126
+ extra: Partial<SoftwareMapElementTopologyChange> = {},
127
+ ): SoftwareMapElementTopologyChange {
128
+ return {
129
+ path: element.path,
130
+ status,
131
+ label: element.label,
132
+ type: element.type,
133
+ ...extra,
134
+ };
135
+ }
136
+
137
+ function elementTopologySignature(element: NormalizedSoftwareElement): string {
138
+ return JSON.stringify({
139
+ type: element.type,
140
+ label: element.label,
141
+ parentPath: element.parentPath ?? null,
142
+ external: element.external ?? null,
143
+ coverage: element.coverage ?? null,
144
+ sourceRanges: element.sourceRanges ?? null,
145
+ });
146
+ }
147
+
148
+ function diffRelationships(
149
+ base: NormalizedSoftwareModel,
150
+ head: NormalizedSoftwareModel,
151
+ ): SoftwareMapRelationshipTopologyChange[] {
152
+ const changes: SoftwareMapRelationshipTopologyChange[] = [];
153
+ const baseByKey = relationshipMap(base.relationships);
154
+ const headByKey = relationshipMap(head.relationships);
155
+
156
+ for (const [key, headRelationship] of headByKey) {
157
+ const baseRelationship = baseByKey.get(key);
158
+
159
+ if (!baseRelationship) {
160
+ changes.push(relationshipChange(key, headRelationship, "added"));
161
+ continue;
162
+ }
163
+
164
+ if (
165
+ relationshipTopologySignature(baseRelationship) !==
166
+ relationshipTopologySignature(headRelationship)
167
+ ) {
168
+ changes.push(relationshipChange(key, headRelationship, "modified"));
169
+ }
170
+ }
171
+
172
+ for (const [key, baseRelationship] of baseByKey) {
173
+ if (!headByKey.has(key)) {
174
+ changes.push(relationshipChange(key, baseRelationship, "removed"));
175
+ }
176
+ }
177
+
178
+ return changes.sort(compareRelationshipChanges);
179
+ }
180
+
181
+ function relationshipMap(
182
+ relationships: readonly NormalizedSoftwareRelationship[],
183
+ ): Map<string, NormalizedSoftwareRelationship> {
184
+ return new Map(
185
+ relationships.map((relationship) => [
186
+ relationshipTopologyKey(relationship),
187
+ relationship,
188
+ ]),
189
+ );
190
+ }
191
+
192
+ function relationshipTopologyKey(
193
+ relationship: NormalizedSoftwareRelationship,
194
+ ): string {
195
+ return [
196
+ relationship.from,
197
+ relationship.to,
198
+ relationship.kind,
199
+ relationship.kind === "semantic" ? (relationship.semanticKind ?? "") : "",
200
+ relationship.kind === "call" ? String(relationship.nthCallSite) : "",
201
+ ].join("\u0000");
202
+ }
203
+
204
+ function relationshipTopologySignature(
205
+ relationship: NormalizedSoftwareRelationship,
206
+ ): string {
207
+ return JSON.stringify({
208
+ label: relationship.label ?? null,
209
+ description: relationship.description ?? null,
210
+ sourceRanges:
211
+ relationship.kind === "semantic"
212
+ ? (relationship.sourceRanges ?? null)
213
+ : null,
214
+ });
215
+ }
216
+
217
+ function relationshipChange(
218
+ key: string,
219
+ relationship: NormalizedSoftwareRelationship,
220
+ status: Exclude<SoftwareChangeStatus, "unchanged">,
221
+ ): SoftwareMapRelationshipTopologyChange {
222
+ return {
223
+ key,
224
+ status,
225
+ from: relationship.from,
226
+ to: relationship.to,
227
+ kind: relationship.kind,
228
+ label: relationship.label,
229
+ };
230
+ }
231
+
232
+ function compareElementChanges(
233
+ left: SoftwareMapElementTopologyChange,
234
+ right: SoftwareMapElementTopologyChange,
235
+ ) {
236
+ return (
237
+ statusOrder(left.status) - statusOrder(right.status) ||
238
+ left.path.localeCompare(right.path)
239
+ );
240
+ }
241
+
242
+ function compareRelationshipChanges(
243
+ left: SoftwareMapRelationshipTopologyChange,
244
+ right: SoftwareMapRelationshipTopologyChange,
245
+ ) {
246
+ return (
247
+ statusOrder(left.status) - statusOrder(right.status) ||
248
+ left.from.localeCompare(right.from) ||
249
+ left.to.localeCompare(right.to) ||
250
+ left.key.localeCompare(right.key)
251
+ );
252
+ }
253
+
254
+ function statusOrder(status: Exclude<SoftwareChangeStatus, "unchanged">) {
255
+ if (status === "added") return 0;
256
+
257
+ if (status === "modified") return 1;
258
+
259
+ return 2;
260
+ }
package/src/source.ts ADDED
@@ -0,0 +1,92 @@
1
+ import { z } from "zod";
2
+
3
+ const label = z.string().trim().min(1);
4
+
5
+ /** The repository and commits one source reference was read from. A
6
+ * reference without pins resolves against its document's pins; one with
7
+ * pins names them itself, so a document can quote several repositories or
8
+ * commits. `base` is only needed by references to the base side. */
9
+ export const sourcePinsSchema = z.strictObject({
10
+ repositoryId: label,
11
+ head: label,
12
+ base: label.optional(),
13
+ });
14
+
15
+ export type SourcePins = z.infer<typeof sourcePinsSchema>;
16
+
17
+ /** Internal coordinates for reading one revision or storing resolved coverage.
18
+ * Authored document attachments use DiffSelection instead. */
19
+ export const fileLineRangeSchema = z
20
+ .strictObject({
21
+ side: z.enum(["base", "head"]).default("head"),
22
+ file: label,
23
+ fromLine: z.number().int().positive(),
24
+ toLine: z.number().int().positive(),
25
+ pins: sourcePinsSchema.optional(),
26
+ })
27
+ .refine((s) => s.toLine >= s.fromLine, "Source range ends before it starts.")
28
+ .refine(
29
+ (s) => s.side === "head" || !s.pins || s.pins.base !== undefined,
30
+ "A base-side source needs base pins.",
31
+ );
32
+
33
+ export type FileLineRange = z.infer<typeof fileLineRangeSchema>;
34
+
35
+ export type SourceRange = Pick<FileLineRange, "file" | "fromLine" | "toLine">;
36
+
37
+ /** Thrown by the pure checks; each boundary translates it for its clients. */
38
+ export class SourceRangeError extends Error {}
39
+
40
+ /** Git and jj read committed objects, so a lexical check suffices: nothing
41
+ * here follows a working-copy symlink. An empty string names the root. */
42
+ export function checkSourcePath(file: string): void {
43
+ if (
44
+ file.startsWith("/") ||
45
+ file.includes("\\") ||
46
+ file.split("/").some((part) => part === ".." || part === ".") ||
47
+ /[\u0000-\u001f]/.test(file)
48
+ )
49
+ throw new SourceRangeError(
50
+ "Source file must be a repository-relative path.",
51
+ );
52
+ }
53
+
54
+ /** The range's text from a whole file. A trailing newline is not a line. */
55
+ export function sliceSourceRange(text: string, range: SourceRange): string {
56
+ const lines = text.split(/\r?\n/);
57
+
58
+ if (text.endsWith("\n")) lines.pop();
59
+
60
+ if (text === "" || range.toLine > lines.length)
61
+ throw new SourceRangeError(
62
+ `Source range ${range.file}:${range.fromLine}-${range.toLine} exceeds the pinned file (${lines.length} lines).`,
63
+ );
64
+
65
+ return lines.slice(range.fromLine - 1, range.toLine).join("\n");
66
+ }
67
+
68
+ /** A source shown as a peek must contain visible text; a whitespace-only
69
+ * range is an authoring mistake. Prose links may point at blank lines, so
70
+ * only peek-rendering references call this. */
71
+ export function requireVisibleSource(text: string, range: SourceRange): void {
72
+ if (text.trim() === "")
73
+ throw new SourceRangeError(
74
+ `Source range ${range.file}:${range.fromLine}-${range.toLine} contains only whitespace.`,
75
+ );
76
+ }
77
+
78
+ /** The authoring input names the diff side `graph`; the document names it
79
+ * `side`. */
80
+ export function codePeekSource(props: {
81
+ file: string;
82
+ fromLine: number;
83
+ toLine: number;
84
+ graph?: "head" | "base";
85
+ }): FileLineRange {
86
+ return {
87
+ side: props.graph ?? "head",
88
+ file: props.file,
89
+ fromLine: props.fromLine,
90
+ toLine: props.toLine,
91
+ };
92
+ }
@@ -0,0 +1,232 @@
1
+ // CLI profiling instrumentation. Enabled when either variable is set:
2
+ //
3
+ // DEV_FAST_REVIEW_TRACE=<file> write this process's span tree to <file>
4
+ // DEV_FAST_REVIEW_TRACE_DIR=<dir> write one file per process into <dir>
5
+ //
6
+ // Otherwise every helper is a zero-cost passthrough. Spans are wall-clock
7
+ // intervals (ms since process start, i.e. performance.timeOrigin) with parent
8
+ // links propagated through async context, so concurrent work (graph prewarm,
9
+ // browser requests, parallel worktree creation) nests correctly. The whole
10
+ // span tree is flushed as JSON on process exit.
11
+ //
12
+ // The directory form suits runs of many `review` commands (some delegated to a
13
+ // second process): each one lands in its own file, which a caller can join
14
+ // back to what launched it by wall-clock (`timeOrigin`) and `argv`.
15
+ import { AsyncLocalStorage } from "node:async_hooks";
16
+ import { mkdirSync, writeFileSync } from "node:fs";
17
+ import path from "node:path";
18
+ import { performance } from "node:perf_hooks";
19
+
20
+ import { setLocalVcsCommandObserver } from "@dev.fast/local-vcs";
21
+ import { errorMessage } from "@dev.fast/trace-core";
22
+
23
+ interface TraceSpanRecord {
24
+ id: number;
25
+ parentId: number | null;
26
+ name: string;
27
+ detail?: string;
28
+ start: number;
29
+ end: number | null;
30
+ ok?: boolean;
31
+ }
32
+
33
+ const TRACE_FILE_ENV = "DEV_FAST_REVIEW_TRACE";
34
+
35
+ const TRACE_DIR_ENV = "DEV_FAST_REVIEW_TRACE_DIR";
36
+
37
+ const traceFile = resolveTraceFile(process.env);
38
+
39
+ export const traceEnabled = Boolean(traceFile);
40
+
41
+ const spans: TraceSpanRecord[] = [];
42
+
43
+ const attributes: Record<string, string | number | boolean> = {};
44
+
45
+ let nextId = 1;
46
+
47
+ const context = new AsyncLocalStorage<number>();
48
+
49
+ interface SpanHandle {
50
+ id: number;
51
+ end: (detail?: string) => void;
52
+ fail: (detail?: string) => void;
53
+ }
54
+
55
+ const NOOP_HANDLE: SpanHandle = { id: 0, end: () => {}, fail: () => {} };
56
+
57
+ // Manual span: caller owns the lifetime and (optionally) the parent. Used for
58
+ // spans whose start/end don't wrap a single function call (HTTP requests,
59
+ // "waiting for submission").
60
+ export function startSpan(
61
+ name: string,
62
+ options: { parentId?: number | null; detail?: string } = {},
63
+ ): SpanHandle {
64
+ if (!traceEnabled) return NOOP_HANDLE;
65
+
66
+ const record: TraceSpanRecord = {
67
+ id: nextId++,
68
+ parentId:
69
+ options.parentId !== undefined
70
+ ? options.parentId
71
+ : (context.getStore() ?? null),
72
+ name,
73
+ detail: options.detail,
74
+ start: performance.now(),
75
+ end: null,
76
+ };
77
+
78
+ spans.push(record);
79
+
80
+ return {
81
+ id: record.id,
82
+ end: (detail) => {
83
+ if (record.end === null) record.end = performance.now();
84
+
85
+ if (detail) record.detail = detail;
86
+ },
87
+ fail: (detail) => {
88
+ if (record.end === null) record.end = performance.now();
89
+ record.ok = false;
90
+
91
+ if (detail) record.detail = detail;
92
+ },
93
+ };
94
+ }
95
+
96
+ // Wrap an async (or sync) call; children created inside inherit this span as
97
+ // their parent via AsyncLocalStorage. A thrown error marks the span failed.
98
+ export async function span<T>(
99
+ name: string,
100
+ fn: () => Promise<T> | T,
101
+ detail?: string,
102
+ ): Promise<T> {
103
+ if (!traceEnabled) return fn();
104
+ const handle = startSpan(name, { detail });
105
+
106
+ try {
107
+ const result = await context.run(handle.id, fn);
108
+ handle.end();
109
+
110
+ return result;
111
+ } catch (error) {
112
+ handle.fail(errorMessage(error));
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ // Subprocess span. Name is `$ <cmd>` truncated; detail carries the full
118
+ // command line and cwd so the harness can group by executable and verb.
119
+ export function traceCommand<T>(
120
+ file: string,
121
+ args: string[],
122
+ fn: () => Promise<T>,
123
+ options: { cwd?: string } = {},
124
+ ): Promise<T> {
125
+ return span(
126
+ commandSpanName(file, args),
127
+ fn,
128
+ commandDetail(file, args, options.cwd),
129
+ );
130
+ }
131
+
132
+ // Zero-duration marker (e.g. "browser websocket connected").
133
+ export function traceEvent(
134
+ name: string,
135
+ options: { parentId?: number | null; detail?: string } = {},
136
+ ): void {
137
+ startSpan(name, options).end();
138
+ }
139
+
140
+ // Process-wide attributes (command path, command run id, review uuid) that
141
+ // the harness uses to join this trace to telemetry and to the agent's tool
142
+ // call.
143
+ export function setTraceAttribute(
144
+ key: string,
145
+ value: string | number | boolean,
146
+ ): void {
147
+ if (!traceEnabled) return;
148
+ attributes[key] = value;
149
+ }
150
+
151
+ function flushTrace(): void {
152
+ if (!traceEnabled || !traceFile) return;
153
+ const now = performance.now();
154
+
155
+ try {
156
+ mkdirSync(path.dirname(traceFile), { recursive: true });
157
+ writeFileSync(
158
+ traceFile,
159
+ `${JSON.stringify({
160
+ timeOrigin: performance.timeOrigin,
161
+ pid: process.pid,
162
+ ppid: process.ppid,
163
+ argv: process.argv,
164
+ cwd: process.cwd(),
165
+ delegated: Boolean(process.env.DEV_FAST_REVIEW_CLI_DELEGATED),
166
+ attributes,
167
+ durationMs: now,
168
+ spans: spans.map((record) => ({
169
+ ...record,
170
+ end: record.end ?? now,
171
+ openAtExit: record.end === null || undefined,
172
+ })),
173
+ })}\n`,
174
+ "utf8",
175
+ );
176
+ } catch {
177
+ // Profiling must never break the CLI.
178
+ }
179
+ }
180
+
181
+ function commandSpanName(file: string, args: string[]): string {
182
+ return `$ ${[path.basename(file), ...args].join(" ").slice(0, 100)}`;
183
+ }
184
+
185
+ function commandDetail(
186
+ file: string,
187
+ args: string[],
188
+ cwd: string | undefined,
189
+ ): string {
190
+ const line = [file, ...args].join(" ");
191
+
192
+ return cwd ? `${line}\n(cwd ${cwd})` : line;
193
+ }
194
+
195
+ function resolveTraceFile(env: NodeJS.ProcessEnv): string | undefined {
196
+ const file = env[TRACE_FILE_ENV]?.trim();
197
+
198
+ if (file) return file;
199
+ const dir = env[TRACE_DIR_ENV]?.trim();
200
+
201
+ if (!dir) return undefined;
202
+
203
+ const argvSlug = process.argv
204
+ .slice(2)
205
+ .filter((arg) => !arg.startsWith("-"))
206
+ .slice(0, 3)
207
+ .join("-")
208
+ .replace(/[^A-Za-z0-9_.-]+/g, "_")
209
+ .slice(0, 60);
210
+
211
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
212
+
213
+ return path.join(
214
+ dir,
215
+ `${stamp}-${process.pid}${argvSlug ? `-${argvSlug}` : ""}.json`,
216
+ );
217
+ }
218
+
219
+ if (traceEnabled) {
220
+ process.on("exit", flushTrace);
221
+ // Every git/jj spawn inside @dev.fast/local-vcs becomes a `$ …` span under
222
+ // whatever span is active in the caller's async context.
223
+ setLocalVcsCommandObserver({
224
+ start({ file, args, cwd }) {
225
+ const handle = startSpan(commandSpanName(file, args), {
226
+ detail: commandDetail(file, args, cwd),
227
+ });
228
+
229
+ return ({ ok }) => (ok ? handle.end() : handle.fail());
230
+ },
231
+ });
232
+ }