@1agh/maude 1.0.11 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/index.ts +43 -7
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/annotations-layer.tsx +191 -116
- package/apps/studio/annotations-model.ts +39 -0
- package/apps/studio/annotations-sync.ts +50 -0
- package/apps/studio/api.ts +821 -55
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/bin/annotate.mjs +3 -1
- package/apps/studio/bin/server-up.sh +17 -2
- package/apps/studio/canvas-edit.ts +391 -7
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/canvas-lib.tsx +82 -18
- package/apps/studio/canvas-notice-message.ts +16 -0
- package/apps/studio/canvas-notifications.tsx +19 -0
- package/apps/studio/canvas-shell.tsx +57 -6
- package/apps/studio/client/app.jsx +943 -248
- package/apps/studio/client/apply-edit-request.ts +55 -0
- package/apps/studio/client/export-center.jsx +125 -67
- package/apps/studio/client/github.js +38 -1
- package/apps/studio/client/index-loader.ts +76 -0
- package/apps/studio/client/panels/CloudBar.jsx +52 -6
- package/apps/studio/client/panels/GitPanel.jsx +103 -0
- package/apps/studio/client/panels/OnboardingWizard.jsx +36 -7
- package/apps/studio/client/panels/RepoBranchSwitcher.jsx +92 -27
- package/apps/studio/client/panels/SourceConflictPanel.jsx +166 -0
- package/apps/studio/client/panels/SyncPanel.jsx +238 -4
- package/apps/studio/client/panels/TeamProjects.jsx +376 -0
- package/apps/studio/client/panels/file-deep-link-dialog.jsx +54 -0
- package/apps/studio/client/photo-knobs.jsx +3 -0
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/share-dialog.jsx +91 -0
- package/apps/studio/client/share-link.js +94 -0
- package/apps/studio/client/styles/3-shell-maude.css +69 -18
- package/apps/studio/client/styles/4-components.css +44 -1
- package/apps/studio/client/tree-row-menu.jsx +48 -3
- package/apps/studio/client/whats-new.jsx +26 -32
- package/apps/studio/cloud/endpoints.ts +132 -0
- package/apps/studio/collab/index.ts +4 -0
- package/apps/studio/collab/persistence.ts +137 -17
- package/apps/studio/collab/registry.ts +19 -21
- package/apps/studio/collab/room.ts +13 -2
- package/apps/studio/context.ts +59 -0
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1911 -1879
- package/apps/studio/dist/runtime/.min-sizes.json +1 -0
- package/apps/studio/dist/runtime/sonner.js +1 -0
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +296 -19
- package/apps/studio/inspect.ts +19 -0
- package/apps/studio/managed-projects.ts +150 -0
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/notifications.tsx +292 -0
- package/apps/studio/runtime-bundle.ts +2 -0
- package/apps/studio/server.ts +69 -6
- package/apps/studio/sync/accepted-cold-start.ts +225 -0
- package/apps/studio/sync/accepted-link.ts +320 -0
- package/apps/studio/sync/action-stage.ts +343 -0
- package/apps/studio/sync/agent.ts +104 -45
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/cell-file-events.ts +3 -0
- package/apps/studio/sync/codec.ts +192 -34
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/ctl-provider.ts +15 -2
- package/apps/studio/sync/document-discovery.ts +81 -0
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-membership.ts +10 -2
- package/apps/studio/sync/file-plane.ts +926 -47
- package/apps/studio/sync/index.ts +1786 -36
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +150 -22
- package/apps/studio/sync/poke.ts +4 -2
- package/apps/studio/sync/presentation.ts +311 -5
- package/apps/studio/sync/projection.ts +820 -23
- package/apps/studio/sync/repeated-module.ts +73 -0
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/revision-barrier.ts +129 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/seed-repair.ts +46 -0
- package/apps/studio/sync/source-merge.ts +100 -0
- package/apps/studio/sync/source-ops.ts +289 -0
- package/apps/studio/sync/source-recovery.ts +70 -0
- package/apps/studio/sync/source-validation.ts +56 -0
- package/apps/studio/sync/status.ts +201 -6
- package/apps/studio/sync/transaction-client.ts +460 -0
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/sync/writer-registry.ts +236 -0
- package/apps/studio/text-caret.ts +35 -0
- package/apps/studio/undo-hud.tsx +9 -87
- package/apps/studio/use-canvas-media-drop.tsx +4 -39
- package/apps/studio/whats-new.json +173 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +892 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +33 -9
- package/plugins/design/dependencies.json +3 -3
- package/plugins/design/templates/_shell.html +47 -0
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/plugins/flow/dependencies.json +3 -3
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -545
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/backgrounding-claude-auth.sh +0 -12
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -23
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/noisy-claude-auth.mjs +0 -27
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/fixtures/wedged-claude-auth.sh +0 -11
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -337
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// AI and multi-file action boundaries — plan T16 (DDR-241).
|
|
2
|
+
//
|
|
3
|
+
// An agent turn (the in-app chat) or a `/design:edit` run changes several
|
|
4
|
+
// files, one tool call at a time. Proposed as they land, the project would
|
|
5
|
+
// accept a canvas edited against a module the agent has not written yet, and
|
|
6
|
+
// an agent that fails half way would leave that half published. So while an
|
|
7
|
+
// AI action is OPEN, the file changes it makes are STAGED here instead of
|
|
8
|
+
// proposed: kept on disk, remembered with the value each was derived from,
|
|
9
|
+
// and proposed together — one transaction, one history action — when the
|
|
10
|
+
// action ends well.
|
|
11
|
+
//
|
|
12
|
+
// When it does not (the agent errors, is cancelled, stops at its token limit,
|
|
13
|
+
// or its heartbeat goes silent) the stage is HELD: nothing is published and
|
|
14
|
+
// nothing is thrown away. The person decides — publish it as it stands, or
|
|
15
|
+
// discard it (the accepted version returns; the candidate stays in the
|
|
16
|
+
// recovery slots). A held stage survives a restart (`_state/ai-stage.json`),
|
|
17
|
+
// so a crash can never turn into a partial publish by the next cold start.
|
|
18
|
+
//
|
|
19
|
+
// What is staged is only a WATCHER change (a tool writing a file). An edit the
|
|
20
|
+
// person makes in the UI is theirs and is proposed at once — unless it is made
|
|
21
|
+
// on top of a staged file, in which case it carries the agent's unpublished
|
|
22
|
+
// bytes and waits behind the stage (`dependents`), exactly as U2 waits on U1.
|
|
23
|
+
|
|
24
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
|
|
27
|
+
import type { LaneProposal, ProposalOutcome } from './projection.ts';
|
|
28
|
+
import type { Operation, ProposalResult } from './transaction-client.ts';
|
|
29
|
+
|
|
30
|
+
export type StageState = 'open' | 'held';
|
|
31
|
+
|
|
32
|
+
export interface StagedLane {
|
|
33
|
+
slug: string;
|
|
34
|
+
doc: string;
|
|
35
|
+
lane: LaneProposal['lane'];
|
|
36
|
+
/** The value the FIRST staged change was derived from — the group's base. */
|
|
37
|
+
baseContent: string;
|
|
38
|
+
/** The latest staged value. */
|
|
39
|
+
content: string;
|
|
40
|
+
writeId?: string;
|
|
41
|
+
/** Every staged proposal's transaction id — aliased to the group's on commit. */
|
|
42
|
+
txs: string[];
|
|
43
|
+
resolvers: Array<(o: ProposalOutcome) => void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StageSummary {
|
|
47
|
+
state: StageState;
|
|
48
|
+
label: string;
|
|
49
|
+
canvases: string[];
|
|
50
|
+
since: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface Dependent {
|
|
54
|
+
send: (dependsOn: string[] | undefined) => void;
|
|
55
|
+
drop: (o: ProposalOutcome) => void;
|
|
56
|
+
dependsOn: string[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ActionStageOptions {
|
|
60
|
+
designRoot: string;
|
|
61
|
+
/** Send one grouped action; resolves with the final result. */
|
|
62
|
+
propose: (action: {
|
|
63
|
+
label: string;
|
|
64
|
+
kind: string;
|
|
65
|
+
operations: Operation[];
|
|
66
|
+
transactionId: string;
|
|
67
|
+
}) => Promise<ProposalResult>;
|
|
68
|
+
newTransactionId: () => string;
|
|
69
|
+
onChange?: (summary: StageSummary | null) => void;
|
|
70
|
+
log?: Pick<Console, 'log' | 'warn'>;
|
|
71
|
+
now?: () => number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createActionStage(opts: ActionStageOptions) {
|
|
75
|
+
const log = opts.log ?? console;
|
|
76
|
+
const now = opts.now ?? Date.now;
|
|
77
|
+
const file = path.join(opts.designRoot, '_state', 'ai-stage.json');
|
|
78
|
+
|
|
79
|
+
let state: StageState | null = null;
|
|
80
|
+
let label = '';
|
|
81
|
+
let since = 0;
|
|
82
|
+
const keys = new Set<string>();
|
|
83
|
+
let failed = false;
|
|
84
|
+
const lanes = new Map<string, StagedLane>();
|
|
85
|
+
const dependents: Dependent[] = [];
|
|
86
|
+
/** A staged transaction id → the group transaction that carried it. */
|
|
87
|
+
const alias = new Map<string, string>();
|
|
88
|
+
/** Slugs a restored (held) stage covers — cold-start changes to them are staged. */
|
|
89
|
+
const restoredSlugs = new Set<string>();
|
|
90
|
+
/** Bases restored from disk after a restart: `${slug}|${lane}` → baseContent. */
|
|
91
|
+
const restoredBases = new Map<string, string>();
|
|
92
|
+
|
|
93
|
+
function summary(): StageSummary | null {
|
|
94
|
+
if (!state) return null;
|
|
95
|
+
const canvases = new Set<string>([...restoredSlugs]);
|
|
96
|
+
for (const l of lanes.values()) canvases.add(l.slug);
|
|
97
|
+
return { state, label, canvases: [...canvases].sort(), since };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function persist(): void {
|
|
101
|
+
try {
|
|
102
|
+
if (!state || (lanes.size === 0 && restoredSlugs.size === 0)) {
|
|
103
|
+
rmSync(file, { force: true });
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
107
|
+
const body = {
|
|
108
|
+
v: 1,
|
|
109
|
+
state,
|
|
110
|
+
label,
|
|
111
|
+
since,
|
|
112
|
+
lanes: [...lanes.values()].map((l) => ({
|
|
113
|
+
slug: l.slug,
|
|
114
|
+
doc: l.doc,
|
|
115
|
+
lane: l.lane,
|
|
116
|
+
baseContent: l.baseContent,
|
|
117
|
+
})),
|
|
118
|
+
restored: [...restoredBases.entries()].map(([k, baseContent]) => {
|
|
119
|
+
const [slug, lane] = k.split('|');
|
|
120
|
+
return { slug, lane, baseContent };
|
|
121
|
+
}),
|
|
122
|
+
};
|
|
123
|
+
writeFileSync(`${file}.tmp`, JSON.stringify(body));
|
|
124
|
+
renameSync(`${file}.tmp`, file);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
log.warn(`[sync/ai] could not record the unfinished action: ${(err as Error).message}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function changed(): void {
|
|
131
|
+
persist();
|
|
132
|
+
opts.onChange?.(summary());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** A previous process left a stage: it comes back HELD. */
|
|
136
|
+
function restore(): void {
|
|
137
|
+
if (!existsSync(file)) return;
|
|
138
|
+
try {
|
|
139
|
+
const body = JSON.parse(readFileSync(file, 'utf8')) as {
|
|
140
|
+
label?: string;
|
|
141
|
+
since?: number;
|
|
142
|
+
lanes?: { slug: string; lane: string; baseContent: string }[];
|
|
143
|
+
restored?: { slug: string; lane: string; baseContent: string }[];
|
|
144
|
+
};
|
|
145
|
+
const all = [...(body.lanes ?? []), ...(body.restored ?? [])];
|
|
146
|
+
if (all.length === 0) return;
|
|
147
|
+
state = 'held';
|
|
148
|
+
label = typeof body.label === 'string' ? body.label : 'AI edit';
|
|
149
|
+
since = typeof body.since === 'number' ? body.since : now();
|
|
150
|
+
for (const l of all) {
|
|
151
|
+
restoredSlugs.add(l.slug);
|
|
152
|
+
restoredBases.set(`${l.slug}|${l.lane}`, l.baseContent);
|
|
153
|
+
}
|
|
154
|
+
log.warn(
|
|
155
|
+
`[sync/ai] an unfinished AI edit (${label}) to ${restoredSlugs.size} canvas(es) was kept from the last session — it is not published until you choose.`
|
|
156
|
+
);
|
|
157
|
+
opts.onChange?.(summary());
|
|
158
|
+
} catch {
|
|
159
|
+
/* unreadable → nothing to restore */
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function begin(key: string, nextLabel: string): void {
|
|
164
|
+
keys.add(key);
|
|
165
|
+
if (state === 'open') return;
|
|
166
|
+
if (state === 'held') {
|
|
167
|
+
// A new action on top of an unfinished one continues it: its files may
|
|
168
|
+
// already carry the held bytes. It stays one decision for the person.
|
|
169
|
+
state = 'open';
|
|
170
|
+
failed = false;
|
|
171
|
+
changed();
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
state = 'open';
|
|
175
|
+
failed = false;
|
|
176
|
+
label = nextLabel.slice(0, 120) || 'AI edit';
|
|
177
|
+
since = now();
|
|
178
|
+
changed();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Should this proposal be staged instead of sent? */
|
|
182
|
+
function captures(slug: string, stageable: boolean): boolean {
|
|
183
|
+
if (!stageable || !state) return false;
|
|
184
|
+
return (
|
|
185
|
+
state === 'open' ||
|
|
186
|
+
restoredSlugs.has(slug) ||
|
|
187
|
+
[...lanes.values()].some((l) => l.slug === slug)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function capture(slug: string, doc: string, p: LaneProposal): Promise<ProposalOutcome> {
|
|
192
|
+
const k = `${slug}|${p.lane}`;
|
|
193
|
+
return new Promise((resolve) => {
|
|
194
|
+
const existing = lanes.get(k);
|
|
195
|
+
if (existing) {
|
|
196
|
+
existing.content = p.content;
|
|
197
|
+
existing.txs.push(p.transactionId);
|
|
198
|
+
existing.resolvers.push(resolve);
|
|
199
|
+
if (p.writeId) existing.writeId = p.writeId;
|
|
200
|
+
} else {
|
|
201
|
+
lanes.set(k, {
|
|
202
|
+
slug,
|
|
203
|
+
doc,
|
|
204
|
+
lane: p.lane,
|
|
205
|
+
baseContent: restoredBases.get(k) ?? p.baseContent,
|
|
206
|
+
content: p.content,
|
|
207
|
+
...(p.writeId ? { writeId: p.writeId } : {}),
|
|
208
|
+
txs: [p.transactionId],
|
|
209
|
+
resolvers: [resolve],
|
|
210
|
+
});
|
|
211
|
+
changed();
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isStagedTx(id: string): boolean {
|
|
217
|
+
for (const l of lanes.values()) if (l.txs.includes(id)) return true;
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** A non-staged proposal authored on top of a staged one waits behind the stage. */
|
|
222
|
+
function holdsDependency(dependsOn: string[] | undefined): boolean {
|
|
223
|
+
return !!dependsOn?.some((id) => isStagedTx(id));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function wait(dep: Dependent): void {
|
|
227
|
+
dependents.push(dep);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Rewrite a dependency on a staged proposal to the group that carried it. */
|
|
231
|
+
function mapDeps(dependsOn: string[] | undefined): string[] | undefined {
|
|
232
|
+
if (!dependsOn?.length) return dependsOn;
|
|
233
|
+
return dependsOn.map((id) => alias.get(id) ?? id);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function laneOutcome(r: ProposalResult, l: StagedLane): ProposalOutcome {
|
|
237
|
+
const mine = r.doc === l.doc && r.lane === l.lane;
|
|
238
|
+
return {
|
|
239
|
+
status: r.status,
|
|
240
|
+
...(r.code ? { code: r.code } : {}),
|
|
241
|
+
...(mine && typeof r.head === 'string' ? { head: r.head } : {}),
|
|
242
|
+
...(typeof r.actionId === 'string' ? { actionId: r.actionId } : {}),
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function reset(): void {
|
|
247
|
+
state = null;
|
|
248
|
+
label = '';
|
|
249
|
+
since = 0;
|
|
250
|
+
keys.clear();
|
|
251
|
+
failed = false;
|
|
252
|
+
lanes.clear();
|
|
253
|
+
restoredSlugs.clear();
|
|
254
|
+
restoredBases.clear();
|
|
255
|
+
changed();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Propose everything staged as ONE action. */
|
|
259
|
+
async function publish(): Promise<ProposalResult | null> {
|
|
260
|
+
if (!state) return null;
|
|
261
|
+
const staged = [...lanes.values()];
|
|
262
|
+
const waiting = dependents.splice(0);
|
|
263
|
+
const groupLabel = label || 'AI edit';
|
|
264
|
+
if (staged.length === 0) {
|
|
265
|
+
reset();
|
|
266
|
+
for (const d of waiting) d.send(mapDeps(d.dependsOn));
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const transactionId = opts.newTransactionId();
|
|
270
|
+
for (const l of staged) for (const tx of l.txs) alias.set(tx, transactionId);
|
|
271
|
+
const operations: Operation[] = staged.map((l) => ({
|
|
272
|
+
op: 'lane.replace',
|
|
273
|
+
doc: l.doc,
|
|
274
|
+
lane: l.lane,
|
|
275
|
+
content: l.content,
|
|
276
|
+
baseContent: l.baseContent,
|
|
277
|
+
...(l.writeId ? { writeId: l.writeId } : {}),
|
|
278
|
+
}));
|
|
279
|
+
// The stage is closed before the answer: the next action starts clean,
|
|
280
|
+
// and the dependents go out right behind the group (same ordered outbox).
|
|
281
|
+
reset();
|
|
282
|
+
const sent = opts.propose({ label: groupLabel, kind: 'ai', operations, transactionId });
|
|
283
|
+
for (const d of waiting) d.send(mapDeps(d.dependsOn));
|
|
284
|
+
const r = await sent.catch((err: unknown) => {
|
|
285
|
+
const code = (err as { code?: string })?.code ?? 'retryable';
|
|
286
|
+
return { protocol: 1, status: 'rejected', code, transactionId } as ProposalResult;
|
|
287
|
+
});
|
|
288
|
+
for (const l of staged) for (const fn of l.resolvers) fn(laneOutcome(r, l));
|
|
289
|
+
log.log(
|
|
290
|
+
`[sync/ai] ${groupLabel}: ${staged.length} change(s) ${r.status === 'accepted' ? 'published as one action' : `refused (${r.code ?? 'rejected'})`}.`
|
|
291
|
+
);
|
|
292
|
+
return r;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** End one participant. The last `done` publishes; any failure holds. */
|
|
296
|
+
async function end(key: string, outcome: 'done' | 'failed'): Promise<ProposalResult | null> {
|
|
297
|
+
if (!keys.has(key)) return null;
|
|
298
|
+
keys.delete(key);
|
|
299
|
+
if (outcome === 'failed') failed = true;
|
|
300
|
+
if (keys.size > 0 || state !== 'open') return null;
|
|
301
|
+
if (failed) {
|
|
302
|
+
state = 'held';
|
|
303
|
+
changed();
|
|
304
|
+
log.warn(
|
|
305
|
+
`[sync/ai] ${label}: the action did not finish — ${lanes.size} change(s) kept on this device, not published.`
|
|
306
|
+
);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return publish();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** Throw the held changes away: every lane returns to the accepted version. */
|
|
313
|
+
function discard(): number {
|
|
314
|
+
if (state !== 'held') return 0;
|
|
315
|
+
const staged = [...lanes.values()];
|
|
316
|
+
const waiting = dependents.splice(0);
|
|
317
|
+
const slugs = new Set([...restoredSlugs, ...staged.map((l) => l.slug)]);
|
|
318
|
+
reset();
|
|
319
|
+
for (const l of staged)
|
|
320
|
+
for (const fn of l.resolvers) fn({ status: 'rejected', code: 'discarded' });
|
|
321
|
+
for (const d of waiting) d.drop({ status: 'rejected', code: 'discarded' });
|
|
322
|
+
return slugs.size;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
restore,
|
|
327
|
+
begin,
|
|
328
|
+
end,
|
|
329
|
+
publish,
|
|
330
|
+
discard,
|
|
331
|
+
captures,
|
|
332
|
+
capture,
|
|
333
|
+
holdsDependency,
|
|
334
|
+
wait,
|
|
335
|
+
mapDeps,
|
|
336
|
+
summary,
|
|
337
|
+
get state() {
|
|
338
|
+
return state;
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export type ActionStage = ReturnType<typeof createActionStage>;
|
|
@@ -52,12 +52,10 @@ import {
|
|
|
52
52
|
cssFromDoc,
|
|
53
53
|
htmlFromDoc,
|
|
54
54
|
isEmptyAnnotationsSvg,
|
|
55
|
-
markSeeded,
|
|
56
55
|
mergeSharedMetaIntoLocal,
|
|
57
56
|
metaFromDoc,
|
|
58
57
|
movedToFromDoc,
|
|
59
58
|
repairSharedMeta,
|
|
60
|
-
seededByFromDoc,
|
|
61
59
|
stampAnnotationsEdit,
|
|
62
60
|
stampBodyEdit,
|
|
63
61
|
Y_SYNC_TYPES,
|
|
@@ -65,12 +63,14 @@ import {
|
|
|
65
63
|
import {
|
|
66
64
|
decideAnnotationsColdStart,
|
|
67
65
|
decideColdStart,
|
|
68
|
-
|
|
66
|
+
decideCssColdStart,
|
|
69
67
|
unionCommentsById,
|
|
70
68
|
} from './cold-start.ts';
|
|
71
|
-
import { applyColdStart } from './cold-start-apply.ts';
|
|
69
|
+
import { applyColdStart, type ColdStartSnapshotReason } from './cold-start-apply.ts';
|
|
70
|
+
import { dedupeCommentsById, hasDuplicateComments } from './comment-identity.ts';
|
|
72
71
|
import { type EchoGuard, hashBytes } from './echo-guard.ts';
|
|
73
72
|
import type { SyncJournal } from './journal.ts';
|
|
73
|
+
import { rememberSeed, repairSeedDuplication } from './seed-repair.ts';
|
|
74
74
|
|
|
75
75
|
export const DOC_FLUSH_MS = 800;
|
|
76
76
|
|
|
@@ -80,7 +80,7 @@ export const DOC_FLUSH_MS = 800;
|
|
|
80
80
|
* the cold-start race — a genuine peer edit that arrives minutes later is NEVER
|
|
81
81
|
* collapsed back to our original seed. Generous relative to the hub sync RTT.
|
|
82
82
|
*/
|
|
83
|
-
export
|
|
83
|
+
export { SEED_REPAIR_WINDOW_MS } from './seed-repair.ts';
|
|
84
84
|
|
|
85
85
|
export interface CanvasSyncPaths {
|
|
86
86
|
/** Absolute path to <designRoot>/<canvas>.html. */
|
|
@@ -126,7 +126,7 @@ export interface CanvasSyncAgentOptions {
|
|
|
126
126
|
* without it the conflict path still resolves newest-wins, just without the
|
|
127
127
|
* recovery snapshots (test-only constructions).
|
|
128
128
|
*/
|
|
129
|
-
snapshot?: (content: string, reason:
|
|
129
|
+
snapshot?: (content: string, reason: ColdStartSnapshotReason) => Promise<string | null>;
|
|
130
130
|
/**
|
|
131
131
|
* Called when a non-adopt reconcile (cold-start / post-git-pull) found
|
|
132
132
|
* divergent non-empty content on both sides (DDR-102). `winner` is the side
|
|
@@ -185,13 +185,6 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
185
185
|
let lastMeta: string | null = null;
|
|
186
186
|
let lastCss: string | null = null;
|
|
187
187
|
|
|
188
|
-
// F1 — the body this agent pushed on `seed-local-up`, plus the deadline past
|
|
189
|
-
// which the de-dup repair stops firing. Set on seed; nulled only when the
|
|
190
|
-
// window lapses (after convergence the repair is a cheap no-op — the
|
|
191
|
-
// `body === seedInfo.body` guard short-circuits, so we don't bother clearing).
|
|
192
|
-
// Null = this agent never seeded (so it never repairs).
|
|
193
|
-
let seedInfo: { body: string; until: number } | null = null;
|
|
194
|
-
|
|
195
188
|
function onDocUpdate(_update: Uint8Array, updateOrigin: unknown): void {
|
|
196
189
|
if (stopped) return;
|
|
197
190
|
// Self-applied (we just synced from disk) — disk is already current.
|
|
@@ -200,6 +193,7 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
200
193
|
// concurrent peer's identical seed onto ours. Collapse it (single elected
|
|
201
194
|
// writer) BEFORE the flush below can mirror a doubled body to disk.
|
|
202
195
|
maybeRepairSeedDuplication();
|
|
196
|
+
maybeCollapseCommentsDuplication();
|
|
203
197
|
scheduleFlush();
|
|
204
198
|
}
|
|
205
199
|
|
|
@@ -238,24 +232,53 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
238
232
|
* variant is an accepted follow-up, not handled in-flight.
|
|
239
233
|
*/
|
|
240
234
|
function maybeRepairSeedDuplication(): void {
|
|
241
|
-
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
235
|
+
const repaired = repairSeedDuplication(doc, origin);
|
|
236
|
+
if (repaired.includes('body')) {
|
|
237
|
+
lastHtml = htmlFromDoc(doc);
|
|
238
|
+
opts.journal?.record(slug, { bodyHash: hashBytes(lastHtml) });
|
|
245
239
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
240
|
+
if (repaired.includes('css')) {
|
|
241
|
+
lastCss = cssFromDoc(doc);
|
|
242
|
+
if (lastCss !== null) opts.journal?.record(slug, { cssHash: hashBytes(lastCss) });
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (repaired.length > 0) {
|
|
246
|
+
console.warn(
|
|
247
|
+
`[sync/${slug}] concurrent cold-seed duplication detected in ${repaired.join(
|
|
248
|
+
' + '
|
|
249
|
+
)} — collapsed to one copy (F1).`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* The comments half of the same collision (issue #112) — and deliberately
|
|
256
|
+
* NOT shaped like the body's.
|
|
257
|
+
*
|
|
258
|
+
* The body repair needs the `seededBy` election and an exact-repeat proof
|
|
259
|
+
* because collapsing a Y.Text can only be justified when the discarded bytes
|
|
260
|
+
* are provably a copy. Comments carry stable ids, so "the same comment twice"
|
|
261
|
+
* is decidable per entry, not per lane: the collapse keeps one of two
|
|
262
|
+
* byte-identical-by-identity entries and discards nothing a reader could
|
|
263
|
+
* want. That makes it safe for EVERY peer to run — no election, no seed
|
|
264
|
+
* window, no snapshot (per issue #114's follow-up, a collapse that preserves
|
|
265
|
+
* zero unique bytes must not consume the `_history/` snapshot ring).
|
|
266
|
+
*
|
|
267
|
+
* It converges because `applyCommentsToDoc` emits the collapse as a pure
|
|
268
|
+
* delete of items every peer holds, and concurrent deletes of the same items
|
|
269
|
+
* are idempotent. Running it here — on the remote-update path, before the
|
|
270
|
+
* flush — is what stops a duplicated array from reaching disk and becoming
|
|
271
|
+
* the canvas's new truth.
|
|
272
|
+
*/
|
|
273
|
+
function maybeCollapseCommentsDuplication(): void {
|
|
274
|
+
const current = commentsFromDoc(doc);
|
|
275
|
+
if (!hasDuplicateComments(current)) return;
|
|
276
|
+
const collapsed = dedupeCommentsById(current);
|
|
252
277
|
doc.transact(() => {
|
|
253
|
-
|
|
278
|
+
applyCommentsToDoc(doc, collapsed, origin);
|
|
254
279
|
}, origin);
|
|
255
|
-
lastHtml = canonical;
|
|
256
|
-
opts.journal?.record(slug, { bodyHash: hashBytes(canonical) });
|
|
257
280
|
console.warn(
|
|
258
|
-
`[sync/${slug}]
|
|
281
|
+
`[sync/${slug}] comments lane held ${current.length} entries for ${collapsed.length} distinct comments — collapsed (#112).`
|
|
259
282
|
);
|
|
260
283
|
}
|
|
261
284
|
|
|
@@ -298,6 +321,12 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
298
321
|
}
|
|
299
322
|
|
|
300
323
|
function writeCommentsIfChanged(): void {
|
|
324
|
+
// This lane writes `_comments/<slug>.json` DIRECTLY, not through the api's
|
|
325
|
+
// `saveCommentsForFile` — so it does not inherit that boundary's dedupe.
|
|
326
|
+
// Collapse first (issue #112): a duplicated array reaching disk is how the
|
|
327
|
+
// doubling outlived restarts and became the canvas's truth. Idempotent, and
|
|
328
|
+
// self-originated, so it can't re-enter `onDocUpdate`.
|
|
329
|
+
maybeCollapseCommentsDuplication();
|
|
301
330
|
const next = commentsFromDoc(doc);
|
|
302
331
|
const serialized = next.length > 0 ? `${JSON.stringify(next, null, 2)}\n` : '';
|
|
303
332
|
if (serialized === lastComments) return;
|
|
@@ -444,9 +473,8 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
444
473
|
// F1 — adopt is also a "seed local up" (first link / fresh hub); claim
|
|
445
474
|
// it + arm the repair window so two peers adopting the same draft into
|
|
446
475
|
// one hub at once self-heal the same way the decision-table seed does.
|
|
447
|
-
|
|
476
|
+
rememberSeed(doc, localHtml, localCss, origin);
|
|
448
477
|
}, origin);
|
|
449
|
-
seedInfo = { body: localHtml, until: Date.now() + SEED_REPAIR_WINDOW_MS };
|
|
450
478
|
}
|
|
451
479
|
if (localComments !== null) {
|
|
452
480
|
const parsed = tryParseJsonArray(localComments);
|
|
@@ -501,13 +529,13 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
501
529
|
if (applyHtmlToDoc(doc, body, origin)) stampBodyEdit(doc, origin);
|
|
502
530
|
// F1 — claim the seed (clientID marker) in the SAME update so a
|
|
503
531
|
// concurrent second seeder's merge resolves a single elected writer.
|
|
504
|
-
|
|
532
|
+
rememberSeed(doc, body, localCss, origin);
|
|
505
533
|
}, origin);
|
|
506
534
|
lastHtml = body;
|
|
507
|
-
// Arm the de-dup repair window: if another peer seeded the same
|
|
508
|
-
// same instant, the merged Y.Text will double — maybeRepairSeedDuplication
|
|
509
|
-
// (on the elected owner) collapses it back during this window.
|
|
510
|
-
|
|
535
|
+
// Arm the de-dup repair window: if another peer seeded the same content at
|
|
536
|
+
// the same instant, the merged Y.Text will double — maybeRepairSeedDuplication
|
|
537
|
+
// (on the elected owner) collapses it back during this window. `localCss`
|
|
538
|
+
// is remembered too, since it collides identically (issue #114).
|
|
511
539
|
opts.journal?.record(slug, { bodyHash: hashBytes(body) });
|
|
512
540
|
};
|
|
513
541
|
|
|
@@ -594,23 +622,54 @@ export function createCanvasSyncAgent(opts: CanvasSyncAgentOptions): CanvasSyncA
|
|
|
594
622
|
}
|
|
595
623
|
}
|
|
596
624
|
|
|
597
|
-
// ---- css: follow the body winner
|
|
598
|
-
|
|
599
|
-
|
|
625
|
+
// ---- css: PER-LANE resolution (issue #114 — was "follow the body winner")
|
|
626
|
+
// The css lane used to be the only Plane-A lane with no table and no
|
|
627
|
+
// duplication guard, which is why it was the only one that NEVER came out
|
|
628
|
+
// of a multi-peer cold start clean. decideCssColdStart owns the rows now;
|
|
629
|
+
// the two that matter are the exact-repeat collapse and the DDR-064
|
|
630
|
+
// empty-lane guard, and it finally consumes the `cssHash` checkpoint the
|
|
631
|
+
// journal has been writing since DDR-102.
|
|
632
|
+
{
|
|
633
|
+
const cssDecision = decideCssColdStart({
|
|
634
|
+
local: localCss,
|
|
635
|
+
doc: docCss,
|
|
636
|
+
journalHash: opts.journal?.get(slug)?.cssHash ?? null,
|
|
637
|
+
hash: hashBytes,
|
|
638
|
+
bodyWinner,
|
|
639
|
+
});
|
|
640
|
+
if (cssDecision.recoveredDuplication) {
|
|
641
|
+
// WARN, BUT DO NOT SNAPSHOT — symmetric with the body's
|
|
642
|
+
// `recover-seed-dup`, which deliberately doesn't either.
|
|
643
|
+
//
|
|
644
|
+
// A snapshot stood here briefly, on the reasoning that `X + X` is valid
|
|
645
|
+
// CSS (unlike a doubled body, which can't build) so the collapse might
|
|
646
|
+
// discard a real edit. The reasoning was wrong about the bytes:
|
|
647
|
+
// `isExactRepeat` is true only when the doc is EXACTLY local repeated
|
|
648
|
+
// N times, so the copies thrown away contain nothing the retained copy
|
|
649
|
+
// doesn't. The snapshot preserved zero unique bytes — and cost a slot
|
|
650
|
+
// in a per-slug ring buffer that `pruneSnapshots` evicts oldest-first
|
|
651
|
+
// at MAX_SNAPSHOTS_PER_SLUG. A hostile hub that keeps re-doubling this
|
|
652
|
+
// lane could therefore drive one worthless snapshot per reconcile and
|
|
653
|
+
// push the user's genuine `pre-sync-local` and conflict copies off the
|
|
654
|
+
// end — turning DDR-102's recovery spine into the attack surface.
|
|
655
|
+
// Adversarial review found it; the safety net was the vulnerability.
|
|
656
|
+
console.warn(`[sync/${slug}] cold-start css: ${cssDecision.reason}`);
|
|
657
|
+
}
|
|
658
|
+
if (cssDecision.winner === 'local' && paths.css && localCss !== null) {
|
|
600
659
|
applyCssToDoc(doc, localCss, origin);
|
|
601
660
|
lastCss = localCss;
|
|
602
661
|
opts.journal?.record(slug, { cssHash: hashBytes(localCss) });
|
|
662
|
+
} else if (cssDecision.winner === 'hub') {
|
|
663
|
+
lastCss = docCss;
|
|
664
|
+
if (paths.css && docCss !== null && localCss !== docCss) {
|
|
665
|
+
const hash = hashBytes(docCss);
|
|
666
|
+
echoGuard.record(paths.css, hash);
|
|
667
|
+
writer(paths.css, docCss);
|
|
668
|
+
opts.journal?.record(slug, { cssHash: hash });
|
|
669
|
+
}
|
|
603
670
|
} else {
|
|
604
671
|
lastCss = docCss;
|
|
605
672
|
}
|
|
606
|
-
} else {
|
|
607
|
-
lastCss = docCss;
|
|
608
|
-
if (paths.css && docCss !== null && localCss !== docCss) {
|
|
609
|
-
const hash = hashBytes(docCss);
|
|
610
|
-
echoGuard.record(paths.css, hash);
|
|
611
|
-
writer(paths.css, docCss);
|
|
612
|
-
opts.journal?.record(slug, { cssHash: hash });
|
|
613
|
-
}
|
|
614
673
|
}
|
|
615
674
|
|
|
616
675
|
// ---- meta: hub wins where it has an opinion, local seeds where it does not
|
|
@@ -44,6 +44,7 @@ import { type Dirent, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
|
44
44
|
import path from 'node:path';
|
|
45
45
|
|
|
46
46
|
import { type CanvasGroupLike, classifyProjectFile, isFilePlaneClass } from './file-membership.ts';
|
|
47
|
+
import { failureReason, retryAfterMs } from './retry-after.ts';
|
|
47
48
|
|
|
48
49
|
/** Max designRoot-relative depth (matches the classifier's 8-segment cap). */
|
|
49
50
|
const MAX_SEGMENTS = 8;
|
|
@@ -84,27 +85,12 @@ export interface AssetPushProgress {
|
|
|
84
85
|
* the payload reaches `_sync.json` + every open tab, so it stays bounded). */
|
|
85
86
|
export const MAX_LISTED_FAILURES = 20;
|
|
86
87
|
|
|
87
|
-
/** Longest single pause the hub can ask for. Matches the hub's rate-limit
|
|
88
|
-
* window (60 s) — a `Retry-After` larger than that is either a typo or a hub
|
|
89
|
-
* we should not be blocking a boot sweep on. */
|
|
90
|
-
const MAX_RETRY_DELAY_MS = 60_000;
|
|
91
|
-
|
|
92
|
-
/** Where an absent/unparsable `Retry-After` lands. Old hubs (pre-fix) send a
|
|
93
|
-
* bare 429 with no header, and their window is the same 60 s. */
|
|
94
|
-
const DEFAULT_RETRY_DELAY_MS = 60_000;
|
|
95
|
-
|
|
96
88
|
/** Total time ONE sweep may spend waiting out 429s. The paced retry exists to
|
|
97
89
|
* keep an un-upgraded hub livable, not to turn a boot into an hour-long
|
|
98
90
|
* background stall — past this, the remaining refusals fail fast and the
|
|
99
91
|
* next-boot backstop takes them. */
|
|
100
92
|
const MAX_SWEEP_BACKOFF_MS = 5 * 60_000;
|
|
101
93
|
|
|
102
|
-
/** How much of an error body reaches `failed[].reason`. Enough to tell "rate
|
|
103
|
-
* limit exceeded" from a Cloudflare error page — the distinction the 2026-08-11
|
|
104
|
-
* RCA had to reconstruct from edge logs because the client kept only a status
|
|
105
|
-
* code. Hub-supplied text ⇒ bounded and stripped before it reaches the UI. */
|
|
106
|
-
const ERROR_SNIPPET_CHARS = 80;
|
|
107
|
-
|
|
108
94
|
/**
|
|
109
95
|
* Every upload closes its connection. NOT an optimization — a correctness
|
|
110
96
|
* requirement, learned the expensive way (2026-08-11, second pass).
|
|
@@ -333,38 +319,6 @@ async function probePresent(ctx: {
|
|
|
333
319
|
}
|
|
334
320
|
}
|
|
335
321
|
|
|
336
|
-
/** `Retry-After: <seconds>` → ms, clamped. Only the delta-seconds form is
|
|
337
|
-
* parsed; the HTTP-date form is not something our hub emits. */
|
|
338
|
-
function retryAfterMs(header: string | null): number {
|
|
339
|
-
const secs = Number(String(header ?? '').trim());
|
|
340
|
-
if (!Number.isFinite(secs) || secs <= 0) return DEFAULT_RETRY_DELAY_MS;
|
|
341
|
-
return Math.min(secs * 1000, MAX_RETRY_DELAY_MS);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Why an upload was refused, in words — status PLUS a bounded snippet of the
|
|
346
|
-
* body. The hub says `{"error":"rate limit exceeded"}`; an edge that never
|
|
347
|
-
* reached the hub says HTML. Those are different bugs and the Sync panel should
|
|
348
|
-
* not make a person read logs to tell them apart.
|
|
349
|
-
*
|
|
350
|
-
* The body is hub-supplied ⇒ untrusted (DDR-054): control characters stripped,
|
|
351
|
-
* whitespace collapsed, hard length cap, and it only ever renders as text.
|
|
352
|
-
*/
|
|
353
|
-
async function failureReason(res: Response): Promise<string> {
|
|
354
|
-
let snippet = '';
|
|
355
|
-
try {
|
|
356
|
-
snippet = (await res.text())
|
|
357
|
-
// biome-ignore lint/suspicious/noControlCharactersInRegex: stripping them is the point.
|
|
358
|
-
.replace(/[\u0000-\u001f\u007f]/g, ' ')
|
|
359
|
-
.replace(/\s+/g, ' ')
|
|
360
|
-
.trim()
|
|
361
|
-
.slice(0, ERROR_SNIPPET_CHARS);
|
|
362
|
-
} catch {
|
|
363
|
-
/* a body we cannot read tells us nothing — the status still does */
|
|
364
|
-
}
|
|
365
|
-
return snippet ? `HTTP ${res.status} — ${snippet}` : `HTTP ${res.status}`;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
322
|
/**
|
|
369
323
|
* One upload, with the two retries that are worth having in-boot.
|
|
370
324
|
*
|
|
@@ -89,6 +89,9 @@ export function startCellFileEvents(
|
|
|
89
89
|
url: target.url,
|
|
90
90
|
token: target.token,
|
|
91
91
|
onPoke: (head) => healer.onPoke(head),
|
|
92
|
+
// The paired cell runtime does not open a second control provider. Route
|
|
93
|
+
// metadata invalidation to its bounded discovery queue, not the journal.
|
|
94
|
+
onDocuments: () => ctx.bus.emit('sync:documents-changed'),
|
|
92
95
|
});
|
|
93
96
|
|
|
94
97
|
// Every write this process makes surfaces here, because both synthetic-event
|