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