@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
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// unavailable) or equal → hub wins, exactly the v1.1 default — but now both
|
|
20
20
|
// sides are snapshotted first, so even a wrong pick costs one /design:rollback.
|
|
21
21
|
|
|
22
|
+
import { commentKey } from './comment-identity.ts';
|
|
22
23
|
import { hashBytes } from './echo-guard.ts';
|
|
23
24
|
|
|
24
25
|
export type ColdStartAction =
|
|
@@ -75,13 +76,6 @@ export function isExactRepeat(docBody: string, localBody: string): boolean {
|
|
|
75
76
|
return docBody === localBody.repeat(n);
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function commentId(c: unknown): string | null {
|
|
79
|
-
if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
|
|
80
|
-
return (c as { id: string }).id;
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
79
|
/**
|
|
86
80
|
* Union-merge two comment snapshots by stable `id` (DDR-102): doc order first,
|
|
87
81
|
* local-only entries appended. Comments carry stable ids, so union loses
|
|
@@ -90,22 +84,30 @@ function commentId(c: unknown): string | null {
|
|
|
90
84
|
* cold-start paths (agent reconcile + migrate-seed).
|
|
91
85
|
*/
|
|
92
86
|
export function unionCommentsById(docList: unknown[], localList: unknown[]): unknown[] {
|
|
93
|
-
const out = [
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
87
|
+
const out: unknown[] = [];
|
|
88
|
+
const seen = new Set<string>();
|
|
89
|
+
// THE DOC IS DEDUPED AGAINST ITSELF, not just against local (issue #114/#112).
|
|
90
|
+
// `out` used to be seeded as `[...docList]`, which made this union a filter on
|
|
91
|
+
// the LOCAL side only — so a doc whose comments array had already been
|
|
92
|
+
// concurrency-doubled (`applyCommentsToDoc` is delete-all + push: two peers
|
|
93
|
+
// replacing at once keep both pushes) carried every copy forward, and the
|
|
94
|
+
// merged result was re-published as the new truth. That is the amplifier
|
|
95
|
+
// behind "the comment self duplicated like 8 times": 2 → 4 → 8, one doubling
|
|
96
|
+
// per concurrent round, with nothing in the loop able to shrink it again.
|
|
97
|
+
// Deduping the doc half turns the same pass into the repair.
|
|
98
|
+
// `commentKey` is the SHARED identity rule (comment-identity.ts) — the same
|
|
99
|
+
// one `applyCommentsToDoc`'s diff and the room seed use. It has to be shared:
|
|
100
|
+
// if the union's notion of "same comment" ever drifted from the codec's, one
|
|
101
|
+
// layer's repair would be the other's duplicate.
|
|
102
|
+
const admit = (c: unknown): void => {
|
|
103
|
+
const k = commentKey(c);
|
|
104
|
+
if (seen.has(k)) return;
|
|
105
|
+
seen.add(k);
|
|
105
106
|
out.push(c);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
107
|
+
};
|
|
108
|
+
// Doc order first (same-id entries keep the doc's version), local-only appended.
|
|
109
|
+
for (const c of docList) admit(c);
|
|
110
|
+
for (const c of localList) admit(c);
|
|
109
111
|
return out;
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -292,3 +294,97 @@ export function decideAnnotationsColdStart(
|
|
|
292
294
|
reason: `diverged — no per-lane stamp on one side, following the body winner (${bodyWinner})`,
|
|
293
295
|
};
|
|
294
296
|
}
|
|
297
|
+
|
|
298
|
+
/* ------------------------------------------------------- css (per-lane) */
|
|
299
|
+
|
|
300
|
+
export interface CssColdStartInput {
|
|
301
|
+
/** Local sibling `.css` content, or null when the file doesn't exist. */
|
|
302
|
+
local: string | null;
|
|
303
|
+
/** The css currently held by the doc (`null` when the lane is unset/empty). */
|
|
304
|
+
doc: string | null;
|
|
305
|
+
/** Journal entry's `cssHash` for this slug, or null when never checkpointed. */
|
|
306
|
+
journalHash: string | null;
|
|
307
|
+
/** Hash fn — passed in so this table stays as dependency-free as the rest. */
|
|
308
|
+
hash: (s: string) => string;
|
|
309
|
+
/** How the body lane resolved; the tie-break for a genuine divergence. */
|
|
310
|
+
bodyWinner: 'local' | 'hub';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface CssColdStartDecision {
|
|
314
|
+
winner: 'local' | 'hub' | 'none';
|
|
315
|
+
/** True when local won because the doc held local repeated N≥2 times. */
|
|
316
|
+
recoveredDuplication?: boolean;
|
|
317
|
+
reason: string;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Per-lane cold-start resolution for the canvas's sibling `.css` (issue #114).
|
|
322
|
+
*
|
|
323
|
+
* WHY THIS TABLE EXISTS. Every other Plane-A lane had one — `decideColdStart`
|
|
324
|
+
* for the body, `decideAnnotationsColdStart` for annotations, `repairSharedMeta`
|
|
325
|
+
* for meta — and css had none: it simply followed the body winner and re-applied
|
|
326
|
+
* the whole file whenever the two sides disagreed. That left it the only lane
|
|
327
|
+
* with no duplication guard at all, and `applyCssToDoc` is a delete-all+insert,
|
|
328
|
+
* so two peers seeding one empty document concurrently kept BOTH inserts and the
|
|
329
|
+
* lane became `CSS × N` (Yjs merges concurrent inserts at the same position by
|
|
330
|
+
* keeping both — not a corruption, the CRDT doing its job). Worse, the "repair"
|
|
331
|
+
* was itself another full insert, so every subsequent disagreement could double
|
|
332
|
+
* it again. Measured on the reporting machine's 84 persisted docs: the body lane
|
|
333
|
+
* was clean 21 times out of 84, the css lane ZERO times out of 43, and css
|
|
334
|
+
* carried strictly more copies than its own document's body in 43 of 43.
|
|
335
|
+
* Per-lane vigilance is what failed; a table per lane is the shape that holds.
|
|
336
|
+
*
|
|
337
|
+
* The rows mirror `decideColdStart`'s, in the same order and for the same
|
|
338
|
+
* reasons — crucially the `recover-seed-dup` row, checked BEFORE the journal
|
|
339
|
+
* fast-forward so a doubled lane is collapsed rather than read as "the hub moved
|
|
340
|
+
* ahead". Like the body's, it repairs only what it can PROVE: an exact integer
|
|
341
|
+
* repeat of the bytes on disk. A doc carrying genuinely new css is never an
|
|
342
|
+
* exact repeat, so this cannot clobber an edit.
|
|
343
|
+
*/
|
|
344
|
+
export function decideCssColdStart(input: CssColdStartInput): CssColdStartDecision {
|
|
345
|
+
const { local, doc, journalHash, hash, bodyWinner } = input;
|
|
346
|
+
const localEmpty = local === null || local === '';
|
|
347
|
+
const docEmpty = doc === null || doc === '';
|
|
348
|
+
|
|
349
|
+
if (localEmpty && docEmpty) return { winner: 'none', reason: 'both sides empty' };
|
|
350
|
+
if (localEmpty) return { winner: 'hub', reason: 'no local css — materializing the hub lane' };
|
|
351
|
+
if (docEmpty) {
|
|
352
|
+
// The body lane's DDR-064 guard, applied here: an empty css lane means the
|
|
353
|
+
// hub holds no css for this canvas YET, never "this canvas has no css".
|
|
354
|
+
// Without this row a local `.css` next to a hub-winning body never travelled
|
|
355
|
+
// at all — it sat on one disk and no peer ever saw it.
|
|
356
|
+
return { winner: 'local', reason: 'hub css lane empty — seeding local css up (DDR-064 guard)' };
|
|
357
|
+
}
|
|
358
|
+
if (doc === local) return { winner: 'none', reason: 'local and hub css identical' };
|
|
359
|
+
|
|
360
|
+
// Concurrent cold-seed collision, css edition — see the header. Collapsing is
|
|
361
|
+
// re-applying local: `applyCssToDoc` replaces the lane wholesale, so the
|
|
362
|
+
// repeated copies go and one stays.
|
|
363
|
+
if (isExactRepeat(doc, local)) {
|
|
364
|
+
return {
|
|
365
|
+
winner: 'local',
|
|
366
|
+
recoveredDuplication: true,
|
|
367
|
+
reason:
|
|
368
|
+
'hub css is local repeated — concurrent cold-seed duplication; collapsing to one copy',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// The journal checkpoint the css lane has been WRITING since DDR-102 and never
|
|
373
|
+
// reading. Same meaning as the body's: every local byte was already reconciled
|
|
374
|
+
// through this machine, so the hub is simply ahead.
|
|
375
|
+
if (journalHash !== null && journalHash === hash(local)) {
|
|
376
|
+
return {
|
|
377
|
+
winner: 'hub',
|
|
378
|
+
reason: 'local css matches the last-synced journal hash — hub is ahead, fast-forwarding',
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Genuine divergence. The css is visually coupled to the body, so it follows
|
|
383
|
+
// the body's resolution rather than inventing a second, possibly disagreeing
|
|
384
|
+
// winner for the same canvas — the pre-existing behaviour, now reached only
|
|
385
|
+
// when the rows above have ruled out the cases they can decide on their own.
|
|
386
|
+
return {
|
|
387
|
+
winner: bodyWinner,
|
|
388
|
+
reason: `css diverged — following the body winner (${bodyWinner})`,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Comment identity — the one definition of "these two entries are the same
|
|
2
|
+
// comment", shared by every layer that touches the comments lane (issue #112).
|
|
3
|
+
//
|
|
4
|
+
// A LEAF module, exactly like `limits.ts`: it imports NOTHING. That is
|
|
5
|
+
// load-bearing, not tidiness. `collab/persistence.ts` owns `Y_TYPES` and
|
|
6
|
+
// `sync/codec.ts` imports it from there, so persistence can never import the
|
|
7
|
+
// codec back (its own comment says so) — yet the room seed needs the same
|
|
8
|
+
// identity rule the codec's diff uses. A dependency-free leaf is the only
|
|
9
|
+
// place both sides can reach.
|
|
10
|
+
//
|
|
11
|
+
// WHY IDENTITY IS A SHARED CONCERN. Comments travel over the hub as a Y.Array,
|
|
12
|
+
// and the lane's whole failure mode is the same logical comment appearing as
|
|
13
|
+
// several distinct CRDT items (issue #112: 1 → 2 → 4 → 8). Every layer that
|
|
14
|
+
// writes the lane has to agree on which entries are "the same one", or one
|
|
15
|
+
// layer's repair is another layer's duplicate.
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stable identity for one comment entry.
|
|
19
|
+
*
|
|
20
|
+
* `id` is the real key — it is minted once by `commentsAdd` and survives every
|
|
21
|
+
* round-trip through disk, the Y.Array and the hub. Entries WITHOUT a string
|
|
22
|
+
* `id` (hand-edited files, pre-Phase-6 rows, junk pushed by an untrusted peer)
|
|
23
|
+
* fall back to JSON identity, which is deliberately conservative: it dedupes
|
|
24
|
+
* only byte-identical entries and never merges two rows that might differ.
|
|
25
|
+
*
|
|
26
|
+
* The `id:` / `json:` prefixes keep the two spaces from colliding — a comment
|
|
27
|
+
* whose id is literally the JSON of another entry can't shadow it.
|
|
28
|
+
*/
|
|
29
|
+
export function commentKey(c: unknown): string {
|
|
30
|
+
if (c && typeof c === 'object' && typeof (c as { id?: unknown }).id === 'string') {
|
|
31
|
+
return `id:${(c as { id: string }).id}`;
|
|
32
|
+
}
|
|
33
|
+
return `json:${JSON.stringify(c)}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Collapse a comment list to one entry per identity, keeping the FIRST
|
|
38
|
+
* occurrence.
|
|
39
|
+
*
|
|
40
|
+
* First-wins is the same rule `unionCommentsById` uses ("doc order first"), and
|
|
41
|
+
* the two must agree: `commentsAddReply` mutates the first match, so the copy
|
|
42
|
+
* the dedupe keeps has to be the copy the mutations edit.
|
|
43
|
+
*
|
|
44
|
+
* Returns the input array itself when there is nothing to drop, so callers can
|
|
45
|
+
* cheaply test `deduped !== list` to know whether a write is worth making.
|
|
46
|
+
*/
|
|
47
|
+
export function dedupeCommentsById<T>(list: readonly T[]): T[] {
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
const out: T[] = [];
|
|
50
|
+
for (const c of list) {
|
|
51
|
+
const k = commentKey(c);
|
|
52
|
+
if (seen.has(k)) continue;
|
|
53
|
+
seen.add(k);
|
|
54
|
+
out.push(c);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True when `list` carries the same comment identity more than once. */
|
|
60
|
+
export function hasDuplicateComments(list: readonly unknown[]): boolean {
|
|
61
|
+
const seen = new Set<string>();
|
|
62
|
+
for (const c of list) {
|
|
63
|
+
const k = commentKey(c);
|
|
64
|
+
if (seen.has(k)) return true;
|
|
65
|
+
seen.add(k);
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
@@ -310,7 +310,21 @@ export function createConnectionMonitor(opts: ConnectionMonitorOptions = {}): Co
|
|
|
310
310
|
const wasOffline = state === 'offline' || state === 'offline-long';
|
|
311
311
|
clearAllTimers();
|
|
312
312
|
state = 'online';
|
|
313
|
-
lastSyncAt = now()
|
|
313
|
+
// NOT `lastSyncAt = now()`.
|
|
314
|
+
//
|
|
315
|
+
// A SOCKET TRANSITION IS NOT A SYNC. This line used to stamp the
|
|
316
|
+
// last-synced clock from the WS coming back — an event that, by itself,
|
|
317
|
+
// has moved no document and settled no handshake. The field's own contract
|
|
318
|
+
// one screen up already says otherwise ("updated on REAL sync activity
|
|
319
|
+
// (noteSyncActivity), not just on offline→online transitions"); the code
|
|
320
|
+
// simply did not honour it.
|
|
321
|
+
//
|
|
322
|
+
// It was not a cosmetic drift. Live on the reported project (issue #118):
|
|
323
|
+
// `state:"online", docs:{synced:0,pending:85}` with `lastSyncAt` refreshed
|
|
324
|
+
// to the exact millisecond of that transition — so the one number a reader
|
|
325
|
+
// could have used to notice that nothing had synced in 15 minutes was
|
|
326
|
+
// being refreshed BY the failure. `noteSyncActivity` is the only honest
|
|
327
|
+
// source, and it is now the only one.
|
|
314
328
|
offlineSince = null;
|
|
315
329
|
queuedOps = 0;
|
|
316
330
|
if (wasOffline) {
|
|
@@ -45,6 +45,28 @@ import { atomicWrite } from './atomic-write.ts';
|
|
|
45
45
|
/** Debounce for persisting the ledger. Same figure as the doc journal. */
|
|
46
46
|
export const LEDGER_FLUSH_MS = 1_000;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Per-path failure backoff: base delay, ceiling, and the jitter that keeps a
|
|
50
|
+
* few hundred failing paths from re-arriving in lockstep.
|
|
51
|
+
*
|
|
52
|
+
* The ceiling is ten minutes rather than something larger because a path that
|
|
53
|
+
* is waiting is a path a person is waiting for. Ten minutes is long enough to
|
|
54
|
+
* stop being the hub's problem and short enough that a recovered hub converges
|
|
55
|
+
* without anyone restarting anything.
|
|
56
|
+
*/
|
|
57
|
+
export const BACKOFF_BASE_MS = 5_000;
|
|
58
|
+
export const MAX_BACKOFF_MS = 10 * 60_000;
|
|
59
|
+
|
|
60
|
+
/** Exponential with full jitter, clamped. Exported for the tests to reason
|
|
61
|
+
* about the bounds rather than the draw. */
|
|
62
|
+
export function backoffDelayMs(attempts: number, random: () => number = Math.random): number {
|
|
63
|
+
const n = Math.max(1, Math.min(32, Math.trunc(attempts)));
|
|
64
|
+
const ceiling = Math.min(MAX_BACKOFF_MS, BACKOFF_BASE_MS * 2 ** (n - 1));
|
|
65
|
+
// Full jitter: a uniform draw over the whole window, not a band around it.
|
|
66
|
+
// The band shape still lets a synchronised cohort stay synchronised.
|
|
67
|
+
return Math.max(BACKOFF_BASE_MS, Math.round(ceiling * (0.5 + 0.5 * random())));
|
|
68
|
+
}
|
|
69
|
+
|
|
48
70
|
/**
|
|
49
71
|
* How recently a file must have been written for its stat cache to be
|
|
50
72
|
* distrusted. Comfortably above any filesystem's timestamp granularity, and
|
|
@@ -65,6 +87,15 @@ export type DeliveryState =
|
|
|
65
87
|
| 'conflict'
|
|
66
88
|
/** We tried and could not — the reason says what to fix. */
|
|
67
89
|
| 'stuck'
|
|
90
|
+
/**
|
|
91
|
+
* The workspace will NOT take this file, and no amount of waiting changes
|
|
92
|
+
* that — it is over the door's size ceiling, or the hourly allowance is
|
|
93
|
+
* spent. Distinct from `stuck` because the answer is different: `stuck` is
|
|
94
|
+
* retried, this is not, and a path that cannot succeed must stop costing a
|
|
95
|
+
* request slot on every pass (2026-09-03: a 465.8 MB video did exactly that,
|
|
96
|
+
* on every boot, forever).
|
|
97
|
+
*/
|
|
98
|
+
| 'refused'
|
|
68
99
|
/** Something references this asset and NO peer has ever offered it. */
|
|
69
100
|
| 'referenced-but-unoffered'
|
|
70
101
|
/** On this disk, and the hub has not acknowledged it. */
|
|
@@ -122,6 +153,33 @@ export interface LedgerRow {
|
|
|
122
153
|
reason?: string;
|
|
123
154
|
/** Where the conflict copy was parked, so the panel can point at it. */
|
|
124
155
|
conflictCopy?: string;
|
|
156
|
+
/**
|
|
157
|
+
* WHY this row is blocked, as a value WE chose — never re-derived from the
|
|
158
|
+
* reason string.
|
|
159
|
+
*
|
|
160
|
+
* `reason` can embed a bounded snippet of a hub-supplied error body
|
|
161
|
+
* (`failureReason()`), so matching on its text means a hostile hub can steer
|
|
162
|
+
* how its own refusals are classified. The writer knows the class at the
|
|
163
|
+
* moment it refuses; recording it removes the guess.
|
|
164
|
+
*/
|
|
165
|
+
blockedClass?: 'too-large' | 'quota' | 'unreachable' | 'refused';
|
|
166
|
+
/**
|
|
167
|
+
* PER-PATH FAILURE BACKOFF — consecutive failed attempts for this path.
|
|
168
|
+
*
|
|
169
|
+
* The plane had none, so every pass re-attempted the identical failing set at
|
|
170
|
+
* full rate: on 2026-09-03 a single sponsor logo went up 24 times in 10
|
|
171
|
+
* seconds, and 314 PUTs covered only 44 distinct paths. That is not a retry
|
|
172
|
+
* policy, it is the client helping a struggling hub struggle.
|
|
173
|
+
*/
|
|
174
|
+
attempts?: number;
|
|
175
|
+
/**
|
|
176
|
+
* Earliest wall-clock this path may be attempted again.
|
|
177
|
+
*
|
|
178
|
+
* ON THE LEDGER, not in memory, for the same reason the deletion budget is:
|
|
179
|
+
* otherwise "restart the app" is the bypass, and a restart is the ordinary
|
|
180
|
+
* response to "sync seems stuck".
|
|
181
|
+
*/
|
|
182
|
+
nextAttemptAt?: number;
|
|
125
183
|
/**
|
|
126
184
|
* The remote hash whose bytes we have ALREADY parked aside.
|
|
127
185
|
*
|
|
@@ -227,6 +285,20 @@ export interface FileLedger {
|
|
|
227
285
|
setState(rel: string, state: DeliveryState, extra?: Partial<LedgerRow>): void;
|
|
228
286
|
forget(rel: string): void;
|
|
229
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Is this path inside its backoff window right now?
|
|
290
|
+
*
|
|
291
|
+
* A path answering true must be skipped WITHOUT spending a request slot —
|
|
292
|
+
* charging it against `MAX_REQUESTS_PER_PASS` would let a few permanently
|
|
293
|
+
* failing paths starve every healthy one, which is the shape that made two
|
|
294
|
+
* alligators runs move zero files while sending 616 MB.
|
|
295
|
+
*/
|
|
296
|
+
isBackedOff(rel: string, now: number): boolean;
|
|
297
|
+
/** Record one failed attempt and arm the next window. Returns the new delay. */
|
|
298
|
+
noteAttemptFailed(rel: string, now: number): number;
|
|
299
|
+
/** A path that worked: clear the counter and the window. */
|
|
300
|
+
noteAttemptOk(rel: string): void;
|
|
301
|
+
|
|
230
302
|
/** The outbox: hashes this machine has in flight, for self-echo detection. */
|
|
231
303
|
outboxAdd(hash: string): void;
|
|
232
304
|
outboxDone(hash: string): void;
|
|
@@ -335,6 +407,21 @@ export function createFileLedger(opts: FileLedgerOptions): FileLedger {
|
|
|
335
407
|
...(typeof r.state === 'string' ? { state: r.state as DeliveryState } : {}),
|
|
336
408
|
...(typeof r.reason === 'string' ? { reason: r.reason.slice(0, 200) } : {}),
|
|
337
409
|
...(typeof r.conflictCopy === 'string' ? { conflictCopy: r.conflictCopy } : {}),
|
|
410
|
+
...(r.blockedClass === 'too-large' ||
|
|
411
|
+
r.blockedClass === 'quota' ||
|
|
412
|
+
r.blockedClass === 'unreachable' ||
|
|
413
|
+
r.blockedClass === 'refused'
|
|
414
|
+
? { blockedClass: r.blockedClass }
|
|
415
|
+
: {}),
|
|
416
|
+
// BACKOFF STATE IS PART OF THE ROW, so it has to be part of the
|
|
417
|
+
// allowlist. This is an allowlist, not a merge: a field missing from
|
|
418
|
+
// here is written to disk and silently dropped on the next load, which
|
|
419
|
+
// would make the backoff reset on every restart — the exact bypass
|
|
420
|
+
// persisting it was meant to close.
|
|
421
|
+
...(Number.isFinite(r.attempts) && (r.attempts as number) >= 0
|
|
422
|
+
? { attempts: Math.min(Math.trunc(r.attempts as number), 1_000) }
|
|
423
|
+
: {}),
|
|
424
|
+
...(Number.isFinite(r.nextAttemptAt) ? { nextAttemptAt: r.nextAttemptAt } : {}),
|
|
338
425
|
};
|
|
339
426
|
}
|
|
340
427
|
return out;
|
|
@@ -554,6 +641,36 @@ export function createFileLedger(opts: FileLedgerOptions): FileLedger {
|
|
|
554
641
|
schedule();
|
|
555
642
|
},
|
|
556
643
|
|
|
644
|
+
isBackedOff(rel, now) {
|
|
645
|
+
const next = data.rows[rel]?.nextAttemptAt;
|
|
646
|
+
if (!Number.isFinite(next)) return false;
|
|
647
|
+
// A window that ends absurdly far out is a corrupted or hostile row, not
|
|
648
|
+
// a backoff — treat it as expired rather than letting it park a path
|
|
649
|
+
// forever. The ledger is a local file, but a restore or a clock jump can
|
|
650
|
+
// still put nonsense in it.
|
|
651
|
+
if ((next as number) - now > MAX_BACKOFF_MS) return false;
|
|
652
|
+
return now < (next as number);
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
noteAttemptFailed(rel, now) {
|
|
656
|
+
const r = rowFor(rel);
|
|
657
|
+
const attempts = (Number.isFinite(r.attempts) ? (r.attempts as number) : 0) + 1;
|
|
658
|
+
r.attempts = attempts;
|
|
659
|
+
const delay = backoffDelayMs(attempts);
|
|
660
|
+
r.nextAttemptAt = now + delay;
|
|
661
|
+
schedule();
|
|
662
|
+
return delay;
|
|
663
|
+
},
|
|
664
|
+
|
|
665
|
+
noteAttemptOk(rel) {
|
|
666
|
+
const r = data.rows[rel];
|
|
667
|
+
if (!r) return;
|
|
668
|
+
if (r.attempts === undefined && r.nextAttemptAt === undefined) return;
|
|
669
|
+
r.attempts = undefined;
|
|
670
|
+
r.nextAttemptAt = undefined;
|
|
671
|
+
schedule();
|
|
672
|
+
},
|
|
673
|
+
|
|
557
674
|
outboxAdd: (hash) => {
|
|
558
675
|
outbox.add(hash);
|
|
559
676
|
},
|