@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.
- package/README.md +3 -1
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +821 -55
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-edit.ts +391 -7
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +943 -248
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/export-center.jsx +125 -67
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +238 -4
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +69 -18
- package/apps/studio/client/styles/4-components.css +44 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +137 -17
- package/apps/studio/collab/registry.ts +19 -21
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1911 -1879
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +296 -19
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +150 -0
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +69 -6
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +104 -45
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +192 -34
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +926 -47
- package/apps/studio/sync/index.ts +1786 -36
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +150 -22
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +311 -5
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +201 -6
- package/apps/studio/sync/transaction-client.ts +460 -0
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/whats-new.json +173 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +892 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +33 -9
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +47 -0
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/plugins/flow/dependencies.json +3 -3
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -545
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -337
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
// The studio's transaction client — DDR-241 §3, plan T13.
|
|
2
|
+
//
|
|
3
|
+
// In accepted-revisions mode the studio never writes the shared document. A
|
|
4
|
+
// persistent change becomes a PROPOSAL: exact request bytes are written to the
|
|
5
|
+
// durable outbox (`<designRoot>/_state/outbox/`, runtime state) BEFORE they are
|
|
6
|
+
// sent, so a crash, a dropped ACK or a restart can never turn one action into
|
|
7
|
+
// two or lose it. A retry sends the same bytes; an unknown outcome is resolved
|
|
8
|
+
// with `GET /transactions/:id` before anything is resent.
|
|
9
|
+
//
|
|
10
|
+
// Proposals are sent one at a time in creation order — a later edit to the
|
|
11
|
+
// same canvas can never overtake an earlier one.
|
|
12
|
+
|
|
13
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
14
|
+
import { mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
export const PROTOCOL = 1;
|
|
18
|
+
|
|
19
|
+
export type LaneName = 'html' | 'css' | 'meta' | 'annotations' | 'comments';
|
|
20
|
+
|
|
21
|
+
export interface Operation {
|
|
22
|
+
op: string;
|
|
23
|
+
[k: string]: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ProposalResult {
|
|
27
|
+
protocol: number;
|
|
28
|
+
status: 'accepted' | 'rejected';
|
|
29
|
+
code?: string;
|
|
30
|
+
transactionId: string | null;
|
|
31
|
+
revision?: number;
|
|
32
|
+
actionId?: string;
|
|
33
|
+
merged?: { doc: string; lane: string; merged: boolean }[];
|
|
34
|
+
skipped?: { doc?: string; lane?: string; reason: string }[];
|
|
35
|
+
head?: string;
|
|
36
|
+
[k: string]: unknown;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Bootstrap {
|
|
40
|
+
projectId: string;
|
|
41
|
+
mode: string;
|
|
42
|
+
epoch: number;
|
|
43
|
+
revision: number;
|
|
44
|
+
docs: {
|
|
45
|
+
doc: string;
|
|
46
|
+
entry: string;
|
|
47
|
+
generation: number;
|
|
48
|
+
path: string | null;
|
|
49
|
+
retired: boolean;
|
|
50
|
+
lanes: Record<string, { hash: string; effect: string }>;
|
|
51
|
+
}[];
|
|
52
|
+
dirs: string[];
|
|
53
|
+
you?: { actor: string; readOnly: boolean };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface OutboxAction {
|
|
57
|
+
kind?: string;
|
|
58
|
+
label: string;
|
|
59
|
+
operations: Operation[];
|
|
60
|
+
dependsOn?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface OutboxEntry {
|
|
64
|
+
transactionId: string;
|
|
65
|
+
createdAt: number;
|
|
66
|
+
/** The exact request body — resent byte-for-byte once it has been sent. */
|
|
67
|
+
bytes: string;
|
|
68
|
+
label: string;
|
|
69
|
+
/** The action, so an epoch-stale entry can be rebased as a new transaction. */
|
|
70
|
+
action?: OutboxAction;
|
|
71
|
+
/** Written before the project id was known; bound (once) before sending. */
|
|
72
|
+
unbound?: boolean;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface TransactionClientOptions {
|
|
76
|
+
hubUrl: string;
|
|
77
|
+
token: () => string | null;
|
|
78
|
+
designRoot: string;
|
|
79
|
+
fetchImpl?: typeof fetch;
|
|
80
|
+
log?: Pick<Console, 'log' | 'warn' | 'error'>;
|
|
81
|
+
deviceId?: string;
|
|
82
|
+
/** Status hook — how many actions are waiting for acceptance. */
|
|
83
|
+
onPending?: (count: number) => void;
|
|
84
|
+
/** Every final result (accepted or rejected), in order. */
|
|
85
|
+
onResult?: (result: ProposalResult, action: { label: string; operations: Operation[] }) => void;
|
|
86
|
+
/** T29 — what a person waits on: pending count, oldest age, ack latency. */
|
|
87
|
+
onStats?: (stats: TransactionStats) => void;
|
|
88
|
+
now?: () => number;
|
|
89
|
+
/** Backoff between retries of an unacknowledged proposal. */
|
|
90
|
+
retryMs?: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface TransactionStats {
|
|
94
|
+
pending: number;
|
|
95
|
+
/** When the oldest unanswered change was made (ms epoch), or null. */
|
|
96
|
+
oldestPendingAt: number | null;
|
|
97
|
+
/** Durable acknowledgment latency — change made → accepted answer. */
|
|
98
|
+
ackMs: { last: number | null; p95: number | null; n: number };
|
|
99
|
+
/** Final rejections this session (conflicts, invalid source, rights…). */
|
|
100
|
+
rejected: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sha256 = (s: string) => createHash('sha256').update(s, 'utf8').digest('hex');
|
|
104
|
+
|
|
105
|
+
/** The hub's lane hash: sha256 of the canonical UTF-8 lane text. */
|
|
106
|
+
export function laneHash(content: string): string {
|
|
107
|
+
return sha256(content);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const EMPTY_LANE_HASH = laneHash('');
|
|
111
|
+
|
|
112
|
+
export class TransactionError extends Error {
|
|
113
|
+
constructor(
|
|
114
|
+
message: string,
|
|
115
|
+
readonly code: string
|
|
116
|
+
) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = 'TransactionError';
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function createTransactionClient(opts: TransactionClientOptions) {
|
|
123
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
124
|
+
const log = opts.log ?? console;
|
|
125
|
+
const now = opts.now ?? Date.now;
|
|
126
|
+
const retryMs = opts.retryMs ?? 1500;
|
|
127
|
+
const outboxDir = path.join(opts.designRoot, '_state', 'outbox');
|
|
128
|
+
const deviceId = opts.deviceId ?? 'studio';
|
|
129
|
+
const sessionId = randomUUID();
|
|
130
|
+
const base = opts.hubUrl.replace(/\/+$/, '');
|
|
131
|
+
|
|
132
|
+
let projectId: string | null = null;
|
|
133
|
+
let epoch = 0;
|
|
134
|
+
let stopped = false;
|
|
135
|
+
let chain: Promise<unknown> = Promise.resolve();
|
|
136
|
+
let pending = 0;
|
|
137
|
+
|
|
138
|
+
/** transactionId → when the change was made; the unanswered ones. */
|
|
139
|
+
const waiting = new Map<string, number>();
|
|
140
|
+
const acks: number[] = [];
|
|
141
|
+
let rejectedCount = 0;
|
|
142
|
+
function stats(): TransactionStats {
|
|
143
|
+
const sorted = [...acks].sort((a, b) => a - b);
|
|
144
|
+
return {
|
|
145
|
+
pending,
|
|
146
|
+
oldestPendingAt: waiting.size ? Math.min(...waiting.values()) : null,
|
|
147
|
+
ackMs: {
|
|
148
|
+
last: acks.length ? (acks[acks.length - 1] as number) : null,
|
|
149
|
+
p95: sorted.length
|
|
150
|
+
? (sorted[Math.min(sorted.length - 1, Math.floor(0.95 * sorted.length))] as number)
|
|
151
|
+
: null,
|
|
152
|
+
n: acks.length,
|
|
153
|
+
},
|
|
154
|
+
rejected: rejectedCount,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const setPending = (delta: number) => {
|
|
158
|
+
pending = Math.max(0, pending + delta);
|
|
159
|
+
opts.onPending?.(pending);
|
|
160
|
+
opts.onStats?.(stats());
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function headers(): Record<string, string> {
|
|
164
|
+
const token = opts.token();
|
|
165
|
+
return {
|
|
166
|
+
'content-type': 'application/json',
|
|
167
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function request(method: string, route: string, body?: string) {
|
|
172
|
+
const res = await fetchImpl(`${base}/api/projects/${projectId ?? 'current'}/v1/${route}`, {
|
|
173
|
+
method,
|
|
174
|
+
headers: headers(),
|
|
175
|
+
...(body !== undefined ? { body } : {}),
|
|
176
|
+
signal: AbortSignal.timeout(20_000),
|
|
177
|
+
});
|
|
178
|
+
const json = (await res.json().catch(() => null)) as unknown;
|
|
179
|
+
return { status: res.status, json };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function bootstrap(): Promise<Bootstrap> {
|
|
183
|
+
const { status, json } = await request('GET', 'bootstrap');
|
|
184
|
+
if (status === 404 || status === 405) {
|
|
185
|
+
// A hub without the route (or with the project unknown to it) speaks
|
|
186
|
+
// only the legacy protocol — an answer, not a failure.
|
|
187
|
+
throw new TransactionError(`bootstrap absent (${status})`, 'absent');
|
|
188
|
+
}
|
|
189
|
+
if (status !== 200 || !json || typeof json !== 'object') {
|
|
190
|
+
throw new TransactionError(`bootstrap failed (${status})`, 'bootstrap');
|
|
191
|
+
}
|
|
192
|
+
const b = json as Bootstrap;
|
|
193
|
+
projectId = b.projectId;
|
|
194
|
+
epoch = b.epoch;
|
|
195
|
+
return b;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function writeOutbox(entry: OutboxEntry): string {
|
|
199
|
+
mkdirSync(outboxDir, { recursive: true });
|
|
200
|
+
const file = path.join(outboxDir, `${entry.createdAt}-${entry.transactionId}.json`);
|
|
201
|
+
const tmp = `${file}.tmp`;
|
|
202
|
+
writeFileSync(tmp, JSON.stringify(entry));
|
|
203
|
+
renameSync(tmp, file);
|
|
204
|
+
return file;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readOutbox(): { file: string; entry: OutboxEntry }[] {
|
|
208
|
+
let names: string[] = [];
|
|
209
|
+
try {
|
|
210
|
+
names = readdirSync(outboxDir).filter((n) => n.endsWith('.json'));
|
|
211
|
+
} catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
return names
|
|
215
|
+
.sort()
|
|
216
|
+
.map((n) => {
|
|
217
|
+
const file = path.join(outboxDir, n);
|
|
218
|
+
try {
|
|
219
|
+
return { file, entry: JSON.parse(readFileSync(file, 'utf8')) as OutboxEntry };
|
|
220
|
+
} catch {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
.filter((x): x is { file: string; entry: OutboxEntry } => x !== null);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Deliver one outbox entry to a FINAL result. Never gives up on an unknown
|
|
231
|
+
* outcome: a transport failure is resolved by asking for the transaction's
|
|
232
|
+
* result before the same bytes are sent again.
|
|
233
|
+
*/
|
|
234
|
+
async function deliver(file: string, entry: OutboxEntry): Promise<ProposalResult> {
|
|
235
|
+
let attempt = 0;
|
|
236
|
+
for (;;) {
|
|
237
|
+
if (stopped) throw new TransactionError('client stopped', 'stopped');
|
|
238
|
+
try {
|
|
239
|
+
if (attempt > 0) {
|
|
240
|
+
const known = await request('GET', `transactions/${entry.transactionId}`);
|
|
241
|
+
if (known.status === 200 && known.json) {
|
|
242
|
+
rmSync(file, { force: true });
|
|
243
|
+
return known.json as ProposalResult;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const { status, json } = await request('POST', 'proposals', entry.bytes);
|
|
247
|
+
const result = json as ProposalResult | null;
|
|
248
|
+
if (result && (result.status === 'accepted' || result.status === 'rejected')) {
|
|
249
|
+
if (result.status === 'rejected' && (result.code === 'retryable' || status >= 500)) {
|
|
250
|
+
throw new TransactionError('hub asked to retry', 'retryable');
|
|
251
|
+
}
|
|
252
|
+
rmSync(file, { force: true });
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
throw new TransactionError(`unexpected response ${status}`, 'retryable');
|
|
256
|
+
} catch (err) {
|
|
257
|
+
attempt++;
|
|
258
|
+
if (attempt === 1 || attempt % 10 === 0) {
|
|
259
|
+
log.warn(
|
|
260
|
+
`[sync/tx] ${entry.label}: not acknowledged yet (${(err as Error).message}); retrying`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
await sleep(Math.min(retryMs * 2 ** Math.min(attempt - 1, 4), 30_000));
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Outbox files sort by this stamp, so it must be strictly increasing — two
|
|
269
|
+
// proposals in one millisecond must not replay in random-id order.
|
|
270
|
+
let lastStamp = 0;
|
|
271
|
+
const stamp = (): number => {
|
|
272
|
+
lastStamp = Math.max(now(), lastStamp + 1);
|
|
273
|
+
return lastStamp;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const newTransactionId = (): string => `tx_${randomUUID().replace(/-/g, '')}`;
|
|
277
|
+
|
|
278
|
+
/** Entries this process created — delivered by their own `propose`, never by a drain. */
|
|
279
|
+
const owned = new Set<string>();
|
|
280
|
+
/** epoch-stale rebases: old transaction id → the id it was re-proposed under. */
|
|
281
|
+
const aliases = new Map<string, string>();
|
|
282
|
+
|
|
283
|
+
function envelope(action: OutboxAction, transactionId: string): string {
|
|
284
|
+
return JSON.stringify({
|
|
285
|
+
protocol: PROTOCOL,
|
|
286
|
+
projectId,
|
|
287
|
+
epoch,
|
|
288
|
+
transactionId,
|
|
289
|
+
...(action.dependsOn?.length ? { dependsOn: action.dependsOn } : {}),
|
|
290
|
+
origin: { deviceId, sessionId, client: 'studio' },
|
|
291
|
+
action: { kind: action.kind ?? 'edit', label: action.label, operations: action.operations },
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function makeEntry(action: OutboxAction, transactionId: string): OutboxEntry {
|
|
296
|
+
return {
|
|
297
|
+
transactionId,
|
|
298
|
+
createdAt: stamp(),
|
|
299
|
+
bytes: envelope(action, transactionId),
|
|
300
|
+
label: action.label,
|
|
301
|
+
action,
|
|
302
|
+
...(projectId === null ? { unbound: true } : {}),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Take one outbox entry to its FINAL result. An entry written before this
|
|
308
|
+
* process knew the project is bound first — it was never sent, so its bytes
|
|
309
|
+
* may still change; once sent, they never do.
|
|
310
|
+
*/
|
|
311
|
+
async function settle(file: string, entry: OutboxEntry): Promise<ProposalResult> {
|
|
312
|
+
if (entry.unbound || projectId === null) {
|
|
313
|
+
if (projectId === null) await bootstrap();
|
|
314
|
+
if (entry.unbound && entry.action) {
|
|
315
|
+
entry = { ...entry, bytes: envelope(entry.action, entry.transactionId) };
|
|
316
|
+
delete entry.unbound;
|
|
317
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(entry));
|
|
318
|
+
renameSync(`${file}.tmp`, file);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
let result = await deliver(file, entry);
|
|
322
|
+
if (result.status === 'rejected' && result.code === 'epoch-stale' && entry.action) {
|
|
323
|
+
// A rebase is a NEW transaction under the current epoch, never a mutated
|
|
324
|
+
// retry — and anything that depended on the old id now depends on this.
|
|
325
|
+
await bootstrap();
|
|
326
|
+
const action = {
|
|
327
|
+
...entry.action,
|
|
328
|
+
...(entry.action.dependsOn
|
|
329
|
+
? { dependsOn: entry.action.dependsOn.map((id) => aliases.get(id) ?? id) }
|
|
330
|
+
: {}),
|
|
331
|
+
};
|
|
332
|
+
const next = makeEntry(action, newTransactionId());
|
|
333
|
+
aliases.set(entry.transactionId, next.transactionId);
|
|
334
|
+
const nextFile = writeOutbox(next);
|
|
335
|
+
owned.add(nextFile);
|
|
336
|
+
try {
|
|
337
|
+
result = await deliver(nextFile, next);
|
|
338
|
+
} finally {
|
|
339
|
+
owned.delete(nextFile);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (result.status === 'accepted') {
|
|
343
|
+
acks.push(Math.max(0, now() - entry.createdAt));
|
|
344
|
+
if (acks.length > 200) acks.shift();
|
|
345
|
+
} else rejectedCount++;
|
|
346
|
+
opts.onResult?.(result, {
|
|
347
|
+
label: entry.label,
|
|
348
|
+
operations: entry.action?.operations ?? [],
|
|
349
|
+
});
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function enqueue<T>(work: () => Promise<T>): Promise<T> {
|
|
354
|
+
const next = chain.then(work, work);
|
|
355
|
+
chain = next.catch(() => {});
|
|
356
|
+
return next;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Propose one action. Resolves with the FINAL result.
|
|
361
|
+
*
|
|
362
|
+
* The proposal is written to the durable outbox SYNCHRONOUSLY, before this
|
|
363
|
+
* returns — not when its turn to be sent comes. A proposal waiting behind an
|
|
364
|
+
* unanswered one is exactly the one a crash would otherwise lose.
|
|
365
|
+
*
|
|
366
|
+
* `transactionId` may be chosen by the caller (`newTransactionId()`) so a
|
|
367
|
+
* later proposal can name this one in `dependsOn` before it is answered.
|
|
368
|
+
*/
|
|
369
|
+
function propose(action: {
|
|
370
|
+
kind?: string;
|
|
371
|
+
label: string;
|
|
372
|
+
operations: Operation[];
|
|
373
|
+
transactionId?: string;
|
|
374
|
+
dependsOn?: string[];
|
|
375
|
+
}): Promise<ProposalResult> {
|
|
376
|
+
const { transactionId, ...rest } = action;
|
|
377
|
+
let entry: OutboxEntry;
|
|
378
|
+
let file: string;
|
|
379
|
+
try {
|
|
380
|
+
entry = makeEntry(rest, transactionId ?? newTransactionId());
|
|
381
|
+
file = writeOutbox(entry);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
// Local persistence failed — say so; never pretend the edit is safe.
|
|
384
|
+
return Promise.reject(
|
|
385
|
+
new TransactionError(
|
|
386
|
+
`could not save the change locally: ${(err as Error).message}`,
|
|
387
|
+
'local-persistence'
|
|
388
|
+
)
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
owned.add(file);
|
|
392
|
+
waiting.set(entry.transactionId, entry.createdAt);
|
|
393
|
+
setPending(1);
|
|
394
|
+
return enqueue(() =>
|
|
395
|
+
settle(file, entry).finally(() => {
|
|
396
|
+
owned.delete(file);
|
|
397
|
+
waiting.delete(entry.transactionId);
|
|
398
|
+
setPending(-1);
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Resend what a previous process left in the outbox — in creation order, the
|
|
405
|
+
* same bytes, each resolved first in case its answer was simply lost.
|
|
406
|
+
*/
|
|
407
|
+
function drainOutbox(): Promise<ProposalResult[]> {
|
|
408
|
+
return enqueue(async () => {
|
|
409
|
+
const entries = readOutbox().filter(({ file }) => !owned.has(file));
|
|
410
|
+
const results: ProposalResult[] = [];
|
|
411
|
+
for (const { file, entry } of entries) {
|
|
412
|
+
waiting.set(entry.transactionId, entry.createdAt);
|
|
413
|
+
setPending(1);
|
|
414
|
+
try {
|
|
415
|
+
results.push(await settle(file, entry));
|
|
416
|
+
} finally {
|
|
417
|
+
waiting.delete(entry.transactionId);
|
|
418
|
+
setPending(-1);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return results;
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** A read route (`history`, `lane`, `revisions`) — no retry, bounded. */
|
|
426
|
+
async function read(route: string, params: Record<string, string | number>): Promise<unknown> {
|
|
427
|
+
if (projectId === null) await bootstrap();
|
|
428
|
+
const qs = new URLSearchParams(
|
|
429
|
+
Object.fromEntries(Object.entries(params).map(([k, v]) => [k, String(v)]))
|
|
430
|
+
);
|
|
431
|
+
const { status, json } = await request('GET', `${route}?${qs.toString()}`);
|
|
432
|
+
if (status !== 200) throw new TransactionError(`${route} failed (${status})`, 'read');
|
|
433
|
+
return json;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return {
|
|
437
|
+
bootstrap,
|
|
438
|
+
propose,
|
|
439
|
+
read,
|
|
440
|
+
newTransactionId,
|
|
441
|
+
drainOutbox,
|
|
442
|
+
/** The epoch proposals are currently made under. */
|
|
443
|
+
get epoch() {
|
|
444
|
+
return epoch;
|
|
445
|
+
},
|
|
446
|
+
get projectId() {
|
|
447
|
+
return projectId;
|
|
448
|
+
},
|
|
449
|
+
get pending() {
|
|
450
|
+
return pending;
|
|
451
|
+
},
|
|
452
|
+
outboxSize: () => readOutbox().length,
|
|
453
|
+
stats,
|
|
454
|
+
stop() {
|
|
455
|
+
stopped = true;
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export type TransactionClient = ReturnType<typeof createTransactionClient>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// What a THROWN fetch error means, in words a person can act on.
|
|
2
|
+
//
|
|
3
|
+
// THE BUG THIS EXISTS TO FIX. Every `catch` in `file-plane.ts` stored
|
|
4
|
+
// `(err as Error).message` directly into the per-path delivery reason a person
|
|
5
|
+
// reads in the Sync panel. Those messages come from Bun's runtime, not from us:
|
|
6
|
+
//
|
|
7
|
+
// $ bun -e "try{await fetch('http://127.0.0.1:1/x')}catch(e){console.log(e.message)}"
|
|
8
|
+
// Unable to connect. Is the computer able to access the url?
|
|
9
|
+
//
|
|
10
|
+
// On 2026-09-03, 484 of 803 undelivered files in an 8.8 GB project carried
|
|
11
|
+
// exactly those two Bun strings — because the cell they were being pushed to
|
|
12
|
+
// was restarting in a loop. So a person looking for "why is my project not
|
|
13
|
+
// syncing" was told, 135 times, to check their URL for a typo. It also cost a
|
|
14
|
+
// debugging session: the strings appear nowhere in this repo, so grepping for
|
|
15
|
+
// them found nothing and the reason class looked like a mystery.
|
|
16
|
+
//
|
|
17
|
+
// `failureReason()` in `retry-after.ts` is the sibling for RESPONSE refusals —
|
|
18
|
+
// a request that arrived and was declined. This module is for the requests that
|
|
19
|
+
// never arrived at all.
|
|
20
|
+
|
|
21
|
+
/** What kind of transport failure this was. */
|
|
22
|
+
export type TransportErrorClass = 'unreachable' | 'timeout' | 'aborted' | 'other';
|
|
23
|
+
|
|
24
|
+
export interface ClassifiedTransportError {
|
|
25
|
+
class: TransportErrorClass;
|
|
26
|
+
/** What a person reads. Ours, never the runtime's. */
|
|
27
|
+
text: string;
|
|
28
|
+
/** The runtime's own words — for the console and `detail` fields ONLY. */
|
|
29
|
+
raw: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** How much of a raw message we keep. Bounded: it reaches logs, and a hostile
|
|
33
|
+
* peer can influence some of what a fetch throws. */
|
|
34
|
+
const MAX_RAW_CHARS = 200;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bun's connect-failure wording, as of Bun 1.x.
|
|
38
|
+
*
|
|
39
|
+
* MATCHED AS A FALLBACK, NOT AS THE PRIMARY SIGNAL. Error *names* are stable
|
|
40
|
+
* API; these sentences are not, and a Bun upgrade that rewords them must
|
|
41
|
+
* degrade this to `other` with a neutral sentence rather than start leaking the
|
|
42
|
+
* new wording. That is why `other` has a real message of its own instead of
|
|
43
|
+
* falling through to `raw`.
|
|
44
|
+
*/
|
|
45
|
+
const BUN_CONNECT_HINTS = [
|
|
46
|
+
'unable to connect',
|
|
47
|
+
'was there a typo in the url or port',
|
|
48
|
+
'connectionrefused',
|
|
49
|
+
'connection refused',
|
|
50
|
+
'failed to connect',
|
|
51
|
+
'econnrefused',
|
|
52
|
+
'enotfound',
|
|
53
|
+
'eai_again',
|
|
54
|
+
'dns',
|
|
55
|
+
'socket connection was closed unexpectedly',
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const BUN_TIMEOUT_HINTS = ['timeout', 'timed out', 'etimedout'];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Classify a caught `fetch` rejection.
|
|
62
|
+
*
|
|
63
|
+
* Total: every input produces a class and a sentence, including a non-Error
|
|
64
|
+
* throw. A classifier that can return "nothing" would just reintroduce the
|
|
65
|
+
* `err.message` fall-through it replaces.
|
|
66
|
+
*/
|
|
67
|
+
export function classifyTransportError(err: unknown): ClassifiedTransportError {
|
|
68
|
+
const raw = rawTextOf(err).slice(0, MAX_RAW_CHARS);
|
|
69
|
+
const name = typeof (err as { name?: unknown })?.name === 'string' ? (err as Error).name : '';
|
|
70
|
+
const haystack = `${name} ${raw}`.toLowerCase();
|
|
71
|
+
|
|
72
|
+
// Names first — they are the stable half of the contract.
|
|
73
|
+
if (name === 'TimeoutError' || (name === 'AbortError' && haystack.includes('timeout'))) {
|
|
74
|
+
return {
|
|
75
|
+
class: 'timeout',
|
|
76
|
+
text: 'The workspace took too long to answer',
|
|
77
|
+
raw,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (name === 'AbortError') {
|
|
81
|
+
return { class: 'aborted', text: 'Stopped before it finished', raw };
|
|
82
|
+
}
|
|
83
|
+
if (BUN_TIMEOUT_HINTS.some((h) => haystack.includes(h))) {
|
|
84
|
+
return { class: 'timeout', text: 'The workspace took too long to answer', raw };
|
|
85
|
+
}
|
|
86
|
+
if (BUN_CONNECT_HINTS.some((h) => haystack.includes(h))) {
|
|
87
|
+
return { class: 'unreachable', text: 'Could not reach the workspace', raw };
|
|
88
|
+
}
|
|
89
|
+
// NEUTRAL, never the raw string. An unrecognised failure is still a failure
|
|
90
|
+
// we own the wording of.
|
|
91
|
+
return { class: 'other', text: 'The transfer did not complete', raw };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The user-facing sentence alone — the common call site, since almost every
|
|
96
|
+
* `catch` wants exactly this and nothing else.
|
|
97
|
+
*/
|
|
98
|
+
export function transportErrorText(err: unknown): string {
|
|
99
|
+
return classifyTransportError(err).text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function rawTextOf(err: unknown): string {
|
|
103
|
+
if (typeof err === 'string') return err;
|
|
104
|
+
const message = (err as { message?: unknown })?.message;
|
|
105
|
+
if (typeof message === 'string') return message;
|
|
106
|
+
try {
|
|
107
|
+
return String(err);
|
|
108
|
+
} catch {
|
|
109
|
+
return 'unknown error';
|
|
110
|
+
}
|
|
111
|
+
}
|