@1agh/maude 1.0.11 → 1.3.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 (567) hide show
  1. package/README.md +3 -1
  2. package/apps/studio/acp/bridge.ts +18 -13
  3. package/apps/studio/acp/index.ts +43 -7
  4. package/apps/studio/acp/transcript-io.ts +394 -0
  5. package/apps/studio/acp/transcript.ts +103 -38
  6. package/apps/studio/annotations-layer.tsx +191 -116
  7. package/apps/studio/annotations-model.ts +39 -0
  8. package/apps/studio/annotations-sync.ts +50 -0
  9. package/apps/studio/api.ts +821 -55
  10. package/apps/studio/bin/_import-figma.mjs +57 -11
  11. package/apps/studio/bin/_seed-fixture.mjs +163 -0
  12. package/apps/studio/bin/annotate.mjs +3 -1
  13. package/apps/studio/bin/server-up.sh +17 -2
  14. package/apps/studio/canvas-edit.ts +391 -7
  15. package/apps/studio/canvas-imports.ts +89 -0
  16. package/apps/studio/canvas-lib.tsx +82 -18
  17. package/apps/studio/canvas-notice-message.ts +16 -0
  18. package/apps/studio/canvas-notifications.tsx +19 -0
  19. package/apps/studio/canvas-shell.tsx +57 -6
  20. package/apps/studio/client/app.jsx +943 -248
  21. package/apps/studio/client/apply-edit-request.ts +55 -0
  22. package/apps/studio/client/export-center.jsx +125 -67
  23. package/apps/studio/client/github.js +38 -1
  24. package/apps/studio/client/index-loader.ts +76 -0
  25. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  26. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  27. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  28. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  29. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  30. package/apps/studio/client/panels/SyncPanel.jsx +238 -4
  31. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  32. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  33. package/apps/studio/client/photo-knobs.jsx +3 -0
  34. package/apps/studio/client/report-bug.jsx +19 -1
  35. package/apps/studio/client/share-dialog.jsx +91 -0
  36. package/apps/studio/client/share-link.js +94 -0
  37. package/apps/studio/client/styles/3-shell-maude.css +69 -18
  38. package/apps/studio/client/styles/4-components.css +44 -1
  39. package/apps/studio/client/tree-row-menu.jsx +48 -3
  40. package/apps/studio/client/whats-new.jsx +26 -32
  41. package/apps/studio/cloud/endpoints.ts +132 -0
  42. package/apps/studio/collab/index.ts +4 -0
  43. package/apps/studio/collab/persistence.ts +137 -17
  44. package/apps/studio/collab/registry.ts +19 -21
  45. package/apps/studio/collab/room.ts +13 -2
  46. package/apps/studio/context.ts +59 -0
  47. package/apps/studio/debug-bundle.ts +13 -0
  48. package/apps/studio/dist/client.bundle.js +1911 -1879
  49. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  50. package/apps/studio/dist/runtime/sonner.js +1 -0
  51. package/apps/studio/dist/styles.css +1 -1
  52. package/apps/studio/export-dialog.tsx +54 -0
  53. package/apps/studio/exporters/degraded.ts +19 -3
  54. package/apps/studio/exporters/ghostscript.ts +194 -0
  55. package/apps/studio/exporters/pdf-fonts.ts +261 -0
  56. package/apps/studio/exporters/pdf.ts +152 -3
  57. package/apps/studio/exporters/remote.ts +36 -1
  58. package/apps/studio/fs-watch.ts +45 -1
  59. package/apps/studio/http.ts +296 -19
  60. package/apps/studio/inspect.ts +19 -0
  61. package/apps/studio/managed-projects.ts +150 -0
  62. package/apps/studio/mem.ts +45 -9
  63. package/apps/studio/notifications.tsx +292 -0
  64. package/apps/studio/runtime-bundle.ts +2 -0
  65. package/apps/studio/server.ts +69 -6
  66. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  67. package/apps/studio/sync/accepted-link.ts +320 -0
  68. package/apps/studio/sync/action-stage.ts +343 -0
  69. package/apps/studio/sync/agent.ts +104 -45
  70. package/apps/studio/sync/asset-push.ts +1 -47
  71. package/apps/studio/sync/cell-file-events.ts +3 -0
  72. package/apps/studio/sync/codec.ts +192 -34
  73. package/apps/studio/sync/cold-start-apply.ts +7 -0
  74. package/apps/studio/sync/cold-start.ts +118 -22
  75. package/apps/studio/sync/comment-identity.ts +68 -0
  76. package/apps/studio/sync/connection-state.ts +15 -1
  77. package/apps/studio/sync/ctl-provider.ts +15 -2
  78. package/apps/studio/sync/document-discovery.ts +81 -0
  79. package/apps/studio/sync/file-ledger.ts +117 -0
  80. package/apps/studio/sync/file-membership.ts +10 -2
  81. package/apps/studio/sync/file-plane.ts +926 -47
  82. package/apps/studio/sync/index.ts +1786 -36
  83. package/apps/studio/sync/journal-client.ts +13 -1
  84. package/apps/studio/sync/migrate-seed.ts +150 -22
  85. package/apps/studio/sync/poke.ts +4 -2
  86. package/apps/studio/sync/presentation.ts +311 -5
  87. package/apps/studio/sync/projection.ts +820 -23
  88. package/apps/studio/sync/repeated-module.ts +73 -0
  89. package/apps/studio/sync/retry-after.ts +169 -0
  90. package/apps/studio/sync/revision-barrier.ts +129 -0
  91. package/apps/studio/sync/seed-progress.ts +218 -0
  92. package/apps/studio/sync/seed-repair.ts +46 -0
  93. package/apps/studio/sync/source-merge.ts +100 -0
  94. package/apps/studio/sync/source-ops.ts +289 -0
  95. package/apps/studio/sync/source-recovery.ts +70 -0
  96. package/apps/studio/sync/source-validation.ts +56 -0
  97. package/apps/studio/sync/status.ts +201 -6
  98. package/apps/studio/sync/transaction-client.ts +460 -0
  99. package/apps/studio/sync/transport-error.ts +111 -0
  100. package/apps/studio/sync/writer-registry.ts +236 -0
  101. package/apps/studio/text-caret.ts +35 -0
  102. package/apps/studio/undo-hud.tsx +9 -87
  103. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  104. package/apps/studio/whats-new.json +173 -0
  105. package/cli/bin/maude.mjs +2 -0
  106. package/cli/commands/codex.mjs +191 -0
  107. package/cli/commands/harness.mjs +1051 -0
  108. package/cli/commands/help.mjs +13 -0
  109. package/cli/lib/design-link.mjs +190 -2
  110. package/cli/lib/harness/capabilities.mjs +302 -0
  111. package/cli/lib/harness/codex-runtime.mjs +892 -0
  112. package/cli/lib/harness/compatibility.mjs +27 -0
  113. package/cli/lib/harness/discover-claude.mjs +1393 -0
  114. package/cli/lib/harness/managed-state.mjs +1296 -0
  115. package/cli/lib/harness/model.mjs +86 -0
  116. package/cli/lib/harness/secrets.mjs +390 -0
  117. package/cli/lib/harness/targets/codex.mjs +1141 -0
  118. package/cli/lib/harness/targets/opencode.mjs +807 -0
  119. package/cli/lib/harness/transaction.mjs +1099 -0
  120. package/cli/lib/hubs-config.mjs +35 -8
  121. package/cli/templates/harness/opencode/maude-projector.ts +58 -0
  122. package/docs/harness-capability-matrix.md +125 -0
  123. package/docs/harness-environment-projection.md +95 -0
  124. package/package.json +33 -9
  125. package/plugins/design/dependencies.json +3 -3
  126. package/plugins/design/templates/_shell.html +47 -0
  127. package/plugins/flow/.claude-plugin/config.schema.json +8 -0
  128. package/plugins/flow/dependencies.json +3 -3
  129. package/apps/studio/.ai/cache/_stats.json +0 -7
  130. package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
  131. package/apps/studio/bin/_curl-local.test.mjs +0 -280
  132. package/apps/studio/bin/_pw-launch.test.mjs +0 -90
  133. package/apps/studio/bin/_smart-frames.test.mjs +0 -195
  134. package/apps/studio/bin/_transcribe.test.mjs +0 -80
  135. package/apps/studio/bun.lock +0 -928
  136. package/apps/studio/draw/test/animate.test.ts +0 -172
  137. package/apps/studio/draw/test/brush.test.ts +0 -213
  138. package/apps/studio/draw/test/composition.test.ts +0 -141
  139. package/apps/studio/draw/test/geometry.test.ts +0 -199
  140. package/apps/studio/draw/test/gradient.test.ts +0 -167
  141. package/apps/studio/draw/test/layout.test.ts +0 -120
  142. package/apps/studio/draw/test/morph.test.ts +0 -107
  143. package/apps/studio/draw/test/optimize.test.ts +0 -40
  144. package/apps/studio/draw/test/palette.test.ts +0 -123
  145. package/apps/studio/draw/test/primitives.test.ts +0 -129
  146. package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
  147. package/apps/studio/draw/test/serialize.test.ts +0 -105
  148. package/apps/studio/figma/assets.test.ts +0 -464
  149. package/apps/studio/figma/client.test.ts +0 -395
  150. package/apps/studio/figma/codegen-client.test.ts +0 -276
  151. package/apps/studio/figma/codegen-fonts.test.ts +0 -103
  152. package/apps/studio/figma/codegen-values.test.ts +0 -179
  153. package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
  154. package/apps/studio/figma/fig-decode.test.ts +0 -788
  155. package/apps/studio/figma/fig-differential.test.ts +0 -182
  156. package/apps/studio/figma/fig-translator.test.ts +0 -192
  157. package/apps/studio/figma/fig-vector.test.ts +0 -113
  158. package/apps/studio/figma/from-codegen.test.ts +0 -408
  159. package/apps/studio/figma/sanitize.test.ts +0 -325
  160. package/apps/studio/figma/tailwind-map.test.ts +0 -142
  161. package/apps/studio/figma/to-artboard.test.ts +0 -808
  162. package/apps/studio/figma/to-render.test.ts +0 -180
  163. package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
  164. package/apps/studio/figma/to-strokes.test.ts +0 -705
  165. package/apps/studio/figma/to-tokens.test.ts +0 -321
  166. package/apps/studio/figma/url.test.ts +0 -167
  167. package/apps/studio/footage/schema.test.ts +0 -287
  168. package/apps/studio/generation/adapters/groq.test.ts +0 -80
  169. package/apps/studio/generation/audio-library.test.ts +0 -95
  170. package/apps/studio/generation/captions.test.ts +0 -78
  171. package/apps/studio/generation/elevenlabs.test.ts +0 -223
  172. package/apps/studio/generation/gemini.test.ts +0 -350
  173. package/apps/studio/generation/jobs.test.ts +0 -128
  174. package/apps/studio/generation/keys.test.ts +0 -75
  175. package/apps/studio/generation/prefs.test.ts +0 -95
  176. package/apps/studio/generation/types.test.ts +0 -123
  177. package/apps/studio/generation/whisper-models.test.ts +0 -179
  178. package/apps/studio/test/_helpers.ts +0 -108
  179. package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
  180. package/apps/studio/test/acp-activity.test.ts +0 -154
  181. package/apps/studio/test/acp-ai-activity.test.ts +0 -182
  182. package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
  183. package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
  184. package/apps/studio/test/acp-branch-guard.test.ts +0 -123
  185. package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
  186. package/apps/studio/test/acp-bridge.test.ts +0 -545
  187. package/apps/studio/test/acp-capabilities.test.ts +0 -123
  188. package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
  189. package/apps/studio/test/acp-commands.test.ts +0 -119
  190. package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
  191. package/apps/studio/test/acp-elicitation.test.ts +0 -251
  192. package/apps/studio/test/acp-env.test.ts +0 -95
  193. package/apps/studio/test/acp-mode-banner.test.ts +0 -45
  194. package/apps/studio/test/acp-origin-gate.test.ts +0 -160
  195. package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
  196. package/apps/studio/test/acp-permission.test.ts +0 -278
  197. package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
  198. package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
  199. package/apps/studio/test/acp-session-plugins.test.ts +0 -209
  200. package/apps/studio/test/acp-toolgroup.test.ts +0 -76
  201. package/apps/studio/test/acp-transcript-view.test.ts +0 -71
  202. package/apps/studio/test/acp-transcript.test.ts +0 -238
  203. package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
  204. package/apps/studio/test/acp-usage.test.ts +0 -143
  205. package/apps/studio/test/acp-write-gate.test.ts +0 -323
  206. package/apps/studio/test/acp-write-scope.test.ts +0 -533
  207. package/apps/studio/test/active-state.test.ts +0 -186
  208. package/apps/studio/test/activity.test.ts +0 -189
  209. package/apps/studio/test/ai-activity.test.ts +0 -113
  210. package/apps/studio/test/annotate-write.test.ts +0 -1074
  211. package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
  212. package/apps/studio/test/annotations-align.test.ts +0 -88
  213. package/apps/studio/test/annotations-api.test.ts +0 -145
  214. package/apps/studio/test/annotations-bindings.test.ts +0 -262
  215. package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
  216. package/apps/studio/test/annotations-groups.test.ts +0 -231
  217. package/apps/studio/test/annotations-layer.test.ts +0 -1011
  218. package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
  219. package/apps/studio/test/annotations-snap.test.ts +0 -135
  220. package/apps/studio/test/annotations-text-format.test.ts +0 -396
  221. package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
  222. package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
  223. package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
  224. package/apps/studio/test/artboard-kinds.test.tsx +0 -83
  225. package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
  226. package/apps/studio/test/asset-api.test.ts +0 -149
  227. package/apps/studio/test/assets-s3.test.ts +0 -249
  228. package/apps/studio/test/binary-smoke.test.ts +0 -47
  229. package/apps/studio/test/boot-self-heal.test.ts +0 -87
  230. package/apps/studio/test/browse-posture.test.tsx +0 -107
  231. package/apps/studio/test/bundle-smoke.test.ts +0 -46
  232. package/apps/studio/test/camera-reveal.test.tsx +0 -173
  233. package/apps/studio/test/canvas-artifacts.test.ts +0 -125
  234. package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
  235. package/apps/studio/test/canvas-build.test.ts +0 -141
  236. package/apps/studio/test/canvas-create-api.test.ts +0 -676
  237. package/apps/studio/test/canvas-cursors.test.ts +0 -164
  238. package/apps/studio/test/canvas-edit.test.ts +0 -470
  239. package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
  240. package/apps/studio/test/canvas-header.test.ts +0 -127
  241. package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
  242. package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
  243. package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
  244. package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
  245. package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
  246. package/apps/studio/test/canvas-list-watch.test.ts +0 -388
  247. package/apps/studio/test/canvas-media-drop.test.ts +0 -187
  248. package/apps/studio/test/canvas-meta-api.test.ts +0 -707
  249. package/apps/studio/test/canvas-move-api.test.ts +0 -587
  250. package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
  251. package/apps/studio/test/canvas-pipeline.test.ts +0 -180
  252. package/apps/studio/test/canvas-rects.test.ts +0 -198
  253. package/apps/studio/test/canvas-reorder.test.ts +0 -211
  254. package/apps/studio/test/canvas-route.test.ts +0 -179
  255. package/apps/studio/test/canvas-shell-base.test.ts +0 -107
  256. package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
  257. package/apps/studio/test/canvas-url.test.ts +0 -138
  258. package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
  259. package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
  260. package/apps/studio/test/capture-core.test.ts +0 -32
  261. package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
  262. package/apps/studio/test/chat-attachments.test.ts +0 -117
  263. package/apps/studio/test/chat-context.test.ts +0 -129
  264. package/apps/studio/test/chat-markdown.test.tsx +0 -58
  265. package/apps/studio/test/clip-addressing.test.ts +0 -738
  266. package/apps/studio/test/clip-ops.test.ts +0 -922
  267. package/apps/studio/test/cloud-build.test.ts +0 -95
  268. package/apps/studio/test/cloud-connect-note.test.ts +0 -135
  269. package/apps/studio/test/cloud-endpoints.test.ts +0 -590
  270. package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
  271. package/apps/studio/test/cloud-history-posture.test.ts +0 -230
  272. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
  273. package/apps/studio/test/cloud-renew.test.ts +0 -205
  274. package/apps/studio/test/cloud-session-role.test.ts +0 -161
  275. package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
  276. package/apps/studio/test/cold-start-apply.test.ts +0 -303
  277. package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
  278. package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
  279. package/apps/studio/test/collab-bridge.test.ts +0 -79
  280. package/apps/studio/test/collab-loopback.test.ts +0 -63
  281. package/apps/studio/test/collab-origin-gate.test.ts +0 -323
  282. package/apps/studio/test/collab-protocol.test.ts +0 -146
  283. package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
  284. package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
  285. package/apps/studio/test/collab-room.test.ts +0 -188
  286. package/apps/studio/test/collab-session-survival.test.tsx +0 -176
  287. package/apps/studio/test/collab-stress.test.ts +0 -129
  288. package/apps/studio/test/comment-mount.test.ts +0 -97
  289. package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
  290. package/apps/studio/test/comments-api.test.ts +0 -277
  291. package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
  292. package/apps/studio/test/comments-overlay.test.ts +0 -117
  293. package/apps/studio/test/compile-entry.test.ts +0 -133
  294. package/apps/studio/test/component-map.test.ts +0 -48
  295. package/apps/studio/test/config-projection.test.ts +0 -104
  296. package/apps/studio/test/config-reload.test.ts +0 -230
  297. package/apps/studio/test/config-version.test.ts +0 -88
  298. package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
  299. package/apps/studio/test/convert-to-absolute.test.ts +0 -333
  300. package/apps/studio/test/credential-grammar.test.ts +0 -107
  301. package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
  302. package/apps/studio/test/csrf-write-guard.test.ts +0 -141
  303. package/apps/studio/test/debug-bundle.test.ts +0 -105
  304. package/apps/studio/test/design-setup-readiness.test.ts +0 -155
  305. package/apps/studio/test/detach-component.test.ts +0 -94
  306. package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
  307. package/apps/studio/test/dom-selection.test.ts +0 -198
  308. package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
  309. package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
  310. package/apps/studio/test/edit-persistence.test.ts +0 -91
  311. package/apps/studio/test/edit-scope-api.test.ts +0 -119
  312. package/apps/studio/test/edit-source-command.test.ts +0 -142
  313. package/apps/studio/test/edit-suppress.test.ts +0 -170
  314. package/apps/studio/test/editing-presence.test.ts +0 -103
  315. package/apps/studio/test/element-resize.test.ts +0 -136
  316. package/apps/studio/test/element-structural-api.test.ts +0 -434
  317. package/apps/studio/test/element-structural-edit.test.ts +0 -750
  318. package/apps/studio/test/ensure-browser.test.ts +0 -92
  319. package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
  320. package/apps/studio/test/export-assemble.test.ts +0 -53
  321. package/apps/studio/test/export-browser-lane.test.ts +0 -126
  322. package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
  323. package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
  324. package/apps/studio/test/export-center.test.tsx +0 -287
  325. package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
  326. package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
  327. package/apps/studio/test/export-lane.test.ts +0 -245
  328. package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
  329. package/apps/studio/test/exporters/canva.test.ts +0 -63
  330. package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
  331. package/apps/studio/test/exporters/endpoint.test.ts +0 -121
  332. package/apps/studio/test/exporters/history.test.ts +0 -107
  333. package/apps/studio/test/exporters/html.test.ts +0 -26
  334. package/apps/studio/test/exporters/jobs.test.ts +0 -342
  335. package/apps/studio/test/exporters/pdf.test.ts +0 -85
  336. package/apps/studio/test/exporters/png.test.ts +0 -103
  337. package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
  338. package/apps/studio/test/exporters/pptx.test.ts +0 -31
  339. package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
  340. package/apps/studio/test/exporters/runtime.test.ts +0 -59
  341. package/apps/studio/test/exporters/scope.test.ts +0 -292
  342. package/apps/studio/test/exporters/svg.test.ts +0 -29
  343. package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
  344. package/apps/studio/test/exporters/zip.test.ts +0 -104
  345. package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
  346. package/apps/studio/test/figjam-v3-model.test.ts +0 -342
  347. package/apps/studio/test/figma-explode.test.ts +0 -438
  348. package/apps/studio/test/figma-provenance.test.ts +0 -108
  349. package/apps/studio/test/figma-routes.test.ts +0 -294
  350. package/apps/studio/test/file-lock.test.ts +0 -84
  351. package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
  352. package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
  353. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
  354. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
  355. package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
  356. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
  357. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
  358. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
  359. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
  360. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
  361. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
  362. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
  363. package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
  364. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
  365. package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
  366. package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
  367. package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
  368. package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
  369. package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
  370. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  371. package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
  372. package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
  373. package/apps/studio/test/footage-store.test.ts +0 -100
  374. package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
  375. package/apps/studio/test/fs-watch.test.ts +0 -41
  376. package/apps/studio/test/gemma-models.test.ts +0 -245
  377. package/apps/studio/test/generate-route.test.ts +0 -106
  378. package/apps/studio/test/git-api.test.ts +0 -0
  379. package/apps/studio/test/git-branches.test.ts +0 -341
  380. package/apps/studio/test/git-cloud-posture.test.ts +0 -59
  381. package/apps/studio/test/git-grouping.test.ts +0 -106
  382. package/apps/studio/test/git-lifecycle.test.ts +0 -100
  383. package/apps/studio/test/git-log-format.test.ts +0 -95
  384. package/apps/studio/test/git-safe-rel.test.ts +0 -132
  385. package/apps/studio/test/git-system-engine.test.ts +0 -135
  386. package/apps/studio/test/git-watch.test.ts +0 -97
  387. package/apps/studio/test/github-api.test.ts +0 -577
  388. package/apps/studio/test/grid-track-handles.test.ts +0 -160
  389. package/apps/studio/test/handoff-static-frames.test.ts +0 -215
  390. package/apps/studio/test/handoff.test.ts +0 -329
  391. package/apps/studio/test/history-rollback.test.ts +0 -88
  392. package/apps/studio/test/hmr-broadcast.test.ts +0 -236
  393. package/apps/studio/test/hmr-classify.test.ts +0 -87
  394. package/apps/studio/test/hub-link.test.ts +0 -80
  395. package/apps/studio/test/identity-cache.test.ts +0 -83
  396. package/apps/studio/test/import-asset-browser.test.ts +0 -64
  397. package/apps/studio/test/import-asset-route.test.ts +0 -71
  398. package/apps/studio/test/import-asset.test.ts +0 -427
  399. package/apps/studio/test/import-brand-browser.test.ts +0 -88
  400. package/apps/studio/test/import-brand-route.test.ts +0 -120
  401. package/apps/studio/test/import-brand.test.ts +0 -206
  402. package/apps/studio/test/import-figma.test.ts +0 -667
  403. package/apps/studio/test/import-tokens.test.ts +0 -722
  404. package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
  405. package/apps/studio/test/input-router.test.ts +0 -757
  406. package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
  407. package/apps/studio/test/inspect-selections.test.ts +0 -219
  408. package/apps/studio/test/knob-props-authored.test.ts +0 -87
  409. package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
  410. package/apps/studio/test/locator.test.ts +0 -214
  411. package/apps/studio/test/marquee-overlay.test.ts +0 -94
  412. package/apps/studio/test/media-commit-chain.test.ts +0 -210
  413. package/apps/studio/test/mode-toggle.test.tsx +0 -226
  414. package/apps/studio/test/motion-sample.test.ts +0 -35
  415. package/apps/studio/test/move-artboards-command.test.ts +0 -108
  416. package/apps/studio/test/participants-chrome.test.ts +0 -65
  417. package/apps/studio/test/paths.test.ts +0 -88
  418. package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
  419. package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
  420. package/apps/studio/test/perf-harness.ts +0 -193
  421. package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
  422. package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
  423. package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
  424. package/apps/studio/test/photo-edit-api.test.ts +0 -201
  425. package/apps/studio/test/photo-filters.test.ts +0 -149
  426. package/apps/studio/test/photo-pipeline.test.ts +0 -106
  427. package/apps/studio/test/photo-taxonomy.test.ts +0 -96
  428. package/apps/studio/test/print-marks.test.ts +0 -113
  429. package/apps/studio/test/print-units.test.ts +0 -173
  430. package/apps/studio/test/read-annotations.test.ts +0 -727
  431. package/apps/studio/test/read-only-gate.test.ts +0 -188
  432. package/apps/studio/test/read-only-role.test.ts +0 -95
  433. package/apps/studio/test/readiness.test.ts +0 -337
  434. package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
  435. package/apps/studio/test/reorder-api.test.ts +0 -210
  436. package/apps/studio/test/repo-concurrency.test.ts +0 -146
  437. package/apps/studio/test/repo-lock.test.ts +0 -350
  438. package/apps/studio/test/report-proxy.test.ts +0 -125
  439. package/apps/studio/test/ripple.test.ts +0 -197
  440. package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
  441. package/apps/studio/test/runtime-bundle.test.ts +0 -69
  442. package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
  443. package/apps/studio/test/scaffold-design.test.ts +0 -122
  444. package/apps/studio/test/server-lifecycle.test.ts +0 -38
  445. package/apps/studio/test/session-runtime-state.test.ts +0 -226
  446. package/apps/studio/test/session-scope.test.ts +0 -100
  447. package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
  448. package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
  449. package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
  450. package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
  451. package/apps/studio/test/shared-doc-projection.test.ts +0 -238
  452. package/apps/studio/test/shell-importmap.test.ts +0 -56
  453. package/apps/studio/test/sizing-mode.test.ts +0 -102
  454. package/apps/studio/test/slash-commands.test.ts +0 -117
  455. package/apps/studio/test/snap-distance-pill.test.ts +0 -68
  456. package/apps/studio/test/spacing-handles.test.ts +0 -138
  457. package/apps/studio/test/specimen-select.test.ts +0 -88
  458. package/apps/studio/test/sync-agent.test.ts +0 -660
  459. package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
  460. package/apps/studio/test/sync-asset-push.test.ts +0 -931
  461. package/apps/studio/test/sync-atomic-write.test.ts +0 -62
  462. package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
  463. package/apps/studio/test/sync-autocommit.test.ts +0 -537
  464. package/apps/studio/test/sync-canvas-path.test.ts +0 -200
  465. package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
  466. package/apps/studio/test/sync-codec.test.ts +0 -230
  467. package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
  468. package/apps/studio/test/sync-cold-start.test.ts +0 -304
  469. package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
  470. package/apps/studio/test/sync-connection-state.test.ts +0 -337
  471. package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
  472. package/apps/studio/test/sync-decide-file.test.ts +0 -420
  473. package/apps/studio/test/sync-doc-name.test.ts +0 -281
  474. package/apps/studio/test/sync-echo-guard.test.ts +0 -96
  475. package/apps/studio/test/sync-file-ledger.test.ts +0 -334
  476. package/apps/studio/test/sync-file-membership.test.ts +0 -347
  477. package/apps/studio/test/sync-file-plane.test.ts +0 -976
  478. package/apps/studio/test/sync-file-pull.test.ts +0 -333
  479. package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
  480. package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
  481. package/apps/studio/test/sync-hardening.test.ts +0 -586
  482. package/apps/studio/test/sync-hub-listing.test.ts +0 -46
  483. package/apps/studio/test/sync-hubs-config.test.ts +0 -133
  484. package/apps/studio/test/sync-incident-replay.test.ts +0 -211
  485. package/apps/studio/test/sync-journal.test.ts +0 -176
  486. package/apps/studio/test/sync-meta-codec.test.ts +0 -199
  487. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
  488. package/apps/studio/test/sync-move-retirement.test.ts +0 -231
  489. package/apps/studio/test/sync-panel-surface.test.ts +0 -153
  490. package/apps/studio/test/sync-path-pull.test.ts +0 -531
  491. package/apps/studio/test/sync-presentation.test.ts +0 -262
  492. package/apps/studio/test/sync-pull-budget.test.ts +0 -169
  493. package/apps/studio/test/sync-remote-docs.test.ts +0 -333
  494. package/apps/studio/test/sync-resync-routes.test.ts +0 -168
  495. package/apps/studio/test/sync-runtime.test.ts +0 -1675
  496. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
  497. package/apps/studio/test/sync-settings-routes.test.ts +0 -195
  498. package/apps/studio/test/sync-settings.test.ts +0 -151
  499. package/apps/studio/test/sync-status.test.ts +0 -261
  500. package/apps/studio/test/sync-supervisor.test.ts +0 -262
  501. package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
  502. package/apps/studio/test/sync-trash.test.ts +0 -132
  503. package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
  504. package/apps/studio/test/sync-untrusted.test.ts +0 -103
  505. package/apps/studio/test/system-endpoint.test.ts +0 -144
  506. package/apps/studio/test/text-editability-stamp.test.ts +0 -131
  507. package/apps/studio/test/timeline-comp-target.test.ts +0 -139
  508. package/apps/studio/test/timeline-media-cache.test.ts +0 -41
  509. package/apps/studio/test/timeline-parse.test.ts +0 -184
  510. package/apps/studio/test/timeline-scale.test.ts +0 -65
  511. package/apps/studio/test/timeline-snap.test.ts +0 -85
  512. package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
  513. package/apps/studio/test/tool-palette.test.tsx +0 -55
  514. package/apps/studio/test/tour-overlay.test.tsx +0 -90
  515. package/apps/studio/test/trusted-request-host.test.ts +0 -66
  516. package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
  517. package/apps/studio/test/undo-stack.test.ts +0 -244
  518. package/apps/studio/test/use-agent-presence.test.tsx +0 -117
  519. package/apps/studio/test/use-annotation-resize.test.ts +0 -402
  520. package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
  521. package/apps/studio/test/use-artboard-drag.test.ts +0 -324
  522. package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
  523. package/apps/studio/test/use-collab.test.ts +0 -0
  524. package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
  525. package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
  526. package/apps/studio/test/use-selection-set.test.tsx +0 -166
  527. package/apps/studio/test/use-snap-guides.test.ts +0 -271
  528. package/apps/studio/test/use-tool-mode.test.tsx +0 -186
  529. package/apps/studio/test/use-undo-stack.test.tsx +0 -223
  530. package/apps/studio/test/video-asset.test.ts +0 -163
  531. package/apps/studio/test/video-comp-fixture.test.ts +0 -50
  532. package/apps/studio/test/video-comp.test.ts +0 -270
  533. package/apps/studio/test/video-encode-lib.test.ts +0 -63
  534. package/apps/studio/test/video-render-bridge.test.ts +0 -171
  535. package/apps/studio/test/whats-new.test.ts +0 -113
  536. package/apps/studio/test/whisper-setup.test.ts +0 -97
  537. package/apps/studio/test/workspace-containment.test.ts +0 -382
  538. package/apps/studio/test/workspace-signin.test.ts +0 -292
  539. package/apps/studio/test/ws-handshake.test.ts +0 -33
  540. package/cli/commands/cache.test.mjs +0 -166
  541. package/cli/commands/design-link.test.mjs +0 -419
  542. package/cli/commands/design.test.mjs +0 -240
  543. package/cli/commands/doctor.test.mjs +0 -237
  544. package/cli/commands/hub-workspace.test.mjs +0 -275
  545. package/cli/commands/hub.test.mjs +0 -225
  546. package/cli/commands/kg.test.mjs +0 -142
  547. package/cli/commands/preflight.test.mjs +0 -46
  548. package/cli/lib/argv.test.mjs +0 -46
  549. package/cli/lib/cache.test.mjs +0 -303
  550. package/cli/lib/cell-plan.test.mjs +0 -225
  551. package/cli/lib/config-lint.test.mjs +0 -117
  552. package/cli/lib/ddr-to-kgai.test.mjs +0 -118
  553. package/cli/lib/design-ownership.test.mjs +0 -329
  554. package/cli/lib/fetch-asset.test.mjs +0 -213
  555. package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
  556. package/cli/lib/figma-import-controls.test.mjs +0 -70
  557. package/cli/lib/flow-design-integration.test.mjs +0 -165
  558. package/cli/lib/gitignore-block.test.mjs +0 -136
  559. package/cli/lib/gitignore-drift.test.mjs +0 -156
  560. package/cli/lib/hubs-config.test.mjs +0 -165
  561. package/cli/lib/pkg-root.test.mjs +0 -111
  562. package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
  563. package/cli/lib/plugin-name-namespace.test.mjs +0 -71
  564. package/cli/lib/reconstruct-toolset.test.mjs +0 -194
  565. package/cli/lib/stack-detect.test.mjs +0 -121
  566. package/cli/lib/update-check.test.mjs +0 -32
  567. package/cli/lib/workspace-plan.test.mjs +0 -699
@@ -37,14 +37,28 @@ import { parseSync } from 'oxc-parser';
37
37
  export class CanvasEditError extends Error {
38
38
  readonly canvas: string;
39
39
  readonly id: string;
40
- constructor(message: string, info: { canvas: string; id: string }) {
40
+ /** The target no longer holds the value the caller expected (a peer changed it). */
41
+ readonly conflict: boolean;
42
+ constructor(message: string, info: { canvas: string; id: string; conflict?: boolean }) {
41
43
  super(message);
42
44
  this.name = 'CanvasEditError';
43
45
  this.canvas = info.canvas;
44
46
  this.id = info.id;
47
+ this.conflict = info.conflict === true;
45
48
  }
46
49
  }
47
50
 
51
+ /**
52
+ * Expected-current-value guard for a single-attribute write (audit 2026-09-13
53
+ * P1 #5). `expected` is what the caller believes the source holds now, `next`
54
+ * is what it is about to write; `null` means "attribute / style key absent".
55
+ * Values are the RAW strings the edit routes take (not JSON-encoded).
56
+ */
57
+ export interface AttributePrecondition {
58
+ expected: string | null;
59
+ next: string | null;
60
+ }
61
+
48
62
  const PASCAL_CASE = /^[A-Z][A-Za-z0-9_]*$/;
49
63
  // biome-ignore lint/suspicious/noExplicitAny: oxc-parser AST nodes are heterogeneous.
50
64
  type AnyNode = any;
@@ -268,6 +282,12 @@ export interface EditResult {
268
282
  * real write with delta 0 (RC1 rim-suppression finding).
269
283
  */
270
284
  changed?: boolean;
285
+ /**
286
+ * What the target held BEFORE this write, read under the same lock. The
287
+ * value an undo must restore is what the write replaced — never what a
288
+ * client panel last believed was there (it can be a teammate's edit old).
289
+ */
290
+ previous?: AttributeState;
271
291
  }
272
292
 
273
293
  /**
@@ -280,7 +300,8 @@ export async function editAttribute(
280
300
  id: string,
281
301
  attr: string,
282
302
  value: string,
283
- occurrence?: number
303
+ occurrence?: number,
304
+ precondition?: AttributePrecondition
284
305
  ): Promise<EditResult> {
285
306
  return withLock(canvasAbsPath, async () => {
286
307
  const file = Bun.file(canvasAbsPath);
@@ -291,13 +312,22 @@ export async function editAttribute(
291
312
  });
292
313
  }
293
314
  const source = await file.text();
315
+ // Read under the same per-file lock as the write — no gap between the read
316
+ // that proves the precondition (and records what is replaced) and the rename.
317
+ const previous = readAttributeState(canvasAbsPath, source, id, attr, occurrence);
318
+ if (
319
+ precondition &&
320
+ checkPrecondition(previous, id, attr, canvasAbsPath, precondition) === 'already'
321
+ ) {
322
+ return { source, delta: 0, changed: false, previous };
323
+ }
294
324
  const next = applyEdit(canvasAbsPath, source, id, attr, value, occurrence);
295
- if (next.source === source) return { source, delta: 0, changed: false };
325
+ if (next.source === source) return { source, delta: 0, changed: false, previous };
296
326
  const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
297
327
  await Bun.write(tmp, next.source);
298
328
  const { rename } = await import('node:fs/promises');
299
329
  await rename(tmp, canvasAbsPath);
300
- return { ...next, changed: true };
330
+ return { ...next, changed: true, previous };
301
331
  });
302
332
  }
303
333
 
@@ -313,7 +343,8 @@ export async function removeAttribute(
313
343
  canvasAbsPath: string,
314
344
  id: string,
315
345
  attr: string,
316
- occurrence?: number
346
+ occurrence?: number,
347
+ precondition?: AttributePrecondition
317
348
  ): Promise<EditResult> {
318
349
  return withLock(canvasAbsPath, async () => {
319
350
  const file = Bun.file(canvasAbsPath);
@@ -324,13 +355,124 @@ export async function removeAttribute(
324
355
  });
325
356
  }
326
357
  const source = await file.text();
358
+ const previous = readAttributeState(canvasAbsPath, source, id, attr, occurrence);
359
+ if (
360
+ precondition &&
361
+ checkPrecondition(previous, id, attr, canvasAbsPath, precondition) === 'already'
362
+ ) {
363
+ return { source, delta: 0, changed: false, previous };
364
+ }
327
365
  const next = applyRemove(canvasAbsPath, source, id, attr, occurrence);
328
- if (next.source === source) return { source, delta: 0, changed: false };
366
+ if (next.source === source) return { source, delta: 0, changed: false, previous };
329
367
  const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
330
368
  await Bun.write(tmp, next.source);
331
369
  const { rename } = await import('node:fs/promises');
332
370
  await rename(tmp, canvasAbsPath);
333
- return { ...next, changed: true };
371
+ return { ...next, changed: true, previous };
372
+ });
373
+ }
374
+
375
+ /** What one plain attribute or inline style key currently holds in source. */
376
+ export type AttributeState =
377
+ | { kind: 'absent' }
378
+ | { kind: 'literal'; value: string }
379
+ /** Present, but not a literal this module can compare (an expression, a spread). */
380
+ | { kind: 'expression' };
381
+
382
+ function literalText(node: AnyNode): string | null {
383
+ if (!node) return null;
384
+ if (node.type === 'Literal' || node.type === 'StringLiteral' || node.type === 'NumericLiteral') {
385
+ return typeof node.value === 'string' || typeof node.value === 'number'
386
+ ? String(node.value)
387
+ : null;
388
+ }
389
+ if (node.type === 'TemplateLiteral' && node.expressions?.length === 0) {
390
+ return node.quasis?.[0]?.value?.cooked ?? null;
391
+ }
392
+ return null;
393
+ }
394
+
395
+ /**
396
+ * Read the current value of `attr` (`style.<prop>` or a plain attribute name)
397
+ * on the element with `data-cd-id` `id`, with the same occurrence routing as
398
+ * `applyEdit` / `applyRemove`. Pure — exposed for tests and preconditions.
399
+ */
400
+ export function readAttributeState(
401
+ canvasAbsPath: string,
402
+ source: string,
403
+ id: string,
404
+ attr: string,
405
+ occurrence?: number
406
+ ): AttributeState {
407
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
408
+ if (parsed.errors && parsed.errors.length > 0) {
409
+ throw new CanvasEditError(
410
+ `oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
411
+ { canvas: canvasAbsPath, id }
412
+ );
413
+ }
414
+ if (typeof occurrence === 'number' && Number.isFinite(occurrence)) {
415
+ id = resolveUsageId(parsed.program, id, occurrence);
416
+ }
417
+ const hit = findOpening(parsed.program, id);
418
+ if (!hit) {
419
+ throw new CanvasEditError(`data-cd-id "${id}" not found in ${canvasAbsPath}`, {
420
+ canvas: canvasAbsPath,
421
+ id,
422
+ });
423
+ }
424
+ if (attr.startsWith('style.')) {
425
+ const style = findAttribute(hit.opening, 'style');
426
+ if (!style) return { kind: 'absent' };
427
+ const obj = style.value?.type === 'JSXExpressionContainer' ? style.value.expression : null;
428
+ if (obj?.type !== 'ObjectExpression') return { kind: 'expression' };
429
+ const prop = attr.slice('style.'.length);
430
+ const propCamel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
431
+ for (const p of obj.properties as AnyNode[]) {
432
+ if (p?.type !== 'Property' && p?.type !== 'ObjectProperty') continue;
433
+ const k = p.key;
434
+ const kname =
435
+ k?.type === 'Identifier' ? k.name : k?.type === 'Literal' ? String(k.value) : null;
436
+ if (kname !== prop && kname !== propCamel) continue;
437
+ const text = literalText(p.value);
438
+ return text === null ? { kind: 'expression' } : { kind: 'literal', value: text };
439
+ }
440
+ // A spread may supply the key at runtime; "absent" would be a guess.
441
+ return (obj.properties as AnyNode[]).some((p) => p?.type === 'SpreadElement')
442
+ ? { kind: 'expression' }
443
+ : { kind: 'absent' };
444
+ }
445
+ const found = findAttribute(hit.opening, attr);
446
+ if (!found) return { kind: 'absent' };
447
+ if (found.value == null) return { kind: 'literal', value: '' };
448
+ const direct = literalText(found.value);
449
+ if (direct !== null) return { kind: 'literal', value: direct };
450
+ const inner =
451
+ found.value.type === 'JSXExpressionContainer' ? literalText(found.value.expression) : null;
452
+ return inner === null ? { kind: 'expression' } : { kind: 'literal', value: inner };
453
+ }
454
+
455
+ /**
456
+ * `'apply'` when the source still holds `expected`; `'already'` when it holds
457
+ * `next` (a retried or already-applied write — idempotent success). Anything
458
+ * else is a peer's newer value: refuse rather than overwrite it.
459
+ */
460
+ function checkPrecondition(
461
+ state: AttributeState,
462
+ id: string,
463
+ attr: string,
464
+ canvasAbsPath: string,
465
+ pre: AttributePrecondition
466
+ ): 'apply' | 'already' {
467
+ const holds = (value: string | null) =>
468
+ value === null ? state.kind === 'absent' : state.kind === 'literal' && state.value === value;
469
+ if (holds(pre.next)) return 'already';
470
+ if (holds(pre.expected)) return 'apply';
471
+ const name = attr.startsWith('style.') ? attr.slice('style.'.length) : attr;
472
+ throw new CanvasEditError(`${name} was changed by someone else — kept their value`, {
473
+ canvas: canvasAbsPath,
474
+ id,
475
+ conflict: true,
334
476
  });
335
477
  }
336
478
 
@@ -3930,6 +4072,86 @@ export function applySetArtboardKind(
3930
4072
  return { source: out };
3931
4073
  }
3932
4074
 
4075
+ /** Longest artboard name the label edit accepts (the chrome truncates long ones). */
4076
+ export const MAX_ARTBOARD_LABEL = 80;
4077
+
4078
+ /**
4079
+ * Rename an artboard — write its DCArtboard `label` string prop (plan T25/L08,
4080
+ * the double-click-the-name gesture). Addressed by the `id` prop like every
4081
+ * other artboard verb; the label is a JSX string attribute, so `editStringAttr`
4082
+ * escapes it. Pure; reparse-gated.
4083
+ */
4084
+ export function applySetArtboardLabel(
4085
+ canvasAbsPath: string,
4086
+ source: string,
4087
+ artboardId: string,
4088
+ label: string
4089
+ ): { source: string } {
4090
+ // Control characters never reach a JSX attribute.
4091
+ const clean = [...label]
4092
+ .filter((c) => c.charCodeAt(0) >= 0x20 && c.charCodeAt(0) !== 0x7f)
4093
+ .join('')
4094
+ .trim();
4095
+ if (!clean || clean.length > MAX_ARTBOARD_LABEL) {
4096
+ throw new CanvasEditError(`artboard name must be 1–${MAX_ARTBOARD_LABEL} characters`, {
4097
+ canvas: canvasAbsPath,
4098
+ id: artboardId,
4099
+ });
4100
+ }
4101
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
4102
+ if (parsed.errors && parsed.errors.length > 0) {
4103
+ throw new CanvasEditError(
4104
+ `oxc-parser failed on ${canvasAbsPath}: ${parsed.errors[0]?.message ?? 'unknown'}`,
4105
+ { canvas: canvasAbsPath, id: artboardId }
4106
+ );
4107
+ }
4108
+ const target = collectJsxByTag(parsed.program, 'DCArtboard').find(
4109
+ (a) => getStringAttr(a.openingElement, 'id') === artboardId
4110
+ );
4111
+ if (!target) {
4112
+ throw new CanvasEditError(`<DCArtboard id="${artboardId}"> not found in ${canvasAbsPath}`, {
4113
+ canvas: canvasAbsPath,
4114
+ id: artboardId,
4115
+ });
4116
+ }
4117
+ const s = new MagicString(source);
4118
+ editStringAttr(s, target.openingElement, 'label', clean, canvasAbsPath, artboardId);
4119
+ const out = s.toString();
4120
+ const check = parseSync(canvasAbsPath, out, { sourceType: 'module' });
4121
+ if (check.errors && check.errors.length > 0) {
4122
+ throw new CanvasEditError(
4123
+ `artboard rename produced invalid source (${check.errors[0]?.message ?? 'parse error'})`,
4124
+ { canvas: canvasAbsPath, id: artboardId }
4125
+ );
4126
+ }
4127
+ return { source: out };
4128
+ }
4129
+
4130
+ /** Rename an artboard on disk (atomic write + cross-process lock). */
4131
+ export async function setArtboardLabel(
4132
+ canvasAbsPath: string,
4133
+ artboardId: string,
4134
+ label: string
4135
+ ): Promise<{ source: string }> {
4136
+ return withLock(canvasAbsPath, async () => {
4137
+ const file = Bun.file(canvasAbsPath);
4138
+ if (!(await file.exists())) {
4139
+ throw new CanvasEditError(`Canvas not found: ${canvasAbsPath}`, {
4140
+ canvas: canvasAbsPath,
4141
+ id: artboardId,
4142
+ });
4143
+ }
4144
+ const source = await file.text();
4145
+ const next = applySetArtboardLabel(canvasAbsPath, source, artboardId, label);
4146
+ if (next.source === source) return next;
4147
+ const tmp = `${canvasAbsPath}.tmp.${Math.random().toString(36).slice(2, 10)}`;
4148
+ await Bun.write(tmp, next.source);
4149
+ const { rename } = await import('node:fs/promises');
4150
+ await rename(tmp, canvasAbsPath);
4151
+ return next;
4152
+ });
4153
+ }
4154
+
3933
4155
  /** Set an artboard's `kind` on disk (atomic write + cross-process lock). */
3934
4156
  export async function setArtboardKind(
3935
4157
  canvasAbsPath: string,
@@ -5354,3 +5576,165 @@ if (import.meta.main) {
5354
5576
  process.exit(2);
5355
5577
  }
5356
5578
  }
5579
+
5580
+ // ---------------------------------------------------------------------------
5581
+ // Position-independent element identity — plan T23/T24 (DDR-241).
5582
+ //
5583
+ // `data-cd-id` is positional (component + pre-order index), so a sibling a
5584
+ // teammate inserted earlier in the same component renumbers everything after
5585
+ // it. An edit made through the UI is re-applied onto the project's newer
5586
+ // version when its own proposal lost a race (sync/source-ops.ts); before it
5587
+ // touches anything there, it has to find THE SAME element — by what the
5588
+ // element is, not where it was.
5589
+ //
5590
+ // A print is the enclosing component, the tag, the chain of ancestor tags and
5591
+ // the element's literal attributes (minus the one being edited, minus the
5592
+ // pipeline's own `data-cd-*`). It identifies an element when exactly one
5593
+ // element in the newer source carries it; anything else is ambiguous, and an
5594
+ // ambiguous edit is never guessed at.
5595
+
5596
+ export interface ElementPrint {
5597
+ component: string;
5598
+ tag: string;
5599
+ chain: string[];
5600
+ attrs: string;
5601
+ /** The element's own literal text (bounded) — absent for a text edit, whose text is what changes. */
5602
+ text?: string;
5603
+ }
5604
+
5605
+ function ownText(node: AnyNode): string {
5606
+ let t = '';
5607
+ for (const c of Array.isArray(node?.children) ? node.children : []) {
5608
+ if (c?.type === 'JSXText') t += String(c.value);
5609
+ }
5610
+ return t.replace(/\s+/g, ' ').trim().slice(0, 80);
5611
+ }
5612
+
5613
+ function literalAttrs(opening: AnyNode, ignore: string | null): string {
5614
+ const out: string[] = [];
5615
+ for (const a of Array.isArray(opening?.attributes) ? opening.attributes : []) {
5616
+ if (a?.type !== 'JSXAttribute' || a.name?.type !== 'JSXIdentifier') continue;
5617
+ const name = String(a.name.name);
5618
+ if (name.startsWith('data-cd-') || name === 'style' || name === ignore || name === 'key')
5619
+ continue;
5620
+ const v = a.value;
5621
+ const lit =
5622
+ v?.type === 'Literal' || v?.type === 'StringLiteral'
5623
+ ? String(v.value)
5624
+ : v?.type === 'JSXExpressionContainer' &&
5625
+ (v.expression?.type === 'Literal' || v.expression?.type === 'StringLiteral')
5626
+ ? String(v.expression.value)
5627
+ : v === null || v === undefined
5628
+ ? 'true'
5629
+ : '{expr}';
5630
+ out.push(`${name}=${lit}`);
5631
+ }
5632
+ return out.sort().join('|');
5633
+ }
5634
+
5635
+ function printAll(
5636
+ canvasAbsPath: string,
5637
+ source: string,
5638
+ ignoreAttr: string | null,
5639
+ withText = true
5640
+ ): Array<{ id: string; print: ElementPrint }> {
5641
+ const parsed = parseSync(canvasAbsPath, source, { sourceType: 'module' });
5642
+ if (parsed.errors && parsed.errors.length > 0) return [];
5643
+ const frames: Array<{ componentName: string; jsxIndex: number }> = [
5644
+ { componentName: '', jsxIndex: 0 },
5645
+ ];
5646
+ const chain: string[] = [];
5647
+ const out: Array<{ id: string; print: ElementPrint }> = [];
5648
+ function visit(node: AnyNode): void {
5649
+ if (!node || typeof node !== 'object') return;
5650
+ if (Array.isArray(node)) {
5651
+ for (const c of node) visit(c);
5652
+ return;
5653
+ }
5654
+ if (typeof node.type !== 'string') return;
5655
+ const comp = componentNameOf(node);
5656
+ if (comp !== null) frames.push({ componentName: comp, jsxIndex: 0 });
5657
+ if (node.type === 'JSXElement') {
5658
+ const frame = frames[frames.length - 1] as { componentName: string; jsxIndex: number };
5659
+ const idx = frame.jsxIndex;
5660
+ frame.jsxIndex += 1;
5661
+ const tag = jsxTagName(node) ?? '?';
5662
+ out.push({
5663
+ id: authoredCdId(node.openingElement) ?? computeId(frame.componentName, idx),
5664
+ print: {
5665
+ component: frame.componentName,
5666
+ tag,
5667
+ chain: [...chain],
5668
+ attrs: literalAttrs(node.openingElement, ignoreAttr),
5669
+ ...(withText ? { text: ownText(node) } : {}),
5670
+ },
5671
+ });
5672
+ chain.push(tag);
5673
+ if (node.openingElement) visit(node.openingElement.attributes);
5674
+ visit(node.children);
5675
+ chain.pop();
5676
+ if (comp !== null) frames.pop();
5677
+ return;
5678
+ }
5679
+ for (const k of Object.keys(node)) {
5680
+ if (k === 'loc' || k === 'range' || k === 'start' || k === 'end' || k === 'type') continue;
5681
+ visit(node[k]);
5682
+ }
5683
+ if (comp !== null) frames.pop();
5684
+ }
5685
+ visit(parsed.program);
5686
+ return out;
5687
+ }
5688
+
5689
+ const samePrint = (a: ElementPrint, b: ElementPrint) =>
5690
+ a.component === b.component &&
5691
+ a.tag === b.tag &&
5692
+ a.attrs === b.attrs &&
5693
+ (a.text ?? null) === (b.text ?? null) &&
5694
+ a.chain.length === b.chain.length &&
5695
+ a.chain.every((t, i) => t === b.chain[i]);
5696
+
5697
+ /** The print of the element `id` in `source`, or null. */
5698
+ export function elementPrint(
5699
+ canvasAbsPath: string,
5700
+ source: string,
5701
+ id: string,
5702
+ ignoreAttr: string | null = null,
5703
+ withText = true
5704
+ ): ElementPrint | null {
5705
+ return (
5706
+ printAll(canvasAbsPath, source, ignoreAttr, withText).find((e) => e.id === id)?.print ?? null
5707
+ );
5708
+ }
5709
+
5710
+ /**
5711
+ * The id the element printed `print` has in `source` — `hint` first (the
5712
+ * common case: nothing moved), else the ONE element carrying the print. Null
5713
+ * when it is gone or ambiguous.
5714
+ */
5715
+ export function relocateElement(
5716
+ canvasAbsPath: string,
5717
+ source: string,
5718
+ hint: string,
5719
+ print: ElementPrint,
5720
+ ignoreAttr: string | null = null
5721
+ ): string | null {
5722
+ const all = printAll(canvasAbsPath, source, ignoreAttr, print.text !== undefined);
5723
+ const atHint = all.find((e) => e.id === hint);
5724
+ if (atHint && samePrint(atHint.print, print)) return hint;
5725
+ const matches = all.filter((e) => samePrint(e.print, print));
5726
+ return matches.length === 1 ? (matches[0] as { id: string }).id : null;
5727
+ }
5728
+
5729
+ /** T23 — how many elements of `source` carry a print no other element shares. */
5730
+ export function printUniqueness(
5731
+ canvasAbsPath: string,
5732
+ source: string
5733
+ ): { elements: number; unique: number } {
5734
+ const all = printAll(canvasAbsPath, source, null);
5735
+ const key = (p: ElementPrint) =>
5736
+ `${p.component}|${p.tag}|${p.chain.join('>')}|${p.attrs}|${p.text ?? ''}`;
5737
+ const counts = new Map<string, number>();
5738
+ for (const e of all) counts.set(key(e.print), (counts.get(key(e.print)) ?? 0) + 1);
5739
+ return { elements: all.length, unique: all.filter((e) => counts.get(key(e.print)) === 1).length };
5740
+ }
@@ -0,0 +1,89 @@
1
+ // Relative-import rewriting for a canvas that CHANGES FOLDER (issue #114).
2
+ //
3
+ // THE BUG THIS MODULE EXISTS TO FIX. `moveCanvas` / `moveFolder` relocated a
4
+ // canvas with a bare `rename()` — bytes moved, specifiers didn't. A canvas
5
+ // dragged from `ui/` into `ui/print/` kept
6
+ //
7
+ // import { … } from "../system/alligators/preview/_kit";
8
+ // import "../system/alligators/preview/_layout.css";
9
+ //
10
+ // which is only correct one level up, so the canvas 500s on the next build with
11
+ // `Could not resolve`. Nothing anywhere in the tree rewrote imports; the
12
+ // capability simply did not exist. It reads like a sync bug from the outside
13
+ // (the broken file shows up on every peer), which is how it got reported
14
+ // alongside one — but it is entirely local to the move.
15
+ //
16
+ // Deliberately regex-over-known-vocabulary rather than a parser: it matches the
17
+ // house style of the other static readers (`_canvas-rects-static.mjs`), it
18
+ // cannot fail closed on a canvas whose syntax a parser would reject, and the
19
+ // vocabulary is small and fixed. Only `./`- and `../`-prefixed specifiers are
20
+ // touched — a bare or aliased specifier (`react`, `@maude/canvas-lib`) is
21
+ // resolved by the bundler, not by depth, and rewriting one would break it.
22
+
23
+ import path from 'node:path';
24
+
25
+ /**
26
+ * Every shape that can carry a module specifier in a canvas body, as
27
+ * [pattern, index of the capture group holding the quoted specifier].
28
+ *
29
+ * from "…" — `import x from`, `export … from`
30
+ * import "…" — side-effect import (this is the one that carries `_layout.css`)
31
+ * import("…") — dynamic import
32
+ *
33
+ * `require("…")` is deliberately absent: canvases are ESM (Bun.build), and a
34
+ * `require` in one is not something this should quietly rewrite.
35
+ */
36
+ const SPECIFIER_PATTERNS: readonly RegExp[] = [
37
+ /(\bfrom\s*)(["'])([^"']+)\2/g,
38
+ /(\bimport\s*)(["'])([^"']+)\2/g,
39
+ /(\bimport\s*\(\s*)(["'])([^"']+)\2/g,
40
+ ];
41
+
42
+ /** Is this a path-relative specifier (the only kind whose meaning depends on
43
+ * which folder the importing file sits in)? */
44
+ export function isRelativeSpecifier(spec: string): boolean {
45
+ return spec.startsWith('./') || spec.startsWith('../');
46
+ }
47
+
48
+ /**
49
+ * Re-root one relative specifier from `fromDir` to `toDir`.
50
+ *
51
+ * Both dirs are POSIX, design-root-relative (`''` = the design root itself).
52
+ * Returns the specifier unchanged when it isn't relative.
53
+ */
54
+ export function rerootSpecifier(spec: string, fromDir: string, toDir: string): string {
55
+ if (!isRelativeSpecifier(spec)) return spec;
56
+ // What the specifier pointed AT, as a design-root-relative path. `join` +
57
+ // `normalize` collapse the `..` segments; a target above the design root stays
58
+ // expressed with leading `..` and re-roots just as correctly.
59
+ const target = path.posix.normalize(path.posix.join(fromDir, spec));
60
+ let next = path.posix.relative(toDir, target);
61
+ // `relative` drops the leading `./` that makes a same-dir specifier relative
62
+ // rather than bare — without it `sibling.css` would resolve as a PACKAGE.
63
+ if (!next.startsWith('.')) next = `./${next}`;
64
+ // A trailing slash in the source (`from "../lib/"`) is meaningful to a
65
+ // resolver; `normalize`/`relative` drop it, so put it back.
66
+ if (spec.endsWith('/') && !next.endsWith('/')) next = `${next}/`;
67
+ return next;
68
+ }
69
+
70
+ /**
71
+ * Rewrite every relative import in `source` for a file that moved from
72
+ * `fromDir` to `toDir` (both POSIX, design-root-relative).
73
+ *
74
+ * Returns the source unchanged when the two dirs are the same or nothing
75
+ * relative was found, so a caller can cheaply skip the write.
76
+ */
77
+ export function rewriteRelativeImports(source: string, fromDir: string, toDir: string): string {
78
+ const from = path.posix.normalize(fromDir === '' ? '.' : fromDir);
79
+ const to = path.posix.normalize(toDir === '' ? '.' : toDir);
80
+ if (from === to) return source;
81
+ let out = source;
82
+ for (const pattern of SPECIFIER_PATTERNS) {
83
+ out = out.replace(pattern, (whole, lead: string, quote: string, spec: string) => {
84
+ if (!isRelativeSpecifier(spec)) return whole;
85
+ return `${lead}${quote}${rerootSpecifier(spec, from, to)}${quote}`;
86
+ });
87
+ }
88
+ return out;
89
+ }
@@ -475,6 +475,17 @@ button.dc-artboard-label {
475
475
  width: 100%;
476
476
  }
477
477
  button.dc-artboard-label:focus-visible { outline: 2px solid var(--maude-hud-accent, #d63b1f); outline-offset: -2px; }
478
+ input.dc-artboard-rename {
479
+ appearance: none;
480
+ border-width: 0 0 1px 0;
481
+ font: inherit;
482
+ display: block;
483
+ width: 100%;
484
+ box-sizing: border-box;
485
+ text-transform: none;
486
+ outline: 2px solid var(--maude-hud-accent, #d63b1f);
487
+ outline-offset: -2px;
488
+ }
478
489
  /* DDR-148 — video-artboard badge, overlaid top-right of the header. Opens
479
490
  the timeline panel (same postMessage the context-menu entry sends). */
480
491
  .dc-canvas .dc-artboard-video-badge {
@@ -2447,6 +2458,25 @@ export function DCArtboard({
2447
2458
  // edited), so it never ratchets up from a previously-larger measured value
2448
2459
  // the way feeding `rect.h` back as the floor would.
2449
2460
  const heightFloor = typeof height === 'number' ? height : VP_GRID.h;
2461
+ // Plan T25/L08 — rename in place: double-click the name, Enter commits (the
2462
+ // shell writes the `label` prop), Escape or an unchanged name cancels.
2463
+ const [renaming, setRenaming] = useState(false);
2464
+ const renameDoneRef = useRef(false);
2465
+ const commitRename = (value: string) => {
2466
+ if (renameDoneRef.current) return;
2467
+ renameDoneRef.current = true;
2468
+ setRenaming(false);
2469
+ const next = value.trim();
2470
+ if (!next || next === label) return;
2471
+ try {
2472
+ window.parent.postMessage(
2473
+ { dgn: 'rename-artboard-request', artboardId: id, label: next },
2474
+ '*'
2475
+ );
2476
+ } catch {
2477
+ /* detached / cross-origin */
2478
+ }
2479
+ };
2450
2480
  const articleRef = useRef<HTMLElement | null>(null);
2451
2481
  const measured = useArtboardBounds(articleRef as RefObject<HTMLElement | null>);
2452
2482
  useEffect(() => {
@@ -2590,24 +2620,58 @@ export function DCArtboard({
2590
2620
  }}
2591
2621
  {...handleProps}
2592
2622
  >
2593
- <button
2594
- type="button"
2595
- className="dc-artboard-label sku"
2596
- // a11y-auditor (T3 review) — the kind chip is aria-hidden (decorative,
2597
- // redundant with the Inspector's Kind picker), so a non-digital kind
2598
- // must still reach the artboard's own accessible name or it's
2599
- // invisible to AT users entirely.
2600
- aria-label={
2601
- resolvedKind !== 'digital' ? `Artboard ${label}, ${resolvedKind}` : `Artboard ${label}`
2602
- }
2603
- >
2604
- {resolvedKind !== 'digital' ? (
2605
- <span className="dc-artboard-kind-chip" aria-hidden="true">
2606
- <ArtboardKindIcon kind={resolvedKind} />
2607
- </span>
2608
- ) : null}
2609
- {label}
2610
- </button>
2623
+ {renaming ? (
2624
+ <input
2625
+ className="dc-artboard-label sku dc-artboard-rename"
2626
+ data-testid={`artboard-rename-${id}`}
2627
+ aria-label="Artboard name"
2628
+ defaultValue={label}
2629
+ maxLength={80}
2630
+ // biome-ignore lint/a11y/noAutofocus: the rename field opens on the designer's own double-click
2631
+ autoFocus
2632
+ onFocus={(e) => e.currentTarget.select()}
2633
+ // The label is the drag handle; typing and clicking in the field
2634
+ // must not start a move or reach the canvas shortcuts.
2635
+ onPointerDown={(e) => e.stopPropagation()}
2636
+ onMouseDown={(e) => e.stopPropagation()}
2637
+ onKeyDown={(e) => {
2638
+ e.stopPropagation();
2639
+ if (e.key === 'Enter') commitRename(e.currentTarget.value);
2640
+ else if (e.key === 'Escape') {
2641
+ renameDoneRef.current = true;
2642
+ setRenaming(false);
2643
+ }
2644
+ }}
2645
+ onBlur={(e) => commitRename(e.currentTarget.value)}
2646
+ />
2647
+ ) : (
2648
+ <button
2649
+ type="button"
2650
+ className="dc-artboard-label sku"
2651
+ onDoubleClick={(e) => {
2652
+ if (isReadOnlyCanvas()) return;
2653
+ e.stopPropagation();
2654
+ renameDoneRef.current = false;
2655
+ setRenaming(true);
2656
+ }}
2657
+ // a11y-auditor (T3 review) — the kind chip is aria-hidden (decorative,
2658
+ // redundant with the Inspector's Kind picker), so a non-digital kind
2659
+ // must still reach the artboard's own accessible name or it's
2660
+ // invisible to AT users entirely.
2661
+ aria-label={
2662
+ resolvedKind !== 'digital'
2663
+ ? `Artboard ${label}, ${resolvedKind}`
2664
+ : `Artboard ${label}`
2665
+ }
2666
+ >
2667
+ {resolvedKind !== 'digital' ? (
2668
+ <span className="dc-artboard-kind-chip" aria-hidden="true">
2669
+ <ArtboardKindIcon kind={resolvedKind} />
2670
+ </span>
2671
+ ) : null}
2672
+ {label}
2673
+ </button>
2674
+ )}
2611
2675
  {hasVideo ? (
2612
2676
  <button
2613
2677
  type="button"