@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,176 @@
1
+ // Sync journal unit tests — hub-sync cold-start safety (DDR-102).
2
+ //
3
+ // The journal is the divergence detector's memory: it records, per slug, the
4
+ // content hash this machine last reconciled disk↔doc. These tests cover the
5
+ // load/record/flush/invalidate API, the corrupt-file → absent posture, and
6
+ // the per-hub invalidation rule.
7
+
8
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
9
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
10
+ import { tmpdir } from 'node:os';
11
+ import { join } from 'node:path';
12
+
13
+ import { hashBytes } from '../sync/echo-guard.ts';
14
+ import { journalPath, loadJournal, type SyncJournal } from '../sync/journal.ts';
15
+
16
+ let dir: string;
17
+ let journal: SyncJournal | null = null;
18
+
19
+ beforeEach(() => {
20
+ dir = mkdtempSync(join(tmpdir(), 'sync-journal-'));
21
+ journal = null;
22
+ });
23
+
24
+ afterEach(() => {
25
+ journal?.stop();
26
+ rmSync(dir, { recursive: true, force: true });
27
+ });
28
+
29
+ describe('loadJournal — basic API', () => {
30
+ test('get returns null for a never-recorded slug', () => {
31
+ journal = loadJournal(dir, { flushMs: 0 });
32
+ expect(journal.get('ui-maskot')).toBeNull();
33
+ });
34
+
35
+ test('record + get round-trip with bodyHash and optional cssHash', () => {
36
+ journal = loadJournal(dir, { flushMs: 0, now: () => 42 });
37
+ const bodyHash = hashBytes('<div>v1</div>');
38
+ journal.record('ui-maskot', { bodyHash });
39
+ expect(journal.get('ui-maskot')).toEqual({ bodyHash, at: 42 });
40
+
41
+ const cssHash = hashBytes('.a{}');
42
+ journal.record('ui-maskot', { bodyHash, cssHash });
43
+ expect(journal.get('ui-maskot')).toEqual({ bodyHash, cssHash, at: 42 });
44
+ });
45
+
46
+ test('flush persists to <designRoot>/_state/sync-journal.json, creating _state/ on demand', () => {
47
+ journal = loadJournal(dir, { flushMs: 0 });
48
+ expect(existsSync(join(dir, '_state'))).toBe(false);
49
+ journal.record('ui-a', { bodyHash: 'h1' });
50
+
51
+ const file = journalPath(dir);
52
+ expect(existsSync(file)).toBe(true);
53
+ const parsed = JSON.parse(readFileSync(file, 'utf8'));
54
+ expect(parsed.slugs['ui-a'].bodyHash).toBe('h1');
55
+ expect(typeof parsed.updatedAt).toBe('number');
56
+ });
57
+
58
+ test('a fresh journal instance reads back what a previous one persisted', () => {
59
+ const first = loadJournal(dir, { flushMs: 0, now: () => 7 });
60
+ first.record('ui-a', { bodyHash: 'h1' });
61
+ first.stop();
62
+
63
+ journal = loadJournal(dir);
64
+ expect(journal.get('ui-a')).toEqual({ bodyHash: 'h1', at: 7 });
65
+ expect(journal.size()).toBe(1);
66
+ });
67
+
68
+ test('debounces rapid records into one write', async () => {
69
+ let writes = 0;
70
+ journal = loadJournal(dir, {
71
+ flushMs: 20,
72
+ writer: () => {
73
+ writes++;
74
+ },
75
+ });
76
+ for (let i = 0; i < 10; i++) journal.record('ui-a', { bodyHash: `h${i}` });
77
+ expect(writes).toBe(0); // still pending
78
+ await new Promise((res) => setTimeout(res, 60));
79
+ expect(writes).toBe(1);
80
+ });
81
+
82
+ test('stop() flushes the pending debounce', () => {
83
+ let written: string | null = null;
84
+ journal = loadJournal(dir, {
85
+ flushMs: 10_000,
86
+ writer: (_p, bytes) => {
87
+ written = bytes;
88
+ },
89
+ });
90
+ journal.record('ui-a', { bodyHash: 'h1' });
91
+ expect(written).toBeNull();
92
+ journal.stop();
93
+ expect(written).not.toBeNull();
94
+ expect(JSON.parse(written as unknown as string).slugs['ui-a'].bodyHash).toBe('h1');
95
+ });
96
+ });
97
+
98
+ describe('loadJournal — corrupt / hostile file posture', () => {
99
+ test('corrupt JSON → treated as absent, never throws', () => {
100
+ const file = journalPath(dir);
101
+ // Persist a valid journal first (creates _state/), then corrupt it.
102
+ journal = loadJournal(dir, { flushMs: 0 });
103
+ journal.record('ui-a', { bodyHash: 'h1' });
104
+ journal.stop();
105
+
106
+ writeFileSync(file, '{not json');
107
+ journal = loadJournal(dir);
108
+ expect(journal.get('ui-a')).toBeNull();
109
+ expect(journal.size()).toBe(0);
110
+ });
111
+
112
+ test('wrong-shape JSON (array / non-object slugs / bad entries) → absent entries', () => {
113
+ const file = journalPath(dir);
114
+ journal = loadJournal(dir, { flushMs: 0 });
115
+ journal.record('seed', { bodyHash: 'x' }); // creates _state/
116
+ journal.stop();
117
+
118
+ writeFileSync(file, JSON.stringify([1, 2, 3]));
119
+ expect(loadJournal(dir).size()).toBe(0);
120
+
121
+ writeFileSync(file, JSON.stringify({ hubUrl: 1, slugs: { 'ui-a': { bodyHash: 99 } } }));
122
+ const j = loadJournal(dir);
123
+ expect(j.get('ui-a')).toBeNull();
124
+ expect(j.size()).toBe(0);
125
+ });
126
+
127
+ test('writer failure is swallowed (never throws into the sync path)', () => {
128
+ journal = loadJournal(dir, {
129
+ flushMs: 0,
130
+ writer: () => {
131
+ throw new Error('disk full');
132
+ },
133
+ });
134
+ expect(() => journal?.record('ui-a', { bodyHash: 'h1' })).not.toThrow();
135
+ expect(journal.get('ui-a')?.bodyHash).toBe('h1'); // in-memory state intact
136
+ });
137
+ });
138
+
139
+ describe('loadJournal — per-hub invalidation', () => {
140
+ test('first invalidateIfHubChanged adopts the URL without wiping', () => {
141
+ journal = loadJournal(dir, { flushMs: 0 });
142
+ journal.record('ui-a', { bodyHash: 'h1' });
143
+ journal.invalidateIfHubChanged('https://hub.example.com');
144
+ expect(journal.get('ui-a')?.bodyHash).toBe('h1');
145
+ });
146
+
147
+ test('same hub URL on a later boot keeps entries', () => {
148
+ const first = loadJournal(dir, { flushMs: 0 });
149
+ first.invalidateIfHubChanged('https://hub.example.com');
150
+ first.record('ui-a', { bodyHash: 'h1' });
151
+ first.stop();
152
+
153
+ journal = loadJournal(dir, { flushMs: 0 });
154
+ journal.invalidateIfHubChanged('https://hub.example.com');
155
+ expect(journal.get('ui-a')?.bodyHash).toBe('h1');
156
+ });
157
+
158
+ test('a DIFFERENT hub URL wipes every entry (hashes are per-hub)', () => {
159
+ const first = loadJournal(dir, { flushMs: 0 });
160
+ first.invalidateIfHubChanged('https://hub-one.example.com');
161
+ first.record('ui-a', { bodyHash: 'h1' });
162
+ first.record('ui-b', { bodyHash: 'h2' });
163
+ first.stop();
164
+
165
+ journal = loadJournal(dir, { flushMs: 0 });
166
+ journal.invalidateIfHubChanged('https://hub-two.example.com');
167
+ expect(journal.get('ui-a')).toBeNull();
168
+ expect(journal.get('ui-b')).toBeNull();
169
+ expect(journal.size()).toBe(0);
170
+
171
+ // …and the wipe is persisted.
172
+ const parsed = JSON.parse(readFileSync(journalPath(dir), 'utf8'));
173
+ expect(parsed.hubUrl).toBe('https://hub-two.example.com');
174
+ expect(Object.keys(parsed.slugs)).toHaveLength(0);
175
+ });
176
+ });
@@ -11,6 +11,7 @@ import {
11
11
  existsSync,
12
12
  mkdirSync,
13
13
  mkdtempSync,
14
+ readdirSync,
14
15
  readFileSync,
15
16
  rmSync,
16
17
  writeFileSync,
@@ -50,7 +51,6 @@ beforeEach(() => {
50
51
  afterEach(() => {
51
52
  // Node's process.env stringifies on assignment; assigning undefined yields
52
53
  // the literal string "undefined". delete is the correct restoration.
53
- // biome-ignore lint/performance/noDelete: process.env semantics.
54
54
  if (cfgPathEnv === undefined) delete process.env.HUBS_CONFIG_PATH;
55
55
  else process.env.HUBS_CONFIG_PATH = cfgPathEnv;
56
56
  rmSync(dir, { recursive: true, force: true });
@@ -424,14 +424,15 @@ describe('createSyncRuntime', () => {
424
424
  await runtime?.stop();
425
425
  });
426
426
 
427
- test('Task 8: hub-wins reconcile over divergent local content records a conflict', async () => {
427
+ test('DDR-102: reconcile over divergent local content records a diverged conflict + snapshots both sides', async () => {
428
428
  const url = 'https://hub.example.com';
429
429
  writeHubsConfig(url, 'mau_test');
430
430
  const ctx = makeCtx({ url, linkedAt: 1 });
431
- // Local disk has divergent, non-empty content.
431
+ // Local disk has divergent, non-empty content (and no journal — divergence).
432
432
  writeFileSync(join(ctx.paths.designRoot, 'ui', 'screen.html'), '<button>LOCAL EDIT</button>');
433
433
 
434
- // Provider whose doc already carries different hub state.
434
+ // Provider whose doc already carries different hub state. No syncMeta
435
+ // stamp (older peer) → newest-wins falls back to hub.
435
436
  const factory = () => {
436
437
  const document = new Y.Doc();
437
438
  document.getText('html').insert(0, '<button>HUB STATE</button>');
@@ -451,7 +452,25 @@ describe('createSyncRuntime', () => {
451
452
 
452
453
  const status = runtime?.status();
453
454
  expect(status?.conflicts.length).toBeGreaterThanOrEqual(1);
454
- expect(status?.conflicts[0].kind).toBe('cold-start-hub-wins');
455
+ const conflict = status?.conflicts[0];
456
+ expect(conflict?.kind).toBe('cold-start-diverged');
457
+ expect(conflict?.winner).toBe('hub');
458
+ // Both pre-resolution versions were snapshotted to _history/<slug>/.
459
+ expect(conflict?.snapshots?.local).toBeDefined();
460
+ expect(conflict?.snapshots?.hub).toBeDefined();
461
+ const historyDir = join(ctx.paths.historyDir, 'ui-screen');
462
+ const blobs = readdirSync(historyDir).filter((f) => f.endsWith('.html'));
463
+ expect(blobs.length).toBe(2);
464
+ // The loser (local) is recoverable byte-identical from its snapshot.
465
+ const metas = readdirSync(historyDir).filter((f) => f.endsWith('.json'));
466
+ const snapshotContents = blobs.map((f) => readFileSync(join(historyDir, f), 'utf8'));
467
+ expect(snapshotContents).toContain('<button>LOCAL EDIT</button>');
468
+ expect(snapshotContents).toContain('<button>HUB STATE</button>');
469
+ expect(metas.length).toBe(2);
470
+ // Hub won → disk now carries hub state.
471
+ expect(readFileSync(join(ctx.paths.designRoot, 'ui', 'screen.html'), 'utf8')).toBe(
472
+ '<button>HUB STATE</button>'
473
+ );
455
474
  await runtime?.stop();
456
475
  });
457
476
  });
@@ -885,3 +904,339 @@ describe('toWsUrl', () => {
885
904
  expect(toWsUrl('ws://localhost:1234')).toBe('ws://localhost:1234');
886
905
  });
887
906
  });
907
+
908
+ // DDR-102 — WS multiplexing: the default factory shares ONE
909
+ // HocuspocusProviderWebsocket per hub URL across all providers.
910
+ describe('createDefaultProviderFactory — shared socket multiplexing', () => {
911
+ class FakeEmitter {
912
+ handlers = new Map<string, Set<(arg?: unknown) => void>>();
913
+ on(evt: string, cb: (arg?: unknown) => void) {
914
+ if (!this.handlers.has(evt)) this.handlers.set(evt, new Set());
915
+ this.handlers.get(evt)?.add(cb);
916
+ }
917
+ off(evt: string, cb: (arg?: unknown) => void) {
918
+ this.handlers.get(evt)?.delete(cb);
919
+ }
920
+ emit(evt: string, arg?: unknown) {
921
+ for (const cb of this.handlers.get(evt) ?? []) cb(arg);
922
+ }
923
+ }
924
+
925
+ function makeFakeModule() {
926
+ const sockets: Array<{ url: string; destroyed: boolean }> = [];
927
+ const providers: Array<{
928
+ name: string;
929
+ attached: boolean;
930
+ destroyed: boolean;
931
+ socket: unknown;
932
+ }> = [];
933
+
934
+ class HocuspocusProviderWebsocket extends FakeEmitter {
935
+ url: string;
936
+ status = 'connecting';
937
+ destroyed = false;
938
+ constructor(cfg: { url: string }) {
939
+ super();
940
+ this.url = cfg.url;
941
+ sockets.push(this);
942
+ }
943
+ destroy() {
944
+ this.destroyed = true;
945
+ }
946
+ }
947
+
948
+ class HocuspocusProvider extends FakeEmitter {
949
+ configuration: { name: string; websocketProvider: unknown; document: unknown };
950
+ awareness = undefined;
951
+ synced = false;
952
+ attached = false;
953
+ destroyed = false;
954
+ name: string;
955
+ socket: unknown;
956
+ constructor(cfg: { name: string; websocketProvider: unknown; document: unknown }) {
957
+ super();
958
+ this.configuration = cfg;
959
+ this.name = cfg.name;
960
+ this.socket = cfg.websocketProvider;
961
+ providers.push(this);
962
+ }
963
+ attach() {
964
+ this.attached = true;
965
+ }
966
+ destroy() {
967
+ this.destroyed = true;
968
+ }
969
+ }
970
+
971
+ return { mod: { HocuspocusProviderWebsocket, HocuspocusProvider }, sockets, providers };
972
+ }
973
+
974
+ test('N providers for one hub URL share ONE socket, each explicitly attached', async () => {
975
+ const { mod, sockets, providers } = makeFakeModule();
976
+ const { createDefaultProviderFactory } = await import('../sync/index.ts');
977
+ const factory = createDefaultProviderFactory(async () => mod);
978
+
979
+ await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-a' });
980
+ await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-b' });
981
+ await factory({ url: 'https://hub.example.com', token: 't', documentName: 'ui-c' });
982
+
983
+ expect(sockets).toHaveLength(1);
984
+ expect(sockets[0].url).toBe('wss://hub.example.com');
985
+ expect(providers).toHaveLength(3);
986
+ for (const p of providers) {
987
+ expect(p.attached).toBe(true);
988
+ expect(p.socket).toBe(sockets[0]);
989
+ }
990
+ });
991
+
992
+ test('provider destroy() detaches only; dispose() destroys the shared socket', async () => {
993
+ const { mod, sockets, providers } = makeFakeModule();
994
+ const { createDefaultProviderFactory } = await import('../sync/index.ts');
995
+ const factory = createDefaultProviderFactory(async () => mod);
996
+
997
+ const p1 = await factory({ url: 'https://h.example.com', token: 't', documentName: 'ui-a' });
998
+ p1.destroy();
999
+ expect(providers[0].destroyed).toBe(true);
1000
+ expect(sockets[0].destroyed).toBe(false); // socket survives provider death
1001
+
1002
+ factory.dispose();
1003
+ expect(sockets[0].destroyed).toBe(true);
1004
+ });
1005
+
1006
+ test('onStatus seeds from the SOCKET status and forwards provider status events', async () => {
1007
+ const { mod, sockets, providers } = makeFakeModule();
1008
+ const { createDefaultProviderFactory } = await import('../sync/index.ts');
1009
+ const factory = createDefaultProviderFactory(async () => mod);
1010
+
1011
+ const wrapped = await factory({
1012
+ url: 'https://h.example.com',
1013
+ token: 't',
1014
+ documentName: 'ui-a',
1015
+ });
1016
+ const seen: string[] = [];
1017
+ const unsub = wrapped.onStatus?.((s) => seen.push(s));
1018
+ // Seeded from socket.status ('connecting') immediately.
1019
+ expect(seen).toEqual(['connecting']);
1020
+ // Provider-level forwarded events keep flowing.
1021
+ (providers[0] as unknown as FakeEmitter).emit('status', { status: 'connected' });
1022
+ expect(seen).toEqual(['connecting', 'connected']);
1023
+ unsub?.();
1024
+ (providers[0] as unknown as FakeEmitter).emit('status', { status: 'disconnected' });
1025
+ expect(seen).toEqual(['connecting', 'connected']); // unsubscribed
1026
+ expect(sockets).toHaveLength(1);
1027
+ });
1028
+ });
1029
+
1030
+ // DDR-102 — auth-failure intelligence (classification, aggregation,
1031
+ // destroy-on-permanent + re-probe) and the honest settled boot summary.
1032
+ describe('DDR-102 — auth-failure intelligence + boot summary', () => {
1033
+ function fakeTimerQueue() {
1034
+ let nextId = 1;
1035
+ const timers = new Map<number, { cb: () => void; ms: number }>();
1036
+ return {
1037
+ setTimer: (cb: () => void, ms: number) => {
1038
+ const id = nextId++;
1039
+ timers.set(id, { cb, ms });
1040
+ return id as unknown as ReturnType<typeof setTimeout>;
1041
+ },
1042
+ clearTimer: (h: ReturnType<typeof setTimeout>) => {
1043
+ timers.delete(h as unknown as number);
1044
+ },
1045
+ /** Fire every pending timer with ms <= maxMs (one sweep). */
1046
+ fire(maxMs: number) {
1047
+ for (const [id, t] of [...timers.entries()]) {
1048
+ if (t.ms <= maxMs) {
1049
+ timers.delete(id);
1050
+ t.cb();
1051
+ }
1052
+ }
1053
+ },
1054
+ pending: () => timers.size,
1055
+ };
1056
+ }
1057
+
1058
+ /** Stub factory whose providers expose onAuthFailed + track destroys. */
1059
+ function authStubFactory() {
1060
+ const made: Array<{
1061
+ documentName: string;
1062
+ destroyed: boolean;
1063
+ emitAuthFailure: (reason: string) => void;
1064
+ document: Y.Doc;
1065
+ }> = [];
1066
+ const factory = (args: { documentName: string; document?: Y.Doc }) => {
1067
+ const document = args.document ?? new Y.Doc();
1068
+ const cbs = new Set<(info: { reason: string }) => void>();
1069
+ const entry = {
1070
+ documentName: args.documentName,
1071
+ destroyed: false,
1072
+ emitAuthFailure: (reason: string) => {
1073
+ for (const cb of cbs) cb({ reason });
1074
+ },
1075
+ document,
1076
+ };
1077
+ made.push(entry);
1078
+ return {
1079
+ document,
1080
+ onAuthFailed(cb: (info: { reason: string }) => void) {
1081
+ cbs.add(cb);
1082
+ return () => cbs.delete(cb);
1083
+ },
1084
+ onceSynced() {
1085
+ return new Promise<void>(() => {}); // never settles (rejected docs)
1086
+ },
1087
+ destroy() {
1088
+ entry.destroyed = true;
1089
+ },
1090
+ };
1091
+ };
1092
+ return { factory, made };
1093
+ }
1094
+
1095
+ test('classification + ONE aggregated warn + destroy-on-permanent + re-probe', async () => {
1096
+ const url = 'https://hub.example.com';
1097
+ writeHubsConfig(url, 'mau_test');
1098
+ const ctx = makeCtx({ url, linkedAt: 1 });
1099
+ writeFileSync(join(ctx.paths.designRoot, 'ui', 'a.html'), '<a/>');
1100
+ writeFileSync(join(ctx.paths.designRoot, 'ui', 'b.html'), '<b/>');
1101
+ writeFileSync(join(ctx.paths.designRoot, 'ui', 'c.html'), '<c/>');
1102
+
1103
+ const timers = fakeTimerQueue();
1104
+ const { factory, made } = authStubFactory();
1105
+ const warns: string[] = [];
1106
+ const origWarn = console.warn;
1107
+ console.warn = (...args: unknown[]) => {
1108
+ warns.push(args.map(String).join(' '));
1109
+ };
1110
+
1111
+ try {
1112
+ const runtime = createSyncRuntime(ctx, {
1113
+ providerFactory: factory,
1114
+ auth: {
1115
+ warnDebounceMs: 2_000,
1116
+ reprobeMs: 300_000,
1117
+ settleTimeoutMs: 15_000,
1118
+ setTimer: timers.setTimer,
1119
+ clearTimer: timers.clearTimer,
1120
+ },
1121
+ });
1122
+ await runtime?.start();
1123
+ expect(made).toHaveLength(3);
1124
+ // readdir order is not deterministic — address providers by slug.
1125
+ const bySlug = (slug: string) => {
1126
+ const m = made.find((p) => p.documentName === slug);
1127
+ if (!m) throw new Error(`no provider for ${slug}`);
1128
+ return m;
1129
+ };
1130
+
1131
+ // Hub rejects: a + b permanently (scope), c transiently (rate limit).
1132
+ bySlug('ui-a').emitAuthFailure('token not authorized for this documentName');
1133
+ bySlug('ui-b').emitAuthFailure('token not authorized for this documentName');
1134
+ bySlug('ui-c').emitAuthFailure('rate limit exceeded for this token');
1135
+
1136
+ // Per-slug statuses are honest immediately.
1137
+ const status = runtime?.status();
1138
+ expect(status?.docs?.rejected).toBe(3);
1139
+ expect(status?.rejectedSlugs?.sort()).toEqual(['ui-a', 'ui-b', 'ui-c']);
1140
+
1141
+ // Permanent classes destroyed (retry storm stopped); transient keeps its
1142
+ // provider (built-in backoff).
1143
+ expect(bySlug('ui-a').destroyed).toBe(true);
1144
+ expect(bySlug('ui-b').destroyed).toBe(true);
1145
+ expect(bySlug('ui-c').destroyed).toBe(false);
1146
+
1147
+ // No warn yet (debounced); ONE aggregated warn after the window.
1148
+ const authWarnsBefore = warns.filter((w) => w.includes('auth rejections')).length;
1149
+ expect(authWarnsBefore).toBe(0);
1150
+ timers.fire(2_000);
1151
+ const authWarns = warns.filter((w) => w.includes('auth rejections'));
1152
+ expect(authWarns).toHaveLength(1);
1153
+ // Reason-specific hints, both classes in the one warn.
1154
+ expect(authWarns[0]).toContain('not-authorized');
1155
+ expect(authWarns[0]).toContain('rate-limit');
1156
+ expect(authWarns[0]).toContain('mint a hub-wide token');
1157
+ expect(authWarns[0]).toContain('HUB_CONN_RATE_LIMIT');
1158
+
1159
+ // Re-probe (5 min): the two permanently-rejected docs reconnect with the
1160
+ // SAME doc instance (agent wiring survives the provider swap).
1161
+ const docA = bySlug('ui-a').document;
1162
+ timers.fire(300_000);
1163
+ await new Promise((res) => setTimeout(res, 10));
1164
+ const reprobed = made.slice(3);
1165
+ expect(reprobed.map((m) => m.documentName).sort()).toEqual(['ui-a', 'ui-b']);
1166
+ expect(reprobed.find((m) => m.documentName === 'ui-a')?.document).toBe(docA);
1167
+
1168
+ await runtime?.stop();
1169
+ } finally {
1170
+ console.warn = origWarn;
1171
+ }
1172
+ });
1173
+
1174
+ test('boot summary prints AFTER settle with honest counts (not the premature N/N)', async () => {
1175
+ const url = 'https://hub.example.com';
1176
+ writeHubsConfig(url, 'mau_test');
1177
+ const ctx = makeCtx({ url, linkedAt: 1 });
1178
+ writeFileSync(join(ctx.paths.designRoot, 'ui', 'good.html'), '<g/>');
1179
+ writeFileSync(join(ctx.paths.designRoot, 'ui', 'bad.html'), '<b/>');
1180
+
1181
+ const timers = fakeTimerQueue();
1182
+ const logs: string[] = [];
1183
+ const origLog = console.log;
1184
+ const origWarn = console.warn;
1185
+ console.log = (...args: unknown[]) => {
1186
+ logs.push(args.map(String).join(' '));
1187
+ };
1188
+ console.warn = () => {};
1189
+
1190
+ // 'good' syncs immediately; 'bad' is auth-rejected and never settles.
1191
+ const { factory, made } = authStubFactory();
1192
+ const mixedFactory = (args: { documentName: string; document?: Y.Doc }) => {
1193
+ if (args.documentName === 'ui-good') {
1194
+ const document = args.document ?? new Y.Doc();
1195
+ return {
1196
+ document,
1197
+ async onceSynced() {},
1198
+ destroy() {},
1199
+ };
1200
+ }
1201
+ return factory(args);
1202
+ };
1203
+
1204
+ try {
1205
+ const runtime = createSyncRuntime(ctx, {
1206
+ providerFactory: mixedFactory,
1207
+ auth: {
1208
+ settleTimeoutMs: 15_000,
1209
+ warnDebounceMs: 2_000,
1210
+ setTimer: timers.setTimer,
1211
+ clearTimer: timers.clearTimer,
1212
+ },
1213
+ });
1214
+ await runtime?.start();
1215
+ made.find((m) => m.documentName === 'ui-bad')?.emitAuthFailure('invalid token');
1216
+ // Boot prints the linking line immediately, NOT a premature N/N.
1217
+ expect(logs.some((l) => l.includes('linking to'))).toBe(true);
1218
+ expect(logs.some((l) => l.includes('synced'))).toBe(false);
1219
+
1220
+ // Let the good handshake's microtask chain run, then hit the ceiling.
1221
+ await new Promise((res) => setTimeout(res, 10));
1222
+ timers.fire(15_000);
1223
+ await new Promise((res) => setTimeout(res, 10));
1224
+
1225
+ const summary = logs.find((l) => l.includes('synced'));
1226
+ expect(summary).toBeDefined();
1227
+ expect(summary).toContain('1/2 synced');
1228
+ expect(summary).toContain('1 auth-rejected');
1229
+ expect(summary).toContain('ui-bad');
1230
+ expect(summary).toContain('invalid-token');
1231
+
1232
+ // lastSyncAt reflects the real reconcile activity of the good canvas.
1233
+ expect(runtime?.status()?.lastSyncAt).not.toBeNull();
1234
+ expect(runtime?.status()?.docs?.synced).toBe(1);
1235
+
1236
+ await runtime?.stop();
1237
+ } finally {
1238
+ console.log = origLog;
1239
+ console.warn = origWarn;
1240
+ }
1241
+ });
1242
+ });
@@ -78,3 +78,69 @@ describe('sync status store', () => {
78
78
  expect(() => store.update(snap({ state: 'offline' }))).not.toThrow();
79
79
  });
80
80
  });
81
+
82
+ // DDR-102 — the payload carries the per-doc rollup + rich conflicts additively.
83
+ describe('sync status store — DDR-102 additive fields', () => {
84
+ test('docs + rejectedSlugs flow through update() into the payload', () => {
85
+ const writes: SyncStatusPayload[] = [];
86
+ const store = createSyncStatusStore({
87
+ url: 'https://h.example.com',
88
+ canvases: 3,
89
+ write: (p) => writes.push(p),
90
+ });
91
+ store.update({
92
+ state: 'online',
93
+ queuedOps: 0,
94
+ lastSyncAt: 123,
95
+ offlineSince: null,
96
+ flash: null,
97
+ updatedAt: 456,
98
+ docs: { synced: 1, pending: 0, rejected: 2 },
99
+ rejectedSlugs: ['ui-x', 'ui-y'],
100
+ });
101
+ const p = store.get();
102
+ expect(p.docs).toEqual({ synced: 1, pending: 0, rejected: 2 });
103
+ expect(p.rejectedSlugs).toEqual(['ui-x', 'ui-y']);
104
+ expect(p.lastSyncAt).toBe(123);
105
+ });
106
+
107
+ test('addConflict carries winner + snapshots (cold-start-diverged)', () => {
108
+ const store = createSyncStatusStore({
109
+ url: 'https://h.example.com',
110
+ canvases: 1,
111
+ write: () => {},
112
+ now: () => 999,
113
+ });
114
+ store.addConflict({
115
+ slug: 'ui-maskot',
116
+ kind: 'cold-start-diverged',
117
+ winner: 'local',
118
+ snapshots: { local: '2026-06-11T10:00:00.000Z', hub: '2026-06-11T10:00:00.001Z' },
119
+ });
120
+ const c = store.get().conflicts[0];
121
+ expect(c.kind).toBe('cold-start-diverged');
122
+ expect(c.winner).toBe('local');
123
+ expect(c.snapshots?.local).toBe('2026-06-11T10:00:00.000Z');
124
+ expect(c.at).toBe(999);
125
+ });
126
+
127
+ test('old-shape payload readers: a snapshot WITHOUT docs/rejectedSlugs still works', () => {
128
+ const store = createSyncStatusStore({
129
+ url: 'https://h.example.com',
130
+ canvases: 1,
131
+ write: () => {},
132
+ });
133
+ store.update({
134
+ state: 'online',
135
+ queuedOps: 0,
136
+ lastSyncAt: null,
137
+ offlineSince: null,
138
+ flash: null,
139
+ updatedAt: 1,
140
+ });
141
+ const p = store.get();
142
+ expect(p.docs).toBeUndefined();
143
+ expect(p.rejectedSlugs).toBeUndefined();
144
+ expect(p.state).toBe('online');
145
+ });
146
+ });