@1agh/maude 1.0.9 → 1.2.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 (506) hide show
  1. package/apps/studio/acp/bridge.ts +18 -13
  2. package/apps/studio/acp/probe.ts +268 -16
  3. package/apps/studio/acp/transcript-io.ts +394 -0
  4. package/apps/studio/acp/transcript.ts +103 -38
  5. package/apps/studio/api.ts +139 -26
  6. package/apps/studio/bin/_import-figma.mjs +57 -11
  7. package/apps/studio/bin/_seed-fixture.mjs +163 -0
  8. package/apps/studio/canvas-imports.ts +89 -0
  9. package/apps/studio/client/app.jsx +257 -7
  10. package/apps/studio/client/export-center.jsx +15 -7
  11. package/apps/studio/client/panels/ReadinessList.jsx +7 -1
  12. package/apps/studio/client/panels/SyncPanel.jsx +125 -4
  13. package/apps/studio/client/report-bug.jsx +19 -1
  14. package/apps/studio/client/styles/3-shell-maude.css +34 -0
  15. package/apps/studio/client/styles/4-components.css +26 -0
  16. package/apps/studio/collab/persistence.ts +132 -16
  17. package/apps/studio/collab/registry.ts +10 -18
  18. package/apps/studio/debug-bundle.ts +13 -0
  19. package/apps/studio/dist/client.bundle.js +1128 -1128
  20. package/apps/studio/dist/styles.css +1 -1
  21. package/apps/studio/export-dialog.tsx +54 -0
  22. package/apps/studio/exporters/degraded.ts +19 -3
  23. package/apps/studio/exporters/ghostscript.ts +194 -0
  24. package/apps/studio/exporters/pdf-fonts.ts +261 -0
  25. package/apps/studio/exporters/pdf.ts +152 -3
  26. package/apps/studio/exporters/remote.ts +36 -1
  27. package/apps/studio/fs-watch.ts +45 -1
  28. package/apps/studio/http.ts +7 -1
  29. package/apps/studio/mem.ts +45 -9
  30. package/apps/studio/readiness.ts +42 -7
  31. package/apps/studio/server.ts +11 -4
  32. package/apps/studio/sync/agent.ts +136 -29
  33. package/apps/studio/sync/asset-push.ts +1 -47
  34. package/apps/studio/sync/codec.ts +122 -31
  35. package/apps/studio/sync/cold-start-apply.ts +7 -0
  36. package/apps/studio/sync/cold-start.ts +118 -22
  37. package/apps/studio/sync/comment-identity.ts +68 -0
  38. package/apps/studio/sync/connection-state.ts +15 -1
  39. package/apps/studio/sync/file-ledger.ts +117 -0
  40. package/apps/studio/sync/file-plane.ts +623 -27
  41. package/apps/studio/sync/index.ts +711 -20
  42. package/apps/studio/sync/journal-client.ts +13 -1
  43. package/apps/studio/sync/migrate-seed.ts +40 -3
  44. package/apps/studio/sync/presentation.ts +26 -3
  45. package/apps/studio/sync/retry-after.ts +169 -0
  46. package/apps/studio/sync/seed-progress.ts +218 -0
  47. package/apps/studio/sync/status.ts +122 -5
  48. package/apps/studio/sync/transport-error.ts +111 -0
  49. package/apps/studio/whats-new.json +91 -0
  50. package/cli/bin/maude.mjs +2 -0
  51. package/cli/commands/codex.mjs +191 -0
  52. package/cli/commands/harness.mjs +1051 -0
  53. package/cli/commands/help.mjs +13 -0
  54. package/cli/lib/design-link.mjs +190 -2
  55. package/cli/lib/harness/capabilities.mjs +302 -0
  56. package/cli/lib/harness/codex-runtime.mjs +889 -0
  57. package/cli/lib/harness/compatibility.mjs +27 -0
  58. package/cli/lib/harness/discover-claude.mjs +1393 -0
  59. package/cli/lib/harness/managed-state.mjs +1296 -0
  60. package/cli/lib/harness/model.mjs +86 -0
  61. package/cli/lib/harness/secrets.mjs +390 -0
  62. package/cli/lib/harness/targets/codex.mjs +1141 -0
  63. package/cli/lib/harness/targets/opencode.mjs +807 -0
  64. package/cli/lib/harness/transaction.mjs +1099 -0
  65. package/cli/lib/hubs-config.mjs +35 -8
  66. package/cli/templates/harness/opencode/maude-projector.ts +58 -0
  67. package/docs/harness-capability-matrix.md +125 -0
  68. package/docs/harness-environment-projection.md +95 -0
  69. package/package.json +32 -9
  70. package/plugins/flow/.claude-plugin/config.schema.json +8 -0
  71. package/apps/studio/.ai/cache/_stats.json +0 -7
  72. package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
  73. package/apps/studio/bin/_curl-local.test.mjs +0 -280
  74. package/apps/studio/bin/_pw-launch.test.mjs +0 -90
  75. package/apps/studio/bin/_smart-frames.test.mjs +0 -195
  76. package/apps/studio/bin/_transcribe.test.mjs +0 -80
  77. package/apps/studio/bun.lock +0 -928
  78. package/apps/studio/draw/test/animate.test.ts +0 -172
  79. package/apps/studio/draw/test/brush.test.ts +0 -213
  80. package/apps/studio/draw/test/composition.test.ts +0 -141
  81. package/apps/studio/draw/test/geometry.test.ts +0 -199
  82. package/apps/studio/draw/test/gradient.test.ts +0 -167
  83. package/apps/studio/draw/test/layout.test.ts +0 -120
  84. package/apps/studio/draw/test/morph.test.ts +0 -107
  85. package/apps/studio/draw/test/optimize.test.ts +0 -40
  86. package/apps/studio/draw/test/palette.test.ts +0 -123
  87. package/apps/studio/draw/test/primitives.test.ts +0 -129
  88. package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
  89. package/apps/studio/draw/test/serialize.test.ts +0 -105
  90. package/apps/studio/figma/assets.test.ts +0 -464
  91. package/apps/studio/figma/client.test.ts +0 -395
  92. package/apps/studio/figma/codegen-client.test.ts +0 -276
  93. package/apps/studio/figma/codegen-fonts.test.ts +0 -103
  94. package/apps/studio/figma/codegen-values.test.ts +0 -179
  95. package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
  96. package/apps/studio/figma/fig-decode.test.ts +0 -788
  97. package/apps/studio/figma/fig-differential.test.ts +0 -182
  98. package/apps/studio/figma/fig-translator.test.ts +0 -192
  99. package/apps/studio/figma/fig-vector.test.ts +0 -113
  100. package/apps/studio/figma/from-codegen.test.ts +0 -408
  101. package/apps/studio/figma/sanitize.test.ts +0 -325
  102. package/apps/studio/figma/tailwind-map.test.ts +0 -142
  103. package/apps/studio/figma/to-artboard.test.ts +0 -808
  104. package/apps/studio/figma/to-render.test.ts +0 -180
  105. package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
  106. package/apps/studio/figma/to-strokes.test.ts +0 -705
  107. package/apps/studio/figma/to-tokens.test.ts +0 -321
  108. package/apps/studio/figma/url.test.ts +0 -167
  109. package/apps/studio/footage/schema.test.ts +0 -287
  110. package/apps/studio/generation/adapters/groq.test.ts +0 -80
  111. package/apps/studio/generation/audio-library.test.ts +0 -95
  112. package/apps/studio/generation/captions.test.ts +0 -78
  113. package/apps/studio/generation/elevenlabs.test.ts +0 -223
  114. package/apps/studio/generation/gemini.test.ts +0 -350
  115. package/apps/studio/generation/jobs.test.ts +0 -128
  116. package/apps/studio/generation/keys.test.ts +0 -75
  117. package/apps/studio/generation/prefs.test.ts +0 -95
  118. package/apps/studio/generation/types.test.ts +0 -123
  119. package/apps/studio/generation/whisper-models.test.ts +0 -179
  120. package/apps/studio/test/_helpers.ts +0 -108
  121. package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
  122. package/apps/studio/test/acp-activity.test.ts +0 -154
  123. package/apps/studio/test/acp-ai-activity.test.ts +0 -182
  124. package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
  125. package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
  126. package/apps/studio/test/acp-branch-guard.test.ts +0 -123
  127. package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
  128. package/apps/studio/test/acp-bridge.test.ts +0 -301
  129. package/apps/studio/test/acp-capabilities.test.ts +0 -123
  130. package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
  131. package/apps/studio/test/acp-commands.test.ts +0 -119
  132. package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
  133. package/apps/studio/test/acp-elicitation.test.ts +0 -251
  134. package/apps/studio/test/acp-env.test.ts +0 -95
  135. package/apps/studio/test/acp-mode-banner.test.ts +0 -45
  136. package/apps/studio/test/acp-origin-gate.test.ts +0 -160
  137. package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
  138. package/apps/studio/test/acp-permission.test.ts +0 -278
  139. package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
  140. package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
  141. package/apps/studio/test/acp-session-plugins.test.ts +0 -209
  142. package/apps/studio/test/acp-toolgroup.test.ts +0 -76
  143. package/apps/studio/test/acp-transcript-view.test.ts +0 -71
  144. package/apps/studio/test/acp-transcript.test.ts +0 -238
  145. package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
  146. package/apps/studio/test/acp-usage.test.ts +0 -143
  147. package/apps/studio/test/acp-write-gate.test.ts +0 -323
  148. package/apps/studio/test/acp-write-scope.test.ts +0 -533
  149. package/apps/studio/test/active-state.test.ts +0 -186
  150. package/apps/studio/test/activity.test.ts +0 -189
  151. package/apps/studio/test/ai-activity.test.ts +0 -113
  152. package/apps/studio/test/annotate-write.test.ts +0 -1074
  153. package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
  154. package/apps/studio/test/annotations-align.test.ts +0 -88
  155. package/apps/studio/test/annotations-api.test.ts +0 -145
  156. package/apps/studio/test/annotations-bindings.test.ts +0 -262
  157. package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
  158. package/apps/studio/test/annotations-groups.test.ts +0 -231
  159. package/apps/studio/test/annotations-layer.test.ts +0 -1011
  160. package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
  161. package/apps/studio/test/annotations-snap.test.ts +0 -135
  162. package/apps/studio/test/annotations-text-format.test.ts +0 -396
  163. package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
  164. package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
  165. package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
  166. package/apps/studio/test/artboard-kinds.test.tsx +0 -83
  167. package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
  168. package/apps/studio/test/asset-api.test.ts +0 -149
  169. package/apps/studio/test/assets-s3.test.ts +0 -249
  170. package/apps/studio/test/binary-smoke.test.ts +0 -47
  171. package/apps/studio/test/boot-self-heal.test.ts +0 -87
  172. package/apps/studio/test/browse-posture.test.tsx +0 -107
  173. package/apps/studio/test/bundle-smoke.test.ts +0 -46
  174. package/apps/studio/test/camera-reveal.test.tsx +0 -173
  175. package/apps/studio/test/canvas-artifacts.test.ts +0 -125
  176. package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
  177. package/apps/studio/test/canvas-build.test.ts +0 -141
  178. package/apps/studio/test/canvas-create-api.test.ts +0 -676
  179. package/apps/studio/test/canvas-cursors.test.ts +0 -164
  180. package/apps/studio/test/canvas-edit.test.ts +0 -470
  181. package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
  182. package/apps/studio/test/canvas-header.test.ts +0 -127
  183. package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
  184. package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
  185. package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
  186. package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
  187. package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
  188. package/apps/studio/test/canvas-list-watch.test.ts +0 -388
  189. package/apps/studio/test/canvas-media-drop.test.ts +0 -187
  190. package/apps/studio/test/canvas-meta-api.test.ts +0 -707
  191. package/apps/studio/test/canvas-move-api.test.ts +0 -587
  192. package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
  193. package/apps/studio/test/canvas-pipeline.test.ts +0 -180
  194. package/apps/studio/test/canvas-rects.test.ts +0 -198
  195. package/apps/studio/test/canvas-reorder.test.ts +0 -211
  196. package/apps/studio/test/canvas-route.test.ts +0 -179
  197. package/apps/studio/test/canvas-shell-base.test.ts +0 -107
  198. package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
  199. package/apps/studio/test/canvas-url.test.ts +0 -138
  200. package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
  201. package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
  202. package/apps/studio/test/capture-core.test.ts +0 -32
  203. package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
  204. package/apps/studio/test/chat-attachments.test.ts +0 -117
  205. package/apps/studio/test/chat-context.test.ts +0 -129
  206. package/apps/studio/test/chat-markdown.test.tsx +0 -58
  207. package/apps/studio/test/clip-addressing.test.ts +0 -738
  208. package/apps/studio/test/clip-ops.test.ts +0 -922
  209. package/apps/studio/test/cloud-build.test.ts +0 -95
  210. package/apps/studio/test/cloud-connect-note.test.ts +0 -135
  211. package/apps/studio/test/cloud-endpoints.test.ts +0 -590
  212. package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
  213. package/apps/studio/test/cloud-history-posture.test.ts +0 -230
  214. package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
  215. package/apps/studio/test/cloud-renew.test.ts +0 -205
  216. package/apps/studio/test/cloud-session-role.test.ts +0 -161
  217. package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
  218. package/apps/studio/test/cold-start-apply.test.ts +0 -303
  219. package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
  220. package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
  221. package/apps/studio/test/collab-bridge.test.ts +0 -79
  222. package/apps/studio/test/collab-loopback.test.ts +0 -63
  223. package/apps/studio/test/collab-origin-gate.test.ts +0 -323
  224. package/apps/studio/test/collab-protocol.test.ts +0 -146
  225. package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
  226. package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
  227. package/apps/studio/test/collab-room.test.ts +0 -188
  228. package/apps/studio/test/collab-session-survival.test.tsx +0 -176
  229. package/apps/studio/test/collab-stress.test.ts +0 -129
  230. package/apps/studio/test/comment-mount.test.ts +0 -97
  231. package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
  232. package/apps/studio/test/comments-api.test.ts +0 -277
  233. package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
  234. package/apps/studio/test/comments-overlay.test.ts +0 -117
  235. package/apps/studio/test/compile-entry.test.ts +0 -133
  236. package/apps/studio/test/component-map.test.ts +0 -48
  237. package/apps/studio/test/config-projection.test.ts +0 -104
  238. package/apps/studio/test/config-reload.test.ts +0 -230
  239. package/apps/studio/test/config-version.test.ts +0 -88
  240. package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
  241. package/apps/studio/test/convert-to-absolute.test.ts +0 -333
  242. package/apps/studio/test/credential-grammar.test.ts +0 -107
  243. package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
  244. package/apps/studio/test/csrf-write-guard.test.ts +0 -141
  245. package/apps/studio/test/debug-bundle.test.ts +0 -105
  246. package/apps/studio/test/design-setup-readiness.test.ts +0 -155
  247. package/apps/studio/test/detach-component.test.ts +0 -94
  248. package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
  249. package/apps/studio/test/dom-selection.test.ts +0 -198
  250. package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
  251. package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
  252. package/apps/studio/test/edit-persistence.test.ts +0 -91
  253. package/apps/studio/test/edit-scope-api.test.ts +0 -119
  254. package/apps/studio/test/edit-source-command.test.ts +0 -142
  255. package/apps/studio/test/edit-suppress.test.ts +0 -170
  256. package/apps/studio/test/editing-presence.test.ts +0 -103
  257. package/apps/studio/test/element-resize.test.ts +0 -136
  258. package/apps/studio/test/element-structural-api.test.ts +0 -434
  259. package/apps/studio/test/element-structural-edit.test.ts +0 -750
  260. package/apps/studio/test/ensure-browser.test.ts +0 -92
  261. package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
  262. package/apps/studio/test/export-assemble.test.ts +0 -53
  263. package/apps/studio/test/export-browser-lane.test.ts +0 -126
  264. package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
  265. package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
  266. package/apps/studio/test/export-center.test.tsx +0 -287
  267. package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
  268. package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
  269. package/apps/studio/test/export-lane.test.ts +0 -245
  270. package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
  271. package/apps/studio/test/exporters/canva.test.ts +0 -63
  272. package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
  273. package/apps/studio/test/exporters/endpoint.test.ts +0 -121
  274. package/apps/studio/test/exporters/history.test.ts +0 -107
  275. package/apps/studio/test/exporters/html.test.ts +0 -26
  276. package/apps/studio/test/exporters/jobs.test.ts +0 -342
  277. package/apps/studio/test/exporters/pdf.test.ts +0 -85
  278. package/apps/studio/test/exporters/png.test.ts +0 -103
  279. package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
  280. package/apps/studio/test/exporters/pptx.test.ts +0 -31
  281. package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
  282. package/apps/studio/test/exporters/runtime.test.ts +0 -59
  283. package/apps/studio/test/exporters/scope.test.ts +0 -292
  284. package/apps/studio/test/exporters/svg.test.ts +0 -29
  285. package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
  286. package/apps/studio/test/exporters/zip.test.ts +0 -104
  287. package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
  288. package/apps/studio/test/figjam-v3-model.test.ts +0 -342
  289. package/apps/studio/test/figma-explode.test.ts +0 -438
  290. package/apps/studio/test/figma-provenance.test.ts +0 -108
  291. package/apps/studio/test/figma-routes.test.ts +0 -294
  292. package/apps/studio/test/file-lock.test.ts +0 -84
  293. package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -13
  294. package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
  295. package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
  296. package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
  297. package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
  298. package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
  299. package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
  300. package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
  301. package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
  302. package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
  303. package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
  304. package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
  305. package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
  306. package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
  307. package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
  308. package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
  309. package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
  310. package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
  311. package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
  312. package/apps/studio/test/footage-store.test.ts +0 -100
  313. package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
  314. package/apps/studio/test/fs-watch.test.ts +0 -41
  315. package/apps/studio/test/gemma-models.test.ts +0 -245
  316. package/apps/studio/test/generate-route.test.ts +0 -106
  317. package/apps/studio/test/git-api.test.ts +0 -0
  318. package/apps/studio/test/git-branches.test.ts +0 -341
  319. package/apps/studio/test/git-cloud-posture.test.ts +0 -59
  320. package/apps/studio/test/git-grouping.test.ts +0 -106
  321. package/apps/studio/test/git-lifecycle.test.ts +0 -100
  322. package/apps/studio/test/git-log-format.test.ts +0 -95
  323. package/apps/studio/test/git-safe-rel.test.ts +0 -132
  324. package/apps/studio/test/git-system-engine.test.ts +0 -135
  325. package/apps/studio/test/git-watch.test.ts +0 -97
  326. package/apps/studio/test/github-api.test.ts +0 -577
  327. package/apps/studio/test/grid-track-handles.test.ts +0 -160
  328. package/apps/studio/test/handoff-static-frames.test.ts +0 -215
  329. package/apps/studio/test/handoff.test.ts +0 -329
  330. package/apps/studio/test/history-rollback.test.ts +0 -88
  331. package/apps/studio/test/hmr-broadcast.test.ts +0 -236
  332. package/apps/studio/test/hmr-classify.test.ts +0 -87
  333. package/apps/studio/test/hub-link.test.ts +0 -80
  334. package/apps/studio/test/identity-cache.test.ts +0 -83
  335. package/apps/studio/test/import-asset-browser.test.ts +0 -64
  336. package/apps/studio/test/import-asset-route.test.ts +0 -71
  337. package/apps/studio/test/import-asset.test.ts +0 -427
  338. package/apps/studio/test/import-brand-browser.test.ts +0 -88
  339. package/apps/studio/test/import-brand-route.test.ts +0 -120
  340. package/apps/studio/test/import-brand.test.ts +0 -206
  341. package/apps/studio/test/import-figma.test.ts +0 -667
  342. package/apps/studio/test/import-tokens.test.ts +0 -722
  343. package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
  344. package/apps/studio/test/input-router.test.ts +0 -757
  345. package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
  346. package/apps/studio/test/inspect-selections.test.ts +0 -219
  347. package/apps/studio/test/knob-props-authored.test.ts +0 -87
  348. package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
  349. package/apps/studio/test/locator.test.ts +0 -214
  350. package/apps/studio/test/marquee-overlay.test.ts +0 -94
  351. package/apps/studio/test/media-commit-chain.test.ts +0 -210
  352. package/apps/studio/test/mode-toggle.test.tsx +0 -226
  353. package/apps/studio/test/motion-sample.test.ts +0 -35
  354. package/apps/studio/test/move-artboards-command.test.ts +0 -108
  355. package/apps/studio/test/participants-chrome.test.ts +0 -65
  356. package/apps/studio/test/paths.test.ts +0 -88
  357. package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
  358. package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
  359. package/apps/studio/test/perf-harness.ts +0 -193
  360. package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
  361. package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
  362. package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
  363. package/apps/studio/test/photo-edit-api.test.ts +0 -201
  364. package/apps/studio/test/photo-filters.test.ts +0 -149
  365. package/apps/studio/test/photo-pipeline.test.ts +0 -106
  366. package/apps/studio/test/photo-taxonomy.test.ts +0 -96
  367. package/apps/studio/test/print-marks.test.ts +0 -113
  368. package/apps/studio/test/print-units.test.ts +0 -173
  369. package/apps/studio/test/read-annotations.test.ts +0 -727
  370. package/apps/studio/test/read-only-gate.test.ts +0 -188
  371. package/apps/studio/test/read-only-role.test.ts +0 -95
  372. package/apps/studio/test/readiness.test.ts +0 -226
  373. package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
  374. package/apps/studio/test/reorder-api.test.ts +0 -210
  375. package/apps/studio/test/repo-concurrency.test.ts +0 -146
  376. package/apps/studio/test/repo-lock.test.ts +0 -350
  377. package/apps/studio/test/report-proxy.test.ts +0 -125
  378. package/apps/studio/test/ripple.test.ts +0 -197
  379. package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
  380. package/apps/studio/test/runtime-bundle.test.ts +0 -69
  381. package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
  382. package/apps/studio/test/scaffold-design.test.ts +0 -122
  383. package/apps/studio/test/server-lifecycle.test.ts +0 -38
  384. package/apps/studio/test/session-runtime-state.test.ts +0 -226
  385. package/apps/studio/test/session-scope.test.ts +0 -100
  386. package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
  387. package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
  388. package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
  389. package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
  390. package/apps/studio/test/shared-doc-projection.test.ts +0 -238
  391. package/apps/studio/test/shell-importmap.test.ts +0 -56
  392. package/apps/studio/test/sizing-mode.test.ts +0 -102
  393. package/apps/studio/test/slash-commands.test.ts +0 -117
  394. package/apps/studio/test/snap-distance-pill.test.ts +0 -68
  395. package/apps/studio/test/spacing-handles.test.ts +0 -138
  396. package/apps/studio/test/specimen-select.test.ts +0 -88
  397. package/apps/studio/test/sync-agent.test.ts +0 -660
  398. package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
  399. package/apps/studio/test/sync-asset-push.test.ts +0 -931
  400. package/apps/studio/test/sync-atomic-write.test.ts +0 -62
  401. package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
  402. package/apps/studio/test/sync-autocommit.test.ts +0 -537
  403. package/apps/studio/test/sync-canvas-path.test.ts +0 -200
  404. package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
  405. package/apps/studio/test/sync-codec.test.ts +0 -230
  406. package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
  407. package/apps/studio/test/sync-cold-start.test.ts +0 -304
  408. package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
  409. package/apps/studio/test/sync-connection-state.test.ts +0 -337
  410. package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
  411. package/apps/studio/test/sync-decide-file.test.ts +0 -420
  412. package/apps/studio/test/sync-doc-name.test.ts +0 -281
  413. package/apps/studio/test/sync-echo-guard.test.ts +0 -96
  414. package/apps/studio/test/sync-file-ledger.test.ts +0 -334
  415. package/apps/studio/test/sync-file-membership.test.ts +0 -347
  416. package/apps/studio/test/sync-file-plane.test.ts +0 -976
  417. package/apps/studio/test/sync-file-pull.test.ts +0 -333
  418. package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
  419. package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
  420. package/apps/studio/test/sync-hardening.test.ts +0 -586
  421. package/apps/studio/test/sync-hub-listing.test.ts +0 -46
  422. package/apps/studio/test/sync-hubs-config.test.ts +0 -133
  423. package/apps/studio/test/sync-incident-replay.test.ts +0 -211
  424. package/apps/studio/test/sync-journal.test.ts +0 -176
  425. package/apps/studio/test/sync-meta-codec.test.ts +0 -199
  426. package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
  427. package/apps/studio/test/sync-move-retirement.test.ts +0 -231
  428. package/apps/studio/test/sync-panel-surface.test.ts +0 -153
  429. package/apps/studio/test/sync-path-pull.test.ts +0 -531
  430. package/apps/studio/test/sync-presentation.test.ts +0 -262
  431. package/apps/studio/test/sync-pull-budget.test.ts +0 -169
  432. package/apps/studio/test/sync-remote-docs.test.ts +0 -333
  433. package/apps/studio/test/sync-resync-routes.test.ts +0 -168
  434. package/apps/studio/test/sync-runtime.test.ts +0 -1675
  435. package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
  436. package/apps/studio/test/sync-settings-routes.test.ts +0 -195
  437. package/apps/studio/test/sync-settings.test.ts +0 -151
  438. package/apps/studio/test/sync-status.test.ts +0 -261
  439. package/apps/studio/test/sync-supervisor.test.ts +0 -262
  440. package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
  441. package/apps/studio/test/sync-trash.test.ts +0 -132
  442. package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
  443. package/apps/studio/test/sync-untrusted.test.ts +0 -103
  444. package/apps/studio/test/system-endpoint.test.ts +0 -144
  445. package/apps/studio/test/text-editability-stamp.test.ts +0 -131
  446. package/apps/studio/test/timeline-comp-target.test.ts +0 -139
  447. package/apps/studio/test/timeline-media-cache.test.ts +0 -41
  448. package/apps/studio/test/timeline-parse.test.ts +0 -184
  449. package/apps/studio/test/timeline-scale.test.ts +0 -65
  450. package/apps/studio/test/timeline-snap.test.ts +0 -85
  451. package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
  452. package/apps/studio/test/tool-palette.test.tsx +0 -55
  453. package/apps/studio/test/tour-overlay.test.tsx +0 -90
  454. package/apps/studio/test/trusted-request-host.test.ts +0 -66
  455. package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
  456. package/apps/studio/test/undo-stack.test.ts +0 -244
  457. package/apps/studio/test/use-agent-presence.test.tsx +0 -117
  458. package/apps/studio/test/use-annotation-resize.test.ts +0 -402
  459. package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
  460. package/apps/studio/test/use-artboard-drag.test.ts +0 -324
  461. package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
  462. package/apps/studio/test/use-collab.test.ts +0 -0
  463. package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
  464. package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
  465. package/apps/studio/test/use-selection-set.test.tsx +0 -166
  466. package/apps/studio/test/use-snap-guides.test.ts +0 -271
  467. package/apps/studio/test/use-tool-mode.test.tsx +0 -186
  468. package/apps/studio/test/use-undo-stack.test.tsx +0 -223
  469. package/apps/studio/test/video-asset.test.ts +0 -163
  470. package/apps/studio/test/video-comp-fixture.test.ts +0 -50
  471. package/apps/studio/test/video-comp.test.ts +0 -270
  472. package/apps/studio/test/video-encode-lib.test.ts +0 -63
  473. package/apps/studio/test/video-render-bridge.test.ts +0 -171
  474. package/apps/studio/test/whats-new.test.ts +0 -113
  475. package/apps/studio/test/whisper-setup.test.ts +0 -97
  476. package/apps/studio/test/workspace-containment.test.ts +0 -382
  477. package/apps/studio/test/workspace-signin.test.ts +0 -292
  478. package/apps/studio/test/ws-handshake.test.ts +0 -33
  479. package/cli/commands/cache.test.mjs +0 -166
  480. package/cli/commands/design-link.test.mjs +0 -419
  481. package/cli/commands/design.test.mjs +0 -240
  482. package/cli/commands/doctor.test.mjs +0 -237
  483. package/cli/commands/hub-workspace.test.mjs +0 -275
  484. package/cli/commands/hub.test.mjs +0 -225
  485. package/cli/commands/kg.test.mjs +0 -142
  486. package/cli/commands/preflight.test.mjs +0 -46
  487. package/cli/lib/argv.test.mjs +0 -46
  488. package/cli/lib/cache.test.mjs +0 -303
  489. package/cli/lib/cell-plan.test.mjs +0 -225
  490. package/cli/lib/config-lint.test.mjs +0 -117
  491. package/cli/lib/ddr-to-kgai.test.mjs +0 -118
  492. package/cli/lib/design-ownership.test.mjs +0 -329
  493. package/cli/lib/fetch-asset.test.mjs +0 -213
  494. package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
  495. package/cli/lib/figma-import-controls.test.mjs +0 -70
  496. package/cli/lib/flow-design-integration.test.mjs +0 -165
  497. package/cli/lib/gitignore-block.test.mjs +0 -136
  498. package/cli/lib/gitignore-drift.test.mjs +0 -156
  499. package/cli/lib/hubs-config.test.mjs +0 -165
  500. package/cli/lib/pkg-root.test.mjs +0 -111
  501. package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
  502. package/cli/lib/plugin-name-namespace.test.mjs +0 -71
  503. package/cli/lib/reconstruct-toolset.test.mjs +0 -194
  504. package/cli/lib/stack-detect.test.mjs +0 -121
  505. package/cli/lib/update-check.test.mjs +0 -32
  506. package/cli/lib/workspace-plan.test.mjs +0 -699
@@ -86,6 +86,13 @@ function parseEntry(raw: unknown): JournalEntry | null {
86
86
  * Returns null when the hub is unreachable, refuses, or does not have the
87
87
  * route — the `fetchRemoteListing` posture: sync continues either way and we
88
88
  * ask again later. A null is never "nothing changed".
89
+ *
90
+ * It is also never "and there is nothing to learn from it". The cursor read
91
+ * shares the hub's per-label read bucket with the file pulls, so during a rate
92
+ * limit THIS is the request that meets the wall first — and collapsing that to
93
+ * a bare null is how issue #109 stayed invisible: a held plane and a
94
+ * quiet, ordinary, do-nothing pass are indistinguishable to the caller.
95
+ * `onRefused` hands the response to whoever can tell the difference.
89
96
  */
90
97
  export async function fetchJournal(opts: {
91
98
  hubUrl: string;
@@ -94,6 +101,8 @@ export async function fetchJournal(opts: {
94
101
  /** Send the epoch a cursor belongs to so a mismatch fails closed hub-side. */
95
102
  epoch?: string | null;
96
103
  fetchImpl?: typeof fetch;
104
+ /** Called with the response before a non-ok status becomes `null`. */
105
+ onRefused?: (res: Response) => void | Promise<void>;
97
106
  }): Promise<JournalPage | null> {
98
107
  const fetchImpl = opts.fetchImpl ?? fetch;
99
108
  const base = opts.hubUrl.replace(/\/+$/, '');
@@ -105,7 +114,10 @@ export async function fetchJournal(opts: {
105
114
  headers: { authorization: `Bearer ${opts.token}` },
106
115
  signal: AbortSignal.timeout(JOURNAL_TIMEOUT_MS),
107
116
  });
108
- if (!res.ok) return null;
117
+ if (!res.ok) {
118
+ await opts.onRefused?.(res);
119
+ return null;
120
+ }
109
121
  const body = (await res.json()) as Record<string, unknown>;
110
122
  // The head and the epoch are the anchor the whole cursor protocol rests
111
123
  // on, and both arrive from a component DDR-054 calls untrusted. A
@@ -49,8 +49,13 @@ import {
49
49
  Y_SYNC_TYPES,
50
50
  } from './codec.ts';
51
51
  import type { ColdStartAction } from './cold-start.ts';
52
- import { decideAnnotationsColdStart, decideColdStart, unionCommentsById } from './cold-start.ts';
53
- import { applyColdStart } from './cold-start-apply.ts';
52
+ import {
53
+ decideAnnotationsColdStart,
54
+ decideColdStart,
55
+ decideCssColdStart,
56
+ unionCommentsById,
57
+ } from './cold-start.ts';
58
+ import { applyColdStart, type ColdStartSnapshotReason } from './cold-start-apply.ts';
54
59
  import { hashBytes } from './echo-guard.ts';
55
60
  import type { SyncJournal } from './journal.ts';
56
61
  import { ORIGINS } from './origins.ts';
@@ -74,7 +79,7 @@ export interface MigrateSeedOptions {
74
79
  /** DDR-102 — per-machine journal; gates fast-forward vs conflict. */
75
80
  journal?: SyncJournal;
76
81
  /** DDR-102 — body snapshot writer (history.ts), same contract as the agent's. */
77
- snapshot?: (content: string, reason: 'pre-sync-local' | 'pre-sync-hub') => Promise<string | null>;
82
+ snapshot?: (content: string, reason: ColdStartSnapshotReason) => Promise<string | null>;
78
83
  /**
79
84
  * Does the HUB hold state for this slug, per its last document listing?
80
85
  *
@@ -298,6 +303,38 @@ export async function migrateSeed(opts: MigrateSeedOptions): Promise<MigrateSeed
298
303
  // annotations doc→file half and materializes it (persistJson).
299
304
  }
300
305
 
306
+ // ---- css: PER-LANE resolution (issue #114), the same table agent.ts uses.
307
+ // `rebuildBodyFromLocal` above already re-seeds css when the BODY winner was
308
+ // local; this block is what covers every other row — above all the exact-repeat
309
+ // collapse, which is how a `CSS × N` lane from a concurrent cold seed gets
310
+ // back to one copy on a hub-wins boot. It no-ops when the two sides already
311
+ // agree, so running after the body applier costs nothing.
312
+ if (paths.css) {
313
+ const docCssNow = doc.getText(Y_SYNC_TYPES.css).toString();
314
+ const cssDecision = decideCssColdStart({
315
+ local: localCss,
316
+ doc: docCssNow.length > 0 ? docCssNow : null,
317
+ journalHash: opts.journal?.get(slug)?.cssHash ?? null,
318
+ hash: hashBytes,
319
+ bodyWinner: applied.bodyWinner,
320
+ });
321
+ if (cssDecision.recoveredDuplication) {
322
+ // Warn, don't snapshot — see the note on the same branch in agent.ts
323
+ // reconcile. An exact integer repeat carries no unique bytes to recover,
324
+ // and snapshotting it hands a hostile hub an eviction lever against the
325
+ // `_history/` copies that DO.
326
+ console.warn(`[sync/${slug}] shared-doc cold-start css: ${cssDecision.reason}`);
327
+ }
328
+ if (cssDecision.winner === 'local' && localCss !== null) {
329
+ doc.transact(() => {
330
+ applyCssToDoc(doc, localCss, ORIGINS.MIGRATION);
331
+ }, ORIGINS.MIGRATION);
332
+ opts.journal?.record(slug, { cssHash: hashBytes(localCss) });
333
+ }
334
+ // winner 'hub' → nothing to write here: the projection materializes the
335
+ // doc's css to disk, exactly as it does for the body.
336
+ }
337
+
301
338
  // Comments id-union (DDR-102): rebuild from merged JSON via the
302
339
  // delete-then-insert codec — same-id entries keep the doc's version, so the
303
340
  // duplication trap stays closed; local-only comments survive.
@@ -170,7 +170,26 @@ export function syncPresentation(
170
170
  status.startedAt > 0
171
171
  ? status.startedAt
172
172
  : null;
173
- const stalled = (): SyncPresentation => {
173
+ /**
174
+ * @param credentialUnknown Can an expired sign-in still be the cause?
175
+ *
176
+ * ONLY BLAME THE SIGN-IN WHEN THE SIGN-IN COULD BE THE PROBLEM (issue #118).
177
+ * This unconditionally told the person to reconnect the workspace because
178
+ * "the sign-in may have expired" — at call sites that have ALREADY proved
179
+ * `rejected === 0`, i.e. where the hub has refused exactly nothing and the
180
+ * credential is demonstrably fine. Naming a cause we have evidence against is
181
+ * worse than naming none: it sent the reporter to Resync, which re-handshakes
182
+ * every canvas against a hub that is not answering, times out the fresh
183
+ * monitor's grace window, and lands on `offline`. The product talked the
184
+ * person into making its own display worse.
185
+ *
186
+ * A rejection has its OWN branch (`phase: 'refused'`) and outranks this one,
187
+ * and it is the branch that may legitimately say "credential". Here we say
188
+ * what we actually know: the hub is not completing handshakes. Only the
189
+ * legacy no-`docs` payload, where refusals are unknowable, keeps the older
190
+ * hedge — and it is written as a possibility rather than a diagnosis.
191
+ */
192
+ const stalled = (credentialUnknown = false): SyncPresentation => {
174
193
  // `?? now` rather than a cast: every call site is behind `isStalled`, which
175
194
  // has already proved `startedAt` is a number — and a future one that isn't
176
195
  // reads "1 minute" instead of "NaN minutes".
@@ -180,7 +199,9 @@ export function syncPresentation(
180
199
  online: false,
181
200
  label: 'stalled',
182
201
  title: `Nothing has synced with ${project} in the ${min} minute${min === 1 ? '' : 's'} since Maude started trying.`,
183
- next: 'Reconnect the workspace — the sign-in may have expired.',
202
+ next: credentialUnknown
203
+ ? 'Maude keeps retrying. If it persists, reconnect the workspace — the sign-in may have expired.'
204
+ : 'Maude keeps retrying — the workspace is reachable but is not completing handshakes.',
184
205
  names: [],
185
206
  };
186
207
  };
@@ -265,7 +286,9 @@ export function syncPresentation(
265
286
  // Pre-DDR-102 payload: `state` is all there is. Report it as the weaker
266
287
  // evidence it is, rather than promoting it to a document-level claim.
267
288
  const online = status.state === 'online' || status.flash === 'synced';
268
- if (!online && isStalled) return stalled();
289
+ // Pre-DDR-102 payload — no per-document counts, so a refusal cannot be
290
+ // ruled out and the credential hedge stays.
291
+ if (!online && isStalled) return stalled(true);
269
292
  return {
270
293
  phase: online ? 'synced' : 'connecting',
271
294
  online,
@@ -0,0 +1,169 @@
1
+ // What a refusal MEANS — shared by every sync lane that talks to a hub.
2
+ //
3
+ // This module exists because the lesson had to be learned twice. The
4
+ // 2026-08-11 RCA (alligators, 182 assets, ~5 moved per boot) taught the asset
5
+ // push lane that a 429 is not a failure but an instruction: the hub says when
6
+ // to come back, so come back then, and keep enough of the body to tell "rate
7
+ // limit exceeded" from a Cloudflare error page. All of it lived as private
8
+ // constants inside `asset-push.ts`.
9
+ //
10
+ // So when the journal file plane (DDR-226) became the ONE lane for project
11
+ // files, it inherited none of it — `HTTP ${res.status}` and nothing else. A
12
+ // 429 read as an ordinary error, the pass fired its remaining requests into a
13
+ // closed door, and because the cursor only advances on a clean pass the very
14
+ // next tick re-requested the identical set. The retries were what kept the
15
+ // bucket empty. Issue #109: every asset of a linked project refused, forever,
16
+ // while the status bar said `synced` and a person saw broken images.
17
+ //
18
+ // One copy, imported by both lanes, so there is no third time.
19
+
20
+ /** Longest single pause a hub can ask for. Matches the hub's rate-limit
21
+ * window (60 s) — a `Retry-After` larger than that is either a typo or a hub
22
+ * we should not be blocking a boot sweep on. */
23
+ export const MAX_RETRY_DELAY_MS = 60_000;
24
+
25
+ /** Where an absent/unparsable `Retry-After` lands. Old hubs (pre-fix) send a
26
+ * bare 429 with no header, and their window is the same 60 s. */
27
+ export const DEFAULT_RETRY_DELAY_MS = 60_000;
28
+
29
+ /**
30
+ * Shortest pause a hub can talk us into.
31
+ *
32
+ * The clamp above stops a hub asking for a pause so long it bricks the lane.
33
+ * This is the other end of the same argument, and it is the one that matters:
34
+ * the header is hub-controlled, `Retry-After: 0.001` parses as a perfectly
35
+ * valid millisecond, and a hold of one millisecond is not a hold. A hub that
36
+ * refuses everything and asks us straight back would put us into the issue-#109
37
+ * storm again — a refusal loop at full poll rate — with the client believing it
38
+ * was being obedient the whole time.
39
+ */
40
+ export const MIN_RETRY_DELAY_MS = 1_000;
41
+
42
+ /** How much of an error body reaches a `reason`. Enough to tell "rate limit
43
+ * exceeded" from a Cloudflare error page — the distinction the 2026-08-11 RCA
44
+ * had to reconstruct from edge logs because the client kept only a status. */
45
+ export const ERROR_SNIPPET_CHARS = 80;
46
+
47
+ /** How much of an error body we will READ to produce those characters.
48
+ * Bounded on purpose: the body comes from a component DDR-054 calls
49
+ * untrusted, and `await res.text()` on a body that never ends is a heap fill
50
+ * dressed as diagnostics. */
51
+ const MAX_SNIPPET_READ_BYTES = 8 * 1024;
52
+
53
+ /**
54
+ * `Retry-After: <seconds>` → ms, clamped.
55
+ *
56
+ * Only the delta-seconds form is parsed; the HTTP-date form is not something
57
+ * our hub emits, and guessing at clock skew to support it would make the
58
+ * pause less predictable, not more.
59
+ */
60
+ export function retryAfterMs(header: string | null | undefined): number {
61
+ const secs = Number(String(header ?? '').trim());
62
+ if (!Number.isFinite(secs) || secs <= 0) return DEFAULT_RETRY_DELAY_MS;
63
+ // Clamped at BOTH ends — the ceiling stops a hub bricking the lane, the
64
+ // floor stops one talking us out of pausing at all.
65
+ return Math.min(Math.max(secs * 1000, MIN_RETRY_DELAY_MS), MAX_RETRY_DELAY_MS);
66
+ }
67
+
68
+ /** Read at most `MAX_SNIPPET_READ_BYTES` of a body, then hang up. */
69
+ async function readBounded(res: Response): Promise<string> {
70
+ const body = res.body;
71
+ if (!body?.getReader) {
72
+ // No stream to cap (a fixture `Response`, an older polyfill). The text is
73
+ // still bounded on the way out by `ERROR_SNIPPET_CHARS`.
74
+ try {
75
+ return (await res.text()).slice(0, MAX_SNIPPET_READ_BYTES);
76
+ } catch {
77
+ return '';
78
+ }
79
+ }
80
+ const reader = body.getReader();
81
+ const chunks: Uint8Array[] = [];
82
+ let total = 0;
83
+ try {
84
+ while (total < MAX_SNIPPET_READ_BYTES) {
85
+ const { done, value } = await reader.read();
86
+ if (done) break;
87
+ if (value) {
88
+ chunks.push(value);
89
+ total += value.byteLength;
90
+ }
91
+ }
92
+ } catch {
93
+ /* a body we cannot read tells us nothing — the status still does */
94
+ } finally {
95
+ void reader.cancel().catch(() => {});
96
+ }
97
+ const joined = new Uint8Array(total);
98
+ let at = 0;
99
+ for (const c of chunks) {
100
+ joined.set(c, at);
101
+ at += c.byteLength;
102
+ }
103
+ return new TextDecoder().decode(joined);
104
+ }
105
+
106
+ /**
107
+ * Why a request was refused, in words — status PLUS a bounded snippet of the
108
+ * body. The hub says `{"error":"rate limit exceeded"}`; an edge that never
109
+ * reached the hub says HTML. Those are different bugs and the Sync panel
110
+ * should not make a person read logs to tell them apart.
111
+ *
112
+ * The body is hub-supplied ⇒ untrusted (DDR-054): control characters stripped,
113
+ * whitespace collapsed, hard length cap, and it only ever renders as text.
114
+ */
115
+ export async function failureReason(res: Response): Promise<string> {
116
+ const snippet = (await readBounded(res))
117
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point.
118
+ .replace(/[\u0000-\u001f\u007f]/g, ' ')
119
+ .replace(/\s+/g, ' ')
120
+ .trim()
121
+ .slice(0, ERROR_SNIPPET_CHARS);
122
+ return snippet ? `HTTP ${res.status} — ${snippet}` : `HTTP ${res.status}`;
123
+ }
124
+
125
+ /** A refusal that names a WALL rather than a fault: the hub is willing, just
126
+ * not now. Every lane that can hit one reports it in this shape. */
127
+ export interface RateLimited {
128
+ rateLimited: true;
129
+ /** How long the hub asked us to wait, already clamped. */
130
+ retryAfterMs: number;
131
+ }
132
+
133
+ /** Is this response the hub telling us to slow down? */
134
+ export function isRateLimited(res: Pick<Response, 'status'>): boolean {
135
+ return res.status === 429;
136
+ }
137
+
138
+ /**
139
+ * Is this response BACKPRESSURE — the peer is willing, just not right now?
140
+ *
141
+ * Wider than `isRateLimited`, and the two are deliberately both exported:
142
+ * a 429 has one precise meaning that other call sites depend on, while this is
143
+ * the question the transfer lanes actually want to ask.
144
+ *
145
+ * Two cases:
146
+ * - 429, always. It has one meaning even bare, and `retryAfterMs` supplies a
147
+ * sane default when the header is missing.
148
+ * - 503 or 502 that CARRIES a `Retry-After`. The header is the peer saying
149
+ * "come back at this time", which is precisely what distinguishes a busy
150
+ * peer from a broken one.
151
+ *
152
+ * THE HEADER IS THE DISCRIMINATOR, and it is doing real work rather than being
153
+ * a formality. A cell that is starting answers `503 Retry-After: <n>` (see
154
+ * `cell-do.mjs` `#resolveStorageCredentials`), and firing this pass's whole
155
+ * request budget into that window is how a slow start becomes a failed one.
156
+ * A cell that genuinely cannot obtain storage answers a BARE 503 — a broken
157
+ * deployment, not a wall — and that one must stay an ordinary refusal so the
158
+ * request ceiling bounds it and the failure surfaces per file.
159
+ *
160
+ * Treating every 5xx as backpressure would erase exactly that distinction and
161
+ * make a broken hub look like a busy one, forever.
162
+ */
163
+ export function isBackpressure(res: Pick<Response, 'status'> & { headers?: Headers }): boolean {
164
+ if (res.status === 429) return true;
165
+ if (res.status === 503 || res.status === 502) {
166
+ return Boolean(res.headers?.get?.('retry-after'));
167
+ }
168
+ return false;
169
+ }
@@ -0,0 +1,218 @@
1
+ // How far the file plane has got — derived from the LEDGER, which is the one
2
+ // thing that stayed correct.
3
+ //
4
+ // THE PROBLEM THIS SOLVES. During two real runs against an 8.8 GB project the
5
+ // status payload reported `files: {synced: 0, pushed: 0, pulled: 0}` for twenty
6
+ // minutes while 2 961 ledger rows changed underneath it. The counters were not
7
+ // broken so much as mis-sourced: `sync/index.ts` derived them from PER-PASS
8
+ // results, and a pass where nothing converges is legitimately all zeros. The
9
+ // ledger, meanwhile, was current to within a second the whole time and nothing
10
+ // read it.
11
+ //
12
+ // And even correct counters could not answer the question a seed actually
13
+ // raises. `synced`, `pushed`, `pulled` are numerators with no denominator, so
14
+ // no surface could say "1 412 of 2 961" — only "1 412", which is
15
+ // indistinguishable from "1 412 and finished".
16
+ //
17
+ // A LEAF MODULE, like `limits.ts`, and for the same reason: `status.ts` will
18
+ // import this, and this must never import back. It takes ledger rows and
19
+ // returns a shape. No disk, no clock beyond what it is handed, no I/O.
20
+
21
+ import type { DeliveryState, LedgerRow } from './file-ledger.ts';
22
+
23
+ /** Why nothing is moving, when nothing is moving. */
24
+ export type BlockedClass = 'too-large' | 'quota' | 'unreachable' | 'refused';
25
+
26
+ export type SeedPhase =
27
+ /** Walking the tree; the denominator is not final yet. */
28
+ | 'scanning'
29
+ /** Moving files, with work outstanding. */
30
+ | 'seeding'
31
+ /** A wall we are waiting out — a rate limit, a quota, an unreachable peer. */
32
+ | 'paused'
33
+ /** Outstanding work that no amount of waiting will move on its own. */
34
+ | 'blocked'
35
+ /** Nothing outstanding. */
36
+ | 'converged';
37
+
38
+ export interface SeedProgress {
39
+ phase: SeedPhase;
40
+ /** Rows in scope — THE DENOMINATOR the old counters never had. */
41
+ tracked: number;
42
+ /** Rows in a terminal-good state. */
43
+ delivered: number;
44
+ /** `tracked - delivered - blocked`, floored at 0. */
45
+ remaining: number;
46
+ /** Bytes still to move, as far as the ledger knows. Best-effort. */
47
+ bytesRemaining: number;
48
+ /** Outstanding rows no retry will clear on its own, by class. */
49
+ blocked: { class: BlockedClass; count: number }[];
50
+ /** Which ceiling ended the last pass, when one did. */
51
+ passCapped?: 'requests' | 'files' | 'bytes';
52
+ /** null until a real throughput sample exists — NEVER a guess. */
53
+ etaMs: number | null;
54
+ /** When this seed was first observed to have outstanding work. */
55
+ startedAt: number | null;
56
+ }
57
+
58
+ /**
59
+ * States that mean "this file is where it needs to be".
60
+ *
61
+ * `on-hub` counts. A stricter bar (`durable`, `everywhere`) would be more
62
+ * truthful about replication but would make the denominator un-completable for
63
+ * a project whose hub has not finished mirroring — a progress bar that never
64
+ * reaches the end is worse than one that reaches it slightly early, and the
65
+ * mirroring state has its own surface.
66
+ */
67
+ const DELIVERED: ReadonlySet<DeliveryState> = new Set<DeliveryState>([
68
+ 'on-hub',
69
+ 'durable',
70
+ 'at-peer',
71
+ 'ui-healed',
72
+ 'everywhere',
73
+ ]);
74
+
75
+ /** States that will not move again without something changing. */
76
+ const BLOCKED: ReadonlySet<DeliveryState> = new Set<DeliveryState>([
77
+ 'refused',
78
+ 'referenced-but-unoffered',
79
+ ]);
80
+
81
+ export interface SeedProgressInput {
82
+ rows: Record<string, LedgerRow>;
83
+ now: number;
84
+ /** The lane is holding until this instant, if it is. */
85
+ pausedUntil?: number | null;
86
+ /** Why it is holding — carried through to the phase and the wording. */
87
+ pauseCause?: 'hub-asked' | 'unreachable' | 'quota' | null;
88
+ /** Set when the last pass stopped on a ceiling rather than on the work. */
89
+ passCapped?: 'requests' | 'files' | 'bytes';
90
+ /** True while the first scan of this boot has not finished. */
91
+ scanning?: boolean;
92
+ /** Previous result, for `startedAt` continuity and the throughput sample. */
93
+ previous?: SeedProgress | null;
94
+ /** Bytes delivered since `previous` was computed, if the caller knows. */
95
+ deliveredSince?: { bytes: number; ms: number } | null;
96
+ }
97
+
98
+ /**
99
+ * Fold the ledger into one progress shape.
100
+ *
101
+ * Total and pure. Every field is derivable from the rows plus what the caller
102
+ * observed; nothing is inferred from a wall clock this function reads itself.
103
+ */
104
+ export function computeSeedProgress(input: SeedProgressInput): SeedProgress {
105
+ const { rows, now } = input;
106
+ let tracked = 0;
107
+ let delivered = 0;
108
+ let bytesRemaining = 0;
109
+ const blockedCounts = new Map<BlockedClass, number>();
110
+
111
+ for (const row of Object.values(rows)) {
112
+ tracked += 1;
113
+ const state = row.state;
114
+ if (state && DELIVERED.has(state)) {
115
+ delivered += 1;
116
+ continue;
117
+ }
118
+ if (state && BLOCKED.has(state)) {
119
+ const cls = classifyBlocked(row);
120
+ blockedCounts.set(cls, (blockedCounts.get(cls) ?? 0) + 1);
121
+ continue;
122
+ }
123
+ // Outstanding. `size` is the stat cache, so it is what we know rather than
124
+ // what is true — a row we have never stat'd contributes nothing, which is
125
+ // why `bytesRemaining` is documented as best-effort rather than exact.
126
+ if (Number.isFinite(row.size)) bytesRemaining += row.size as number;
127
+ }
128
+
129
+ const blocked = [...blockedCounts.entries()]
130
+ .map(([cls, count]) => ({ class: cls, count }))
131
+ .sort((a, b) => b.count - a.count);
132
+ const blockedTotal = blocked.reduce((n, b) => n + b.count, 0);
133
+ const remaining = Math.max(0, tracked - delivered - blockedTotal);
134
+
135
+ const paused =
136
+ typeof input.pausedUntil === 'number' && Number.isFinite(input.pausedUntil)
137
+ ? input.pausedUntil > now
138
+ : false;
139
+
140
+ const phase: SeedPhase = input.scanning
141
+ ? 'scanning'
142
+ : paused
143
+ ? 'paused'
144
+ : remaining > 0
145
+ ? 'seeding'
146
+ : blockedTotal > 0
147
+ ? 'blocked'
148
+ : 'converged';
149
+
150
+ return {
151
+ phase,
152
+ tracked,
153
+ delivered,
154
+ remaining,
155
+ bytesRemaining,
156
+ blocked,
157
+ ...(input.passCapped ? { passCapped: input.passCapped } : {}),
158
+ etaMs: estimateEta(bytesRemaining, input.deliveredSince ?? null),
159
+ startedAt: resolveStartedAt(input, remaining, now),
160
+ };
161
+ }
162
+
163
+ /**
164
+ * An ETA, or `null`.
165
+ *
166
+ * NULL UNTIL THERE IS A REAL SAMPLE, and that restraint is the point. During
167
+ * the 2026-09-03 investigation a two-second throughput sample read as "4 MB/s,
168
+ * about ten minutes left" and was reported to a person as such; the sustained
169
+ * rate was ~270 kB/s of retry traffic and the true answer was "never". A
170
+ * fabricated ETA does not merely mislead, it stops people looking.
171
+ *
172
+ * So: no sample, no number. And a sample of zero delivered bytes is not a
173
+ * slow rate, it is no information at all.
174
+ */
175
+ function estimateEta(
176
+ bytesRemaining: number,
177
+ sample: { bytes: number; ms: number } | null
178
+ ): number | null {
179
+ if (!sample || sample.bytes <= 0 || sample.ms <= 0) return null;
180
+ const bytesPerMs = sample.bytes / sample.ms;
181
+ if (!Number.isFinite(bytesPerMs) || bytesPerMs <= 0) return null;
182
+ const ms = bytesRemaining / bytesPerMs;
183
+ if (!Number.isFinite(ms)) return null;
184
+ // A week is not an estimate, it is a way of saying we do not know.
185
+ return ms > 7 * 24 * 3_600_000 ? null : Math.round(ms);
186
+ }
187
+
188
+ /** Keep the original start across ticks; clear it once there is nothing left. */
189
+ function resolveStartedAt(input: SeedProgressInput, remaining: number, now: number): number | null {
190
+ if (remaining <= 0) return null;
191
+ const prev = input.previous?.startedAt;
192
+ return typeof prev === 'number' && Number.isFinite(prev) ? prev : now;
193
+ }
194
+
195
+ /**
196
+ * Which wall this row is behind.
197
+ *
198
+ * FROM THE ROW'S OWN `blockedClass`, which the writer set at the moment it
199
+ * refused. The first version matched on `row.reason` and claimed in this very
200
+ * comment that the matching was "never on hub-supplied text" — which was wrong
201
+ * by one hop: `reason` can be a `failureReason()` string embedding a bounded
202
+ * snippet of a hub error body, so a hostile hub answering with a body
203
+ * containing "too big" could steer how its own refusals were labelled. Only
204
+ * cosmetic, and only in the panel, but a classifier a peer can influence is not
205
+ * a classifier.
206
+ *
207
+ * The string fallback stays for rows written before the field existed — bounded
208
+ * to our own sentences, and it degrades to `refused`, which is the honest
209
+ * answer when we do not know.
210
+ */
211
+ function classifyBlocked(row: LedgerRow): BlockedClass {
212
+ if (row.blockedClass) return row.blockedClass;
213
+ const reason = (row.reason ?? '').toLowerCase();
214
+ if (reason.startsWith('too big')) return 'too-large';
215
+ if (reason.startsWith("this project's upload allowance")) return 'quota';
216
+ if (reason.startsWith('could not reach')) return 'unreachable';
217
+ return 'refused';
218
+ }