@1agh/maude 1.0.11 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (567) hide show
  1. package/README.md +3 -1
  2. package/apps/studio/acp/bridge.ts +18 -13
  3. package/apps/studio/acp/index.ts +43 -7
  4. package/apps/studio/acp/transcript-io.ts +394 -0
  5. package/apps/studio/acp/transcript.ts +103 -38
  6. package/apps/studio/annotations-layer.tsx +191 -116
  7. package/apps/studio/annotations-model.ts +39 -0
  8. package/apps/studio/annotations-sync.ts +50 -0
  9. package/apps/studio/api.ts +821 -55
  10. package/apps/studio/bin/_import-figma.mjs +57 -11
  11. package/apps/studio/bin/_seed-fixture.mjs +163 -0
  12. package/apps/studio/bin/annotate.mjs +3 -1
  13. package/apps/studio/bin/server-up.sh +17 -2
  14. package/apps/studio/canvas-edit.ts +391 -7
  15. package/apps/studio/canvas-imports.ts +89 -0
  16. package/apps/studio/canvas-lib.tsx +82 -18
  17. package/apps/studio/canvas-notice-message.ts +16 -0
  18. package/apps/studio/canvas-notifications.tsx +19 -0
  19. package/apps/studio/canvas-shell.tsx +57 -6
  20. package/apps/studio/client/app.jsx +943 -248
  21. package/apps/studio/client/apply-edit-request.ts +55 -0
  22. package/apps/studio/client/export-center.jsx +125 -67
  23. package/apps/studio/client/github.js +38 -1
  24. package/apps/studio/client/index-loader.ts +76 -0
  25. package/apps/studio/client/panels/CloudBar.jsx +52 -6
  26. package/apps/studio/client/panels/GitPanel.jsx +103 -0
  27. package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
  28. package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
  29. package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
  30. package/apps/studio/client/panels/SyncPanel.jsx +238 -4
  31. package/apps/studio/client/panels/TeamProjects.jsx +376 -0
  32. package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
  33. package/apps/studio/client/photo-knobs.jsx +3 -0
  34. package/apps/studio/client/report-bug.jsx +19 -1
  35. package/apps/studio/client/share-dialog.jsx +91 -0
  36. package/apps/studio/client/share-link.js +94 -0
  37. package/apps/studio/client/styles/3-shell-maude.css +69 -18
  38. package/apps/studio/client/styles/4-components.css +44 -1
  39. package/apps/studio/client/tree-row-menu.jsx +48 -3
  40. package/apps/studio/client/whats-new.jsx +26 -32
  41. package/apps/studio/cloud/endpoints.ts +132 -0
  42. package/apps/studio/collab/index.ts +4 -0
  43. package/apps/studio/collab/persistence.ts +137 -17
  44. package/apps/studio/collab/registry.ts +19 -21
  45. package/apps/studio/collab/room.ts +13 -2
  46. package/apps/studio/context.ts +59 -0
  47. package/apps/studio/debug-bundle.ts +13 -0
  48. package/apps/studio/dist/client.bundle.js +1911 -1879
  49. package/apps/studio/dist/runtime/.min-sizes.json +1 -0
  50. package/apps/studio/dist/runtime/sonner.js +1 -0
  51. package/apps/studio/dist/styles.css +1 -1
  52. package/apps/studio/export-dialog.tsx +54 -0
  53. package/apps/studio/exporters/degraded.ts +19 -3
  54. package/apps/studio/exporters/ghostscript.ts +194 -0
  55. package/apps/studio/exporters/pdf-fonts.ts +261 -0
  56. package/apps/studio/exporters/pdf.ts +152 -3
  57. package/apps/studio/exporters/remote.ts +36 -1
  58. package/apps/studio/fs-watch.ts +45 -1
  59. package/apps/studio/http.ts +296 -19
  60. package/apps/studio/inspect.ts +19 -0
  61. package/apps/studio/managed-projects.ts +150 -0
  62. package/apps/studio/mem.ts +45 -9
  63. package/apps/studio/notifications.tsx +292 -0
  64. package/apps/studio/runtime-bundle.ts +2 -0
  65. package/apps/studio/server.ts +69 -6
  66. package/apps/studio/sync/accepted-cold-start.ts +225 -0
  67. package/apps/studio/sync/accepted-link.ts +320 -0
  68. package/apps/studio/sync/action-stage.ts +343 -0
  69. package/apps/studio/sync/agent.ts +104 -45
  70. package/apps/studio/sync/asset-push.ts +1 -47
  71. package/apps/studio/sync/cell-file-events.ts +3 -0
  72. package/apps/studio/sync/codec.ts +192 -34
  73. package/apps/studio/sync/cold-start-apply.ts +7 -0
  74. package/apps/studio/sync/cold-start.ts +118 -22
  75. package/apps/studio/sync/comment-identity.ts +68 -0
  76. package/apps/studio/sync/connection-state.ts +15 -1
  77. package/apps/studio/sync/ctl-provider.ts +15 -2
  78. package/apps/studio/sync/document-discovery.ts +81 -0
  79. package/apps/studio/sync/file-ledger.ts +117 -0
  80. package/apps/studio/sync/file-membership.ts +10 -2
  81. package/apps/studio/sync/file-plane.ts +926 -47
  82. package/apps/studio/sync/index.ts +1786 -36
  83. package/apps/studio/sync/journal-client.ts +13 -1
  84. package/apps/studio/sync/migrate-seed.ts +150 -22
  85. package/apps/studio/sync/poke.ts +4 -2
  86. package/apps/studio/sync/presentation.ts +311 -5
  87. package/apps/studio/sync/projection.ts +820 -23
  88. package/apps/studio/sync/repeated-module.ts +73 -0
  89. package/apps/studio/sync/retry-after.ts +169 -0
  90. package/apps/studio/sync/revision-barrier.ts +129 -0
  91. package/apps/studio/sync/seed-progress.ts +218 -0
  92. package/apps/studio/sync/seed-repair.ts +46 -0
  93. package/apps/studio/sync/source-merge.ts +100 -0
  94. package/apps/studio/sync/source-ops.ts +289 -0
  95. package/apps/studio/sync/source-recovery.ts +70 -0
  96. package/apps/studio/sync/source-validation.ts +56 -0
  97. package/apps/studio/sync/status.ts +201 -6
  98. package/apps/studio/sync/transaction-client.ts +460 -0
  99. package/apps/studio/sync/transport-error.ts +111 -0
  100. package/apps/studio/sync/writer-registry.ts +236 -0
  101. package/apps/studio/text-caret.ts +35 -0
  102. package/apps/studio/undo-hud.tsx +9 -87
  103. package/apps/studio/use-canvas-media-drop.tsx +4 -39
  104. package/apps/studio/whats-new.json +173 -0
  105. package/cli/bin/maude.mjs +2 -0
  106. package/cli/commands/codex.mjs +191 -0
  107. package/cli/commands/harness.mjs +1051 -0
  108. package/cli/commands/help.mjs +13 -0
  109. package/cli/lib/design-link.mjs +190 -2
  110. package/cli/lib/harness/capabilities.mjs +302 -0
  111. package/cli/lib/harness/codex-runtime.mjs +892 -0
  112. package/cli/lib/harness/compatibility.mjs +27 -0
  113. package/cli/lib/harness/discover-claude.mjs +1393 -0
  114. package/cli/lib/harness/managed-state.mjs +1296 -0
  115. package/cli/lib/harness/model.mjs +86 -0
  116. package/cli/lib/harness/secrets.mjs +390 -0
  117. package/cli/lib/harness/targets/codex.mjs +1141 -0
  118. package/cli/lib/harness/targets/opencode.mjs +807 -0
  119. package/cli/lib/harness/transaction.mjs +1099 -0
  120. package/cli/lib/hubs-config.mjs +35 -8
  121. package/cli/templates/harness/opencode/maude-projector.ts +58 -0
  122. package/docs/harness-capability-matrix.md +125 -0
  123. package/docs/harness-environment-projection.md +95 -0
  124. package/package.json +33 -9
  125. package/plugins/design/dependencies.json +3 -3
  126. package/plugins/design/templates/_shell.html +47 -0
  127. package/plugins/flow/.claude-plugin/config.schema.json +8 -0
  128. package/plugins/flow/dependencies.json +3 -3
  129. package/apps/studio/.ai/cache/_stats.json +0 -7
  130. package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
  131. package/apps/studio/bin/_curl-local.test.mjs +0 -280
  132. package/apps/studio/bin/_pw-launch.test.mjs +0 -90
  133. package/apps/studio/bin/_smart-frames.test.mjs +0 -195
  134. package/apps/studio/bin/_transcribe.test.mjs +0 -80
  135. package/apps/studio/bun.lock +0 -928
  136. package/apps/studio/draw/test/animate.test.ts +0 -172
  137. package/apps/studio/draw/test/brush.test.ts +0 -213
  138. package/apps/studio/draw/test/composition.test.ts +0 -141
  139. package/apps/studio/draw/test/geometry.test.ts +0 -199
  140. package/apps/studio/draw/test/gradient.test.ts +0 -167
  141. package/apps/studio/draw/test/layout.test.ts +0 -120
  142. package/apps/studio/draw/test/morph.test.ts +0 -107
  143. package/apps/studio/draw/test/optimize.test.ts +0 -40
  144. package/apps/studio/draw/test/palette.test.ts +0 -123
  145. package/apps/studio/draw/test/primitives.test.ts +0 -129
  146. package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
  147. package/apps/studio/draw/test/serialize.test.ts +0 -105
  148. package/apps/studio/figma/assets.test.ts +0 -464
  149. package/apps/studio/figma/client.test.ts +0 -395
  150. package/apps/studio/figma/codegen-client.test.ts +0 -276
  151. package/apps/studio/figma/codegen-fonts.test.ts +0 -103
  152. package/apps/studio/figma/codegen-values.test.ts +0 -179
  153. package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
  154. package/apps/studio/figma/fig-decode.test.ts +0 -788
  155. package/apps/studio/figma/fig-differential.test.ts +0 -182
  156. package/apps/studio/figma/fig-translator.test.ts +0 -192
  157. package/apps/studio/figma/fig-vector.test.ts +0 -113
  158. package/apps/studio/figma/from-codegen.test.ts +0 -408
  159. package/apps/studio/figma/sanitize.test.ts +0 -325
  160. package/apps/studio/figma/tailwind-map.test.ts +0 -142
  161. package/apps/studio/figma/to-artboard.test.ts +0 -808
  162. package/apps/studio/figma/to-render.test.ts +0 -180
  163. package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
  164. package/apps/studio/figma/to-strokes.test.ts +0 -705
  165. package/apps/studio/figma/to-tokens.test.ts +0 -321
  166. package/apps/studio/figma/url.test.ts +0 -167
  167. package/apps/studio/footage/schema.test.ts +0 -287
  168. package/apps/studio/generation/adapters/groq.test.ts +0 -80
  169. package/apps/studio/generation/audio-library.test.ts +0 -95
  170. package/apps/studio/generation/captions.test.ts +0 -78
  171. package/apps/studio/generation/elevenlabs.test.ts +0 -223
  172. package/apps/studio/generation/gemini.test.ts +0 -350
  173. package/apps/studio/generation/jobs.test.ts +0 -128
  174. package/apps/studio/generation/keys.test.ts +0 -75
  175. package/apps/studio/generation/prefs.test.ts +0 -95
  176. package/apps/studio/generation/types.test.ts +0 -123
  177. package/apps/studio/generation/whisper-models.test.ts +0 -179
  178. package/apps/studio/test/_helpers.ts +0 -108
  179. package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
  180. package/apps/studio/test/acp-activity.test.ts +0 -154
  181. package/apps/studio/test/acp-ai-activity.test.ts +0 -182
  182. package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
  183. package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
  184. package/apps/studio/test/acp-branch-guard.test.ts +0 -123
  185. package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
  186. package/apps/studio/test/acp-bridge.test.ts +0 -545
  187. package/apps/studio/test/acp-capabilities.test.ts +0 -123
  188. package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
  189. package/apps/studio/test/acp-commands.test.ts +0 -119
  190. package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
  191. package/apps/studio/test/acp-elicitation.test.ts +0 -251
  192. package/apps/studio/test/acp-env.test.ts +0 -95
  193. package/apps/studio/test/acp-mode-banner.test.ts +0 -45
  194. package/apps/studio/test/acp-origin-gate.test.ts +0 -160
  195. package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
  196. package/apps/studio/test/acp-permission.test.ts +0 -278
  197. package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
  198. package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
  199. package/apps/studio/test/acp-session-plugins.test.ts +0 -209
  200. package/apps/studio/test/acp-toolgroup.test.ts +0 -76
  201. package/apps/studio/test/acp-transcript-view.test.ts +0 -71
  202. package/apps/studio/test/acp-transcript.test.ts +0 -238
  203. package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
  204. package/apps/studio/test/acp-usage.test.ts +0 -143
  205. package/apps/studio/test/acp-write-gate.test.ts +0 -323
  206. package/apps/studio/test/acp-write-scope.test.ts +0 -533
  207. package/apps/studio/test/active-state.test.ts +0 -186
  208. package/apps/studio/test/activity.test.ts +0 -189
  209. package/apps/studio/test/ai-activity.test.ts +0 -113
  210. package/apps/studio/test/annotate-write.test.ts +0 -1074
  211. package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
  212. package/apps/studio/test/annotations-align.test.ts +0 -88
  213. package/apps/studio/test/annotations-api.test.ts +0 -145
  214. package/apps/studio/test/annotations-bindings.test.ts +0 -262
  215. package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
  216. package/apps/studio/test/annotations-groups.test.ts +0 -231
  217. package/apps/studio/test/annotations-layer.test.ts +0 -1011
  218. package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
  219. package/apps/studio/test/annotations-snap.test.ts +0 -135
  220. package/apps/studio/test/annotations-text-format.test.ts +0 -396
  221. package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
  222. package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
  223. package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
  224. package/apps/studio/test/artboard-kinds.test.tsx +0 -83
  225. package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
  226. package/apps/studio/test/asset-api.test.ts +0 -149
  227. package/apps/studio/test/assets-s3.test.ts +0 -249
  228. package/apps/studio/test/binary-smoke.test.ts +0 -47
  229. package/apps/studio/test/boot-self-heal.test.ts +0 -87
  230. package/apps/studio/test/browse-posture.test.tsx +0 -107
  231. package/apps/studio/test/bundle-smoke.test.ts +0 -46
  232. package/apps/studio/test/camera-reveal.test.tsx +0 -173
  233. package/apps/studio/test/canvas-artifacts.test.ts +0 -125
  234. package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
  235. package/apps/studio/test/canvas-build.test.ts +0 -141
  236. package/apps/studio/test/canvas-create-api.test.ts +0 -676
  237. package/apps/studio/test/canvas-cursors.test.ts +0 -164
  238. package/apps/studio/test/canvas-edit.test.ts +0 -470
  239. package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
  240. package/apps/studio/test/canvas-header.test.ts +0 -127
  241. package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
  242. package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
  243. package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
  244. package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
  245. package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
  246. package/apps/studio/test/canvas-list-watch.test.ts +0 -388
  247. package/apps/studio/test/canvas-media-drop.test.ts +0 -187
  248. package/apps/studio/test/canvas-meta-api.test.ts +0 -707
  249. package/apps/studio/test/canvas-move-api.test.ts +0 -587
  250. package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
  251. package/apps/studio/test/canvas-pipeline.test.ts +0 -180
  252. package/apps/studio/test/canvas-rects.test.ts +0 -198
  253. package/apps/studio/test/canvas-reorder.test.ts +0 -211
  254. package/apps/studio/test/canvas-route.test.ts +0 -179
  255. package/apps/studio/test/canvas-shell-base.test.ts +0 -107
  256. package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
  257. package/apps/studio/test/canvas-url.test.ts +0 -138
  258. package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
  259. package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
  260. package/apps/studio/test/capture-core.test.ts +0 -32
  261. package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
  262. package/apps/studio/test/chat-attachments.test.ts +0 -117
  263. package/apps/studio/test/chat-context.test.ts +0 -129
  264. package/apps/studio/test/chat-markdown.test.tsx +0 -58
  265. package/apps/studio/test/clip-addressing.test.ts +0 -738
  266. package/apps/studio/test/clip-ops.test.ts +0 -922
  267. package/apps/studio/test/cloud-build.test.ts +0 -95
  268. package/apps/studio/test/cloud-connect-note.test.ts +0 -135
  269. package/apps/studio/test/cloud-endpoints.test.ts +0 -590
  270. package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
  271. package/apps/studio/test/cloud-history-posture.test.ts +0 -230
  272. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
  273. package/apps/studio/test/cloud-renew.test.ts +0 -205
  274. package/apps/studio/test/cloud-session-role.test.ts +0 -161
  275. package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
  276. package/apps/studio/test/cold-start-apply.test.ts +0 -303
  277. package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
  278. package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
  279. package/apps/studio/test/collab-bridge.test.ts +0 -79
  280. package/apps/studio/test/collab-loopback.test.ts +0 -63
  281. package/apps/studio/test/collab-origin-gate.test.ts +0 -323
  282. package/apps/studio/test/collab-protocol.test.ts +0 -146
  283. package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
  284. package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
  285. package/apps/studio/test/collab-room.test.ts +0 -188
  286. package/apps/studio/test/collab-session-survival.test.tsx +0 -176
  287. package/apps/studio/test/collab-stress.test.ts +0 -129
  288. package/apps/studio/test/comment-mount.test.ts +0 -97
  289. package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
  290. package/apps/studio/test/comments-api.test.ts +0 -277
  291. package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
  292. package/apps/studio/test/comments-overlay.test.ts +0 -117
  293. package/apps/studio/test/compile-entry.test.ts +0 -133
  294. package/apps/studio/test/component-map.test.ts +0 -48
  295. package/apps/studio/test/config-projection.test.ts +0 -104
  296. package/apps/studio/test/config-reload.test.ts +0 -230
  297. package/apps/studio/test/config-version.test.ts +0 -88
  298. package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
  299. package/apps/studio/test/convert-to-absolute.test.ts +0 -333
  300. package/apps/studio/test/credential-grammar.test.ts +0 -107
  301. package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
  302. package/apps/studio/test/csrf-write-guard.test.ts +0 -141
  303. package/apps/studio/test/debug-bundle.test.ts +0 -105
  304. package/apps/studio/test/design-setup-readiness.test.ts +0 -155
  305. package/apps/studio/test/detach-component.test.ts +0 -94
  306. package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
  307. package/apps/studio/test/dom-selection.test.ts +0 -198
  308. package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
  309. package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
  310. package/apps/studio/test/edit-persistence.test.ts +0 -91
  311. package/apps/studio/test/edit-scope-api.test.ts +0 -119
  312. package/apps/studio/test/edit-source-command.test.ts +0 -142
  313. package/apps/studio/test/edit-suppress.test.ts +0 -170
  314. package/apps/studio/test/editing-presence.test.ts +0 -103
  315. package/apps/studio/test/element-resize.test.ts +0 -136
  316. package/apps/studio/test/element-structural-api.test.ts +0 -434
  317. package/apps/studio/test/element-structural-edit.test.ts +0 -750
  318. package/apps/studio/test/ensure-browser.test.ts +0 -92
  319. package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
  320. package/apps/studio/test/export-assemble.test.ts +0 -53
  321. package/apps/studio/test/export-browser-lane.test.ts +0 -126
  322. package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
  323. package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
  324. package/apps/studio/test/export-center.test.tsx +0 -287
  325. package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
  326. package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
  327. package/apps/studio/test/export-lane.test.ts +0 -245
  328. package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
  329. package/apps/studio/test/exporters/canva.test.ts +0 -63
  330. package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
  331. package/apps/studio/test/exporters/endpoint.test.ts +0 -121
  332. package/apps/studio/test/exporters/history.test.ts +0 -107
  333. package/apps/studio/test/exporters/html.test.ts +0 -26
  334. package/apps/studio/test/exporters/jobs.test.ts +0 -342
  335. package/apps/studio/test/exporters/pdf.test.ts +0 -85
  336. package/apps/studio/test/exporters/png.test.ts +0 -103
  337. package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
  338. package/apps/studio/test/exporters/pptx.test.ts +0 -31
  339. package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
  340. package/apps/studio/test/exporters/runtime.test.ts +0 -59
  341. package/apps/studio/test/exporters/scope.test.ts +0 -292
  342. package/apps/studio/test/exporters/svg.test.ts +0 -29
  343. package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
  344. package/apps/studio/test/exporters/zip.test.ts +0 -104
  345. package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
  346. package/apps/studio/test/figjam-v3-model.test.ts +0 -342
  347. package/apps/studio/test/figma-explode.test.ts +0 -438
  348. package/apps/studio/test/figma-provenance.test.ts +0 -108
  349. package/apps/studio/test/figma-routes.test.ts +0 -294
  350. package/apps/studio/test/file-lock.test.ts +0 -84
  351. package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
  352. package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
  353. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
  354. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
  355. package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
  356. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
  357. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
  358. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
  359. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
  360. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
  361. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
  362. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
  363. package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
  364. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
  365. package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
  366. package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
  367. package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
  368. package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
  369. package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
  370. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  371. package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
  372. package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
  373. package/apps/studio/test/footage-store.test.ts +0 -100
  374. package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
  375. package/apps/studio/test/fs-watch.test.ts +0 -41
  376. package/apps/studio/test/gemma-models.test.ts +0 -245
  377. package/apps/studio/test/generate-route.test.ts +0 -106
  378. package/apps/studio/test/git-api.test.ts +0 -0
  379. package/apps/studio/test/git-branches.test.ts +0 -341
  380. package/apps/studio/test/git-cloud-posture.test.ts +0 -59
  381. package/apps/studio/test/git-grouping.test.ts +0 -106
  382. package/apps/studio/test/git-lifecycle.test.ts +0 -100
  383. package/apps/studio/test/git-log-format.test.ts +0 -95
  384. package/apps/studio/test/git-safe-rel.test.ts +0 -132
  385. package/apps/studio/test/git-system-engine.test.ts +0 -135
  386. package/apps/studio/test/git-watch.test.ts +0 -97
  387. package/apps/studio/test/github-api.test.ts +0 -577
  388. package/apps/studio/test/grid-track-handles.test.ts +0 -160
  389. package/apps/studio/test/handoff-static-frames.test.ts +0 -215
  390. package/apps/studio/test/handoff.test.ts +0 -329
  391. package/apps/studio/test/history-rollback.test.ts +0 -88
  392. package/apps/studio/test/hmr-broadcast.test.ts +0 -236
  393. package/apps/studio/test/hmr-classify.test.ts +0 -87
  394. package/apps/studio/test/hub-link.test.ts +0 -80
  395. package/apps/studio/test/identity-cache.test.ts +0 -83
  396. package/apps/studio/test/import-asset-browser.test.ts +0 -64
  397. package/apps/studio/test/import-asset-route.test.ts +0 -71
  398. package/apps/studio/test/import-asset.test.ts +0 -427
  399. package/apps/studio/test/import-brand-browser.test.ts +0 -88
  400. package/apps/studio/test/import-brand-route.test.ts +0 -120
  401. package/apps/studio/test/import-brand.test.ts +0 -206
  402. package/apps/studio/test/import-figma.test.ts +0 -667
  403. package/apps/studio/test/import-tokens.test.ts +0 -722
  404. package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
  405. package/apps/studio/test/input-router.test.ts +0 -757
  406. package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
  407. package/apps/studio/test/inspect-selections.test.ts +0 -219
  408. package/apps/studio/test/knob-props-authored.test.ts +0 -87
  409. package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
  410. package/apps/studio/test/locator.test.ts +0 -214
  411. package/apps/studio/test/marquee-overlay.test.ts +0 -94
  412. package/apps/studio/test/media-commit-chain.test.ts +0 -210
  413. package/apps/studio/test/mode-toggle.test.tsx +0 -226
  414. package/apps/studio/test/motion-sample.test.ts +0 -35
  415. package/apps/studio/test/move-artboards-command.test.ts +0 -108
  416. package/apps/studio/test/participants-chrome.test.ts +0 -65
  417. package/apps/studio/test/paths.test.ts +0 -88
  418. package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
  419. package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
  420. package/apps/studio/test/perf-harness.ts +0 -193
  421. package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
  422. package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
  423. package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
  424. package/apps/studio/test/photo-edit-api.test.ts +0 -201
  425. package/apps/studio/test/photo-filters.test.ts +0 -149
  426. package/apps/studio/test/photo-pipeline.test.ts +0 -106
  427. package/apps/studio/test/photo-taxonomy.test.ts +0 -96
  428. package/apps/studio/test/print-marks.test.ts +0 -113
  429. package/apps/studio/test/print-units.test.ts +0 -173
  430. package/apps/studio/test/read-annotations.test.ts +0 -727
  431. package/apps/studio/test/read-only-gate.test.ts +0 -188
  432. package/apps/studio/test/read-only-role.test.ts +0 -95
  433. package/apps/studio/test/readiness.test.ts +0 -337
  434. package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
  435. package/apps/studio/test/reorder-api.test.ts +0 -210
  436. package/apps/studio/test/repo-concurrency.test.ts +0 -146
  437. package/apps/studio/test/repo-lock.test.ts +0 -350
  438. package/apps/studio/test/report-proxy.test.ts +0 -125
  439. package/apps/studio/test/ripple.test.ts +0 -197
  440. package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
  441. package/apps/studio/test/runtime-bundle.test.ts +0 -69
  442. package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
  443. package/apps/studio/test/scaffold-design.test.ts +0 -122
  444. package/apps/studio/test/server-lifecycle.test.ts +0 -38
  445. package/apps/studio/test/session-runtime-state.test.ts +0 -226
  446. package/apps/studio/test/session-scope.test.ts +0 -100
  447. package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
  448. package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
  449. package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
  450. package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
  451. package/apps/studio/test/shared-doc-projection.test.ts +0 -238
  452. package/apps/studio/test/shell-importmap.test.ts +0 -56
  453. package/apps/studio/test/sizing-mode.test.ts +0 -102
  454. package/apps/studio/test/slash-commands.test.ts +0 -117
  455. package/apps/studio/test/snap-distance-pill.test.ts +0 -68
  456. package/apps/studio/test/spacing-handles.test.ts +0 -138
  457. package/apps/studio/test/specimen-select.test.ts +0 -88
  458. package/apps/studio/test/sync-agent.test.ts +0 -660
  459. package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
  460. package/apps/studio/test/sync-asset-push.test.ts +0 -931
  461. package/apps/studio/test/sync-atomic-write.test.ts +0 -62
  462. package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
  463. package/apps/studio/test/sync-autocommit.test.ts +0 -537
  464. package/apps/studio/test/sync-canvas-path.test.ts +0 -200
  465. package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
  466. package/apps/studio/test/sync-codec.test.ts +0 -230
  467. package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
  468. package/apps/studio/test/sync-cold-start.test.ts +0 -304
  469. package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
  470. package/apps/studio/test/sync-connection-state.test.ts +0 -337
  471. package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
  472. package/apps/studio/test/sync-decide-file.test.ts +0 -420
  473. package/apps/studio/test/sync-doc-name.test.ts +0 -281
  474. package/apps/studio/test/sync-echo-guard.test.ts +0 -96
  475. package/apps/studio/test/sync-file-ledger.test.ts +0 -334
  476. package/apps/studio/test/sync-file-membership.test.ts +0 -347
  477. package/apps/studio/test/sync-file-plane.test.ts +0 -976
  478. package/apps/studio/test/sync-file-pull.test.ts +0 -333
  479. package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
  480. package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
  481. package/apps/studio/test/sync-hardening.test.ts +0 -586
  482. package/apps/studio/test/sync-hub-listing.test.ts +0 -46
  483. package/apps/studio/test/sync-hubs-config.test.ts +0 -133
  484. package/apps/studio/test/sync-incident-replay.test.ts +0 -211
  485. package/apps/studio/test/sync-journal.test.ts +0 -176
  486. package/apps/studio/test/sync-meta-codec.test.ts +0 -199
  487. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
  488. package/apps/studio/test/sync-move-retirement.test.ts +0 -231
  489. package/apps/studio/test/sync-panel-surface.test.ts +0 -153
  490. package/apps/studio/test/sync-path-pull.test.ts +0 -531
  491. package/apps/studio/test/sync-presentation.test.ts +0 -262
  492. package/apps/studio/test/sync-pull-budget.test.ts +0 -169
  493. package/apps/studio/test/sync-remote-docs.test.ts +0 -333
  494. package/apps/studio/test/sync-resync-routes.test.ts +0 -168
  495. package/apps/studio/test/sync-runtime.test.ts +0 -1675
  496. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
  497. package/apps/studio/test/sync-settings-routes.test.ts +0 -195
  498. package/apps/studio/test/sync-settings.test.ts +0 -151
  499. package/apps/studio/test/sync-status.test.ts +0 -261
  500. package/apps/studio/test/sync-supervisor.test.ts +0 -262
  501. package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
  502. package/apps/studio/test/sync-trash.test.ts +0 -132
  503. package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
  504. package/apps/studio/test/sync-untrusted.test.ts +0 -103
  505. package/apps/studio/test/system-endpoint.test.ts +0 -144
  506. package/apps/studio/test/text-editability-stamp.test.ts +0 -131
  507. package/apps/studio/test/timeline-comp-target.test.ts +0 -139
  508. package/apps/studio/test/timeline-media-cache.test.ts +0 -41
  509. package/apps/studio/test/timeline-parse.test.ts +0 -184
  510. package/apps/studio/test/timeline-scale.test.ts +0 -65
  511. package/apps/studio/test/timeline-snap.test.ts +0 -85
  512. package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
  513. package/apps/studio/test/tool-palette.test.tsx +0 -55
  514. package/apps/studio/test/tour-overlay.test.tsx +0 -90
  515. package/apps/studio/test/trusted-request-host.test.ts +0 -66
  516. package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
  517. package/apps/studio/test/undo-stack.test.ts +0 -244
  518. package/apps/studio/test/use-agent-presence.test.tsx +0 -117
  519. package/apps/studio/test/use-annotation-resize.test.ts +0 -402
  520. package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
  521. package/apps/studio/test/use-artboard-drag.test.ts +0 -324
  522. package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
  523. package/apps/studio/test/use-collab.test.ts +0 -0
  524. package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
  525. package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
  526. package/apps/studio/test/use-selection-set.test.tsx +0 -166
  527. package/apps/studio/test/use-snap-guides.test.ts +0 -271
  528. package/apps/studio/test/use-tool-mode.test.tsx +0 -186
  529. package/apps/studio/test/use-undo-stack.test.tsx +0 -223
  530. package/apps/studio/test/video-asset.test.ts +0 -163
  531. package/apps/studio/test/video-comp-fixture.test.ts +0 -50
  532. package/apps/studio/test/video-comp.test.ts +0 -270
  533. package/apps/studio/test/video-encode-lib.test.ts +0 -63
  534. package/apps/studio/test/video-render-bridge.test.ts +0 -171
  535. package/apps/studio/test/whats-new.test.ts +0 -113
  536. package/apps/studio/test/whisper-setup.test.ts +0 -97
  537. package/apps/studio/test/workspace-containment.test.ts +0 -382
  538. package/apps/studio/test/workspace-signin.test.ts +0 -292
  539. package/apps/studio/test/ws-handshake.test.ts +0 -33
  540. package/cli/commands/cache.test.mjs +0 -166
  541. package/cli/commands/design-link.test.mjs +0 -419
  542. package/cli/commands/design.test.mjs +0 -240
  543. package/cli/commands/doctor.test.mjs +0 -237
  544. package/cli/commands/hub-workspace.test.mjs +0 -275
  545. package/cli/commands/hub.test.mjs +0 -225
  546. package/cli/commands/kg.test.mjs +0 -142
  547. package/cli/commands/preflight.test.mjs +0 -46
  548. package/cli/lib/argv.test.mjs +0 -46
  549. package/cli/lib/cache.test.mjs +0 -303
  550. package/cli/lib/cell-plan.test.mjs +0 -225
  551. package/cli/lib/config-lint.test.mjs +0 -117
  552. package/cli/lib/ddr-to-kgai.test.mjs +0 -118
  553. package/cli/lib/design-ownership.test.mjs +0 -329
  554. package/cli/lib/fetch-asset.test.mjs +0 -213
  555. package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
  556. package/cli/lib/figma-import-controls.test.mjs +0 -70
  557. package/cli/lib/flow-design-integration.test.mjs +0 -165
  558. package/cli/lib/gitignore-block.test.mjs +0 -136
  559. package/cli/lib/gitignore-drift.test.mjs +0 -156
  560. package/cli/lib/hubs-config.test.mjs +0 -165
  561. package/cli/lib/pkg-root.test.mjs +0 -111
  562. package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
  563. package/cli/lib/plugin-name-namespace.test.mjs +0 -71
  564. package/cli/lib/reconstruct-toolset.test.mjs +0 -194
  565. package/cli/lib/stack-detect.test.mjs +0 -121
  566. package/cli/lib/update-check.test.mjs +0 -32
  567. package/cli/lib/workspace-plan.test.mjs +0 -699
@@ -10,6 +10,7 @@ import type { Api } from '../api.ts';
10
10
  import type { Context } from '../context.ts';
11
11
  // From the LEAF, never from `sync/codec.ts` — codec imports `Y_TYPES` from this
12
12
  // file, so reaching for it here would close a cycle (see sync/limits.ts).
13
+ import { commentKey } from '../sync/comment-identity.ts';
13
14
  import { MAX_ANNOTATIONS_BYTES, MAX_COMMENTS_BYTES, withinByteCap } from '../sync/limits.ts';
14
15
  import { ensureStateDir, type RoomCallbacks } from './room.ts';
15
16
 
@@ -82,15 +83,91 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
82
83
  const { ctx, api, fileForSlug } = deps;
83
84
  const stateDir = ensureStateDir(ctx.paths.designRoot);
84
85
 
85
- // Per-slug latch: have we ever projected a NON-empty comments array for this
86
- // canvas? We write an empty `[]` to disk only AFTER content has been seen — a
87
- // genuine delete-all — never from a doc that was never populated (cold start
88
- // before seed/migrate completes), which would clobber a non-empty local file.
89
- // This closes the receiving-peer gap where a delete-all on one peer left
90
- // stragglers on the other: the old `arr.length > 0` guard skipped the write
91
- // when the array emptied, so the deletion never reached the peer's JSON file
92
- // (the file the sidebar reads), even though the Y.Doc had converged (DDR-064).
93
- const seenComments = new Set<string>();
86
+ // Per-slug: every comment identity this doc has EVER carried (issue #111).
87
+ //
88
+ // This subsumes the old `seenComments` latch — "have we ever projected a
89
+ // NON-empty array for this canvas", which gated the delete-all write so a
90
+ // never-populated doc could not clobber a non-empty local file with `[]`
91
+ // (DDR-064's receiving-peer gap). Same question, better evidence.
92
+ //
93
+ // It also answers the question the projection could not ask at all before,
94
+ // and the one this issue is about: is the doc AHEAD of the file (a real
95
+ // change, write it) or BEHIND it (a write the doc has not caught up with —
96
+ // writing would erase it)?
97
+ //
98
+ // Identity settles it without a wire-format change. An id on disk that the
99
+ // doc has held and dropped is a genuine delete and must materialize. An id on
100
+ // disk the doc has NEVER held is something the doc has not seen yet, and
101
+ // dropping it is silent data loss — the reporter's "add a second comment and
102
+ // it disappears after a few seconds". `Room.flush()` fires on ANY doc update
103
+ // and every hub update re-arms it, so on a linked project a flush lands in
104
+ // that window constantly; on a local one it essentially never does, which is
105
+ // exactly the scoping the reporter gave ("connected to a custom hub").
106
+ const docSeenComments = new Map<string, Set<string>>();
107
+
108
+ /**
109
+ * Ceiling on identities remembered per canvas — DDR-054 §2d applied to a set
110
+ * rather than a byte length.
111
+ *
112
+ * The doc is populated by the hub, so what lands in this set is peer-supplied
113
+ * and the set only ever grows: without a bound, a hostile hub streaming
114
+ * distinct comment ids is unbounded memory on every linked peer. Eviction is
115
+ * oldest-first (JS sets keep insertion order), and it degrades in the SAFE
116
+ * direction — an evicted id reads as "never seen", so the projection defers
117
+ * the write instead of deleting. The cost is that a delete of a very old
118
+ * comment on a canvas that has carried more than this many distinct comments
119
+ * may not materialize, which is a far better failure than losing one.
120
+ */
121
+ const MAX_SEEN_COMMENT_IDS = 10_000;
122
+
123
+ /**
124
+ * Docs already wired to their slug's identity set.
125
+ *
126
+ * WEAK on purpose: a room dropped when its last browser leaves destroys its
127
+ * doc, and a strong reference here would keep every doc of every canvas the
128
+ * user ever opened alive for the process's lifetime. The observer closes over
129
+ * the SET, not the other way round, so nothing else pins the doc either.
130
+ */
131
+ const observedCommentDocs = new WeakSet<Y.Doc>();
132
+
133
+ /**
134
+ * The identity set for `slug`, observing `doc` so it records an id the moment
135
+ * it ENTERS the array.
136
+ *
137
+ * Sampling at flush time alone is not enough, and the gap is a real delete
138
+ * bug rather than a theoretical one: an id that arrives and is deleted again
139
+ * between two flushes (a local add, then a remote delete inside the same
140
+ * 800 ms debounce — every doc update re-arms the timer, so this is the common
141
+ * shape, not a rare one) would never be recorded, the guard below would read
142
+ * the surviving disk copy as "never seen", and the delete could never
143
+ * materialize. The observer only ever ADDS, which is what makes it a record
144
+ * of what the doc has held rather than of what it holds.
145
+ *
146
+ * The set is per SLUG and the wiring is per DOC: a slug's room can be rebuilt
147
+ * on a fresh doc, and the ids carry over because this peer did see them.
148
+ */
149
+ function commentIdsSeenBy(slug: string, doc: Y.Doc): Set<string> {
150
+ let ids = docSeenComments.get(slug);
151
+ if (!ids) {
152
+ ids = new Set<string>();
153
+ docSeenComments.set(slug, ids);
154
+ }
155
+ if (observedCommentDocs.has(doc)) return ids;
156
+ observedCommentDocs.add(doc);
157
+
158
+ const seen = ids;
159
+ const arr = doc.getArray<unknown>(Y_TYPES.comments);
160
+ const absorb = () => {
161
+ for (const c of arr.toArray()) seen.add(commentKey(c));
162
+ for (const stale of seen) {
163
+ if (seen.size <= MAX_SEEN_COMMENT_IDS) break;
164
+ seen.delete(stale); // oldest first — insertion order
165
+ }
166
+ };
167
+ absorb();
168
+ arr.observe(absorb);
169
+ return ids;
170
+ }
94
171
 
95
172
  function ydocBinPath(slug: string): string {
96
173
  return path.join(stateDir, `${slug}.ydoc.bin`);
@@ -108,6 +185,13 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
108
185
  }
109
186
 
110
187
  async function seed(slug: string, doc: Y.Doc): Promise<void> {
188
+ // Start recording comment identities at ROOM MOUNT, before anything can
189
+ // gate out (issue #111). `persistJson` would otherwise be the first thing
190
+ // to observe this doc, which is one flush too late: an id added and deleted
191
+ // before that flush would never be recorded, and its delete could never
192
+ // materialize. This is the earliest callback that sees the doc.
193
+ commentIdsSeenBy(slug, doc);
194
+
111
195
  // Phase 9.2 (DDR-064) — under sharedDoc the migrate-seed + hub provider own
112
196
  // initial population for a pinned slug; a local file-seed here would push
113
197
  // fresh Y.Array items that DUPLICATE the hub's canonical items on merge
@@ -136,7 +220,22 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
136
220
  doc.transact(() => {
137
221
  if (comments.length > 0) {
138
222
  const arr = doc.getArray<unknown>(Y_TYPES.comments);
139
- arr.push(comments);
223
+ // Push only what the array does not already hold (issue #112). This was
224
+ // an unconditional `arr.push(comments)`, which concatenates whenever the
225
+ // doc is NOT empty at seed time — the DDR-064 Risk 1 window that
226
+ // `shouldSeed`/`isPinned` closes only when the pin already exists, i.e.
227
+ // not when a room is mounted a beat before the hub provider attaches.
228
+ // The identity rule is `commentKey`, shared with the codec's diff and
229
+ // the cold-start union; the leaf import is deliberate — this file owns
230
+ // `Y_TYPES`, so it can never import `sync/codec.ts` back (see above).
231
+ const present = new Set(arr.toArray().map(commentKey));
232
+ const missing = comments.filter((c) => {
233
+ const k = commentKey(c);
234
+ if (present.has(k)) return false;
235
+ present.add(k);
236
+ return true;
237
+ });
238
+ if (missing.length > 0) arr.push(missing);
140
239
  }
141
240
  if (svg && typeof svg === 'string') {
142
241
  const map = doc.getMap<string>(Y_TYPES.annotations);
@@ -151,14 +250,31 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
151
250
 
152
251
  // Comments — Y.Array projection back to JSON. Write whenever the doc holds
153
252
  // comments, OR when it just emptied after previously holding some (so a
154
- // delete-all materializes on EVERY peer's disk, not just the originator's).
155
- // The seenComments latch keeps a never-populated doc (cold start) from
156
- // clobbering a non-empty local file with [].
253
+ // delete-all materializes on EVERY peer's disk, not just the originator's),
254
+ // and never from a doc that was never populated at all (cold start before
255
+ // seed/migrate completes), which would clobber a non-empty local file.
256
+ //
257
+ // `everSeen` is that latch as well as the freshness oracle below — one
258
+ // notion of "this doc has held comments" instead of two that can disagree.
259
+ // It replaces a per-slug flag sampled HERE, which could not see a comment
260
+ // that arrived and was deleted again between two flushes: the flag stayed
261
+ // unset, so the delete-all read as "never populated" and never reached
262
+ // disk. Both readings need the same fact, and only the observer has it.
157
263
  const arr = doc.getArray(Y_TYPES.comments);
158
264
  const list = arr.toArray() as Parameters<Api['saveCommentsForFile']>[1];
159
- if (list.length > 0) seenComments.add(slug);
160
- if (list.length > 0 || seenComments.has(slug)) {
161
- if (withinCap(slug, 'comments', JSON.stringify(list), MAX_COMMENTS_BYTES)) {
265
+ const everSeen = commentIdsSeenBy(slug, doc);
266
+ if (list.length > 0 || everSeen.size > 0) {
267
+ // Issue #111 — refuse to project a doc that is BEHIND the file. An id on
268
+ // disk this doc has never carried is a write still in flight towards it
269
+ // (a mutation whose file→doc import has not landed, an external edit to
270
+ // `_comments/<slug>.json`); overwriting it deletes a real comment with no
271
+ // error and no recovery. Deferring is safe and self-clearing: whatever
272
+ // brings that id into the doc is itself a doc update, which re-arms the
273
+ // flush, and the next pass writes the merged state. A delete still
274
+ // materializes — its id IS in `everSeen`, so the write proceeds.
275
+ const onDisk = await api.loadCommentsForFile(file);
276
+ const behind = onDisk.some((c) => !everSeen.has(commentKey(c)));
277
+ if (!behind && withinCap(slug, 'comments', JSON.stringify(list), MAX_COMMENTS_BYTES)) {
162
278
  await api.saveCommentsForFile(file, list);
163
279
  }
164
280
  }
@@ -168,7 +284,11 @@ export function createPersistence(deps: PersistenceDeps): RoomCallbacks {
168
284
  const svg = map.get('svg');
169
285
  if (typeof svg === 'string' && svg) {
170
286
  if (withinCap(slug, 'annotations', svg, MAX_ANNOTATIONS_BYTES)) {
171
- await api.saveAnnotations(file, svg);
287
+ // Projection must never re-enter onAnnotationsChanged: an old flush
288
+ // finishing after a new edit otherwise republishes the old SVG and
289
+ // rolls back every peer. The API checks freshness after async IO and
290
+ // before its atomic rename; a later doc update schedules a new flush.
291
+ await api.projectAnnotations(file, svg, () => map.get('svg') === svg);
172
292
  }
173
293
  }
174
294
  }
@@ -8,17 +8,13 @@
8
8
 
9
9
  import type { Awareness } from 'y-protocols/awareness';
10
10
 
11
+ import { ANNOTATION_WRITE_ID, validAnnotationWriteId } from '../annotations-sync.ts';
12
+ import { applyCommentsToDoc } from '../sync/codec.ts';
11
13
  import { bridgeAwareness } from './awareness-bridge.ts';
12
14
  import { Y_TYPES } from './persistence.ts';
13
15
  import type { Room, RoomCallbacks } from './room.ts';
14
16
  import { createRoom } from './room.ts';
15
17
 
16
- /** Structural equality via canonical JSON — used by the syncRoomFrom* no-op
17
- * guards. Comment lists are small; stringify is cheap + dependency-free. */
18
- function jsonEqual(a: unknown, b: unknown): boolean {
19
- return JSON.stringify(a) === JSON.stringify(b);
20
- }
21
-
22
18
  export interface Registry {
23
19
  /** Get-or-create. Reuses an existing room for the same slug. */
24
20
  get(slug: string): Room;
@@ -57,7 +53,7 @@ export interface Registry {
57
53
  * post-write SVG; the room replaces `Y.Map.svg` so collab peers see the
58
54
  * updated stroke set without waiting for a cold-open re-seed.
59
55
  */
60
- syncRoomFromAnnotations(slug: string, svg: string): void;
56
+ syncRoomFromAnnotations(slug: string, svg: string, writeId?: string): void;
61
57
  /**
62
58
  * Phase 30 — project agent editing-presence onto a slug's room awareness so
63
59
  * it crosses the hub (the loopback `ai-activity` bus event does not). `null`
@@ -186,27 +182,29 @@ export function createRegistry(callbacks: RoomCallbacks): Registry {
186
182
  function syncRoomFromComments(slug: string, comments: readonly unknown[]): void {
187
183
  const room = rooms.get(slug);
188
184
  if (!room) return;
189
- const arr = room.doc.getArray<unknown>(Y_TYPES.comments);
190
- // No-op guard (load-bearing): skip when the room already holds this exact
191
- // list. The wholesale delete+push always emits a doc update, which schedules
192
- // a persist → file write → fs event → re-seed … so without this equality
193
- // short-circuit, re-seeding the live room from a disk change (sync-agent or
194
- // design:edit write — see createCollab's fs hook) would spin an 800ms
195
- // persist storm. Equality breaks the loop after a single convergence.
196
- if (jsonEqual(arr.toArray(), comments)) return;
197
- room.doc.transact(() => {
198
- if (arr.length > 0) arr.delete(0, arr.length);
199
- if (comments.length > 0) arr.push(comments as unknown[]);
200
- }, 'inspector-write');
185
+ // Through the codec's identity-keyed diff, NOT a local delete-all + push
186
+ // (issue #112). This bridge fires after EVERY comment mutation, so it was
187
+ // one of the three writers that could collide with an in-flight hub update
188
+ // and leave the array holding both runs. The codec keeps the no-op guard
189
+ // this function used to carry inline — an unchanged list emits no
190
+ // transaction, so re-seeding the live room from a disk change (sync-agent
191
+ // or design:edit write — see createCollab's fs hook) still can't spin the
192
+ // 800 ms persist storm the old equality short-circuit existed to prevent.
193
+ applyCommentsToDoc(room.doc, comments as unknown[], 'inspector-write');
201
194
  }
202
195
 
203
- function syncRoomFromAnnotations(slug: string, svg: string): void {
196
+ function syncRoomFromAnnotations(slug: string, svg: string, writeId?: string): void {
204
197
  const room = rooms.get(slug);
205
198
  if (!room) return;
206
199
  const map = room.doc.getMap<string>(Y_TYPES.annotations);
207
- if (map.get('svg') === svg) return; // no-op guard — same rationale as comments
200
+ // Identical disk notifications remain no-ops. A new UI operation may
201
+ // deliberately restore identical content while an author has pending work.
202
+ const id = validAnnotationWriteId(writeId) ? writeId : undefined;
203
+ if (map.get('svg') === svg && (!id || map.get(ANNOTATION_WRITE_ID) === id)) return;
208
204
  room.doc.transact(() => {
209
205
  map.set('svg', svg);
206
+ if (id) map.set(ANNOTATION_WRITE_ID, id);
207
+ else map.delete(ANNOTATION_WRITE_ID);
210
208
  }, 'inspector-write');
211
209
  }
212
210
 
@@ -77,6 +77,15 @@ export interface RoomCallbacks {
77
77
  * the seeded state.
78
78
  */
79
79
  seed(slug: string, doc: Y.Doc): Promise<void> | void;
80
+
81
+ /**
82
+ * Accepted revisions (DDR-241 §7): while true, this room's document is a
83
+ * replica of the project's accepted state — no browser connection may write
84
+ * a persistent lane into it (the hub would drop it and the replica would
85
+ * diverge). Comments and annotations go through the API as proposals.
86
+ * Awareness is unaffected. Optional; absent means false.
87
+ */
88
+ acceptedMode?: () => boolean;
80
89
  }
81
90
 
82
91
  export interface Room {
@@ -216,6 +225,8 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
216
225
  */
217
226
  function refusedLanes(conn: RoomConn, roots: ReadonlySet<string>): string[] {
218
227
  const refused: string[] = [];
228
+ // Accepted revisions: every persistent lane is the hub's to change.
229
+ if (callbacks.acceptedMode?.()) return [...roots];
219
230
  for (const r of roots) {
220
231
  if (conn.realm === 'canvas' && (isBodyLane(r) || r === '<unresolved>')) {
221
232
  refused.push(r);
@@ -255,7 +266,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
255
266
  gateRefusalCount += 1;
256
267
  discardMirror();
257
268
  console.warn(
258
- `[collab/${slug}] origin gate REFUSED a ${conn.readOnly ? 'read-only' : 'canvas-realm'} ` +
269
+ `[collab/${slug}] origin gate REFUSED a ${callbacks.acceptedMode?.() ? 'accepted-replica' : conn.readOnly ? 'read-only' : 'canvas-realm'} ` +
259
270
  `sync frame (lanes: ${refused.join(', ') || 'unclassifiable'}). ` +
260
271
  (conn.readOnly
261
272
  ? 'A viewer-role socket may only write comments — role matrix, cloud collab lane.'
@@ -351,7 +362,7 @@ export function createRoom(slug: string, callbacks: RoomCallbacks): Room {
351
362
  }
352
363
 
353
364
  function receive(conn: RoomConn, payload: Uint8Array): void {
354
- if (conn.realm === 'canvas' || conn.readOnly === true) {
365
+ if (conn.realm === 'canvas' || conn.readOnly === true || callbacks.acceptedMode?.()) {
355
366
  receiveGated(conn, payload);
356
367
  return;
357
368
  }
@@ -259,6 +259,65 @@ export interface Context {
259
259
  current?(): {
260
260
  cancelAssetSweep(): boolean;
261
261
  retireForMove?(fromSlug: string, toRel: string): Promise<boolean>;
262
+ /** Accepted-revisions mode (DDR-241) — see SyncRuntime.proposeLane. */
263
+ proposeLane?(
264
+ slug: string,
265
+ lane: 'comments' | 'annotations' | 'meta',
266
+ text: string,
267
+ opts?: { baseText?: string; writeId?: string }
268
+ ): Promise<{ status: 'accepted' | 'rejected'; code?: string }> | null;
269
+ /** Accepted-revisions mode (DDR-241) — see SyncRuntime.proposeFolder. */
270
+ proposeFolder?(
271
+ op:
272
+ | { op: 'dir.create'; path: string }
273
+ | { op: 'dir.delete'; path: string }
274
+ | { op: 'dir.move'; from: string; to: string }
275
+ ): Promise<{ status: 'accepted' | 'rejected'; code?: string; queued?: boolean }> | null;
276
+ acceptedMode?(): boolean;
277
+ acceptedHistory?(q: {
278
+ limit?: number;
279
+ before?: number | null;
280
+ path?: string | null;
281
+ }): Promise<unknown[] | null>;
282
+ acceptedVersion?(repoRel: string, revision: number): Promise<string | null>;
283
+ acceptedRestore?(
284
+ repoRel: string,
285
+ revision: number
286
+ ): Promise<{ status: 'accepted' | 'rejected'; code?: string; queued?: boolean } | null>;
287
+ acceptedUndo?(
288
+ actionId: string,
289
+ redo?: boolean
290
+ ): Promise<{
291
+ status: 'accepted' | 'rejected';
292
+ code?: string;
293
+ queued?: boolean;
294
+ actionId?: string;
295
+ } | null>;
296
+ acceptedActionForContent?(repoRel: string, content: string): string | null;
297
+ prepareOffline?(): Promise<{
298
+ complete: boolean;
299
+ pulled: number;
300
+ passes: number;
301
+ failed: number;
302
+ } | null>;
303
+ conflictVersions?(
304
+ repoRel: string
305
+ ): { slug: string; mine: string | null; theirs: string } | null;
306
+ resolveConflict?(
307
+ repoRel: string,
308
+ choice: 'mine' | 'theirs'
309
+ ): Promise<{ status: 'accepted' | 'rejected' | 'taken'; code?: string } | null>;
310
+ /** T16 — AI action boundaries (see sync/action-stage.ts). */
311
+ beginAiAction?(key: string, label: string): void;
312
+ endAiAction?(
313
+ key: string,
314
+ outcome: 'done' | 'failed'
315
+ ): Promise<{ status: 'accepted' | 'rejected'; code?: string } | null>;
316
+ resolveAiAction?(choice: 'publish' | 'discard'): Promise<{
317
+ status: 'accepted' | 'rejected' | 'discarded';
318
+ code?: string;
319
+ canvases?: number;
320
+ } | null>;
262
321
  } | null;
263
322
  };
264
323
  }
@@ -120,6 +120,13 @@ export function scrub(text: string, opts: ScrubOptions = {}): string {
120
120
  export interface DebugBundle {
121
121
  app: { maudeVersion: string; platform: string; arch: string };
122
122
  context: { projectName: string | null; activeCanvas: string | null };
123
+ /** Server process memory at report time (#119). A runaway-memory report used
124
+ * to arrive as a number the user eyeballed in Activity Monitor, with nothing
125
+ * in the bundle to confirm or refute it — and the log ring was silent
126
+ * because the heap watch was measuring the wrong counter. `rss` is the same
127
+ * number the user sees, so a future report of this class carries its own
128
+ * evidence. Bytes; process-level, never per-request. */
129
+ process: { rssBytes: number; heapUsedBytes: number; uptimeSeconds: number };
123
130
  logs: { serverLogTail: string };
124
131
  }
125
132
 
@@ -137,6 +144,7 @@ export function buildDebugBundle(args: {
137
144
  tailLines?: number;
138
145
  }): DebugBundle {
139
146
  const scrubOpts: ScrubOptions = { repoRoot: args.repoRoot };
147
+ const memory = process.memoryUsage();
140
148
  return {
141
149
  app: {
142
150
  maudeVersion: args.maudeVersion,
@@ -147,6 +155,11 @@ export function buildDebugBundle(args: {
147
155
  projectName: args.projectName,
148
156
  activeCanvas: args.activeCanvas ? scrub(args.activeCanvas, scrubOpts) : null,
149
157
  },
158
+ process: {
159
+ rssBytes: memory.rss,
160
+ heapUsedBytes: memory.heapUsed,
161
+ uptimeSeconds: Math.round(process.uptime()),
162
+ },
150
163
  logs: {
151
164
  serverLogTail: logRingLines(args.tailLines ?? 200)
152
165
  .map((l) => scrub(l, scrubOpts))