@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.
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/probe.ts +268 -16
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/api.ts +139 -26
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/client/app.jsx +257 -7
- package/apps/studio/client/export-center.jsx +15 -7
- package/apps/studio/client/panels/ReadinessList.jsx +7 -1
- package/apps/studio/client/panels/SyncPanel.jsx +125 -4
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/styles/3-shell-maude.css +34 -0
- package/apps/studio/client/styles/4-components.css +26 -0
- package/apps/studio/collab/persistence.ts +132 -16
- package/apps/studio/collab/registry.ts +10 -18
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1128 -1128
- 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 +7 -1
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/readiness.ts +42 -7
- package/apps/studio/server.ts +11 -4
- package/apps/studio/sync/agent.ts +136 -29
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/codec.ts +122 -31
- 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/file-ledger.ts +117 -0
- package/apps/studio/sync/file-plane.ts +623 -27
- package/apps/studio/sync/index.ts +711 -20
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +40 -3
- package/apps/studio/sync/presentation.ts +26 -3
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/status.ts +122 -5
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/whats-new.json +91 -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 +889 -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 +32 -9
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- 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 -301
- 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/fake-claude-auth.mjs +0 -13
- 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/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/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 -226
- 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
|
@@ -65,10 +65,12 @@ import {
|
|
|
65
65
|
import {
|
|
66
66
|
decideAnnotationsColdStart,
|
|
67
67
|
decideColdStart,
|
|
68
|
+
decideCssColdStart,
|
|
68
69
|
isExactRepeat,
|
|
69
70
|
unionCommentsById,
|
|
70
71
|
} from './cold-start.ts';
|
|
71
|
-
import { applyColdStart } from './cold-start-apply.ts';
|
|
72
|
+
import { applyColdStart, type ColdStartSnapshotReason } from './cold-start-apply.ts';
|
|
73
|
+
import { dedupeCommentsById, hasDuplicateComments } from './comment-identity.ts';
|
|
72
74
|
import { type EchoGuard, hashBytes } from './echo-guard.ts';
|
|
73
75
|
import type { SyncJournal } from './journal.ts';
|
|
74
76
|
|
|
@@ -126,7 +128,7 @@ export interface CanvasSyncAgentOptions {
|
|
|
126
128
|
* without it the conflict path still resolves newest-wins, just without the
|
|
127
129
|
* recovery snapshots (test-only constructions).
|
|
128
130
|
*/
|
|
129
|
-
snapshot?: (content: string, reason:
|
|
131
|
+
snapshot?: (content: string, reason: ColdStartSnapshotReason) => Promise<string | null>;
|
|
130
132
|
/**
|
|
131
133
|
* Called when a non-adopt reconcile (cold-start / post-git-pull) found
|
|
132
134
|
* divergent non-empty content on both sides (DDR-102). `winner` is the side
|
|
@@ -185,12 +187,17 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
185
187
|
let lastMeta: string | null = null;
|
|
186
188
|
let lastCss: string | null = null;
|
|
187
189
|
|
|
188
|
-
// F1 — the
|
|
189
|
-
// which the de-dup repair stops firing. Set on seed; nulled only when the
|
|
190
|
+
// F1 — the content this agent pushed on `seed-local-up`, plus the deadline
|
|
191
|
+
// past which the de-dup repair stops firing. Set on seed; nulled only when the
|
|
190
192
|
// window lapses (after convergence the repair is a cheap no-op — the
|
|
191
|
-
//
|
|
193
|
+
// `=== seedInfo.x` guards short-circuit, so we don't bother clearing).
|
|
192
194
|
// Null = this agent never seeded (so it never repairs).
|
|
193
|
-
|
|
195
|
+
//
|
|
196
|
+
// `css` rides along with the body (issue #114): a seed writes BOTH lanes in
|
|
197
|
+
// the same breath, so both can collide with a concurrent peer's identical
|
|
198
|
+
// seed, and repairing only the body left the canvas un-buildable anyway —
|
|
199
|
+
// duplicated css is as fatal to the build as a duplicated `export default`.
|
|
200
|
+
let seedInfo: { body: string; css: string | null; until: number } | null = null;
|
|
194
201
|
|
|
195
202
|
function onDocUpdate(_update: Uint8Array, updateOrigin: unknown): void {
|
|
196
203
|
if (stopped) return;
|
|
@@ -200,6 +207,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
200
207
|
// concurrent peer's identical seed onto ours. Collapse it (single elected
|
|
201
208
|
// writer) BEFORE the flush below can mirror a doubled body to disk.
|
|
202
209
|
maybeRepairSeedDuplication();
|
|
210
|
+
maybeCollapseCommentsDuplication();
|
|
203
211
|
scheduleFlush();
|
|
204
212
|
}
|
|
205
213
|
|
|
@@ -245,17 +253,77 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
245
253
|
}
|
|
246
254
|
const owner = seededByFromDoc(doc);
|
|
247
255
|
if (owner === null || owner !== doc.clientID) return; // not the elected writer
|
|
256
|
+
const seeded = seedInfo;
|
|
257
|
+
const repaired: string[] = [];
|
|
258
|
+
|
|
248
259
|
const body = htmlFromDoc(doc);
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
260
|
+
// `=== seeded.body` → already a single copy; not an exact repeat → a
|
|
261
|
+
// divergent edit, which carries genuine bytes and belongs to the conflict
|
|
262
|
+
// path, not to a silent collapse.
|
|
263
|
+
if (body !== seeded.body && isExactRepeat(body, seeded.body)) {
|
|
264
|
+
const canonical = seeded.body;
|
|
265
|
+
doc.transact(() => {
|
|
266
|
+
if (applyHtmlToDoc(doc, canonical, origin)) stampBodyEdit(doc, origin);
|
|
267
|
+
}, origin);
|
|
268
|
+
lastHtml = canonical;
|
|
269
|
+
opts.journal?.record(slug, { bodyHash: hashBytes(canonical) });
|
|
270
|
+
repaired.push('body');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// The css half of the same collision (issue #114). Same proof obligation as
|
|
274
|
+
// the body — an exact integer repeat of what WE seeded — and the same
|
|
275
|
+
// idempotence: the collapse deletes the same CRDT items on every peer that
|
|
276
|
+
// runs it, so concurrent recoveries converge instead of fighting.
|
|
277
|
+
if (paths.css && seeded.css !== null) {
|
|
278
|
+
const css = cssFromDoc(doc);
|
|
279
|
+
if (css !== null && css !== seeded.css && isExactRepeat(css, seeded.css)) {
|
|
280
|
+
const canonicalCss = seeded.css;
|
|
281
|
+
doc.transact(() => {
|
|
282
|
+
applyCssToDoc(doc, canonicalCss, origin);
|
|
283
|
+
}, origin);
|
|
284
|
+
lastCss = canonicalCss;
|
|
285
|
+
opts.journal?.record(slug, { cssHash: hashBytes(canonicalCss) });
|
|
286
|
+
repaired.push('css');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (repaired.length > 0) {
|
|
291
|
+
console.warn(
|
|
292
|
+
`[sync/${slug}] concurrent cold-seed duplication detected in ${repaired.join(
|
|
293
|
+
' + '
|
|
294
|
+
)} — collapsed to one copy (F1).`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* The comments half of the same collision (issue #112) — and deliberately
|
|
301
|
+
* NOT shaped like the body's.
|
|
302
|
+
*
|
|
303
|
+
* The body repair needs the `seededBy` election and an exact-repeat proof
|
|
304
|
+
* because collapsing a Y.Text can only be justified when the discarded bytes
|
|
305
|
+
* are provably a copy. Comments carry stable ids, so "the same comment twice"
|
|
306
|
+
* is decidable per entry, not per lane: the collapse keeps one of two
|
|
307
|
+
* byte-identical-by-identity entries and discards nothing a reader could
|
|
308
|
+
* want. That makes it safe for EVERY peer to run — no election, no seed
|
|
309
|
+
* window, no snapshot (per issue #114's follow-up, a collapse that preserves
|
|
310
|
+
* zero unique bytes must not consume the `_history/` snapshot ring).
|
|
311
|
+
*
|
|
312
|
+
* It converges because `applyCommentsToDoc` emits the collapse as a pure
|
|
313
|
+
* delete of items every peer holds, and concurrent deletes of the same items
|
|
314
|
+
* are idempotent. Running it here — on the remote-update path, before the
|
|
315
|
+
* flush — is what stops a duplicated array from reaching disk and becoming
|
|
316
|
+
* the canvas's new truth.
|
|
317
|
+
*/
|
|
318
|
+
function maybeCollapseCommentsDuplication(): void {
|
|
319
|
+
const current = commentsFromDoc(doc);
|
|
320
|
+
if (!hasDuplicateComments(current)) return;
|
|
321
|
+
const collapsed = dedupeCommentsById(current);
|
|
252
322
|
doc.transact(() => {
|
|
253
|
-
|
|
323
|
+
applyCommentsToDoc(doc, collapsed, origin);
|
|
254
324
|
}, origin);
|
|
255
|
-
lastHtml = canonical;
|
|
256
|
-
opts.journal?.record(slug, { bodyHash: hashBytes(canonical) });
|
|
257
325
|
console.warn(
|
|
258
|
-
`[sync/${slug}]
|
|
326
|
+
`[sync/${slug}] comments lane held ${current.length} entries for ${collapsed.length} distinct comments — collapsed (#112).`
|
|
259
327
|
);
|
|
260
328
|
}
|
|
261
329
|
|
|
@@ -298,6 +366,12 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
298
366
|
}
|
|
299
367
|
|
|
300
368
|
function writeCommentsIfChanged(): void {
|
|
369
|
+
// This lane writes `_comments/<slug>.json` DIRECTLY, not through the api's
|
|
370
|
+
// `saveCommentsForFile` — so it does not inherit that boundary's dedupe.
|
|
371
|
+
// Collapse first (issue #112): a duplicated array reaching disk is how the
|
|
372
|
+
// doubling outlived restarts and became the canvas's truth. Idempotent, and
|
|
373
|
+
// self-originated, so it can't re-enter `onDocUpdate`.
|
|
374
|
+
maybeCollapseCommentsDuplication();
|
|
301
375
|
const next = commentsFromDoc(doc);
|
|
302
376
|
const serialized = next.length > 0 ? `${JSON.stringify(next, null, 2)}\n` : '';
|
|
303
377
|
if (serialized === lastComments) return;
|
|
@@ -446,7 +520,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
446
520
|
// one hub at once self-heal the same way the decision-table seed does.
|
|
447
521
|
markSeeded(doc, origin);
|
|
448
522
|
}, origin);
|
|
449
|
-
seedInfo = { body: localHtml, until: Date.now() + SEED_REPAIR_WINDOW_MS };
|
|
523
|
+
seedInfo = { body: localHtml, css: localCss, until: Date.now() + SEED_REPAIR_WINDOW_MS };
|
|
450
524
|
}
|
|
451
525
|
if (localComments !== null) {
|
|
452
526
|
const parsed = tryParseJsonArray(localComments);
|
|
@@ -504,10 +578,12 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
504
578
|
markSeeded(doc, origin);
|
|
505
579
|
}, origin);
|
|
506
580
|
lastHtml = body;
|
|
507
|
-
// Arm the de-dup repair window: if another peer seeded the same
|
|
508
|
-
// same instant, the merged Y.Text will double — maybeRepairSeedDuplication
|
|
509
|
-
// (on the elected owner) collapses it back during this window.
|
|
510
|
-
|
|
581
|
+
// Arm the de-dup repair window: if another peer seeded the same content at
|
|
582
|
+
// the same instant, the merged Y.Text will double — maybeRepairSeedDuplication
|
|
583
|
+
// (on the elected owner) collapses it back during this window. `localCss`
|
|
584
|
+
// is armed too: the css lane is seeded by the same cold start a few lines
|
|
585
|
+
// below and collides identically (issue #114).
|
|
586
|
+
seedInfo = { body, css: localCss, until: Date.now() + SEED_REPAIR_WINDOW_MS };
|
|
511
587
|
opts.journal?.record(slug, { bodyHash: hashBytes(body) });
|
|
512
588
|
};
|
|
513
589
|
|
|
@@ -594,23 +670,54 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
594
670
|
}
|
|
595
671
|
}
|
|
596
672
|
|
|
597
|
-
// ---- css: follow the body winner
|
|
598
|
-
|
|
599
|
-
|
|
673
|
+
// ---- css: PER-LANE resolution (issue #114 — was "follow the body winner")
|
|
674
|
+
// The css lane used to be the only Plane-A lane with no table and no
|
|
675
|
+
// duplication guard, which is why it was the only one that NEVER came out
|
|
676
|
+
// of a multi-peer cold start clean. decideCssColdStart owns the rows now;
|
|
677
|
+
// the two that matter are the exact-repeat collapse and the DDR-064
|
|
678
|
+
// empty-lane guard, and it finally consumes the `cssHash` checkpoint the
|
|
679
|
+
// journal has been writing since DDR-102.
|
|
680
|
+
{
|
|
681
|
+
const cssDecision = decideCssColdStart({
|
|
682
|
+
local: localCss,
|
|
683
|
+
doc: docCss,
|
|
684
|
+
journalHash: opts.journal?.get(slug)?.cssHash ?? null,
|
|
685
|
+
hash: hashBytes,
|
|
686
|
+
bodyWinner,
|
|
687
|
+
});
|
|
688
|
+
if (cssDecision.recoveredDuplication) {
|
|
689
|
+
// WARN, BUT DO NOT SNAPSHOT — symmetric with the body's
|
|
690
|
+
// `recover-seed-dup`, which deliberately doesn't either.
|
|
691
|
+
//
|
|
692
|
+
// A snapshot stood here briefly, on the reasoning that `X + X` is valid
|
|
693
|
+
// CSS (unlike a doubled body, which can't build) so the collapse might
|
|
694
|
+
// discard a real edit. The reasoning was wrong about the bytes:
|
|
695
|
+
// `isExactRepeat` is true only when the doc is EXACTLY local repeated
|
|
696
|
+
// N times, so the copies thrown away contain nothing the retained copy
|
|
697
|
+
// doesn't. The snapshot preserved zero unique bytes — and cost a slot
|
|
698
|
+
// in a per-slug ring buffer that `pruneSnapshots` evicts oldest-first
|
|
699
|
+
// at MAX_SNAPSHOTS_PER_SLUG. A hostile hub that keeps re-doubling this
|
|
700
|
+
// lane could therefore drive one worthless snapshot per reconcile and
|
|
701
|
+
// push the user's genuine `pre-sync-local` and conflict copies off the
|
|
702
|
+
// end — turning DDR-102's recovery spine into the attack surface.
|
|
703
|
+
// Adversarial review found it; the safety net was the vulnerability.
|
|
704
|
+
console.warn(`[sync/${slug}] cold-start css: ${cssDecision.reason}`);
|
|
705
|
+
}
|
|
706
|
+
if (cssDecision.winner === 'local' && paths.css && localCss !== null) {
|
|
600
707
|
applyCssToDoc(doc, localCss, origin);
|
|
601
708
|
lastCss = localCss;
|
|
602
709
|
opts.journal?.record(slug, { cssHash: hashBytes(localCss) });
|
|
710
|
+
} else if (cssDecision.winner === 'hub') {
|
|
711
|
+
lastCss = docCss;
|
|
712
|
+
if (paths.css && docCss !== null && localCss !== docCss) {
|
|
713
|
+
const hash = hashBytes(docCss);
|
|
714
|
+
echoGuard.record(paths.css, hash);
|
|
715
|
+
writer(paths.css, docCss);
|
|
716
|
+
opts.journal?.record(slug, { cssHash: hash });
|
|
717
|
+
}
|
|
603
718
|
} else {
|
|
604
719
|
lastCss = docCss;
|
|
605
720
|
}
|
|
606
|
-
} else {
|
|
607
|
-
lastCss = docCss;
|
|
608
|
-
if (paths.css && docCss !== null && localCss !== docCss) {
|
|
609
|
-
const hash = hashBytes(docCss);
|
|
610
|
-
echoGuard.record(paths.css, hash);
|
|
611
|
-
writer(paths.css, docCss);
|
|
612
|
-
opts.journal?.record(slug, { cssHash: hash });
|
|
613
|
-
}
|
|
614
721
|
}
|
|
615
722
|
|
|
616
723
|
// ---- meta: hub wins where it has an opinion, local seeds where it does not
|
|
@@ -44,6 +44,7 @@ import { type Dirent, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
44
44
|
import path from 'node:path';
|
|
45
45
|
|
|
46
46
|
import { type CanvasGroupLike, classifyProjectFile, isFilePlaneClass } from './file-membership.ts';
|
|
47
|
+
import { failureReason, retryAfterMs } from './retry-after.ts';
|
|
47
48
|
|
|
48
49
|
/** Max designRoot-relative depth (matches the classifier's 8-segment cap). */
|
|
49
50
|
const MAX_SEGMENTS = 8;
|
|
@@ -84,27 +85,12 @@ export interface AssetPushProgress {
|
|
|
84
85
|
* the payload reaches `_sync.json` + every open tab, so it stays bounded). */
|
|
85
86
|
export const MAX_LISTED_FAILURES = 20;
|
|
86
87
|
|
|
87
|
-
/** Longest single pause the hub can ask for. Matches the hub's rate-limit
|
|
88
|
-
* window (60 s) — a `Retry-After` larger than that is either a typo or a hub
|
|
89
|
-
* we should not be blocking a boot sweep on. */
|
|
90
|
-
const MAX_RETRY_DELAY_MS = 60_000;
|
|
91
|
-
|
|
92
|
-
/** Where an absent/unparsable `Retry-After` lands. Old hubs (pre-fix) send a
|
|
93
|
-
* bare 429 with no header, and their window is the same 60 s. */
|
|
94
|
-
const DEFAULT_RETRY_DELAY_MS = 60_000;
|
|
95
|
-
|
|
96
88
|
/** Total time ONE sweep may spend waiting out 429s. The paced retry exists to
|
|
97
89
|
* keep an un-upgraded hub livable, not to turn a boot into an hour-long
|
|
98
90
|
* background stall — past this, the remaining refusals fail fast and the
|
|
99
91
|
* next-boot backstop takes them. */
|
|
100
92
|
const MAX_SWEEP_BACKOFF_MS = 5 * 60_000;
|
|
101
93
|
|
|
102
|
-
/** How much of an error body reaches `failed[].reason`. Enough to tell "rate
|
|
103
|
-
* limit exceeded" from a Cloudflare error page — the distinction the 2026-08-11
|
|
104
|
-
* RCA had to reconstruct from edge logs because the client kept only a status
|
|
105
|
-
* code. Hub-supplied text ⇒ bounded and stripped before it reaches the UI. */
|
|
106
|
-
const ERROR_SNIPPET_CHARS = 80;
|
|
107
|
-
|
|
108
94
|
/**
|
|
109
95
|
* Every upload closes its connection. NOT an optimization — a correctness
|
|
110
96
|
* requirement, learned the expensive way (2026-08-11, second pass).
|
|
@@ -333,38 +319,6 @@ async function probePresent(ctx: {
|
|
|
333
319
|
}
|
|
334
320
|
}
|
|
335
321
|
|
|
336
|
-
/** `Retry-After: <seconds>` → ms, clamped. Only the delta-seconds form is
|
|
337
|
-
* parsed; the HTTP-date form is not something our hub emits. */
|
|
338
|
-
function retryAfterMs(header: string | null): number {
|
|
339
|
-
const secs = Number(String(header ?? '').trim());
|
|
340
|
-
if (!Number.isFinite(secs) || secs <= 0) return DEFAULT_RETRY_DELAY_MS;
|
|
341
|
-
return Math.min(secs * 1000, MAX_RETRY_DELAY_MS);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Why an upload was refused, in words — status PLUS a bounded snippet of the
|
|
346
|
-
* body. The hub says `{"error":"rate limit exceeded"}`; an edge that never
|
|
347
|
-
* reached the hub says HTML. Those are different bugs and the Sync panel should
|
|
348
|
-
* not make a person read logs to tell them apart.
|
|
349
|
-
*
|
|
350
|
-
* The body is hub-supplied ⇒ untrusted (DDR-054): control characters stripped,
|
|
351
|
-
* whitespace collapsed, hard length cap, and it only ever renders as text.
|
|
352
|
-
*/
|
|
353
|
-
async function failureReason(res: Response): Promise<string> {
|
|
354
|
-
let snippet = '';
|
|
355
|
-
try {
|
|
356
|
-
snippet = (await res.text())
|
|
357
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point.
|
|
358
|
-
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
359
|
-
.replace(/\s+/g, ' ')
|
|
360
|
-
.trim()
|
|
361
|
-
.slice(0, ERROR_SNIPPET_CHARS);
|
|
362
|
-
} catch {
|
|
363
|
-
/* a body we cannot read tells us nothing — the status still does */
|
|
364
|
-
}
|
|
365
|
-
return snippet ? `HTTP ${res.status} — ${snippet}` : `HTTP ${res.status}`;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
322
|
/**
|
|
369
323
|
* One upload, with the two retries that are worth having in-boot.
|
|
370
324
|
*
|
|
@@ -26,6 +26,7 @@ import { hostname } from 'node:os';
|
|
|
26
26
|
import type * as Y from 'yjs';
|
|
27
27
|
|
|
28
28
|
import { Y_TYPES } from '../collab/persistence.ts';
|
|
29
|
+
import { commentKey } from './comment-identity.ts';
|
|
29
30
|
import {
|
|
30
31
|
MAX_ANNOTATIONS_BYTES,
|
|
31
32
|
MAX_COMMENTS_BYTES,
|
|
@@ -96,20 +97,27 @@ export function htmlFromDoc(doc: Y.Doc): string {
|
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
99
|
-
* Apply `next` to
|
|
100
|
-
*
|
|
100
|
+
* Apply `next` to one Y.Text lane as a minimal common-prefix / common-suffix
|
|
101
|
+
* replace, so peers see a small op rather than a full replace.
|
|
102
|
+
*
|
|
103
|
+
* THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #114).
|
|
104
|
+
* Collapsing a concurrency-duplicated lane — `X + X` back to `X` — comes out of
|
|
105
|
+
* this diff as a PURE DELETE of the trailing run: no insert at all. Two peers
|
|
106
|
+
* that independently decide to collapse therefore delete the SAME CRDT items,
|
|
107
|
+
* and deletes are idempotent, so they converge.
|
|
108
|
+
*
|
|
109
|
+
* A wholesale `delete(0, len)` + `insert(0, next)` does not have that property.
|
|
110
|
+
* Both peers' inserts are new items at the same position, and Yjs keeps both —
|
|
111
|
+
* so the "repair" re-creates exactly the duplication it was meant to undo. The
|
|
112
|
+
* css lane was written that way, which is why it was the lane that never came
|
|
113
|
+
* out of a multi-peer cold start clean (measured: 0 of 43 lanes). Any lane whose
|
|
114
|
+
* duplication is repairable MUST go through here.
|
|
101
115
|
*
|
|
102
116
|
* Pass `origin` as the transaction origin so a downstream observer can
|
|
103
117
|
* distinguish self-originated updates from peer/remote ones.
|
|
104
118
|
*/
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
console.warn(
|
|
108
|
-
`[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
|
|
109
|
-
);
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
const yText = doc.getText(Y_SYNC_TYPES.html);
|
|
119
|
+
function applyTextLane(doc: Y.Doc, lane: string, next: string, origin?: unknown): boolean {
|
|
120
|
+
const yText = doc.getText(lane);
|
|
113
121
|
const current = yText.toString();
|
|
114
122
|
if (current === next) return false;
|
|
115
123
|
|
|
@@ -140,6 +148,19 @@ export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): bool
|
|
|
140
148
|
return true;
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Apply `next` to the canvas body Y.Text inside `doc`. See `applyTextLane`.
|
|
153
|
+
*/
|
|
154
|
+
export function applyHtmlToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
|
|
155
|
+
if (byteLengthUtf8(next) > MAX_HTML_BYTES) {
|
|
156
|
+
console.warn(
|
|
157
|
+
`[sync/codec] refusing HTML apply > ${MAX_HTML_BYTES} bytes (got ${byteLengthUtf8(next)}). DDR-054 §2d.`
|
|
158
|
+
);
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return applyTextLane(doc, Y_SYNC_TYPES.html, next, origin);
|
|
162
|
+
}
|
|
163
|
+
|
|
143
164
|
/* ---------------------------------------------------------------- comments */
|
|
144
165
|
|
|
145
166
|
/**
|
|
@@ -153,14 +174,41 @@ export function commentsFromDoc(doc: Y.Doc): CommentsSnapshot {
|
|
|
153
174
|
return arr.toArray();
|
|
154
175
|
}
|
|
155
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Apply `next` to the comments Y.Array as a MINIMAL, IDENTITY-KEYED DIFF.
|
|
179
|
+
*
|
|
180
|
+
* THIS SHAPE IS A CONVERGENCE PROPERTY, NOT AN OPTIMIZATION (issue #112) —
|
|
181
|
+
* the same law `applyTextLane` states for the text lanes, which this lane was
|
|
182
|
+
* missing. It used to be `delete(0, len)` + `push(next)`, and under concurrency
|
|
183
|
+
* that is not a replace at all: two peers delete the SAME items (idempotent)
|
|
184
|
+
* but insert two runs at the same position, and Yjs keeps both. The array
|
|
185
|
+
* becomes `list + list`, `persistJson` writes that to `_comments/<slug>.json`,
|
|
186
|
+
* the file re-imports clean, and the duplication IS the new truth. Every
|
|
187
|
+
* reconciliation round doubled it — 1 → 2 → 4 → 8, the count the #112 reporter
|
|
188
|
+
* saw — and the cold-start union repair, published through the same wholesale
|
|
189
|
+
* write, re-created the duplication it had just undone. Verbatim the css lesson
|
|
190
|
+
* from issue #114, one lane later.
|
|
191
|
+
*
|
|
192
|
+
* The diff gives the lane the property the text lanes got:
|
|
193
|
+
*
|
|
194
|
+
* - Collapsing a duplicated array is a PURE DELETE of items every peer holds,
|
|
195
|
+
* and concurrent deletes of the same items are idempotent — so two peers
|
|
196
|
+
* repairing at once converge instead of re-doubling.
|
|
197
|
+
* - Dedupe happens on EVERY apply, not only at cold start, so an array that
|
|
198
|
+
* doubled anyway (an older peer still on the wholesale write, a genuine
|
|
199
|
+
* concurrent edit of one entry) collapses at the next apply rather than
|
|
200
|
+
* compounding. Duplication became self-healing instead of exponential.
|
|
201
|
+
* - Untouched comments keep their CRDT items, so a peer replacing one entry
|
|
202
|
+
* no longer rewrites the whole list under everyone else's feet.
|
|
203
|
+
*
|
|
204
|
+
* Ordering follows the doc for surviving entries, with genuinely new ones
|
|
205
|
+
* appended. Reordering is deliberately NOT synced: it would be a delete+insert
|
|
206
|
+
* per moved entry — churn with the one shape this fix exists to avoid — and
|
|
207
|
+
* comment order carries no meaning (pins are placed by selector, the sidebar
|
|
208
|
+
* sorts by `created`).
|
|
209
|
+
*/
|
|
156
210
|
export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?: unknown): boolean {
|
|
157
|
-
const arr = doc.getArray(Y_TYPES.comments);
|
|
158
|
-
// Comments are LWW on the JSON file (the snapshot is the source of truth);
|
|
159
|
-
// collapse Y.Array to the new state. For v1.1 we just replace wholesale —
|
|
160
|
-
// structural comment-level merge is deferred along with structured HTML.
|
|
161
|
-
// Check whether anything actually changed to avoid no-op transactions
|
|
162
|
-
// (transactions still fire `update` events, which would re-enter the loop).
|
|
163
|
-
const before = JSON.stringify(arr.toArray());
|
|
211
|
+
const arr = doc.getArray<unknown>(Y_TYPES.comments);
|
|
164
212
|
const after = JSON.stringify(next);
|
|
165
213
|
if (byteLengthUtf8(after) > MAX_COMMENTS_BYTES) {
|
|
166
214
|
console.warn(
|
|
@@ -168,11 +216,51 @@ export function applyCommentsToDoc(doc: Y.Doc, next: CommentsSnapshot, origin?:
|
|
|
168
216
|
);
|
|
169
217
|
return false;
|
|
170
218
|
}
|
|
171
|
-
|
|
219
|
+
|
|
220
|
+
// Desired state, keyed by comment identity. `next` can itself arrive
|
|
221
|
+
// duplicated (a file written before this fix, a list pushed by an older
|
|
222
|
+
// peer), so it is deduped on the way in — first occurrence wins, matching
|
|
223
|
+
// `unionCommentsById` and `dedupeCommentsById`.
|
|
224
|
+
const desired = new Map<string, unknown>();
|
|
225
|
+
for (const c of next) {
|
|
226
|
+
const k = commentKey(c);
|
|
227
|
+
if (!desired.has(k)) desired.set(k, c);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Classify every item currently in the array: keep it, replace it in place,
|
|
231
|
+
// or delete it. Duplicates of an identity we already kept are deletes — that
|
|
232
|
+
// is what makes EVERY apply a repair, not just the cold-start union.
|
|
233
|
+
const current = arr.toArray();
|
|
234
|
+
const deletes: number[] = [];
|
|
235
|
+
const replaces: { index: number; value: unknown }[] = [];
|
|
236
|
+
const kept = new Set<string>();
|
|
237
|
+
for (let i = 0; i < current.length; i++) {
|
|
238
|
+
const k = commentKey(current[i]);
|
|
239
|
+
if (!desired.has(k) || kept.has(k)) {
|
|
240
|
+
deletes.push(i);
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
kept.add(k);
|
|
244
|
+
const want = desired.get(k);
|
|
245
|
+
if (JSON.stringify(current[i]) !== JSON.stringify(want))
|
|
246
|
+
replaces.push({ index: i, value: want });
|
|
247
|
+
}
|
|
248
|
+
const appends: unknown[] = [];
|
|
249
|
+
for (const [k, v] of desired) if (!kept.has(k)) appends.push(v);
|
|
250
|
+
|
|
251
|
+
// No-op guard. Transactions fire `update` events even when they change
|
|
252
|
+
// nothing, and an update re-enters the file→doc→file loop.
|
|
253
|
+
if (deletes.length === 0 && replaces.length === 0 && appends.length === 0) return false;
|
|
172
254
|
|
|
173
255
|
doc.transact(() => {
|
|
174
|
-
|
|
175
|
-
|
|
256
|
+
// Replaces first, while the indices computed above are still valid.
|
|
257
|
+
for (const r of replaces) {
|
|
258
|
+
arr.delete(r.index, 1);
|
|
259
|
+
arr.insert(r.index, [r.value]);
|
|
260
|
+
}
|
|
261
|
+
// Then deletes, descending, so each index stays valid as we go.
|
|
262
|
+
for (let i = deletes.length - 1; i >= 0; i--) arr.delete(deletes[i]!, 1);
|
|
263
|
+
if (appends.length > 0) arr.push(appends);
|
|
176
264
|
}, origin);
|
|
177
265
|
return true;
|
|
178
266
|
}
|
|
@@ -618,10 +706,19 @@ export function cssFromDoc(doc: Y.Doc): string | null {
|
|
|
618
706
|
}
|
|
619
707
|
|
|
620
708
|
/**
|
|
621
|
-
* Apply the canvas's `.css` to the doc as opaque Y.Text
|
|
622
|
-
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
709
|
+
* Apply the canvas's `.css` to the doc as opaque Y.Text.
|
|
710
|
+
*
|
|
711
|
+
* Goes through `applyTextLane` — the same prefix/suffix diff the body uses —
|
|
712
|
+
* and that is load-bearing, not tidiness (issue #114). This was a wholesale
|
|
713
|
+
* `delete(0, len)` + `insert(0, next)`, whose comment argued it "can't churn"
|
|
714
|
+
* because css round-trips byte-identically. True for a SINGLE writer, and the
|
|
715
|
+
* reason the lane looked fine in every single-peer test. Under concurrency it
|
|
716
|
+
* is the bug: two peers each replacing the lane keep both inserts, so the css
|
|
717
|
+
* doubled on a concurrent cold seed AND doubled again every time a peer tried
|
|
718
|
+
* to repair it. Measured in the field at 3×, 4× and 5×, while the body lane —
|
|
719
|
+
* same collision, diffing codec — never exceeded 3× and mostly self-healed.
|
|
720
|
+
*
|
|
721
|
+
* Returns false on over-cap / no change.
|
|
625
722
|
*/
|
|
626
723
|
export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boolean {
|
|
627
724
|
if (byteLengthUtf8(next) > MAX_CSS_BYTES) {
|
|
@@ -630,11 +727,5 @@ export function applyCssToDoc(doc: Y.Doc, next: string, origin?: unknown): boole
|
|
|
630
727
|
);
|
|
631
728
|
return false;
|
|
632
729
|
}
|
|
633
|
-
|
|
634
|
-
if (t.toString() === next) return false;
|
|
635
|
-
doc.transact(() => {
|
|
636
|
-
if (t.length > 0) t.delete(0, t.length);
|
|
637
|
-
t.insert(0, next);
|
|
638
|
-
}, origin);
|
|
639
|
-
return true;
|
|
730
|
+
return applyTextLane(doc, Y_SYNC_TYPES.css, next, origin);
|
|
640
731
|
}
|
|
@@ -39,6 +39,13 @@
|
|
|
39
39
|
|
|
40
40
|
import type { ColdStartAction, ColdStartDecision } from './cold-start.ts';
|
|
41
41
|
|
|
42
|
+
/** Why a cold-start snapshot was taken.
|
|
43
|
+
*
|
|
44
|
+
* Deliberately NOT extended for the duplication collapse (issue #114). A
|
|
45
|
+
* collapse only ever fires on an exact integer repeat, so the discarded copies
|
|
46
|
+
* hold nothing the retained one doesn't — and `pruneSnapshots` evicts
|
|
47
|
+
* oldest-first per slug, so a valueless snapshot on a hub-triggerable path is
|
|
48
|
+
* an eviction lever against the copies that carry real bytes. */
|
|
42
49
|
export type ColdStartSnapshotReason = 'pre-sync-local' | 'pre-sync-hub';
|
|
43
50
|
|
|
44
51
|
export interface ColdStartConflictInfo {
|