@1agh/maude 0.28.1 → 0.30.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 (372) hide show
  1. package/README.md +3 -3
  2. package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
  3. package/apps/studio/annotations-align.ts +149 -0
  4. package/apps/studio/annotations-bindings.ts +197 -0
  5. package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
  6. package/apps/studio/annotations-groups.ts +270 -0
  7. package/apps/studio/annotations-layer.tsx +4537 -0
  8. package/apps/studio/annotations-model.ts +2077 -0
  9. package/apps/studio/annotations-snap.ts +125 -0
  10. package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
  11. package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
  12. package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
  13. package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
  14. package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
  15. package/apps/studio/bin/annotate.mjs +732 -0
  16. package/apps/studio/bin/annotate.sh +17 -0
  17. package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
  18. package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
  19. package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
  20. package/apps/studio/bin/read-annotations.mjs +573 -0
  21. package/apps/studio/bin/read-annotations.sh +11 -0
  22. package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
  23. package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
  24. package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
  25. package/apps/studio/bin/to-lottie-verify.html +68 -0
  26. package/apps/studio/bin/to-lottie.sh +128 -0
  27. package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
  28. package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
  29. package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
  30. package/apps/studio/canvas-create.ts +104 -0
  31. package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
  32. package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
  33. package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
  34. package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
  35. package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
  36. package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
  37. package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
  38. package/apps/studio/client/app.jsx +6742 -0
  39. package/apps/studio/client/canvas-url.js +81 -0
  40. package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
  41. package/apps/studio/client/index.html +16 -0
  42. package/apps/studio/client/styles/1-tokens-maude.css +296 -0
  43. package/apps/studio/client/styles/3-shell-maude.css +1161 -0
  44. package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
  45. package/apps/studio/client/styles/4-components-maude.css +74 -0
  46. package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
  47. package/apps/studio/client/styles/5-maude-overrides.css +137 -0
  48. package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
  49. package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
  50. package/apps/studio/client/tour/overlay.jsx +279 -0
  51. package/apps/studio/client/tour/usage-tour.js +48 -0
  52. package/apps/studio/client/whats-new-seen.js +52 -0
  53. package/apps/studio/client/whats-new.jsx +250 -0
  54. package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
  55. package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
  56. package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
  57. package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
  58. package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
  59. package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
  60. package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
  61. package/apps/studio/dist/client.bundle.js +25545 -0
  62. package/apps/studio/dist/comment-mount.js +2046 -0
  63. package/apps/studio/dist/styles.css +8018 -0
  64. package/apps/studio/dom-selection.ts +342 -0
  65. package/apps/studio/draw/animate.ts +333 -0
  66. package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
  67. package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
  68. package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
  69. package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
  70. package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
  71. package/apps/studio/draw/morph.ts +204 -0
  72. package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
  73. package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
  74. package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
  75. package/apps/studio/draw/serialize-animate.ts +324 -0
  76. package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
  77. package/apps/studio/draw/test/animate.test.ts +172 -0
  78. package/apps/studio/draw/test/morph.test.ts +107 -0
  79. package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
  80. package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
  81. package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
  82. package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
  83. package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
  84. package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
  85. package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
  86. package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
  87. package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
  88. package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
  89. package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
  90. package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
  91. package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
  92. package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
  93. package/apps/studio/sync/cold-start.ts +158 -0
  94. package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
  95. package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
  96. package/apps/studio/sync/journal.ts +190 -0
  97. package/apps/studio/sync/migrate-seed.ts +321 -0
  98. package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
  99. package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
  100. package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
  101. package/apps/studio/test/annotate-write.test.ts +184 -0
  102. package/apps/studio/test/annotations-align.test.ts +88 -0
  103. package/apps/studio/test/annotations-bindings.test.ts +124 -0
  104. package/apps/studio/test/annotations-groups.test.ts +231 -0
  105. package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
  106. package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
  107. package/apps/studio/test/annotations-snap.test.ts +79 -0
  108. package/apps/studio/test/annotations-text-format.test.ts +396 -0
  109. package/apps/studio/test/asset-api.test.ts +149 -0
  110. package/apps/studio/test/canvas-create-api.test.ts +361 -0
  111. package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
  112. package/apps/studio/test/canvas-edit.test.ts +319 -0
  113. package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
  114. package/apps/studio/test/canvas-media-drop.test.ts +158 -0
  115. package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
  116. package/apps/studio/test/canvas-url.test.ts +99 -0
  117. package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
  118. package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
  119. package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
  120. package/apps/studio/test/csrf-write-guard.test.ts +52 -0
  121. package/apps/studio/test/figjam-v3-model.test.ts +342 -0
  122. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
  123. package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
  124. package/apps/studio/test/motion-sample.test.ts +35 -0
  125. package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
  126. package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
  127. package/apps/studio/test/read-annotations.test.ts +409 -0
  128. package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
  129. package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
  130. package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
  131. package/apps/studio/test/sync-agent.test.ts +575 -0
  132. package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
  133. package/apps/studio/test/sync-cold-start.test.ts +244 -0
  134. package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
  135. package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
  136. package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
  137. package/apps/studio/test/sync-incident-replay.test.ts +211 -0
  138. package/apps/studio/test/sync-journal.test.ts +176 -0
  139. package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
  140. package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
  141. package/apps/studio/test/tour-overlay.test.tsx +56 -0
  142. package/apps/studio/test/use-annotation-resize.test.ts +402 -0
  143. package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
  144. package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
  145. package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
  146. package/apps/studio/test/whats-new.test.ts +101 -0
  147. package/apps/studio/text-imports.d.ts +9 -0
  148. package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
  149. package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
  150. package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
  151. package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
  152. package/apps/studio/use-annotation-resize.tsx +912 -0
  153. package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
  154. package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
  155. package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
  156. package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
  157. package/apps/studio/use-canvas-media-drop.tsx +343 -0
  158. package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
  159. package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
  160. package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
  161. package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
  162. package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
  163. package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
  164. package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
  165. package/apps/studio/whats-new.json +264 -0
  166. package/apps/studio/whats-new.schema.json +86 -0
  167. package/apps/studio/whats-new.ts +87 -0
  168. package/cli/bin/maude.mjs +10 -0
  169. package/cli/commands/design-link.test.mjs +84 -0
  170. package/cli/commands/design.mjs +16 -7
  171. package/cli/commands/design.test.mjs +40 -0
  172. package/cli/commands/help.mjs +8 -2
  173. package/cli/commands/hub.mjs +3 -3
  174. package/cli/commands/hub.test.mjs +1 -1
  175. package/cli/commands/scenario-report.mjs +2 -9
  176. package/cli/lib/design-link.mjs +51 -1
  177. package/cli/lib/flow-design-integration.test.mjs +1 -1
  178. package/cli/lib/gitignore-block.mjs +1 -0
  179. package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
  180. package/cli/lib/preflight.mjs +1 -1
  181. package/package.json +15 -15
  182. package/plugins/design/templates/_shell.html +28 -4
  183. package/plugins/design/templates/brief-board.tsx.template +61 -0
  184. package/plugins/design/templates/canvas.tsx.template +1 -1
  185. package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
  186. package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
  187. package/plugins/flow/.claude-plugin/config.schema.json +16 -0
  188. package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
  189. package/plugins/design/dev-server/client/app.jsx +0 -2985
  190. package/plugins/design/dev-server/client/index.html +0 -15
  191. package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
  192. package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
  193. package/plugins/design/dev-server/dist/styles.css +0 -2710
  194. package/plugins/design/dev-server/dom-selection.ts +0 -156
  195. package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
  196. package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
  197. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
  198. package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
  199. package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
  200. package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
  201. /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
  202. /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
  203. /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
  204. /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
  205. /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
  206. /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
  207. /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
  208. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
  209. /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
  210. /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
  211. /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
  212. /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
  213. /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
  214. /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
  215. /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
  216. /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
  217. /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
  218. /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
  219. /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
  220. /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
  221. /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
  222. /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
  223. /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
  224. /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
  225. /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
  226. /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
  227. /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
  228. /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
  229. /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
  230. /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
  231. /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
  232. /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
  233. /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
  234. /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
  235. /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
  236. /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
  237. /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
  238. /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
  239. /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
  240. /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
  241. /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
  242. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
  243. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
  244. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
  245. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
  246. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
  247. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
  248. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
  249. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
  250. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
  251. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
  252. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
  253. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
  254. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
  255. /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
  256. /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
  257. /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
  258. /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
  259. /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
  260. /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
  261. /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
  262. /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
  263. /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
  264. /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
  265. /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
  266. /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
  267. /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
  268. /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
  269. /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
  270. /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
  271. /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
  272. /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
  273. /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
  274. /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
  275. /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
  276. /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
  277. /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
  278. /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
  279. /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
  280. /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
  281. /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
  282. /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
  283. /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
  284. /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
  285. /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
  286. /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
  287. /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
  288. /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
  289. /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
  290. /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
  291. /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
  292. /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
  293. /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
  294. /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
  295. /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
  296. /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
  297. /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
  298. /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
  299. /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
  300. /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
  301. /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
  302. /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
  303. /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
  304. /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
  305. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
  306. /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
  307. /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
  308. /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
  309. /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
  310. /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
  311. /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
  312. /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
  313. /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
  314. /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
  315. /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
  316. /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
  317. /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
  318. /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
  319. /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
  320. /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
  321. /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
  322. /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
  323. /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
  324. /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
  325. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
  326. /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
  327. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
  328. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
  329. /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
  330. /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
  331. /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
  332. /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
  333. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
  334. /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
  335. /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
  336. /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
  337. /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
  338. /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
  339. /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
  340. /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
  341. /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
  342. /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
  343. /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
  344. /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
  345. /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
  346. /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
  347. /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
  348. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
  349. /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
  350. /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
  351. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
  352. /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
  353. /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
  354. /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
  355. /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
  356. /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
  357. /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
  358. /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
  359. /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
  360. /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
  361. /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
  362. /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
  363. /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
  364. /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
  365. /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
  366. /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
  367. /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
  368. /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
  369. /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
  370. /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
  371. /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
  372. /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
@@ -0,0 +1,244 @@
1
+ // Cold-start decision matrix — hub-sync cold-start safety (DDR-102).
2
+ //
3
+ // Exhaustive table over (local: absent/empty/clean/diverged) × (doc:
4
+ // empty/same/different) × (journal: absent/match/stale), plus both winner
5
+ // directions and the null-timestamp fallbacks. Mirrors the table style of
6
+ // sync-codec.test.ts: every case is a row with explicit inputs + expected
7
+ // action.
8
+
9
+ import { describe, expect, test } from 'bun:test';
10
+
11
+ import { type ColdStartInput, decideColdStart } from '../sync/cold-start.ts';
12
+ import { hashBytes } from '../sync/echo-guard.ts';
13
+
14
+ const BODY_LOCAL = '<div>local work — a day of mascot edits</div>';
15
+ const BODY_HUB = '<div>hub state</div>';
16
+
17
+ /** Convenience: build an input with safe defaults, override per case. */
18
+ function input(over: Partial<ColdStartInput>): ColdStartInput {
19
+ return {
20
+ localBody: null,
21
+ docBody: '',
22
+ journalHash: null,
23
+ localMtimeMs: null,
24
+ docBodyEditAtMs: null,
25
+ ...over,
26
+ };
27
+ }
28
+
29
+ describe('decideColdStart — empty/absent sides', () => {
30
+ test('local absent + doc empty → noop', () => {
31
+ expect(decideColdStart(input({})).action).toBe('noop');
32
+ });
33
+
34
+ test('local empty-string + doc empty → noop', () => {
35
+ expect(decideColdStart(input({ localBody: '' })).action).toBe('noop');
36
+ });
37
+
38
+ test('whitespace-only local counts as empty (mirrors the trim guard)', () => {
39
+ const d = decideColdStart(input({ localBody: ' \n\t ', docBody: BODY_HUB }));
40
+ expect(d.action).toBe('materialize-hub');
41
+ });
42
+
43
+ test('local absent + doc non-empty → materialize-hub (clean first sync)', () => {
44
+ const d = decideColdStart(input({ docBody: BODY_HUB }));
45
+ expect(d.action).toBe('materialize-hub');
46
+ });
47
+
48
+ test('local empty + doc non-empty → materialize-hub', () => {
49
+ const d = decideColdStart(input({ localBody: '', docBody: BODY_HUB }));
50
+ expect(d.action).toBe('materialize-hub');
51
+ });
52
+
53
+ test('local non-empty + doc empty → seed-local-up (DDR-064 guard as a named case)', () => {
54
+ const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: '' }));
55
+ expect(d.action).toBe('seed-local-up');
56
+ });
57
+
58
+ test('local non-empty + whitespace-only doc → seed-local-up', () => {
59
+ const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: ' \n ' }));
60
+ expect(d.action).toBe('seed-local-up');
61
+ });
62
+
63
+ test('seed-local-up holds regardless of journal state (stale journal must not block it)', () => {
64
+ const d = decideColdStart(
65
+ input({ localBody: BODY_LOCAL, docBody: '', journalHash: 'stale-hash' })
66
+ );
67
+ expect(d.action).toBe('seed-local-up');
68
+ });
69
+ });
70
+
71
+ describe('decideColdStart — identical sides', () => {
72
+ test('local == doc → noop (caller records journal)', () => {
73
+ const d = decideColdStart(input({ localBody: BODY_HUB, docBody: BODY_HUB }));
74
+ expect(d.action).toBe('noop');
75
+ });
76
+
77
+ test('local == doc with a stale journal is still a noop (content equality wins)', () => {
78
+ const d = decideColdStart(
79
+ input({ localBody: BODY_HUB, docBody: BODY_HUB, journalHash: 'stale' })
80
+ );
81
+ expect(d.action).toBe('noop');
82
+ });
83
+ });
84
+
85
+ describe('decideColdStart — journal-gated fast-forward', () => {
86
+ test('local ≠ doc, journal matches hash(local) → fast-forward-hub (no conflict)', () => {
87
+ const d = decideColdStart(
88
+ input({
89
+ localBody: BODY_LOCAL,
90
+ docBody: BODY_HUB,
91
+ journalHash: hashBytes(BODY_LOCAL),
92
+ })
93
+ );
94
+ expect(d.action).toBe('fast-forward-hub');
95
+ expect(d.winner).toBeUndefined();
96
+ });
97
+
98
+ test('journal match overrides timestamps — even a NEWER local mtime fast-forwards', () => {
99
+ // mtime newer than the doc stamp, but the bytes are exactly what we last
100
+ // synced (e.g. touch / checkout changed mtime only) → not a conflict.
101
+ const d = decideColdStart(
102
+ input({
103
+ localBody: BODY_LOCAL,
104
+ docBody: BODY_HUB,
105
+ journalHash: hashBytes(BODY_LOCAL),
106
+ localMtimeMs: 2_000,
107
+ docBodyEditAtMs: 1_000,
108
+ })
109
+ );
110
+ expect(d.action).toBe('fast-forward-hub');
111
+ });
112
+ });
113
+
114
+ describe('decideColdStart — divergence (conflict + newest-wins)', () => {
115
+ test('journal ABSENT, local newer → conflict, winner=local (the incident shape)', () => {
116
+ const d = decideColdStart(
117
+ input({
118
+ localBody: BODY_LOCAL,
119
+ docBody: BODY_HUB,
120
+ localMtimeMs: 2_000,
121
+ docBodyEditAtMs: 1_000,
122
+ })
123
+ );
124
+ expect(d.action).toBe('conflict');
125
+ expect(d.winner).toBe('local');
126
+ });
127
+
128
+ test('journal ABSENT, doc newer → conflict, winner=hub', () => {
129
+ const d = decideColdStart(
130
+ input({
131
+ localBody: BODY_LOCAL,
132
+ docBody: BODY_HUB,
133
+ localMtimeMs: 1_000,
134
+ docBodyEditAtMs: 2_000,
135
+ })
136
+ );
137
+ expect(d.action).toBe('conflict');
138
+ expect(d.winner).toBe('hub');
139
+ });
140
+
141
+ test('journal STALE (≠ hash(local)), local newer → conflict, winner=local', () => {
142
+ const d = decideColdStart(
143
+ input({
144
+ localBody: BODY_LOCAL,
145
+ docBody: BODY_HUB,
146
+ journalHash: hashBytes('<div>some older synced state</div>'),
147
+ localMtimeMs: 5_000,
148
+ docBodyEditAtMs: 4_000,
149
+ })
150
+ );
151
+ expect(d.action).toBe('conflict');
152
+ expect(d.winner).toBe('local');
153
+ });
154
+
155
+ test('journal STALE, doc newer → conflict, winner=hub', () => {
156
+ const d = decideColdStart(
157
+ input({
158
+ localBody: BODY_LOCAL,
159
+ docBody: BODY_HUB,
160
+ journalHash: hashBytes('<div>some older synced state</div>'),
161
+ localMtimeMs: 4_000,
162
+ docBodyEditAtMs: 5_000,
163
+ })
164
+ );
165
+ expect(d.action).toBe('conflict');
166
+ expect(d.winner).toBe('hub');
167
+ });
168
+
169
+ test('journal matching hash(DOC) — not local — is still divergence', () => {
170
+ // The journal proves we once synced what the HUB now holds reverted to;
171
+ // local moved past it → local is unreconciled work, must conflict.
172
+ const d = decideColdStart(
173
+ input({
174
+ localBody: BODY_LOCAL,
175
+ docBody: BODY_HUB,
176
+ journalHash: hashBytes(BODY_HUB),
177
+ localMtimeMs: 2_000,
178
+ docBodyEditAtMs: 1_000,
179
+ })
180
+ );
181
+ expect(d.action).toBe('conflict');
182
+ expect(d.winner).toBe('local');
183
+ });
184
+
185
+ test('doc stamp NULL (older peer never stamped) → hub-wins fallback', () => {
186
+ const d = decideColdStart(
187
+ input({
188
+ localBody: BODY_LOCAL,
189
+ docBody: BODY_HUB,
190
+ localMtimeMs: 2_000,
191
+ docBodyEditAtMs: null,
192
+ })
193
+ );
194
+ expect(d.action).toBe('conflict');
195
+ expect(d.winner).toBe('hub');
196
+ });
197
+
198
+ test('local mtime NULL → hub-wins fallback', () => {
199
+ const d = decideColdStart(
200
+ input({
201
+ localBody: BODY_LOCAL,
202
+ docBody: BODY_HUB,
203
+ localMtimeMs: null,
204
+ docBodyEditAtMs: 2_000,
205
+ })
206
+ );
207
+ expect(d.action).toBe('conflict');
208
+ expect(d.winner).toBe('hub');
209
+ });
210
+
211
+ test('both timestamps NULL → hub-wins fallback', () => {
212
+ const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: BODY_HUB }));
213
+ expect(d.action).toBe('conflict');
214
+ expect(d.winner).toBe('hub');
215
+ });
216
+
217
+ test('timestamps TIED → hub-wins fallback', () => {
218
+ const d = decideColdStart(
219
+ input({
220
+ localBody: BODY_LOCAL,
221
+ docBody: BODY_HUB,
222
+ localMtimeMs: 1_000,
223
+ docBodyEditAtMs: 1_000,
224
+ })
225
+ );
226
+ expect(d.action).toBe('conflict');
227
+ expect(d.winner).toBe('hub');
228
+ });
229
+
230
+ test('every decision carries a human-readable reason', () => {
231
+ const cases: ColdStartInput[] = [
232
+ input({}),
233
+ input({ docBody: BODY_HUB }),
234
+ input({ localBody: BODY_LOCAL }),
235
+ input({ localBody: BODY_HUB, docBody: BODY_HUB }),
236
+ input({ localBody: BODY_LOCAL, docBody: BODY_HUB, journalHash: hashBytes(BODY_LOCAL) }),
237
+ input({ localBody: BODY_LOCAL, docBody: BODY_HUB }),
238
+ ];
239
+ for (const c of cases) {
240
+ const d = decideColdStart(c);
241
+ expect(d.reason.length).toBeGreaterThan(0);
242
+ }
243
+ });
244
+ });
@@ -144,3 +144,71 @@ describe('connection monitor', () => {
144
144
  expect(changes.length).toBe(before); // no offline transition fired
145
145
  });
146
146
  });
147
+
148
+ // DDR-102 — per-doc states + real lastSyncAt.
149
+ describe('connection monitor — per-doc states (DDR-102)', () => {
150
+ test('noteDocState rolls up into docs counts + rejectedSlugs', () => {
151
+ const { monitor } = makeMonitor();
152
+ monitor.noteDocState('ui-a', 'pending');
153
+ monitor.noteDocState('ui-b', 'connected');
154
+ monitor.noteDocState('ui-c', 'auth-rejected');
155
+ monitor.noteDocState('ui-d', 'auth-rejected');
156
+
157
+ const snap = monitor.snapshot();
158
+ expect(snap.docs).toEqual({ synced: 1, pending: 1, rejected: 2 });
159
+ expect(snap.rejectedSlugs?.sort()).toEqual(['ui-c', 'ui-d']);
160
+ });
161
+
162
+ test('doc state transitions update the rollup (rejected → connected after re-link)', () => {
163
+ const { monitor, changes } = makeMonitor();
164
+ monitor.noteDocState('ui-a', 'auth-rejected');
165
+ expect(monitor.snapshot().docs?.rejected).toBe(1);
166
+ monitor.noteDocState('ui-a', 'connected');
167
+ const snap = monitor.snapshot();
168
+ expect(snap.docs).toEqual({ synced: 1, pending: 0, rejected: 0 });
169
+ expect(snap.rejectedSlugs).toEqual([]);
170
+ // Each transition emitted (no-op repeats don't).
171
+ const emitted = changes.length;
172
+ monitor.noteDocState('ui-a', 'connected');
173
+ expect(changes.length).toBe(emitted);
174
+ });
175
+
176
+ test('rejectedSlugs caps at 20 while docs.rejected carries the true count', () => {
177
+ const { monitor } = makeMonitor();
178
+ for (let i = 0; i < 25; i++) monitor.noteDocState(`ui-${i}`, 'auth-rejected');
179
+ const snap = monitor.snapshot();
180
+ expect(snap.docs?.rejected).toBe(25);
181
+ expect(snap.rejectedSlugs).toHaveLength(20);
182
+ });
183
+
184
+ test('noteSyncActivity sets lastSyncAt to now and promotes a pending doc', () => {
185
+ const { clock, monitor } = makeMonitor();
186
+ monitor.noteDocState('ui-a', 'pending');
187
+ expect(monitor.snapshot().lastSyncAt).toBeNull();
188
+
189
+ clock.advance(5_000);
190
+ monitor.noteSyncActivity('ui-a');
191
+ const snap = monitor.snapshot();
192
+ expect(snap.lastSyncAt).toBe(clock.now());
193
+ expect(snap.docs?.synced).toBe(1);
194
+ expect(snap.docs?.pending).toBe(0);
195
+ });
196
+
197
+ test('noteSyncActivity does NOT resurrect an auth-rejected doc', () => {
198
+ const { monitor } = makeMonitor();
199
+ monitor.noteDocState('ui-a', 'auth-rejected');
200
+ monitor.noteSyncActivity('ui-a');
201
+ expect(monitor.snapshot().docs?.rejected).toBe(1);
202
+ expect(monitor.snapshot().lastSyncAt).not.toBeNull();
203
+ });
204
+
205
+ test('lastSyncAt advances on repeated activity (real activity, not just transitions)', () => {
206
+ const { clock, monitor } = makeMonitor();
207
+ monitor.noteDocState('ui-a', 'connected');
208
+ monitor.noteSyncActivity('ui-a');
209
+ const first = monitor.snapshot().lastSyncAt;
210
+ clock.advance(10_000);
211
+ monitor.noteSyncActivity('ui-a');
212
+ expect(monitor.snapshot().lastSyncAt).toBe((first as number) + 10_000);
213
+ });
214
+ });
@@ -54,25 +54,19 @@ describe('DDR-054 §2a — CI environment gate', () => {
54
54
  savedCi = process.env.CI;
55
55
  savedGha = process.env.GITHUB_ACTIONS;
56
56
  savedOverride = process.env.MAUDE_SYNC_IN_CI;
57
- // biome-ignore lint/performance/noDelete: process.env semantics
58
57
  delete process.env.CI;
59
- // biome-ignore lint/performance/noDelete: process.env semantics
60
58
  delete process.env.GITHUB_ACTIONS;
61
- // biome-ignore lint/performance/noDelete: process.env semantics
62
59
  delete process.env.MAUDE_SYNC_IN_CI;
63
60
  });
64
61
 
65
62
  afterEach(() => {
66
63
  if (savedCi === undefined) {
67
- // biome-ignore lint/performance/noDelete: process.env semantics
68
64
  delete process.env.CI;
69
65
  } else process.env.CI = savedCi;
70
66
  if (savedGha === undefined) {
71
- // biome-ignore lint/performance/noDelete: process.env semantics
72
67
  delete process.env.GITHUB_ACTIONS;
73
68
  } else process.env.GITHUB_ACTIONS = savedGha;
74
69
  if (savedOverride === undefined) {
75
- // biome-ignore lint/performance/noDelete: process.env semantics
76
70
  delete process.env.MAUDE_SYNC_IN_CI;
77
71
  } else process.env.MAUDE_SYNC_IN_CI = savedOverride;
78
72
  });
@@ -443,7 +437,6 @@ describe('DDR-054 §2i — auto-clear adopt after first success', () => {
443
437
  const savedHubsEnv = process.env.HUBS_CONFIG_PATH;
444
438
  process.env.HUBS_CONFIG_PATH = hubsPath;
445
439
  const savedCi = process.env.CI;
446
- // biome-ignore lint/performance/noDelete: process.env semantics
447
440
  delete process.env.CI;
448
441
 
449
442
  try {
@@ -504,7 +497,6 @@ describe('DDR-054 §2i — auto-clear adopt after first success', () => {
504
497
  expect(typeof after.linkedHub.lastAdoptedAt).toBe('number');
505
498
  } finally {
506
499
  if (savedHubsEnv === undefined) {
507
- // biome-ignore lint/performance/noDelete: process.env semantics
508
500
  delete process.env.HUBS_CONFIG_PATH;
509
501
  } else process.env.HUBS_CONFIG_PATH = savedHubsEnv;
510
502
  if (savedCi !== undefined) process.env.CI = savedCi;
@@ -21,7 +21,6 @@ beforeEach(() => {
21
21
  afterEach(() => {
22
22
  // Node's process.env stringifies on assignment; assigning undefined yields
23
23
  // the literal string "undefined". delete is the correct restoration.
24
- // biome-ignore lint/performance/noDelete: process.env semantics.
25
24
  if (savedEnv === undefined) delete process.env.HUBS_CONFIG_PATH;
26
25
  else process.env.HUBS_CONFIG_PATH = savedEnv;
27
26
  rmSync(dir, { recursive: true, force: true });
@@ -0,0 +1,211 @@
1
+ // Incident replay — the 2026-06-11 `ui-maskot` data loss (DDR-102).
2
+ //
3
+ // The shape that ate a day of work: peer A seeded a STALE body to the hub;
4
+ // peer B booted later holding a NEWER divergent local file and no journal
5
+ // (first link on that machine). v1.1 hub-wins overwrote B's disk with A's
6
+ // stale body; the only trace was a conflict notice. This test replays the
7
+ // exact shape against the REAL journal + REAL history snapshots and asserts
8
+ // the DDR-102 outcome: B loses nothing.
9
+ //
10
+ // Scenario 2 is the everyday case that must stay silent: B's local matches
11
+ // what this machine last synced (journal hash) → the hub being ahead is a
12
+ // clean fast-forward, no snapshots, no conflict noise.
13
+
14
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
15
+ import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
16
+ import { tmpdir } from 'node:os';
17
+ import { join } from 'node:path';
18
+ import * as Y from 'yjs';
19
+
20
+ import type { Context } from '../context.ts';
21
+ import { createHistory } from '../history.ts';
22
+ import { createCanvasSyncAgent } from '../sync/agent.ts';
23
+ import { applyHtmlToDoc, htmlFromDoc, stampBodyEdit } from '../sync/codec.ts';
24
+ import { createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
25
+ import { loadJournal, type SyncJournal } from '../sync/journal.ts';
26
+
27
+ const STALE_BODY = '<div class="maskot">stale 2551-byte version that seeded the hub</div>';
28
+ const NEWER_BODY = '<div class="maskot">a full day of mascot work — 6 kB of it</div>';
29
+
30
+ let dir: string; // peer B's design root
31
+ let journal: SyncJournal;
32
+
33
+ function paths() {
34
+ return {
35
+ html: join(dir, 'ui', 'maskot.html'),
36
+ comments: join(dir, '_comments', 'ui-maskot.json'),
37
+ annotations: join(dir, 'ui-maskot.annotations.svg'),
38
+ };
39
+ }
40
+
41
+ /** Minimal ctx for the REAL history lib (snapshots under <dir>/_history). */
42
+ function historyCtx(): Context {
43
+ return {
44
+ paths: {
45
+ repoRoot: dir,
46
+ designRel: '.',
47
+ designRoot: dir,
48
+ historyDir: join(dir, '_history'),
49
+ },
50
+ } as unknown as Context;
51
+ }
52
+
53
+ beforeEach(() => {
54
+ dir = mkdtempSync(join(tmpdir(), 'incident-replay-'));
55
+ mkdirSync(join(dir, 'ui'), { recursive: true });
56
+ journal = loadJournal(dir, { flushMs: 0 });
57
+ });
58
+
59
+ afterEach(() => {
60
+ journal.stop();
61
+ rmSync(dir, { recursive: true, force: true });
62
+ });
63
+
64
+ /** Build peer B's agent against `doc` with the REAL journal + history. */
65
+ function makeAgentB(doc: Y.Doc, conflicts: unknown[]) {
66
+ const history = createHistory(historyCtx());
67
+ const agent = createCanvasSyncAgent({
68
+ slug: 'ui-maskot',
69
+ doc,
70
+ paths: paths(),
71
+ echoGuard: createEchoGuard(),
72
+ flushMs: 0,
73
+ journal,
74
+ snapshot: async (content, reason) => {
75
+ try {
76
+ const snap = await history.writeSnapshot('ui/maskot.html', content, reason);
77
+ return snap.ts;
78
+ } catch {
79
+ return null;
80
+ }
81
+ },
82
+ onConflict: (info) => {
83
+ conflicts.push(info);
84
+ },
85
+ });
86
+ agent.start();
87
+ return agent;
88
+ }
89
+
90
+ describe('incident replay — ui-maskot cold-start divergence (DDR-102)', () => {
91
+ test('peer B (newer divergent local, no journal) loses NOTHING: winner=local, dual snapshot, hub gets B body', async () => {
92
+ // Peer A seeded the hub with the stale body an hour ago (older stamp).
93
+ const hubDoc = new Y.Doc();
94
+ hubDoc.transact(() => {
95
+ applyHtmlToDoc(hubDoc, STALE_BODY);
96
+ stampBodyEdit(hubDoc, undefined, Date.now() - 60 * 60 * 1000);
97
+ });
98
+
99
+ // Peer B holds a NEWER divergent local file (mtime = now) and has never
100
+ // synced on this machine (journal absent).
101
+ writeFileSync(paths().html, NEWER_BODY);
102
+
103
+ const conflicts: Array<{
104
+ kind?: string;
105
+ winner?: string;
106
+ snapshots?: { local?: string; hub?: string };
107
+ }> = [];
108
+ const agent = makeAgentB(hubDoc, conflicts);
109
+ await agent.reconcile();
110
+ agent.stop();
111
+
112
+ // 1) B's file is byte-identical on disk — the day of work survived.
113
+ expect(readFileSync(paths().html, 'utf8')).toBe(NEWER_BODY);
114
+ // 2) Winner = local (newer mtime beats the hub's older stamp).
115
+ expect(conflicts).toHaveLength(1);
116
+ expect(conflicts[0].kind).toBe('cold-start-diverged');
117
+ expect(conflicts[0].winner).toBe('local');
118
+ // 3) BOTH versions snapshotted to _history/ui-maskot/ (real files).
119
+ expect(conflicts[0].snapshots?.local).toBeDefined();
120
+ expect(conflicts[0].snapshots?.hub).toBeDefined();
121
+ const snapDir = join(dir, '_history', 'ui-maskot');
122
+ const blobs = readdirSync(snapDir).filter((f) => f.endsWith('.html'));
123
+ const contents = blobs.map((f) => readFileSync(join(snapDir, f), 'utf8'));
124
+ expect(contents).toContain(NEWER_BODY);
125
+ expect(contents).toContain(STALE_BODY);
126
+ // 4) The hub doc now carries B's body (seeded up, stamped).
127
+ expect(htmlFromDoc(hubDoc)).toBe(NEWER_BODY);
128
+ // 5) The journal checkpointed — the NEXT boot is a clean fast-forward.
129
+ expect(journal.get('ui-maskot')?.bodyHash).toBe(hashBytes(NEWER_BODY));
130
+ });
131
+
132
+ test('peer B with a CLEAN journal fast-forwards silently: no snapshot, no conflict', async () => {
133
+ // B previously synced STALE_BODY on this machine (journal checkpoint),
134
+ // disk still holds exactly that; the hub then moved ahead.
135
+ writeFileSync(paths().html, STALE_BODY);
136
+ journal.record('ui-maskot', { bodyHash: hashBytes(STALE_BODY) });
137
+
138
+ const hubDoc = new Y.Doc();
139
+ hubDoc.transact(() => {
140
+ applyHtmlToDoc(hubDoc, NEWER_BODY);
141
+ stampBodyEdit(hubDoc);
142
+ });
143
+
144
+ const conflicts: unknown[] = [];
145
+ const agent = makeAgentB(hubDoc, conflicts);
146
+ await agent.reconcile();
147
+ agent.stop();
148
+
149
+ // Disk fast-forwarded to hub state…
150
+ expect(readFileSync(paths().html, 'utf8')).toBe(NEWER_BODY);
151
+ // …with zero conflict noise and zero history spam.
152
+ expect(conflicts).toHaveLength(0);
153
+ const snapDir = join(dir, '_history', 'ui-maskot');
154
+ let blobs: string[] = [];
155
+ try {
156
+ blobs = readdirSync(snapDir);
157
+ } catch {
158
+ /* dir never created — exactly the point */
159
+ }
160
+ expect(blobs).toHaveLength(0);
161
+ // Journal advanced to the new state.
162
+ expect(journal.get('ui-maskot')?.bodyHash).toBe(hashBytes(NEWER_BODY));
163
+ });
164
+
165
+ test('boot order REVERSED (B first, then A): A fast-forwards to B state, nothing lost either way', async () => {
166
+ // B boots first against an empty hub → DDR-064 seed-local-up.
167
+ writeFileSync(paths().html, NEWER_BODY);
168
+ const hubDoc = new Y.Doc();
169
+ const conflictsB: unknown[] = [];
170
+ const agentB = makeAgentB(hubDoc, conflictsB);
171
+ await agentB.reconcile();
172
+ agentB.stop();
173
+ expect(htmlFromDoc(hubDoc)).toBe(NEWER_BODY);
174
+ expect(conflictsB).toHaveLength(0); // empty-hub seed is not a conflict
175
+
176
+ // Peer A (separate design root + journal) holds the stale body it synced
177
+ // before — its journal matches its disk → clean fast-forward to B's state.
178
+ const dirA = mkdtempSync(join(tmpdir(), 'incident-replay-A-'));
179
+ mkdirSync(join(dirA, 'ui'), { recursive: true });
180
+ try {
181
+ writeFileSync(join(dirA, 'ui', 'maskot.html'), STALE_BODY);
182
+ const journalA = loadJournal(dirA, { flushMs: 0 });
183
+ journalA.record('ui-maskot', { bodyHash: hashBytes(STALE_BODY) });
184
+ const conflictsA: unknown[] = [];
185
+ const agentA = createCanvasSyncAgent({
186
+ slug: 'ui-maskot',
187
+ doc: hubDoc,
188
+ paths: {
189
+ html: join(dirA, 'ui', 'maskot.html'),
190
+ comments: join(dirA, '_comments', 'ui-maskot.json'),
191
+ annotations: join(dirA, 'ui-maskot.annotations.svg'),
192
+ },
193
+ echoGuard: createEchoGuard(),
194
+ flushMs: 0,
195
+ journal: journalA,
196
+ onConflict: (info) => {
197
+ conflictsA.push(info);
198
+ },
199
+ });
200
+ agentA.start();
201
+ await agentA.reconcile();
202
+ agentA.stop();
203
+ journalA.stop();
204
+
205
+ expect(readFileSync(join(dirA, 'ui', 'maskot.html'), 'utf8')).toBe(NEWER_BODY);
206
+ expect(conflictsA).toHaveLength(0);
207
+ } finally {
208
+ rmSync(dirA, { recursive: true, force: true });
209
+ }
210
+ });
211
+ });