@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
@@ -1,3 +1,4 @@
1
+ import { ANNOTATION_WRITE_ID } from '../annotations-sync.ts';
1
2
  // Y.Doc ↔ disk codecs for the bidirectional file sync agent (Phase 9 Task 4).
2
3
  //
3
4
  // The agent shuttles three classes of files between disk and the Y.Doc the
@@ -22,10 +23,11 @@
22
23
  // in the unchanged regions.
23
24
 
24
25
  import { hostname } from 'node:os';
25
-
26
+ import { diffChars } from 'diff';
26
27
  import type * as Y from 'yjs';
27
28
 
28
29
  import { Y_TYPES } from '../collab/persistence.ts';
30
+ import { commentKey, dedupeCommentsById } from './comment-identity.ts';
29
31
  import {
30
32
  MAX_ANNOTATIONS_BYTES,
31
33
  MAX_COMMENTS_BYTES,
@@ -96,20 +98,27 @@ export function htmlFromDoc(doc: Y.Doc): string {
96
98
  }
97
99
 
98
100
  /**
99
- * Apply `next` to the Y.Text inside `doc`. Uses a minimal common-prefix /
100
- * common-suffix replace so peers see a small op rather than a full replace.
101
+ * Apply `next` as bounded text hunks, preserving unchanged interior CRDT
102
+ * anchors as well as the common prefix/suffix (#121).
103
+ *
104
+ * THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #114).
105
+ * Collapsing a concurrency-duplicated lane — `X + X` back to `X` — comes out of
106
+ * this diff as a PURE DELETE of the trailing run: no insert at all. Two peers
107
+ * that independently decide to collapse therefore delete the SAME CRDT items,
108
+ * and deletes are idempotent, so they converge.
109
+ *
110
+ * A wholesale `delete(0, len)` + `insert(0, next)` does not have that property.
111
+ * Both peers' inserts are new items at the same position, and Yjs keeps both —
112
+ * so the "repair" re-creates exactly the duplication it was meant to undo. The
113
+ * css lane was written that way, which is why it was the lane that never came
114
+ * out of a multi-peer cold start clean (measured: 0 of 43 lanes). Any lane whose
115
+ * duplication is repairable MUST go through here.
101
116
  *
102
117
  * Pass `origin` as the transaction origin so a downstream observer can
103
118
  * distinguish self-originated updates from peer/remote ones.
104
119
  */
105
- export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
106
- if (byteLengthUtf8(next) > MAX_HTML_BYTES) {
107
- console.warn(
108
- `[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
109
- );
110
- return false;
111
- }
112
- const yText = doc.getText(Y_SYNC_TYPES.html);
120
+ function applyTextLane(doc: Y.Doc, lane: string, next: string, origin?: unknown): boolean {
121
+ const yText = doc.getText(lane);
113
122
  const current = yText.toString();
114
123
  if (current === next) return false;
115
124
 
@@ -129,17 +138,55 @@ export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): bool
129
138
  suffix++;
130
139
  }
131
140
 
141
+ // Y.Text offsets are UTF-16; never leave half a surrogate outside a hunk.
142
+ if (prefix > 0 && /[\uD800-\uDBFF]/.test(current[prefix - 1] ?? '')) prefix--;
143
+ if (suffix > 0 && /[\uDC00-\uDFFF]/.test(current[current.length - suffix] ?? '')) suffix--;
132
144
  const deleteLen = current.length - prefix - suffix;
133
145
  const insertStr = next.slice(prefix, next.length - suffix);
134
146
 
147
+ // Keep pure inserts/deletes (especially repeat collapse) as one operation.
148
+ // For replacements preserve interior anchors: one prefix/suffix splice can
149
+ // move a concurrent insertion inside an unrelated JSX attribute (#121).
150
+ const changes =
151
+ deleteLen > 0 && insertStr.length > 0
152
+ ? diffChars(current.slice(prefix, current.length - suffix), insertStr, {
153
+ maxEditLength: 4096,
154
+ timeout: 50,
155
+ })
156
+ : null;
157
+ if (changes === undefined) throw new Error('Source diff exceeds the safe merge budget');
135
158
  doc.transact(() => {
136
- if (deleteLen > 0) yText.delete(prefix, deleteLen);
137
- if (insertStr.length > 0) yText.insert(prefix, insertStr);
159
+ if (changes) {
160
+ let offset = prefix;
161
+ for (const change of changes) {
162
+ if (change.removed) yText.delete(offset, change.value.length);
163
+ else {
164
+ if (change.added) yText.insert(offset, change.value);
165
+ offset += change.value.length;
166
+ }
167
+ }
168
+ } else {
169
+ if (deleteLen > 0) yText.delete(prefix, deleteLen);
170
+ if (insertStr.length > 0) yText.insert(prefix, insertStr);
171
+ }
138
172
  }, origin);
139
173
 
140
174
  return true;
141
175
  }
142
176
 
177
+ /**
178
+ * Apply `next` to the canvas body Y.Text inside `doc`. See `applyTextLane`.
179
+ */
180
+ export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
181
+ if (byteLengthUtf8(next) > MAX_HTML_BYTES) {
182
+ console.warn(
183
+ `[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
184
+ );
185
+ return false;
186
+ }
187
+ return applyTextLane(doc, Y_SYNC_TYPES.html, next, origin);
188
+ }
189
+
143
190
  /* ---------------------------------------------------------------- comments */
144
191
 
145
192
  /**
@@ -153,14 +200,41 @@ export function commentsFromDoc(doc: Y.Doc): CommentsSnapshot {
153
200
  return arr.toArray();
154
201
  }
155
202
 
203
+ /**
204
+ * Apply `next` to the comments Y.Array as a MINIMAL, IDENTITY-KEYED DIFF.
205
+ *
206
+ * THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #112) —
207
+ * the same law `applyTextLane` states for the text lanes, which this lane was
208
+ * missing. It used to be `delete(0, len)` + `push(next)`, and under concurrency
209
+ * that is not a replace at all: two peers delete the SAME items (idempotent)
210
+ * but insert two runs at the same position, and Yjs keeps both. The array
211
+ * becomes `list + list`, `persistJson` writes that to `_comments/<slug>.json`,
212
+ * the file re-imports clean, and the duplication IS the new truth. Every
213
+ * reconciliation round doubled it — 1 → 2 → 4 → 8, the count the #112 reporter
214
+ * saw — and the cold-start union repair, published through the same wholesale
215
+ * write, re-created the duplication it had just undone. Verbatim the css lesson
216
+ * from issue #114, one lane later.
217
+ *
218
+ * The diff gives the lane the property the text lanes got:
219
+ *
220
+ * - Collapsing a duplicated array is a PURE DELETE of items every peer holds,
221
+ * and concurrent deletes of the same items are idempotent — so two peers
222
+ * repairing at once converge instead of re-doubling.
223
+ * - Dedupe happens on EVERY apply, not only at cold start, so an array that
224
+ * doubled anyway (an older peer still on the wholesale write, a genuine
225
+ * concurrent edit of one entry) collapses at the next apply rather than
226
+ * compounding. Duplication became self-healing instead of exponential.
227
+ * - Untouched comments keep their CRDT items, so a peer replacing one entry
228
+ * no longer rewrites the whole list under everyone else's feet.
229
+ *
230
+ * Ordering follows the doc for surviving entries, with genuinely new ones
231
+ * appended. Reordering is deliberately NOT synced: it would be a delete+insert
232
+ * per moved entry — churn with the one shape this fix exists to avoid — and
233
+ * comment order carries no meaning (pins are placed by selector, the sidebar
234
+ * sorts by `created`).
235
+ */
156
236
  export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?: unknown): boolean {
157
- const arr = doc.getArray(Y_TYPES.comments);
158
- // Comments are LWW on the JSON file (the snapshot is the source of truth);
159
- // collapse Y.Array to the new state. For v1.1 we just replace wholesale —
160
- // structural comment-level merge is deferred along with structured HTML.
161
- // Check whether anything actually changed to avoid no-op transactions
162
- // (transactions still fire `update` events, which would re-enter the loop).
163
- const before = JSON.stringify(arr.toArray());
237
+ const arr = doc.getArray<unknown>(Y_TYPES.comments);
164
238
  const after = JSON.stringify(next);
165
239
  if (byteLengthUtf8(after) > MAX_COMMENTS_BYTES) {
166
240
  console.warn(
@@ -168,11 +242,51 @@ export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?:
168
242
  );
169
243
  return false;
170
244
  }
171
- if (before === after) return false;
245
+
246
+ // Desired state, keyed by comment identity. `next` can itself arrive
247
+ // duplicated (a file written before this fix, a list pushed by an older
248
+ // peer), so it is deduped on the way in — first occurrence wins, matching
249
+ // `unionCommentsById` and `dedupeCommentsById`.
250
+ const desired = new Map<string, unknown>();
251
+ for (const c of next) {
252
+ const k = commentKey(c);
253
+ if (!desired.has(k)) desired.set(k, c);
254
+ }
255
+
256
+ // Classify every item currently in the array: keep it, replace it in place,
257
+ // or delete it. Duplicates of an identity we already kept are deletes — that
258
+ // is what makes EVERY apply a repair, not just the cold-start union.
259
+ const current = arr.toArray();
260
+ const deletes: number[] = [];
261
+ const replaces: { index: number; value: unknown }[] = [];
262
+ const kept = new Set<string>();
263
+ for (let i = 0; i < current.length; i++) {
264
+ const k = commentKey(current[i]);
265
+ if (!desired.has(k) || kept.has(k)) {
266
+ deletes.push(i);
267
+ continue;
268
+ }
269
+ kept.add(k);
270
+ const want = desired.get(k);
271
+ if (JSON.stringify(current[i]) !== JSON.stringify(want))
272
+ replaces.push({ index: i, value: want });
273
+ }
274
+ const appends: unknown[] = [];
275
+ for (const [k, v] of desired) if (!kept.has(k)) appends.push(v);
276
+
277
+ // No-op guard. Transactions fire `update` events even when they change
278
+ // nothing, and an update re-enters the file→doc→file loop.
279
+ if (deletes.length === 0 && replaces.length === 0 && appends.length === 0) return false;
172
280
 
173
281
  doc.transact(() => {
174
- if (arr.length > 0) arr.delete(0, arr.length);
175
- if (next.length > 0) arr.push(next);
282
+ // Replaces first, while the indices computed above are still valid.
283
+ for (const r of replaces) {
284
+ arr.delete(r.index, 1);
285
+ arr.insert(r.index, [r.value]);
286
+ }
287
+ // Then deletes, descending, so each index stays valid as we go.
288
+ for (let i = deletes.length - 1; i >= 0; i--) arr.delete(deletes[i]!, 1);
289
+ if (appends.length > 0) arr.push(appends);
176
290
  }, origin);
177
291
  return true;
178
292
  }
@@ -218,6 +332,8 @@ export function applyAnnotationsToDoc(doc: Y.Doc, next: string | null, origin?:
218
332
  if (currentStr === next) return false;
219
333
 
220
334
  doc.transact(() => {
335
+ // A filesystem import is a new operation, never the previous UI author's echo.
336
+ map.delete(ANNOTATION_WRITE_ID);
221
337
  if (next === null || next === '') {
222
338
  map.delete('svg');
223
339
  } else {
@@ -301,6 +417,45 @@ function parsesAsObject(s: string): boolean {
301
417
  }
302
418
 
303
419
  /** The synced shared-meta JSON string held in the doc, or null when unset. */
420
+ /**
421
+ * The canonical LANE value for a file's text — what the accepted-revisions
422
+ * kernel stores and what `readLaneFromDoc` returns once it is accepted
423
+ * (DDR-241). `null` when the file text cannot be a value of that lane.
424
+ */
425
+ export function laneValueFromFile(
426
+ lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments',
427
+ text: string
428
+ ): string | null {
429
+ if (lane === 'html' || lane === 'css' || lane === 'annotations') return text;
430
+ let parsed: unknown;
431
+ try {
432
+ parsed = parseJsonSafe(text);
433
+ } catch {
434
+ return null;
435
+ }
436
+ if (lane === 'meta') {
437
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null;
438
+ return sharedMetaCanonical(parsed as Record<string, unknown>);
439
+ }
440
+ if (!Array.isArray(parsed)) return null;
441
+ const list = dedupeCommentsById(parsed);
442
+ return list.length ? JSON.stringify(list) : '';
443
+ }
444
+
445
+ /** A lane's value as the accepted replica holds it (same canonical form). */
446
+ export function readLaneFromDoc(
447
+ doc: Y.Doc,
448
+ lane: 'html' | 'css' | 'meta' | 'annotations' | 'comments'
449
+ ): string {
450
+ if (lane === 'html' || lane === 'css' || lane === 'meta') return doc.getText(lane).toString();
451
+ if (lane === 'annotations') {
452
+ const svg = doc.getMap<unknown>(Y_TYPES.annotations).get('svg');
453
+ return typeof svg === 'string' ? svg : '';
454
+ }
455
+ const list = doc.getArray<unknown>(Y_TYPES.comments).toArray();
456
+ return list.length ? JSON.stringify(list) : '';
457
+ }
458
+
304
459
  export function metaFromDoc(doc: Y.Doc): string | null {
305
460
  return normalizeSharedMeta(doc.getText(Y_SYNC_TYPES.meta).toString());
306
461
  }
@@ -618,10 +773,19 @@ export function cssFromDoc(doc: Y.Doc): string | null {
618
773
  }
619
774
 
620
775
  /**
621
- * Apply the canvas's `.css` to the doc as opaque Y.Text (wholesale replace).
622
- * CSS round-trips byte-identically (we write exactly the doc string back), so a
623
- * wholesale delete+insert can't churn; the equality short-circuit keeps it quiet
624
- * when unchanged. Returns false on over-cap / no change.
776
+ * Apply the canvas's `.css` to the doc as opaque Y.Text.
777
+ *
778
+ * Goes through `applyTextLane` — the same prefix/suffix diff the body uses —
779
+ * and that is load-bearing, not tidiness (issue #114). This was a wholesale
780
+ * `delete(0, len)` + `insert(0, next)`, whose comment argued it "can't churn"
781
+ * because css round-trips byte-identically. True for a SINGLE writer, and the
782
+ * reason the lane looked fine in every single-peer test. Under concurrency it
783
+ * is the bug: two peers each replacing the lane keep both inserts, so the css
784
+ * doubled on a concurrent cold seed AND doubled again every time a peer tried
785
+ * to repair it. Measured in the field at 3×, 4× and 5×, while the body lane —
786
+ * same collision, diffing codec — never exceeded 3× and mostly self-healed.
787
+ *
788
+ * Returns false on over-cap / no change.
625
789
  */
626
790
  export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
627
791
  if (byteLengthUtf8(next) > MAX_CSS_BYTES) {
@@ -630,11 +794,5 @@ export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boole
630
794
  );
631
795
  return false;
632
796
  }
633
- const t = doc.getText(Y_SYNC_TYPES.css);
634
- if (t.toString() === next) return false;
635
- doc.transact(() => {
636
- if (t.length > 0) t.delete(0, t.length);
637
- t.insert(0, next);
638
- }, origin);
639
- return true;
797
+ return applyTextLane(doc, Y_SYNC_TYPES.css, next, origin);
640
798
  }
@@ -39,6 +39,13 @@
39
39
 
40
40
  import type { ColdStartAction, ColdStartDecision } from './cold-start.ts';
41
41
 
42
+ /** Why a cold-start snapshot was taken.
43
+ *
44
+ * Deliberately NOT extended for the duplication collapse (issue #114). A
45
+ * collapse only ever fires on an exact integer repeat, so the discarded copies
46
+ * hold nothing the retained one doesn't — and `pruneSnapshots` evicts
47
+ * oldest-first per slug, so a valueless snapshot on a hub-triggerable path is
48
+ * an eviction lever against the copies that carry real bytes. */
42
49
  export type ColdStartSnapshotReason = 'pre-sync-local' | 'pre-sync-hub';
43
50
 
44
51
  export interface ColdStartConflictInfo {
@@ -19,6 +19,7 @@
19
19
  // unavailable) or equal → hub wins, exactly the v1.1 default — but now both
20
20
  // sides are snapshotted first, so even a wrong pick costs one /design:rollback.
21
21
 
22
+ import { commentKey } from './comment-identity.ts';
22
23
  import { hashBytes } from './echo-guard.ts';
23
24
 
24
25
  export type ColdStartAction =
@@ -75,13 +76,6 @@ export function isExactRepeat(docBody: string, localBody: string): boolean {
75
76
  return docBody === localBody.repeat(n);
76
77
  }
77
78
 
78
- function commentId(c: unknown): string | null {
79
- if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
80
- return (c as { id: string }).id;
81
- }
82
- return null;
83
- }
84
-
85
79
  /**
86
80
  * Union-merge two comment snapshots by stable `id` (DDR-102): doc order first,
87
81
  * local-only entries appended. Comments carry stable ids, so union loses
@@ -90,22 +84,30 @@ function commentId(c: unknown): string | null {
90
84
  * cold-start paths (agent reconcile + migrate-seed).
91
85
  */
92
86
  export function unionCommentsById(docList: unknown[], localList: unknown[]): unknown[] {
93
- const out = [...docList];
94
- const seenIds = new Set<string>();
95
- const seenJson = new Set<string>();
96
- for (const c of docList) {
97
- const id = commentId(c);
98
- if (id !== null) seenIds.add(id);
99
- else seenJson.add(JSON.stringify(c));
100
- }
101
- for (const c of localList) {
102
- const id = commentId(c);
103
- const dup = id !== null ? seenIds.has(id) : seenJson.has(JSON.stringify(c));
104
- if (dup) continue;
87
+ const out: unknown[] = [];
88
+ const seen = new Set<string>();
89
+ // THE DOC IS DEDUPED AGAINST ITSELF, not just against local (issue #114/#112).
90
+ // `out` used to be seeded as `[...docList]`, which made this union a filter on
91
+ // the LOCAL side only — so a doc whose comments array had already been
92
+ // concurrency-doubled (`applyCommentsToDoc` is delete-all + push: two peers
93
+ // replacing at once keep both pushes) carried every copy forward, and the
94
+ // merged result was re-published as the new truth. That is the amplifier
95
+ // behind "the comment self duplicated like 8 times": 2 → 4 → 8, one doubling
96
+ // per concurrent round, with nothing in the loop able to shrink it again.
97
+ // Deduping the doc half turns the same pass into the repair.
98
+ // `commentKey` is the SHARED identity rule (comment-identity.ts) — the same
99
+ // one `applyCommentsToDoc`'s diff and the room seed use. It has to be shared:
100
+ // if the union's notion of "same comment" ever drifted from the codec's, one
101
+ // layer's repair would be the other's duplicate.
102
+ const admit = (c: unknown): void => {
103
+ const k = commentKey(c);
104
+ if (seen.has(k)) return;
105
+ seen.add(k);
105
106
  out.push(c);
106
- if (id !== null) seenIds.add(id);
107
- else seenJson.add(JSON.stringify(c));
108
- }
107
+ };
108
+ // Doc order first (same-id entries keep the doc's version), local-only appended.
109
+ for (const c of docList) admit(c);
110
+ for (const c of localList) admit(c);
109
111
  return out;
110
112
  }
111
113
 
@@ -292,3 +294,97 @@ export function decideAnnotationsColdStart(
292
294
  reason: `diverged — no per-lane stamp on one side, following the body winner (${bodyWinner})`,
293
295
  };
294
296
  }
297
+
298
+ /* ------------------------------------------------------- css (per-lane) */
299
+
300
+ export interface CssColdStartInput {
301
+ /** Local sibling `.css` content, or null when the file doesn't exist. */
302
+ local: string | null;
303
+ /** The css currently held by the doc (`null` when the lane is unset/empty). */
304
+ doc: string | null;
305
+ /** Journal entry's `cssHash` for this slug, or null when never checkpointed. */
306
+ journalHash: string | null;
307
+ /** Hash fn — passed in so this table stays as dependency-free as the rest. */
308
+ hash: (s: string) => string;
309
+ /** How the body lane resolved; the tie-break for a genuine divergence. */
310
+ bodyWinner: 'local' | 'hub';
311
+ }
312
+
313
+ export interface CssColdStartDecision {
314
+ winner: 'local' | 'hub' | 'none';
315
+ /** True when local won because the doc held local repeated N≥2 times. */
316
+ recoveredDuplication?: boolean;
317
+ reason: string;
318
+ }
319
+
320
+ /**
321
+ * Per-lane cold-start resolution for the canvas's sibling `.css` (issue #114).
322
+ *
323
+ * WHY THIS TABLE EXISTS. Every other Plane-A lane had one — `decideColdStart`
324
+ * for the body, `decideAnnotationsColdStart` for annotations, `repairSharedMeta`
325
+ * for meta — and css had none: it simply followed the body winner and re-applied
326
+ * the whole file whenever the two sides disagreed. That left it the only lane
327
+ * with no duplication guard at all, and `applyCssToDoc` is a delete-all+insert,
328
+ * so two peers seeding one empty document concurrently kept BOTH inserts and the
329
+ * lane became `CSS × N` (Yjs merges concurrent inserts at the same position by
330
+ * keeping both — not a corruption, the CRDT doing its job). Worse, the "repair"
331
+ * was itself another full insert, so every subsequent disagreement could double
332
+ * it again. Measured on the reporting machine's 84 persisted docs: the body lane
333
+ * was clean 21 times out of 84, the css lane ZERO times out of 43, and css
334
+ * carried strictly more copies than its own document's body in 43 of 43.
335
+ * Per-lane vigilance is what failed; a table per lane is the shape that holds.
336
+ *
337
+ * The rows mirror `decideColdStart`'s, in the same order and for the same
338
+ * reasons — crucially the `recover-seed-dup` row, checked BEFORE the journal
339
+ * fast-forward so a doubled lane is collapsed rather than read as "the hub moved
340
+ * ahead". Like the body's, it repairs only what it can PROVE: an exact integer
341
+ * repeat of the bytes on disk. A doc carrying genuinely new css is never an
342
+ * exact repeat, so this cannot clobber an edit.
343
+ */
344
+ export function decideCssColdStart(input: CssColdStartInput): CssColdStartDecision {
345
+ const { local, doc, journalHash, hash, bodyWinner } = input;
346
+ const localEmpty = local === null || local === '';
347
+ const docEmpty = doc === null || doc === '';
348
+
349
+ if (localEmpty && docEmpty) return { winner: 'none', reason: 'both sides empty' };
350
+ if (localEmpty) return { winner: 'hub', reason: 'no local css — materializing the hub lane' };
351
+ if (docEmpty) {
352
+ // The body lane's DDR-064 guard, applied here: an empty css lane means the
353
+ // hub holds no css for this canvas YET, never "this canvas has no css".
354
+ // Without this row a local `.css` next to a hub-winning body never travelled
355
+ // at all — it sat on one disk and no peer ever saw it.
356
+ return { winner: 'local', reason: 'hub css lane empty — seeding local css up (DDR-064 guard)' };
357
+ }
358
+ if (doc === local) return { winner: 'none', reason: 'local and hub css identical' };
359
+
360
+ // Concurrent cold-seed collision, css edition — see the header. Collapsing is
361
+ // re-applying local: `applyCssToDoc` replaces the lane wholesale, so the
362
+ // repeated copies go and one stays.
363
+ if (isExactRepeat(doc, local)) {
364
+ return {
365
+ winner: 'local',
366
+ recoveredDuplication: true,
367
+ reason:
368
+ 'hub css is local repeated — concurrent cold-seed duplication; collapsing to one copy',
369
+ };
370
+ }
371
+
372
+ // The journal checkpoint the css lane has been WRITING since DDR-102 and never
373
+ // reading. Same meaning as the body's: every local byte was already reconciled
374
+ // through this machine, so the hub is simply ahead.
375
+ if (journalHash !== null && journalHash === hash(local)) {
376
+ return {
377
+ winner: 'hub',
378
+ reason: 'local css matches the last-synced journal hash — hub is ahead, fast-forwarding',
379
+ };
380
+ }
381
+
382
+ // Genuine divergence. The css is visually coupled to the body, so it follows
383
+ // the body's resolution rather than inventing a second, possibly disagreeing
384
+ // winner for the same canvas — the pre-existing behaviour, now reached only
385
+ // when the rows above have ruled out the cases they can decide on their own.
386
+ return {
387
+ winner: bodyWinner,
388
+ reason: `css diverged — following the body winner (${bodyWinner})`,
389
+ };
390
+ }
@@ -0,0 +1,68 @@
1
+ // Comment identity — the one definition of "these two entries are the same
2
+ // comment", shared by every layer that touches the comments lane (issue #112).
3
+ //
4
+ // A LEAF module, exactly like `limits.ts`: it imports NOTHING. That is
5
+ // load-bearing, not tidiness. `collab/persistence.ts` owns `Y_TYPES` and
6
+ // `sync/codec.ts` imports it from there, so persistence can never import the
7
+ // codec back (its own comment says so) — yet the room seed needs the same
8
+ // identity rule the codec's diff uses. A dependency-free leaf is the only
9
+ // place both sides can reach.
10
+ //
11
+ // WHY IDENTITY IS A SHARED CONCERN. Comments travel over the hub as a Y.Array,
12
+ // and the lane's whole failure mode is the same logical comment appearing as
13
+ // several distinct CRDT items (issue #112: 1 → 2 → 4 → 8). Every layer that
14
+ // writes the lane has to agree on which entries are "the same one", or one
15
+ // layer's repair is another layer's duplicate.
16
+
17
+ /**
18
+ * Stable identity for one comment entry.
19
+ *
20
+ * `id` is the real key — it is minted once by `commentsAdd` and survives every
21
+ * round-trip through disk, the Y.Array and the hub. Entries WITHOUT a string
22
+ * `id` (hand-edited files, pre-Phase-6 rows, junk pushed by an untrusted peer)
23
+ * fall back to JSON identity, which is deliberately conservative: it dedupes
24
+ * only byte-identical entries and never merges two rows that might differ.
25
+ *
26
+ * The `id:` / `json:` prefixes keep the two spaces from colliding — a comment
27
+ * whose id is literally the JSON of another entry can't shadow it.
28
+ */
29
+ export function commentKey(c: unknown): string {
30
+ if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
31
+ return `id:${(c as { id: string }).id}`;
32
+ }
33
+ return `json:${JSON.stringify(c)}`;
34
+ }
35
+
36
+ /**
37
+ * Collapse a comment list to one entry per identity, keeping the FIRST
38
+ * occurrence.
39
+ *
40
+ * First-wins is the same rule `unionCommentsById` uses ("doc order first"), and
41
+ * the two must agree: `commentsAddReply` mutates the first match, so the copy
42
+ * the dedupe keeps has to be the copy the mutations edit.
43
+ *
44
+ * Returns the input array itself when there is nothing to drop, so callers can
45
+ * cheaply test `deduped !== list` to know whether a write is worth making.
46
+ */
47
+ export function dedupeCommentsById<T>(list: readonly T[]): T[] {
48
+ const seen = new Set<string>();
49
+ const out: T[] = [];
50
+ for (const c of list) {
51
+ const k = commentKey(c);
52
+ if (seen.has(k)) continue;
53
+ seen.add(k);
54
+ out.push(c);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /** True when `list` carries the same comment identity more than once. */
60
+ export function hasDuplicateComments(list: readonly unknown[]): boolean {
61
+ const seen = new Set<string>();
62
+ for (const c of list) {
63
+ const k = commentKey(c);
64
+ if (seen.has(k)) return true;
65
+ seen.add(k);
66
+ }
67
+ return false;
68
+ }
@@ -310,7 +310,21 @@ export function createConnectionMonitor(opts: ConnectionMonitorOptions = {}): Co
310
310
  const wasOffline = state === 'offline' || state === 'offline-long';
311
311
  clearAllTimers();
312
312
  state = 'online';
313
- lastSyncAt = now();
313
+ // NOT `lastSyncAt = now()`.
314
+ //
315
+ // A SOCKET TRANSITION IS NOT A SYNC. This line used to stamp the
316
+ // last-synced clock from the WS coming back — an event that, by itself,
317
+ // has moved no document and settled no handshake. The field's own contract
318
+ // one screen up already says otherwise ("updated on REAL sync activity
319
+ // (noteSyncActivity), not just on offline→online transitions"); the code
320
+ // simply did not honour it.
321
+ //
322
+ // It was not a cosmetic drift. Live on the reported project (issue #118):
323
+ // `state:"online", docs:{synced:0,pending:85}` with `lastSyncAt` refreshed
324
+ // to the exact millisecond of that transition — so the one number a reader
325
+ // could have used to notice that nothing had synced in 15 minutes was
326
+ // being refreshed BY the failure. `noteSyncActivity` is the only honest
327
+ // source, and it is now the only one.
314
328
  offlineSince = null;
315
329
  queuedOps = 0;
316
330
  if (wasOffline) {
@@ -95,6 +95,8 @@ export interface CtlProviderOptions {
95
95
  token: string;
96
96
  /** Fired with the hub's head on every well-formed poke. */
97
97
  onPoke: (head: number) => void;
98
+ /** Optional fast document-list invalidation; older consumers use onPoke. */
99
+ onDocuments?: () => void;
98
100
  documentName?: string;
99
101
  log?: Pick<Console, 'log' | 'warn' | 'error'>;
100
102
  /** Injected in tests; production builds one from `@hocuspocus/provider`. */
@@ -155,9 +157,19 @@ export function createCtlProvider(opts: CtlProviderOptions): CtlProvider {
155
157
 
156
158
  const wire = (p: CtlProviderLike): void => {
157
159
  p.on('status', (data: { status?: string }) => {
158
- if (typeof data?.status === 'string') status = data.status;
160
+ if (typeof data?.status !== 'string') return;
161
+ const reconnect = data.status === 'connected' && status !== 'connected';
162
+ status = data.status;
163
+ if (reconnect && !stopped) {
164
+ try {
165
+ opts.onDocuments?.();
166
+ } catch (error) {
167
+ log.error?.(`[sync/ctl] discovery handler threw: ${(error as Error).message}`);
168
+ }
169
+ }
159
170
  });
160
171
  p.on('stateless', (data: { payload: string }) => {
172
+ if (stopped) return;
161
173
  const poke = parsePoke(data?.payload);
162
174
  if (poke === null) {
163
175
  malformed += 1;
@@ -172,7 +184,8 @@ export function createCtlProvider(opts: CtlProviderOptions): CtlProvider {
172
184
  }
173
185
  received += 1;
174
186
  try {
175
- opts.onPoke(poke.head);
187
+ if (poke.documents && opts.onDocuments) opts.onDocuments();
188
+ else opts.onPoke(poke.head);
176
189
  } catch (err) {
177
190
  log.error?.(`[sync/ctl] poke handler threw: ${(err as Error).message}`);
178
191
  }