@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,190 @@
1
+ // Per-machine sync journal — hub-sync cold-start safety (DDR-102).
2
+ //
3
+ // Records, per slug, the content hash of the LAST state this machine
4
+ // successfully reconciled disk↔doc. It answers git's "do I have uncommitted
5
+ // changes?" question for sync: a cold-start hub-wins overwrite of local disk
6
+ // is allowed ONLY when hash(local) == journal hash (a clean fast-forward —
7
+ // everything local was already synced, the hub is just ahead). Any other
8
+ // combination is divergence and takes the conflict protocol (cold-start.ts:
9
+ // dual snapshot + newest-wins).
10
+ //
11
+ // Per-machine, per-hub: the file lives at `<designRoot>/_state/sync-journal.json`
12
+ // (`_state/` is already in the DDR-056 gitignore block) and carries the hub URL
13
+ // it was recorded against. Relinking to a DIFFERENT hub invalidates every
14
+ // entry — hashes recorded against one hub's docs say nothing about another's.
15
+ //
16
+ // Posture mirrors status.ts: best-effort, NEVER throws into the boot path. A
17
+ // corrupt/unparseable file is treated as absent; the decision module then
18
+ // degrades to the conservative "potential divergence" path, which is safe
19
+ // (snapshot + newest-wins), never destructive. Hashes come exclusively from
20
+ // `hashBytes` (echo-guard.ts) so the journal compares apples to apples with
21
+ // the echo guard and the cold-start decision inputs.
22
+
23
+ import { existsSync, readFileSync } from 'node:fs';
24
+ import path from 'node:path';
25
+
26
+ import { atomicWrite } from './atomic-write.ts';
27
+
28
+ /** Debounce window for persisting the journal to disk. */
29
+ export const JOURNAL_FLUSH_MS = 1_000;
30
+
31
+ export interface JournalEntry {
32
+ /** hashBytes() of the canvas body as last reconciled disk↔doc. */
33
+ bodyHash: string;
34
+ /** hashBytes() of the sibling .css, when one was reconciled. */
35
+ cssHash?: string;
36
+ /** ms epoch of the checkpoint. */
37
+ at: number;
38
+ }
39
+
40
+ interface JournalFileShape {
41
+ hubUrl: string | null;
42
+ updatedAt: number;
43
+ slugs: Record<string, JournalEntry>;
44
+ }
45
+
46
+ export interface SyncJournal {
47
+ /** Last-reconciled hashes for a slug, or null when never checkpointed. */
48
+ get(slug: string): JournalEntry | null;
49
+ /** Checkpoint a successful disk↔doc traversal. Lanes checkpoint
50
+ * independently (body flush vs css flush), so omitted hashes MERGE with the
51
+ * existing entry instead of clearing it. Schedules a debounced persist. */
52
+ record(slug: string, hashes: { bodyHash?: string; cssHash?: string }): void;
53
+ /** Relinked to a different hub → wipe every entry (hashes are per-hub). */
54
+ invalidateIfHubChanged(url: string): void;
55
+ /** Persist now (cancels the pending debounce). Best-effort. */
56
+ flush(): void;
57
+ /** Flush + cancel timers. */
58
+ stop(): void;
59
+ /** Test/inspection — number of slugs tracked. */
60
+ size(): number;
61
+ }
62
+
63
+ export interface LoadJournalOptions {
64
+ /** Override the persist debounce. Tests use 0 to persist synchronously. */
65
+ flushMs?: number;
66
+ now?: () => number;
67
+ /** Injected for tests — defaults to atomicWrite. */
68
+ writer?: (path: string, bytes: string) => void;
69
+ }
70
+
71
+ /** Absolute path of the journal file for a design root. */
72
+ export function journalPath(designRoot: string): string {
73
+ return path.join(designRoot, '_state', 'sync-journal.json');
74
+ }
75
+
76
+ export function loadJournal(designRoot: string, opts: LoadJournalOptions = {}): SyncJournal {
77
+ const file = journalPath(designRoot);
78
+ const flushMs = opts.flushMs ?? JOURNAL_FLUSH_MS;
79
+ const now = opts.now ?? Date.now;
80
+ // atomicWrite mkdirs the parent (`_state/`) on demand, so no explicit mkdir.
81
+ const writer = opts.writer ?? ((p: string, bytes: string) => void atomicWrite(p, bytes));
82
+
83
+ let hubUrl: string | null = null;
84
+ let slugs: Record<string, JournalEntry> = {};
85
+ let dirty = false;
86
+ let flushTimer: ReturnType<typeof setTimeout> | null = null;
87
+ let stopped = false;
88
+
89
+ // Load once at construction. Corrupt / unreadable / wrong-shape → absent.
90
+ try {
91
+ if (existsSync(file)) {
92
+ const parsed = JSON.parse(readFileSync(file, 'utf8')) as Partial<JournalFileShape>;
93
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
94
+ hubUrl = typeof parsed.hubUrl === 'string' ? parsed.hubUrl : null;
95
+ const rawSlugs = parsed.slugs;
96
+ if (rawSlugs && typeof rawSlugs === 'object' && !Array.isArray(rawSlugs)) {
97
+ for (const [slug, raw] of Object.entries(rawSlugs)) {
98
+ const e = raw as JournalEntry;
99
+ if (e && typeof e === 'object' && typeof e.bodyHash === 'string') {
100
+ slugs[slug] = {
101
+ bodyHash: e.bodyHash,
102
+ ...(typeof e.cssHash === 'string' ? { cssHash: e.cssHash } : {}),
103
+ at: typeof e.at === 'number' ? e.at : 0,
104
+ };
105
+ }
106
+ }
107
+ }
108
+ }
109
+ }
110
+ } catch {
111
+ /* corrupt journal → treat as absent; the decision module degrades safely */
112
+ }
113
+
114
+ function persist(): void {
115
+ if (!dirty) return;
116
+ dirty = false;
117
+ if (flushTimer) {
118
+ clearTimeout(flushTimer);
119
+ flushTimer = null;
120
+ }
121
+ const payload: JournalFileShape = { hubUrl, updatedAt: now(), slugs };
122
+ try {
123
+ writer(file, `${JSON.stringify(payload, null, 2)}\n`);
124
+ } catch (err) {
125
+ // Best-effort: a failed persist must never throw into the sync hot path.
126
+ console.warn('[sync/journal] persist failed:', err instanceof Error ? err.message : err);
127
+ }
128
+ }
129
+
130
+ function schedulePersist(): void {
131
+ dirty = true;
132
+ if (stopped) return;
133
+ if (flushMs === 0) {
134
+ persist();
135
+ return;
136
+ }
137
+ if (flushTimer) clearTimeout(flushTimer);
138
+ flushTimer = setTimeout(() => {
139
+ flushTimer = null;
140
+ persist();
141
+ }, flushMs);
142
+ }
143
+
144
+ return {
145
+ get(slug) {
146
+ return slugs[slug] ?? null;
147
+ },
148
+
149
+ record(slug, hashes) {
150
+ const prev = slugs[slug];
151
+ const cssHash = hashes.cssHash ?? prev?.cssHash;
152
+ slugs[slug] = {
153
+ // An entry created by a css-only checkpoint carries an empty bodyHash —
154
+ // it can never match a real body hash, so the decision module stays
155
+ // conservative (divergence path) until a body traversal checkpoints.
156
+ bodyHash: hashes.bodyHash ?? prev?.bodyHash ?? '',
157
+ ...(cssHash !== undefined ? { cssHash } : {}),
158
+ at: now(),
159
+ };
160
+ schedulePersist();
161
+ },
162
+
163
+ invalidateIfHubChanged(url) {
164
+ if (hubUrl === url) return;
165
+ if (hubUrl !== null) {
166
+ // Different hub than the one these hashes were recorded against — every
167
+ // entry is meaningless there. Wipe so the decision module stays
168
+ // conservative (divergence → snapshot) instead of false-fast-forwarding.
169
+ slugs = {};
170
+ }
171
+ hubUrl = url;
172
+ schedulePersist();
173
+ },
174
+
175
+ flush: persist,
176
+
177
+ stop() {
178
+ persist();
179
+ stopped = true;
180
+ if (flushTimer) {
181
+ clearTimeout(flushTimer);
182
+ flushTimer = null;
183
+ }
184
+ },
185
+
186
+ size() {
187
+ return Object.keys(slugs).length;
188
+ },
189
+ };
190
+ }
@@ -0,0 +1,321 @@
1
+ // One-time authoritative seed for the shared-doc cutover — Phase 9.2 (DDR-064
2
+ // Task 9), cold-start resolution upgraded by DDR-102.
3
+ //
4
+ // The trap (dmonad-confirmed, discuss.yjs.dev/t/.../2538): if a shared doc is
5
+ // populated from TWO independent sources — the local file-seed (room.seed
6
+ // pushing `_comments/<slug>.json` as fresh Y.Array items) AND the hub provider
7
+ // (syncing the hub's canonical items) — the two item-sets have different client
8
+ // IDs, so the CRDT merge CONCATENATES them: comment "c1" appears twice. You
9
+ // cannot fix this with `applyUpdate` of two docs; you must pick ONE authoritative
10
+ // source and build the doc from it.
11
+ //
12
+ // This module is that decision, run ONCE per canvas at cutover, AFTER the
13
+ // provider's first sync (so the doc already holds hub state if the hub had any):
14
+ //
15
+ // - hub EMPTY (doc empty) → ADOPT. Clear+rebuild the doc from the
16
+ // local files inside `transact(fn, MIGRATION)`.
17
+ // - hub HAD state, body resolution → the DDR-102 decision table
18
+ // (cold-start.ts): identical → keep; journal-matched local → fast-forward
19
+ // (hub keeps); empty local → materialize; divergence → dual snapshot +
20
+ // NEWEST-WINS — winner `local` rebuilds the body from disk inside ONE
21
+ // MIGRATION transaction (one source per type, never a CRDT merge of two
22
+ // histories); winner `hub` keeps the doc.
23
+ // - comments under a non-empty doc → id-union (plain array rebuild from
24
+ // merged JSON via the delete-then-insert codec — NOT a CRDT merge, so the
25
+ // duplication trap stays closed; same-id entries keep the doc's version).
26
+ //
27
+ // Idempotent: re-running with hub state present and no divergence is a no-op;
28
+ // re-running an adopt rebuilds from the same files → byte-identical.
29
+ // The companion guard is the room's seed being disabled for pinned
30
+ // (provider-attached) slugs under sharedDoc (see collab/index.ts `shouldSeed`).
31
+
32
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs';
33
+ import path from 'node:path';
34
+
35
+ import type * as Y from 'yjs';
36
+
37
+ import { Y_TYPES } from '../collab/persistence.ts';
38
+ import {
39
+ applyAnnotationsToDoc,
40
+ applyCommentsToDoc,
41
+ applyCssToDoc,
42
+ applyHtmlToDoc,
43
+ applyMetaToDoc,
44
+ bodyEditAtFromDoc,
45
+ stampBodyEdit,
46
+ Y_SYNC_TYPES,
47
+ } from './codec.ts';
48
+ import { decideColdStart, unionCommentsById } from './cold-start.ts';
49
+ import { hashBytes } from './echo-guard.ts';
50
+ import type { SyncJournal } from './journal.ts';
51
+ import { ORIGINS } from './origins.ts';
52
+
53
+ export interface MigrateSeedPaths {
54
+ html: string;
55
+ comments: string;
56
+ annotations: string;
57
+ meta?: string;
58
+ css?: string;
59
+ }
60
+
61
+ export interface MigrateSeedOptions {
62
+ slug: string;
63
+ doc: Y.Doc;
64
+ paths: MigrateSeedPaths;
65
+ /** `_history/<slug>/` (or any dir) — when set, local files are snapshotted
66
+ * here (the legacy `pre-shared-doc-migration/` whole-set copy) before any
67
+ * doc-keeps-state path can overwrite them via the projection. Best-effort. */
68
+ historyDir?: string;
69
+ /** DDR-102 — per-machine journal; gates fast-forward vs conflict. */
70
+ journal?: SyncJournal;
71
+ /** DDR-102 — body snapshot writer (history.ts), same contract as the agent's. */
72
+ snapshot?: (content: string, reason: 'pre-sync-local' | 'pre-sync-hub') => Promise<string | null>;
73
+ /** DDR-102 — divergence notification, same contract as the agent's. */
74
+ onConflict?: (info: {
75
+ slug: string;
76
+ kind: 'cold-start-diverged';
77
+ winner?: 'local' | 'hub';
78
+ snapshots?: { local?: string; hub?: string };
79
+ /** DDR-102 fail-closed (F1) — local snapshot didn't land; hub-wins refused. */
80
+ snapshotFailed?: boolean;
81
+ }) => void;
82
+ }
83
+
84
+ export type MigrateSeedResult =
85
+ | 'hub-wins'
86
+ | 'local-adopt'
87
+ | 'empty'
88
+ /** DDR-102 — doc held state but no body; local body seeded up in-place. */
89
+ | 'body-seed-up'
90
+ /** DDR-102 — divergence resolved newest-wins. */
91
+ | 'conflict-local-wins'
92
+ | 'conflict-hub-wins';
93
+
94
+ /** True when the shared doc holds no synced content for any of the five types. */
95
+ export function docIsEmpty(doc: Y.Doc): boolean {
96
+ if (doc.getText(Y_SYNC_TYPES.html).length > 0) return false;
97
+ if (doc.getText(Y_SYNC_TYPES.css).length > 0) return false;
98
+ if (doc.getText(Y_SYNC_TYPES.meta).length > 0) return false;
99
+ if (doc.getArray(Y_TYPES.comments).length > 0) return false;
100
+ const svg = doc.getMap<unknown>(Y_TYPES.annotations).get('svg');
101
+ if (typeof svg === 'string' && svg.length > 0) return false;
102
+ return true;
103
+ }
104
+
105
+ /**
106
+ * Run the one-time authoritative seed. Returns which source won so the caller
107
+ * can log / surface a conflict. Safe to call on every boot (idempotent).
108
+ */
109
+ export async function migrateSeed(opts: MigrateSeedOptions): Promise<MigrateSeedResult> {
110
+ const { slug, doc, paths } = opts;
111
+
112
+ const localHtml = readLocal(paths.html);
113
+ const localComments = readLocal(paths.comments);
114
+ const localAnnotations = readLocal(paths.annotations);
115
+ const localMeta = paths.meta ? readLocal(paths.meta) : null;
116
+ const localCss = paths.css ? readLocal(paths.css) : null;
117
+
118
+ // Hub was empty → adopt local. Build the doc from the local files ONCE, inside
119
+ // a single MIGRATION transaction. The apply* codecs delete-then-insert, so
120
+ // this is a clear+rebuild (re-running is a no-op once content matches).
121
+ if (docIsEmpty(doc)) {
122
+ const hasLocal =
123
+ !!localHtml || !!localComments || !!localAnnotations || !!localMeta || !!localCss;
124
+ if (!hasLocal) return 'empty';
125
+
126
+ doc.transact(() => {
127
+ if (localHtml) {
128
+ if (applyHtmlToDoc(doc, localHtml, ORIGINS.MIGRATION)) {
129
+ stampBodyEdit(doc, ORIGINS.MIGRATION);
130
+ }
131
+ }
132
+ if (localComments) {
133
+ const parsed = tryParseJsonArray(localComments);
134
+ if (parsed) applyCommentsToDoc(doc, parsed, ORIGINS.MIGRATION);
135
+ }
136
+ if (localAnnotations) applyAnnotationsToDoc(doc, localAnnotations, ORIGINS.MIGRATION);
137
+ if (paths.meta && localMeta) applyMetaToDoc(doc, localMeta, ORIGINS.MIGRATION);
138
+ if (paths.css && localCss) applyCssToDoc(doc, localCss, ORIGINS.MIGRATION);
139
+ }, ORIGINS.MIGRATION);
140
+
141
+ // DDR-102 — the adopt is a disk→doc traversal: checkpoint it.
142
+ if (localHtml) {
143
+ opts.journal?.record(slug, {
144
+ bodyHash: hashBytes(localHtml),
145
+ ...(localCss ? { cssHash: hashBytes(localCss) } : {}),
146
+ });
147
+ }
148
+ return 'local-adopt';
149
+ }
150
+
151
+ // Hub had state. Legacy whole-set safety copy (fixed dir, overwritten per
152
+ // boot — not spam) so non-body files keep their pre-cutover backup too.
153
+ snapshotLocal(opts);
154
+
155
+ // Body resolution via the DDR-102 decision table.
156
+ const docHtml = doc.getText(Y_SYNC_TYPES.html).toString();
157
+ const decision = decideColdStart({
158
+ localBody: localHtml,
159
+ docBody: docHtml,
160
+ journalHash: opts.journal?.get(slug)?.bodyHash ?? null,
161
+ localMtimeMs: localMtimeMs(paths.html),
162
+ docBodyEditAtMs: bodyEditAtFromDoc(doc),
163
+ });
164
+
165
+ let result: MigrateSeedResult = 'hub-wins';
166
+
167
+ /** Rebuild body (+ visually-coupled css/annotations) from local, in ONE
168
+ * MIGRATION transaction — one source per type, never a two-history merge. */
169
+ const rebuildBodyFromLocal = (): void => {
170
+ doc.transact(() => {
171
+ if (applyHtmlToDoc(doc, localHtml as string, ORIGINS.MIGRATION)) {
172
+ stampBodyEdit(doc, ORIGINS.MIGRATION);
173
+ }
174
+ if (paths.css && localCss !== null) applyCssToDoc(doc, localCss, ORIGINS.MIGRATION);
175
+ if (localAnnotations !== null) {
176
+ applyAnnotationsToDoc(doc, localAnnotations, ORIGINS.MIGRATION);
177
+ }
178
+ }, ORIGINS.MIGRATION);
179
+ opts.journal?.record(slug, {
180
+ bodyHash: hashBytes(localHtml as string),
181
+ ...(localCss !== null ? { cssHash: hashBytes(localCss) } : {}),
182
+ });
183
+ };
184
+
185
+ switch (decision.action) {
186
+ case 'noop':
187
+ // Identical (or both empty) — checkpoint identity so the next boot
188
+ // fast-forwards even if the hub then moves ahead.
189
+ if (localHtml !== null && localHtml === docHtml && docHtml !== '') {
190
+ opts.journal?.record(slug, { bodyHash: hashBytes(docHtml) });
191
+ }
192
+ break;
193
+ case 'materialize-hub':
194
+ case 'fast-forward-hub':
195
+ // Keep the doc; the projection materializes it to disk (and records the
196
+ // journal checkpoint on its write).
197
+ break;
198
+ case 'seed-local-up':
199
+ // Doc holds state for OTHER types but no body — seed the local body up.
200
+ rebuildBodyFromLocal();
201
+ result = 'body-seed-up';
202
+ break;
203
+ case 'conflict': {
204
+ const snapshots: { local?: string; hub?: string } = {};
205
+ let snapshotAttempted = false;
206
+ if (opts.snapshot) {
207
+ snapshotAttempted = true;
208
+ try {
209
+ const localTs = await opts.snapshot(localHtml as string, 'pre-sync-local');
210
+ if (localTs) snapshots.local = localTs;
211
+ const hubTs = await opts.snapshot(docHtml, 'pre-sync-hub');
212
+ if (hubTs) snapshots.hub = hubTs;
213
+ } catch {
214
+ /* swallowed below — the missing snapshot ref drives the fail-closed guard */
215
+ }
216
+ }
217
+ // DDR-102 fail-closed (security F1): a hub-wins resolution keeps the doc
218
+ // (hub) state, which projection.reconcile() then materializes to disk —
219
+ // OVERWRITING local. If the local snapshot didn't land, rebuild the body
220
+ // from local instead so the projection writes local back (a no-op on
221
+ // disk) and nothing is lost. Gated on `snapshotAttempted` so a
222
+ // snapshot-less standalone call keeps plain newest-wins.
223
+ const localSnapshotMissing = snapshotAttempted && !snapshots.local;
224
+ let winner = decision.winner;
225
+ if (winner === 'hub' && localSnapshotMissing) {
226
+ winner = 'local';
227
+ console.error(
228
+ `[sync/${slug}] shared-doc cold-start divergence: hub won newest-wins but the local snapshot FAILED — REFUSING to overwrite local (DDR-102 fail-closed). Rebuilding the doc from local; resolve the _history/ write failure to restore newest-wins.`
229
+ );
230
+ }
231
+ if (winner === 'local') {
232
+ rebuildBodyFromLocal();
233
+ result = 'conflict-local-wins';
234
+ } else {
235
+ result = 'conflict-hub-wins';
236
+ }
237
+ console.warn(`[sync/${slug}] shared-doc cold-start divergence — ${decision.reason}`);
238
+ opts.onConflict?.({
239
+ slug,
240
+ kind: 'cold-start-diverged',
241
+ winner,
242
+ ...(snapshots.local || snapshots.hub ? { snapshots } : {}),
243
+ ...(localSnapshotMissing ? { snapshotFailed: true } : {}),
244
+ });
245
+ break;
246
+ }
247
+ }
248
+
249
+ // Comments id-union (DDR-102): rebuild from merged JSON via the
250
+ // delete-then-insert codec — same-id entries keep the doc's version, so the
251
+ // duplication trap stays closed; local-only comments survive.
252
+ if (localComments) {
253
+ const parsed = tryParseJsonArray(localComments);
254
+ if (parsed && parsed.length > 0) {
255
+ const docList = doc.getArray(Y_TYPES.comments).toArray();
256
+ const merged = unionCommentsById(docList, parsed);
257
+ if (merged.length !== docList.length) {
258
+ doc.transact(() => {
259
+ applyCommentsToDoc(doc, merged, ORIGINS.MIGRATION);
260
+ }, ORIGINS.MIGRATION);
261
+ }
262
+ }
263
+ }
264
+
265
+ return result;
266
+ }
267
+
268
+ /* ---------------------------------------------------------------- helpers */
269
+
270
+ function snapshotLocal(opts: MigrateSeedOptions): void {
271
+ if (!opts.historyDir) return;
272
+ try {
273
+ const dir = path.join(opts.historyDir, 'pre-shared-doc-migration');
274
+ mkdirSync(dir, { recursive: true });
275
+ for (const p of [
276
+ opts.paths.html,
277
+ opts.paths.comments,
278
+ opts.paths.annotations,
279
+ opts.paths.meta,
280
+ opts.paths.css,
281
+ ]) {
282
+ if (p && existsSync(p)) {
283
+ copyFileSync(p, path.join(dir, path.basename(p)));
284
+ }
285
+ }
286
+ } catch {
287
+ /* best-effort — a snapshot failure must not block the cutover */
288
+ }
289
+ }
290
+
291
+ function readLocal(p: string): string | null {
292
+ if (!existsSync(p)) return null;
293
+ try {
294
+ return readFileSync(p, 'utf8');
295
+ } catch {
296
+ return null;
297
+ }
298
+ }
299
+
300
+ function localMtimeMs(p: string): number | null {
301
+ try {
302
+ return statSync(p).mtimeMs;
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+
308
+ // Proto-pollution-safe (DDR-054 §2g), mirroring the agent + projection comments
309
+ // parse — a planted local `_comments/<slug>.json` must not seed dangerous keys
310
+ // during adopt.
311
+ function tryParseJsonArray(s: string): unknown[] | null {
312
+ try {
313
+ const parsed = JSON.parse(s, (key, value) => {
314
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') return undefined;
315
+ return value;
316
+ });
317
+ return Array.isArray(parsed) ? parsed : null;
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
@@ -42,8 +42,10 @@ import {
42
42
  MAX_META_BYTES,
43
43
  mergeSharedMetaIntoLocal,
44
44
  metaFromDoc,
45
+ stampBodyEdit,
45
46
  } from './codec.ts';
46
47
  import { type EchoGuard, hashBytes } from './echo-guard.ts';
48
+ import type { SyncJournal } from './journal.ts';
47
49
  import { ORIGINS } from './origins.ts';
48
50
 
49
51
  export const PROJECT_FLUSH_MS = 800;
@@ -78,6 +80,9 @@ export interface DocProjectionOptions {
78
80
  flushMs?: number;
79
81
  /** Circuit-breaker threshold (consecutive parse failures per path). */
80
82
  maxStrikes?: number;
83
+ /** DDR-102 — per-machine sync journal; every successful disk↔doc body/css
84
+ * traversal checkpoints here (same discipline as the agent). Optional. */
85
+ journal?: SyncJournal;
81
86
  }
82
87
 
83
88
  export interface DocProjection {
@@ -177,6 +182,7 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
177
182
  recordEcho(paths.html, next);
178
183
  writer(paths.html, next);
179
184
  lastHtml = next;
185
+ opts.journal?.record(slug, { bodyHash: hashBytes(next) }); // DDR-102 checkpoint
180
186
  }
181
187
 
182
188
  function writeCssIfChanged(): void {
@@ -188,6 +194,7 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
188
194
  if (!withinCap(paths.css, next, MAX_CSS_BYTES)) return;
189
195
  recordEcho(paths.css, next);
190
196
  writer(paths.css, next);
197
+ opts.journal?.record(slug, { cssHash: hashBytes(next) }); // DDR-102 checkpoint
191
198
  }
192
199
 
193
200
  function writeMetaIfChanged(): void {
@@ -257,14 +264,26 @@ export function createDocProjection(opts: DocProjectionOptions): DocProjection {
257
264
 
258
265
  if (evt.path === paths.html) {
259
266
  clearStrike(evt.path);
260
- const changed = applyHtmlToDoc(doc, str, importOrigin);
261
- if (changed) lastHtml = htmlFromDoc(doc);
267
+ // Body import + syncMeta stamp in ONE transaction (same FILE_IMPORT
268
+ // origin) peers get a single update carrying the newest-wins stamp.
269
+ let changed = false;
270
+ doc.transact(() => {
271
+ changed = applyHtmlToDoc(doc, str, importOrigin);
272
+ if (changed) stampBodyEdit(doc, importOrigin);
273
+ }, importOrigin);
274
+ if (changed) {
275
+ lastHtml = htmlFromDoc(doc);
276
+ opts.journal?.record(slug, { bodyHash: evt.hash }); // DDR-102 checkpoint
277
+ }
262
278
  return changed;
263
279
  }
264
280
  if (paths.css && evt.path === paths.css) {
265
281
  clearStrike(evt.path);
266
282
  const changed = applyCssToDoc(doc, str, importOrigin);
267
- if (changed) lastCss = str;
283
+ if (changed) {
284
+ lastCss = str;
285
+ opts.journal?.record(slug, { cssHash: evt.hash }); // DDR-102 checkpoint
286
+ }
268
287
  return changed;
269
288
  }
270
289
  if (paths.meta && evt.path === paths.meta) {
@@ -14,12 +14,23 @@
14
14
 
15
15
  import type { SyncStatusSnapshot } from './connection-state.ts';
16
16
 
17
- export type ConflictKind = 'cold-start-hub-wins' | 'git-pull';
17
+ // `cold-start-hub-wins` stays in the union for OLD payload readers (additive
18
+ // evolution — the NoSyncablePayload discriminator pattern); new conflicts are
19
+ // recorded as `cold-start-diverged` with the DDR-102 winner + snapshot refs.
20
+ export type ConflictKind = 'cold-start-hub-wins' | 'cold-start-diverged' | 'git-pull';
18
21
 
19
22
  export interface SyncConflict {
20
23
  slug: string;
21
24
  kind: ConflictKind;
22
25
  at: number;
26
+ /** DDR-102 — which side the newest-wins resolution kept. */
27
+ winner?: 'local' | 'hub';
28
+ /** DDR-102 — ISO timestamps of the `_history/<slug>/` snapshots taken before
29
+ * resolution (`/design:rollback` recovery). */
30
+ snapshots?: { local?: string; hub?: string };
31
+ /** DDR-102 fail-closed (F1) — the local snapshot didn't land, so a hub-wins
32
+ * overwrite was refused (local kept). Surfaces the degraded `_history/` write. */
33
+ snapshotFailed?: boolean;
23
34
  }
24
35
 
25
36
  export interface SyncStatusPayload extends SyncStatusSnapshot {
@@ -55,7 +66,7 @@ export interface SyncStatusStore {
55
66
  /** Merge a fresh ConnectionMonitor snapshot + persist + broadcast. */
56
67
  update(snapshot: SyncStatusSnapshot): void;
57
68
  /** Record a conflict notification + persist + broadcast. */
58
- addConflict(conflict: { slug: string; kind: ConflictKind }): void;
69
+ addConflict(conflict: Omit<SyncConflict, 'at'>): void;
59
70
  /** Current payload (defensive copy). */
60
71
  get(): SyncStatusPayload;
61
72
  }
@@ -105,8 +116,8 @@ export function createSyncStatusStore(opts: SyncStatusStoreOptions): SyncStatusS
105
116
  snapshot = next;
106
117
  flush();
107
118
  },
108
- addConflict({ slug, kind }) {
109
- conflicts.push({ slug, kind, at: now() });
119
+ addConflict(conflict) {
120
+ conflicts.push({ ...conflict, at: now() });
110
121
  if (conflicts.length > maxConflicts) conflicts.splice(0, conflicts.length - maxConflicts);
111
122
  flush();
112
123
  },
@@ -2,13 +2,7 @@
2
2
 
3
3
  import { describe, expect, test } from 'bun:test';
4
4
 
5
- import {
6
- ACTIVITY_IDLE_MS,
7
- type ActivityChange,
8
- createActivity,
9
- diffArtboardIds,
10
- isCanvasFile,
11
- } from '../activity.ts';
5
+ import { type ActivityChange, createActivity, diffArtboardIds, isCanvasFile } from '../activity.ts';
12
6
  import { type Context, createBus } from '../context.ts';
13
7
 
14
8
  function mkCtx(): Context {