@1agh/maude 0.28.1 → 0.30.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 -3
- package/{plugins/design/dev-server → apps/studio}/ai-banner.tsx +1 -2
- package/apps/studio/annotations-align.ts +149 -0
- package/apps/studio/annotations-bindings.ts +197 -0
- package/{plugins/design/dev-server → apps/studio}/annotations-context-toolbar.tsx +681 -151
- package/apps/studio/annotations-groups.ts +270 -0
- package/apps/studio/annotations-layer.tsx +4537 -0
- package/apps/studio/annotations-model.ts +2077 -0
- package/apps/studio/annotations-snap.ts +125 -0
- package/{plugins/design/dev-server → apps/studio}/api.ts +616 -70
- package/{plugins/design/dev-server → apps/studio}/artboard-activity-overlay.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/artboard-marquee.tsx +1 -1
- package/apps/studio/bin/_motion-sample-playwright.mjs +129 -0
- package/{plugins/design/dev-server → apps/studio}/bin/_png-playwright.mjs +1 -1
- package/apps/studio/bin/annotate.mjs +732 -0
- package/apps/studio/bin/annotate.sh +17 -0
- package/{plugins/design/dev-server → apps/studio}/bin/canvas-edit.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/draw-proof.sh +37 -2
- package/{plugins/design/dev-server → apps/studio}/bin/preflight.sh +1 -1
- package/apps/studio/bin/read-annotations.mjs +573 -0
- package/apps/studio/bin/read-annotations.sh +11 -0
- package/{plugins/design/dev-server → apps/studio}/bin/runtime-health.sh +1 -1
- package/{plugins/design/dev-server → apps/studio}/bin/screenshot.sh +62 -2
- package/{plugins/design/dev-server → apps/studio}/bin/server-up.sh +9 -3
- package/apps/studio/bin/to-lottie-verify.html +68 -0
- package/apps/studio/bin/to-lottie.sh +128 -0
- package/{plugins/design/dev-server → apps/studio}/build.ts +6 -4
- package/{plugins/design/dev-server → apps/studio}/canvas-arrowheads.ts +79 -10
- package/{plugins/design/dev-server → apps/studio}/canvas-comment-mount.tsx +1 -1
- package/apps/studio/canvas-create.ts +104 -0
- package/{plugins/design/dev-server → apps/studio}/canvas-cursors.ts +48 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-edit.ts +257 -7
- package/{plugins/design/dev-server → apps/studio}/canvas-icons.tsx +165 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-inline.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib-resolver.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/canvas-lib.tsx +31 -12
- package/{plugins/design/dev-server → apps/studio}/canvas-shell.tsx +360 -48
- package/apps/studio/client/app.jsx +6742 -0
- package/apps/studio/client/canvas-url.js +81 -0
- package/{plugins/design/dev-server → apps/studio}/client/comments-overlay.css +41 -27
- package/apps/studio/client/index.html +16 -0
- package/apps/studio/client/styles/1-tokens-maude.css +296 -0
- package/apps/studio/client/styles/3-shell-maude.css +1161 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/3-shell.css +129 -0
- package/apps/studio/client/styles/4-components-maude.css +74 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/4-components.css +396 -34
- package/apps/studio/client/styles/5-maude-overrides.css +137 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles/_index.css +9 -0
- package/{plugins/design/dev-server → apps/studio}/client/styles.css +1 -1
- package/apps/studio/client/tour/overlay.jsx +279 -0
- package/apps/studio/client/tour/usage-tour.js +48 -0
- package/apps/studio/client/whats-new-seen.js +52 -0
- package/apps/studio/client/whats-new.jsx +250 -0
- package/{plugins/design/dev-server → apps/studio}/commands/annotation-strokes-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/commands/equal-spacing-command.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/commands/move-artboards-command.ts +2 -2
- package/{plugins/design/dev-server → apps/studio}/comments-overlay.tsx +32 -11
- package/{plugins/design/dev-server → apps/studio}/context-menu.tsx +5 -4
- package/{plugins/design/dev-server → apps/studio}/contextual-toolbar.tsx +18 -7
- package/{plugins/design/dev-server → apps/studio}/cursors-overlay.tsx +21 -21
- package/apps/studio/dist/client.bundle.js +25545 -0
- package/apps/studio/dist/comment-mount.js +2046 -0
- package/apps/studio/dist/styles.css +8018 -0
- package/apps/studio/dom-selection.ts +342 -0
- package/apps/studio/draw/animate.ts +333 -0
- package/{plugins/design/dev-server → apps/studio}/draw/brush.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/composition.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/geometry.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/index.ts +4 -1
- package/{plugins/design/dev-server → apps/studio}/draw/layout.ts +1 -1
- package/apps/studio/draw/morph.ts +204 -0
- package/{plugins/design/dev-server → apps/studio}/draw/optimize.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/palette.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/draw/primitives.ts +1 -1
- package/apps/studio/draw/serialize-animate.ts +324 -0
- package/{plugins/design/dev-server → apps/studio}/draw/serialize.ts +9 -3
- package/apps/studio/draw/test/animate.test.ts +172 -0
- package/apps/studio/draw/test/morph.test.ts +107 -0
- package/apps/studio/draw/test/serialize-animate.test.ts +142 -0
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-detector.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/equal-spacing-handles.tsx +7 -29
- package/{plugins/design/dev-server → apps/studio}/export-dialog.tsx +16 -4
- package/{plugins/design/dev-server → apps/studio}/history.ts +20 -3
- package/{plugins/design/dev-server → apps/studio}/http.ts +226 -6
- package/{plugins/design/dev-server → apps/studio}/input-router.tsx +15 -1
- package/{plugins/design/dev-server → apps/studio}/inspect.ts +50 -4
- package/{plugins/design/dev-server → apps/studio}/marquee-overlay.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/participants-chrome.tsx +2 -8
- package/{plugins/design/dev-server → apps/studio}/paths.ts +15 -11
- package/{plugins/design/dev-server → apps/studio}/server.ts +23 -2
- package/{plugins/design/dev-server → apps/studio}/sync/agent.ts +231 -57
- package/{plugins/design/dev-server → apps/studio}/sync/codec.ts +45 -0
- package/apps/studio/sync/cold-start.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/sync/connection-state.ts +58 -2
- package/{plugins/design/dev-server → apps/studio}/sync/index.ts +563 -221
- package/apps/studio/sync/journal.ts +190 -0
- package/apps/studio/sync/migrate-seed.ts +321 -0
- package/{plugins/design/dev-server → apps/studio}/sync/projection.ts +22 -3
- package/{plugins/design/dev-server → apps/studio}/sync/status.ts +15 -4
- package/{plugins/design/dev-server → apps/studio}/test/activity.test.ts +1 -7
- package/apps/studio/test/annotate-write.test.ts +184 -0
- package/apps/studio/test/annotations-align.test.ts +88 -0
- package/apps/studio/test/annotations-bindings.test.ts +124 -0
- package/apps/studio/test/annotations-groups.test.ts +231 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-layer.test.ts +63 -0
- package/{plugins/design/dev-server → apps/studio}/test/annotations-roundtrip.test.ts +97 -0
- package/apps/studio/test/annotations-snap.test.ts +79 -0
- package/apps/studio/test/annotations-text-format.test.ts +396 -0
- package/apps/studio/test/asset-api.test.ts +149 -0
- package/apps/studio/test/canvas-create-api.test.ts +361 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-cursors.test.ts +2 -0
- package/apps/studio/test/canvas-edit.test.ts +319 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-resolver.test.ts +1 -1
- package/apps/studio/test/canvas-media-drop.test.ts +158 -0
- package/{plugins/design/dev-server → apps/studio}/test/canvas-origin-gate.test.ts +25 -1
- package/apps/studio/test/canvas-url.test.ts +99 -0
- package/{plugins/design/dev-server → apps/studio}/test/collab-bridge.test.ts +0 -2
- package/{plugins/design/dev-server → apps/studio}/test/collab-room.test.ts +2 -7
- package/{plugins/design/dev-server → apps/studio}/test/csp-canvas-shell.test.ts +5 -0
- package/apps/studio/test/csrf-write-guard.test.ts +52 -0
- package/apps/studio/test/figjam-v3-model.test.ts +342 -0
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +1 -0
- package/apps/studio/test/index-data-canvas-ds.test.ts +97 -0
- package/apps/studio/test/motion-sample.test.ts +35 -0
- package/{plugins/design/dev-server → apps/studio}/test/perf-harness.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/test/phase-3.6-smoke.test.ts +3 -1
- package/apps/studio/test/read-annotations.test.ts +409 -0
- package/{plugins/design/dev-server → apps/studio}/test/sanitize-annotation-svg.test.ts +108 -2
- package/{plugins/design/dev-server → apps/studio}/test/shared-doc-convergence.test.ts +1 -1
- package/apps/studio/test/shared-doc-migrate.test.ts +369 -0
- package/apps/studio/test/sync-agent.test.ts +575 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-codec.test.ts +65 -0
- package/apps/studio/test/sync-cold-start.test.ts +244 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-connection-state.test.ts +68 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-hardening.test.ts +0 -8
- package/{plugins/design/dev-server → apps/studio}/test/sync-hubs-config.test.ts +0 -1
- package/apps/studio/test/sync-incident-replay.test.ts +211 -0
- package/apps/studio/test/sync-journal.test.ts +176 -0
- package/{plugins/design/dev-server → apps/studio}/test/sync-runtime.test.ts +360 -5
- package/{plugins/design/dev-server → apps/studio}/test/sync-status.test.ts +66 -0
- package/apps/studio/test/tour-overlay.test.tsx +56 -0
- package/apps/studio/test/use-annotation-resize.test.ts +402 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-artboard-drag.test.ts +0 -1
- package/{plugins/design/dev-server → apps/studio}/test/use-collab.test.ts +0 -0
- package/{plugins/design/dev-server → apps/studio}/test/use-tool-mode.test.tsx +8 -2
- package/apps/studio/test/whats-new.test.ts +101 -0
- package/apps/studio/text-imports.d.ts +9 -0
- package/{plugins/design/dev-server → apps/studio}/tool-palette.tsx +15 -70
- package/{plugins/design/dev-server → apps/studio}/undo-hud.tsx +1 -2
- package/{plugins/design/dev-server → apps/studio}/undo-stack.ts +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-agent-presence.tsx +5 -3
- package/apps/studio/use-annotation-resize.tsx +912 -0
- package/{plugins/design/dev-server → apps/studio}/use-annotation-selection.tsx +17 -5
- package/{plugins/design/dev-server → apps/studio}/use-annotations-visibility.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-artboard-drag.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-canvas-activity.tsx +3 -3
- package/apps/studio/use-canvas-media-drop.tsx +343 -0
- package/{plugins/design/dev-server → apps/studio}/use-collab.tsx +21 -16
- package/{plugins/design/dev-server → apps/studio}/use-cursor-modifiers.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-keyboard-discipline.tsx +5 -2
- package/{plugins/design/dev-server → apps/studio}/use-selection-set.tsx +18 -1
- package/{plugins/design/dev-server → apps/studio}/use-snap-guides.tsx +1 -1
- package/{plugins/design/dev-server → apps/studio}/use-tool-mode.tsx +8 -2
- package/{plugins/design/dev-server → apps/studio}/use-undo-stack.tsx +1 -1
- package/apps/studio/whats-new.json +264 -0
- package/apps/studio/whats-new.schema.json +86 -0
- package/apps/studio/whats-new.ts +87 -0
- package/cli/bin/maude.mjs +10 -0
- package/cli/commands/design-link.test.mjs +84 -0
- package/cli/commands/design.mjs +16 -7
- package/cli/commands/design.test.mjs +40 -0
- package/cli/commands/help.mjs +8 -2
- package/cli/commands/hub.mjs +3 -3
- package/cli/commands/hub.test.mjs +1 -1
- package/cli/commands/scenario-report.mjs +2 -9
- package/cli/lib/design-link.mjs +51 -1
- package/cli/lib/flow-design-integration.test.mjs +1 -1
- package/cli/lib/gitignore-block.mjs +1 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +8 -7
- package/cli/lib/preflight.mjs +1 -1
- package/package.json +15 -15
- package/plugins/design/templates/_shell.html +28 -4
- package/plugins/design/templates/brief-board.tsx.template +61 -0
- package/plugins/design/templates/canvas.tsx.template +1 -1
- package/plugins/design/templates/design-system-inspiration/core/config.json.tpl +1 -1
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +1 -1
- package/plugins/flow/.claude-plugin/config.schema.json +16 -0
- package/plugins/design/dev-server/annotations-layer.tsx +0 -3402
- package/plugins/design/dev-server/client/app.jsx +0 -2985
- package/plugins/design/dev-server/client/index.html +0 -15
- package/plugins/design/dev-server/dist/client.bundle.js +0 -20692
- package/plugins/design/dev-server/dist/comment-mount.js +0 -1924
- package/plugins/design/dev-server/dist/styles.css +0 -2710
- package/plugins/design/dev-server/dom-selection.ts +0 -156
- package/plugins/design/dev-server/sync/migrate-seed.ts +0 -163
- package/plugins/design/dev-server/test/canvas-edit.test.ts +0 -139
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +0 -159
- package/plugins/design/dev-server/test/sync-agent.test.ts +0 -305
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +0 -200
- package/plugins/design/dev-server/use-annotation-resize.tsx +0 -449
- /package/{plugins/design/dev-server → apps/studio}/activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_enumerate-artboards-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_html-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pdf-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pptx-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_pw-launch.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_screenshot-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-optimize.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/_svg-playwright.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/asset-sweep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/bootstrap-check.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/check-runtime-bundles.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/draw-build.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/handoff.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/prep.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/scenario-report.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/slug.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/smoke.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/svg-optimize.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/bin/visual-sanity.sh +0 -0
- /package/{plugins/design/dev-server → apps/studio}/boot-self-heal.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-build.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-header.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-meta.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/canvas-pipeline.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/hmr.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/iframe-lazy.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/0-reset.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/1-tokens.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/2-layout.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/client/styles/5-utilities.css +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/ai-activity.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/awareness-bridge.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/git-lifecycle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/persistence.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/protocol.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/registry.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/collab/room.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/config.schema.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/context.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/.min-sizes.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_decoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/lib0_encoding.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/motion_react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/pixi-js.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react-dom_client.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-dev-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/react_jsx-runtime.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_awareness.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/y-protocols_sync.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/dist/runtime/yjs.js +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/brush.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/composition.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/geometry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/gradient.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/layout.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/optimize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/palette.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/primitives.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/draw/test/serialize.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/README.md +0 -0
- /package/{plugins/design/dev-server → apps/studio}/examples/perf-100-artboards.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/_browser-bundles.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva-handoff-prompt.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/canva.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/html.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/index.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pdf.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/png.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/pptx.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/scope.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/svg.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/exporters/zip.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/fs-watch.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/handoff.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/hmr-broadcast.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/locator.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/mem.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/runtime-bundle.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/server.mjs +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/atomic-write.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/echo-guard.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/fs-mirror.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/hubs-config.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/materialize.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/origins.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/sync/untrusted.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/_helpers.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/active-state.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ai-activity.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotation-strokes-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/annotations-draw-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/artboard-activity-overlay.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/binary-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/boot-self-heal.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/bundle-smoke.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-build.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-header.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-hmr-runtime.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-inline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-lib-motion.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-meta-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-pipeline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/canvas-route.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-annotations-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-awareness-bridge.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-loopback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-protocol.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-reseed-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/collab-stress.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comment-mount.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/comments-api.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/compile-entry.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/context-resolve-tokens.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/equal-spacing-detector.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/canva.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/history.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/html.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pdf.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/png.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx-deck.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pptx.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/pw-launch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/scope.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/svg.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/exporters/zip.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-20-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fixtures/phase-21-annotations.svg +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/fs-watch.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/git-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff-static-frames.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/handoff.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/history-rollback.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-broadcast.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/hmr-classify.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/input-router.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/locator.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/marquee-overlay.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/move-artboards-command.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/participants-chrome.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/paths.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle-error-mapping.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/runtime-bundle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/server-lifecycle.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-foundation.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/shared-doc-projection.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/snap-distance-pill.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-atomic-write.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-echo-guard.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-fs-mirror.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-meta-codec.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/sync-untrusted.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/system-endpoint.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/tool-palette.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/undo-stack.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-agent-presence.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-annotation-selection.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-canvas-activity.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-cursor-modifiers.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-keyboard-discipline.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-selection-set.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-snap-guides.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/use-undo-stack.test.tsx +0 -0
- /package/{plugins/design/dev-server → apps/studio}/test/ws-handshake.test.ts +0 -0
- /package/{plugins/design/dev-server → apps/studio}/tsconfig.json +0 -0
- /package/{plugins/design/dev-server → apps/studio}/ws.ts +0 -0
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Cold-start decision matrix — hub-sync cold-start safety (DDR-102).
|
|
2
|
+
//
|
|
3
|
+
// Exhaustive table over (local: absent/empty/clean/diverged) × (doc:
|
|
4
|
+
// empty/same/different) × (journal: absent/match/stale), plus both winner
|
|
5
|
+
// directions and the null-timestamp fallbacks. Mirrors the table style of
|
|
6
|
+
// sync-codec.test.ts: every case is a row with explicit inputs + expected
|
|
7
|
+
// action.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from 'bun:test';
|
|
10
|
+
|
|
11
|
+
import { type ColdStartInput, decideColdStart } from '../sync/cold-start.ts';
|
|
12
|
+
import { hashBytes } from '../sync/echo-guard.ts';
|
|
13
|
+
|
|
14
|
+
const BODY_LOCAL = '<div>local work — a day of mascot edits</div>';
|
|
15
|
+
const BODY_HUB = '<div>hub state</div>';
|
|
16
|
+
|
|
17
|
+
/** Convenience: build an input with safe defaults, override per case. */
|
|
18
|
+
function input(over: Partial<ColdStartInput>): ColdStartInput {
|
|
19
|
+
return {
|
|
20
|
+
localBody: null,
|
|
21
|
+
docBody: '',
|
|
22
|
+
journalHash: null,
|
|
23
|
+
localMtimeMs: null,
|
|
24
|
+
docBodyEditAtMs: null,
|
|
25
|
+
...over,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('decideColdStart — empty/absent sides', () => {
|
|
30
|
+
test('local absent + doc empty → noop', () => {
|
|
31
|
+
expect(decideColdStart(input({})).action).toBe('noop');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('local empty-string + doc empty → noop', () => {
|
|
35
|
+
expect(decideColdStart(input({ localBody: '' })).action).toBe('noop');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('whitespace-only local counts as empty (mirrors the trim guard)', () => {
|
|
39
|
+
const d = decideColdStart(input({ localBody: ' \n\t ', docBody: BODY_HUB }));
|
|
40
|
+
expect(d.action).toBe('materialize-hub');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('local absent + doc non-empty → materialize-hub (clean first sync)', () => {
|
|
44
|
+
const d = decideColdStart(input({ docBody: BODY_HUB }));
|
|
45
|
+
expect(d.action).toBe('materialize-hub');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('local empty + doc non-empty → materialize-hub', () => {
|
|
49
|
+
const d = decideColdStart(input({ localBody: '', docBody: BODY_HUB }));
|
|
50
|
+
expect(d.action).toBe('materialize-hub');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('local non-empty + doc empty → seed-local-up (DDR-064 guard as a named case)', () => {
|
|
54
|
+
const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: '' }));
|
|
55
|
+
expect(d.action).toBe('seed-local-up');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('local non-empty + whitespace-only doc → seed-local-up', () => {
|
|
59
|
+
const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: ' \n ' }));
|
|
60
|
+
expect(d.action).toBe('seed-local-up');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('seed-local-up holds regardless of journal state (stale journal must not block it)', () => {
|
|
64
|
+
const d = decideColdStart(
|
|
65
|
+
input({ localBody: BODY_LOCAL, docBody: '', journalHash: 'stale-hash' })
|
|
66
|
+
);
|
|
67
|
+
expect(d.action).toBe('seed-local-up');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('decideColdStart — identical sides', () => {
|
|
72
|
+
test('local == doc → noop (caller records journal)', () => {
|
|
73
|
+
const d = decideColdStart(input({ localBody: BODY_HUB, docBody: BODY_HUB }));
|
|
74
|
+
expect(d.action).toBe('noop');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('local == doc with a stale journal is still a noop (content equality wins)', () => {
|
|
78
|
+
const d = decideColdStart(
|
|
79
|
+
input({ localBody: BODY_HUB, docBody: BODY_HUB, journalHash: 'stale' })
|
|
80
|
+
);
|
|
81
|
+
expect(d.action).toBe('noop');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('decideColdStart — journal-gated fast-forward', () => {
|
|
86
|
+
test('local ≠ doc, journal matches hash(local) → fast-forward-hub (no conflict)', () => {
|
|
87
|
+
const d = decideColdStart(
|
|
88
|
+
input({
|
|
89
|
+
localBody: BODY_LOCAL,
|
|
90
|
+
docBody: BODY_HUB,
|
|
91
|
+
journalHash: hashBytes(BODY_LOCAL),
|
|
92
|
+
})
|
|
93
|
+
);
|
|
94
|
+
expect(d.action).toBe('fast-forward-hub');
|
|
95
|
+
expect(d.winner).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('journal match overrides timestamps — even a NEWER local mtime fast-forwards', () => {
|
|
99
|
+
// mtime newer than the doc stamp, but the bytes are exactly what we last
|
|
100
|
+
// synced (e.g. touch / checkout changed mtime only) → not a conflict.
|
|
101
|
+
const d = decideColdStart(
|
|
102
|
+
input({
|
|
103
|
+
localBody: BODY_LOCAL,
|
|
104
|
+
docBody: BODY_HUB,
|
|
105
|
+
journalHash: hashBytes(BODY_LOCAL),
|
|
106
|
+
localMtimeMs: 2_000,
|
|
107
|
+
docBodyEditAtMs: 1_000,
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
expect(d.action).toBe('fast-forward-hub');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('decideColdStart — divergence (conflict + newest-wins)', () => {
|
|
115
|
+
test('journal ABSENT, local newer → conflict, winner=local (the incident shape)', () => {
|
|
116
|
+
const d = decideColdStart(
|
|
117
|
+
input({
|
|
118
|
+
localBody: BODY_LOCAL,
|
|
119
|
+
docBody: BODY_HUB,
|
|
120
|
+
localMtimeMs: 2_000,
|
|
121
|
+
docBodyEditAtMs: 1_000,
|
|
122
|
+
})
|
|
123
|
+
);
|
|
124
|
+
expect(d.action).toBe('conflict');
|
|
125
|
+
expect(d.winner).toBe('local');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('journal ABSENT, doc newer → conflict, winner=hub', () => {
|
|
129
|
+
const d = decideColdStart(
|
|
130
|
+
input({
|
|
131
|
+
localBody: BODY_LOCAL,
|
|
132
|
+
docBody: BODY_HUB,
|
|
133
|
+
localMtimeMs: 1_000,
|
|
134
|
+
docBodyEditAtMs: 2_000,
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
expect(d.action).toBe('conflict');
|
|
138
|
+
expect(d.winner).toBe('hub');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('journal STALE (≠ hash(local)), local newer → conflict, winner=local', () => {
|
|
142
|
+
const d = decideColdStart(
|
|
143
|
+
input({
|
|
144
|
+
localBody: BODY_LOCAL,
|
|
145
|
+
docBody: BODY_HUB,
|
|
146
|
+
journalHash: hashBytes('<div>some older synced state</div>'),
|
|
147
|
+
localMtimeMs: 5_000,
|
|
148
|
+
docBodyEditAtMs: 4_000,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
151
|
+
expect(d.action).toBe('conflict');
|
|
152
|
+
expect(d.winner).toBe('local');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('journal STALE, doc newer → conflict, winner=hub', () => {
|
|
156
|
+
const d = decideColdStart(
|
|
157
|
+
input({
|
|
158
|
+
localBody: BODY_LOCAL,
|
|
159
|
+
docBody: BODY_HUB,
|
|
160
|
+
journalHash: hashBytes('<div>some older synced state</div>'),
|
|
161
|
+
localMtimeMs: 4_000,
|
|
162
|
+
docBodyEditAtMs: 5_000,
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
expect(d.action).toBe('conflict');
|
|
166
|
+
expect(d.winner).toBe('hub');
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test('journal matching hash(DOC) — not local — is still divergence', () => {
|
|
170
|
+
// The journal proves we once synced what the HUB now holds reverted to;
|
|
171
|
+
// local moved past it → local is unreconciled work, must conflict.
|
|
172
|
+
const d = decideColdStart(
|
|
173
|
+
input({
|
|
174
|
+
localBody: BODY_LOCAL,
|
|
175
|
+
docBody: BODY_HUB,
|
|
176
|
+
journalHash: hashBytes(BODY_HUB),
|
|
177
|
+
localMtimeMs: 2_000,
|
|
178
|
+
docBodyEditAtMs: 1_000,
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
expect(d.action).toBe('conflict');
|
|
182
|
+
expect(d.winner).toBe('local');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('doc stamp NULL (older peer never stamped) → hub-wins fallback', () => {
|
|
186
|
+
const d = decideColdStart(
|
|
187
|
+
input({
|
|
188
|
+
localBody: BODY_LOCAL,
|
|
189
|
+
docBody: BODY_HUB,
|
|
190
|
+
localMtimeMs: 2_000,
|
|
191
|
+
docBodyEditAtMs: null,
|
|
192
|
+
})
|
|
193
|
+
);
|
|
194
|
+
expect(d.action).toBe('conflict');
|
|
195
|
+
expect(d.winner).toBe('hub');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('local mtime NULL → hub-wins fallback', () => {
|
|
199
|
+
const d = decideColdStart(
|
|
200
|
+
input({
|
|
201
|
+
localBody: BODY_LOCAL,
|
|
202
|
+
docBody: BODY_HUB,
|
|
203
|
+
localMtimeMs: null,
|
|
204
|
+
docBodyEditAtMs: 2_000,
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
expect(d.action).toBe('conflict');
|
|
208
|
+
expect(d.winner).toBe('hub');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test('both timestamps NULL → hub-wins fallback', () => {
|
|
212
|
+
const d = decideColdStart(input({ localBody: BODY_LOCAL, docBody: BODY_HUB }));
|
|
213
|
+
expect(d.action).toBe('conflict');
|
|
214
|
+
expect(d.winner).toBe('hub');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('timestamps TIED → hub-wins fallback', () => {
|
|
218
|
+
const d = decideColdStart(
|
|
219
|
+
input({
|
|
220
|
+
localBody: BODY_LOCAL,
|
|
221
|
+
docBody: BODY_HUB,
|
|
222
|
+
localMtimeMs: 1_000,
|
|
223
|
+
docBodyEditAtMs: 1_000,
|
|
224
|
+
})
|
|
225
|
+
);
|
|
226
|
+
expect(d.action).toBe('conflict');
|
|
227
|
+
expect(d.winner).toBe('hub');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('every decision carries a human-readable reason', () => {
|
|
231
|
+
const cases: ColdStartInput[] = [
|
|
232
|
+
input({}),
|
|
233
|
+
input({ docBody: BODY_HUB }),
|
|
234
|
+
input({ localBody: BODY_LOCAL }),
|
|
235
|
+
input({ localBody: BODY_HUB, docBody: BODY_HUB }),
|
|
236
|
+
input({ localBody: BODY_LOCAL, docBody: BODY_HUB, journalHash: hashBytes(BODY_LOCAL) }),
|
|
237
|
+
input({ localBody: BODY_LOCAL, docBody: BODY_HUB }),
|
|
238
|
+
];
|
|
239
|
+
for (const c of cases) {
|
|
240
|
+
const d = decideColdStart(c);
|
|
241
|
+
expect(d.reason.length).toBeGreaterThan(0);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
});
|
|
@@ -144,3 +144,71 @@ describe('connection monitor', () => {
|
|
|
144
144
|
expect(changes.length).toBe(before); // no offline transition fired
|
|
145
145
|
});
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
// DDR-102 — per-doc states + real lastSyncAt.
|
|
149
|
+
describe('connection monitor — per-doc states (DDR-102)', () => {
|
|
150
|
+
test('noteDocState rolls up into docs counts + rejectedSlugs', () => {
|
|
151
|
+
const { monitor } = makeMonitor();
|
|
152
|
+
monitor.noteDocState('ui-a', 'pending');
|
|
153
|
+
monitor.noteDocState('ui-b', 'connected');
|
|
154
|
+
monitor.noteDocState('ui-c', 'auth-rejected');
|
|
155
|
+
monitor.noteDocState('ui-d', 'auth-rejected');
|
|
156
|
+
|
|
157
|
+
const snap = monitor.snapshot();
|
|
158
|
+
expect(snap.docs).toEqual({ synced: 1, pending: 1, rejected: 2 });
|
|
159
|
+
expect(snap.rejectedSlugs?.sort()).toEqual(['ui-c', 'ui-d']);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('doc state transitions update the rollup (rejected → connected after re-link)', () => {
|
|
163
|
+
const { monitor, changes } = makeMonitor();
|
|
164
|
+
monitor.noteDocState('ui-a', 'auth-rejected');
|
|
165
|
+
expect(monitor.snapshot().docs?.rejected).toBe(1);
|
|
166
|
+
monitor.noteDocState('ui-a', 'connected');
|
|
167
|
+
const snap = monitor.snapshot();
|
|
168
|
+
expect(snap.docs).toEqual({ synced: 1, pending: 0, rejected: 0 });
|
|
169
|
+
expect(snap.rejectedSlugs).toEqual([]);
|
|
170
|
+
// Each transition emitted (no-op repeats don't).
|
|
171
|
+
const emitted = changes.length;
|
|
172
|
+
monitor.noteDocState('ui-a', 'connected');
|
|
173
|
+
expect(changes.length).toBe(emitted);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test('rejectedSlugs caps at 20 while docs.rejected carries the true count', () => {
|
|
177
|
+
const { monitor } = makeMonitor();
|
|
178
|
+
for (let i = 0; i < 25; i++) monitor.noteDocState(`ui-${i}`, 'auth-rejected');
|
|
179
|
+
const snap = monitor.snapshot();
|
|
180
|
+
expect(snap.docs?.rejected).toBe(25);
|
|
181
|
+
expect(snap.rejectedSlugs).toHaveLength(20);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('noteSyncActivity sets lastSyncAt to now and promotes a pending doc', () => {
|
|
185
|
+
const { clock, monitor } = makeMonitor();
|
|
186
|
+
monitor.noteDocState('ui-a', 'pending');
|
|
187
|
+
expect(monitor.snapshot().lastSyncAt).toBeNull();
|
|
188
|
+
|
|
189
|
+
clock.advance(5_000);
|
|
190
|
+
monitor.noteSyncActivity('ui-a');
|
|
191
|
+
const snap = monitor.snapshot();
|
|
192
|
+
expect(snap.lastSyncAt).toBe(clock.now());
|
|
193
|
+
expect(snap.docs?.synced).toBe(1);
|
|
194
|
+
expect(snap.docs?.pending).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('noteSyncActivity does NOT resurrect an auth-rejected doc', () => {
|
|
198
|
+
const { monitor } = makeMonitor();
|
|
199
|
+
monitor.noteDocState('ui-a', 'auth-rejected');
|
|
200
|
+
monitor.noteSyncActivity('ui-a');
|
|
201
|
+
expect(monitor.snapshot().docs?.rejected).toBe(1);
|
|
202
|
+
expect(monitor.snapshot().lastSyncAt).not.toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('lastSyncAt advances on repeated activity (real activity, not just transitions)', () => {
|
|
206
|
+
const { clock, monitor } = makeMonitor();
|
|
207
|
+
monitor.noteDocState('ui-a', 'connected');
|
|
208
|
+
monitor.noteSyncActivity('ui-a');
|
|
209
|
+
const first = monitor.snapshot().lastSyncAt;
|
|
210
|
+
clock.advance(10_000);
|
|
211
|
+
monitor.noteSyncActivity('ui-a');
|
|
212
|
+
expect(monitor.snapshot().lastSyncAt).toBe((first as number) + 10_000);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
@@ -54,25 +54,19 @@ describe('DDR-054 §2a — CI environment gate', () => {
|
|
|
54
54
|
savedCi = process.env.CI;
|
|
55
55
|
savedGha = process.env.GITHUB_ACTIONS;
|
|
56
56
|
savedOverride = process.env.MAUDE_SYNC_IN_CI;
|
|
57
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
58
57
|
delete process.env.CI;
|
|
59
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
60
58
|
delete process.env.GITHUB_ACTIONS;
|
|
61
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
62
59
|
delete process.env.MAUDE_SYNC_IN_CI;
|
|
63
60
|
});
|
|
64
61
|
|
|
65
62
|
afterEach(() => {
|
|
66
63
|
if (savedCi === undefined) {
|
|
67
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
68
64
|
delete process.env.CI;
|
|
69
65
|
} else process.env.CI = savedCi;
|
|
70
66
|
if (savedGha === undefined) {
|
|
71
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
72
67
|
delete process.env.GITHUB_ACTIONS;
|
|
73
68
|
} else process.env.GITHUB_ACTIONS = savedGha;
|
|
74
69
|
if (savedOverride === undefined) {
|
|
75
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
76
70
|
delete process.env.MAUDE_SYNC_IN_CI;
|
|
77
71
|
} else process.env.MAUDE_SYNC_IN_CI = savedOverride;
|
|
78
72
|
});
|
|
@@ -443,7 +437,6 @@ describe('DDR-054 §2i — auto-clear adopt after first success', () => {
|
|
|
443
437
|
const savedHubsEnv = process.env.HUBS_CONFIG_PATH;
|
|
444
438
|
process.env.HUBS_CONFIG_PATH = hubsPath;
|
|
445
439
|
const savedCi = process.env.CI;
|
|
446
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
447
440
|
delete process.env.CI;
|
|
448
441
|
|
|
449
442
|
try {
|
|
@@ -504,7 +497,6 @@ describe('DDR-054 §2i — auto-clear adopt after first success', () => {
|
|
|
504
497
|
expect(typeof after.linkedHub.lastAdoptedAt).toBe('number');
|
|
505
498
|
} finally {
|
|
506
499
|
if (savedHubsEnv === undefined) {
|
|
507
|
-
// biome-ignore lint/performance/noDelete: process.env semantics
|
|
508
500
|
delete process.env.HUBS_CONFIG_PATH;
|
|
509
501
|
} else process.env.HUBS_CONFIG_PATH = savedHubsEnv;
|
|
510
502
|
if (savedCi !== undefined) process.env.CI = savedCi;
|
|
@@ -21,7 +21,6 @@ beforeEach(() => {
|
|
|
21
21
|
afterEach(() => {
|
|
22
22
|
// Node's process.env stringifies on assignment; assigning undefined yields
|
|
23
23
|
// the literal string "undefined". delete is the correct restoration.
|
|
24
|
-
// biome-ignore lint/performance/noDelete: process.env semantics.
|
|
25
24
|
if (savedEnv === undefined) delete process.env.HUBS_CONFIG_PATH;
|
|
26
25
|
else process.env.HUBS_CONFIG_PATH = savedEnv;
|
|
27
26
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Incident replay — the 2026-06-11 `ui-maskot` data loss (DDR-102).
|
|
2
|
+
//
|
|
3
|
+
// The shape that ate a day of work: peer A seeded a STALE body to the hub;
|
|
4
|
+
// peer B booted later holding a NEWER divergent local file and no journal
|
|
5
|
+
// (first link on that machine). v1.1 hub-wins overwrote B's disk with A's
|
|
6
|
+
// stale body; the only trace was a conflict notice. This test replays the
|
|
7
|
+
// exact shape against the REAL journal + REAL history snapshots and asserts
|
|
8
|
+
// the DDR-102 outcome: B loses nothing.
|
|
9
|
+
//
|
|
10
|
+
// Scenario 2 is the everyday case that must stay silent: B's local matches
|
|
11
|
+
// what this machine last synced (journal hash) → the hub being ahead is a
|
|
12
|
+
// clean fast-forward, no snapshots, no conflict noise.
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
15
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import * as Y from 'yjs';
|
|
19
|
+
|
|
20
|
+
import type { Context } from '../context.ts';
|
|
21
|
+
import { createHistory } from '../history.ts';
|
|
22
|
+
import { createCanvasSyncAgent } from '../sync/agent.ts';
|
|
23
|
+
import { applyHtmlToDoc, htmlFromDoc, stampBodyEdit } from '../sync/codec.ts';
|
|
24
|
+
import { createEchoGuard, hashBytes } from '../sync/echo-guard.ts';
|
|
25
|
+
import { loadJournal, type SyncJournal } from '../sync/journal.ts';
|
|
26
|
+
|
|
27
|
+
const STALE_BODY = '<div class="maskot">stale 2551-byte version that seeded the hub</div>';
|
|
28
|
+
const NEWER_BODY = '<div class="maskot">a full day of mascot work — 6 kB of it</div>';
|
|
29
|
+
|
|
30
|
+
let dir: string; // peer B's design root
|
|
31
|
+
let journal: SyncJournal;
|
|
32
|
+
|
|
33
|
+
function paths() {
|
|
34
|
+
return {
|
|
35
|
+
html: join(dir, 'ui', 'maskot.html'),
|
|
36
|
+
comments: join(dir, '_comments', 'ui-maskot.json'),
|
|
37
|
+
annotations: join(dir, 'ui-maskot.annotations.svg'),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Minimal ctx for the REAL history lib (snapshots under <dir>/_history). */
|
|
42
|
+
function historyCtx(): Context {
|
|
43
|
+
return {
|
|
44
|
+
paths: {
|
|
45
|
+
repoRoot: dir,
|
|
46
|
+
designRel: '.',
|
|
47
|
+
designRoot: dir,
|
|
48
|
+
historyDir: join(dir, '_history'),
|
|
49
|
+
},
|
|
50
|
+
} as unknown as Context;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
dir = mkdtempSync(join(tmpdir(), 'incident-replay-'));
|
|
55
|
+
mkdirSync(join(dir, 'ui'), { recursive: true });
|
|
56
|
+
journal = loadJournal(dir, { flushMs: 0 });
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
journal.stop();
|
|
61
|
+
rmSync(dir, { recursive: true, force: true });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/** Build peer B's agent against `doc` with the REAL journal + history. */
|
|
65
|
+
function makeAgentB(doc: Y.Doc, conflicts: unknown[]) {
|
|
66
|
+
const history = createHistory(historyCtx());
|
|
67
|
+
const agent = createCanvasSyncAgent({
|
|
68
|
+
slug: 'ui-maskot',
|
|
69
|
+
doc,
|
|
70
|
+
paths: paths(),
|
|
71
|
+
echoGuard: createEchoGuard(),
|
|
72
|
+
flushMs: 0,
|
|
73
|
+
journal,
|
|
74
|
+
snapshot: async (content, reason) => {
|
|
75
|
+
try {
|
|
76
|
+
const snap = await history.writeSnapshot('ui/maskot.html', content, reason);
|
|
77
|
+
return snap.ts;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
onConflict: (info) => {
|
|
83
|
+
conflicts.push(info);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
agent.start();
|
|
87
|
+
return agent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('incident replay — ui-maskot cold-start divergence (DDR-102)', () => {
|
|
91
|
+
test('peer B (newer divergent local, no journal) loses NOTHING: winner=local, dual snapshot, hub gets B body', async () => {
|
|
92
|
+
// Peer A seeded the hub with the stale body an hour ago (older stamp).
|
|
93
|
+
const hubDoc = new Y.Doc();
|
|
94
|
+
hubDoc.transact(() => {
|
|
95
|
+
applyHtmlToDoc(hubDoc, STALE_BODY);
|
|
96
|
+
stampBodyEdit(hubDoc, undefined, Date.now() - 60 * 60 * 1000);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Peer B holds a NEWER divergent local file (mtime = now) and has never
|
|
100
|
+
// synced on this machine (journal absent).
|
|
101
|
+
writeFileSync(paths().html, NEWER_BODY);
|
|
102
|
+
|
|
103
|
+
const conflicts: Array<{
|
|
104
|
+
kind?: string;
|
|
105
|
+
winner?: string;
|
|
106
|
+
snapshots?: { local?: string; hub?: string };
|
|
107
|
+
}> = [];
|
|
108
|
+
const agent = makeAgentB(hubDoc, conflicts);
|
|
109
|
+
await agent.reconcile();
|
|
110
|
+
agent.stop();
|
|
111
|
+
|
|
112
|
+
// 1) B's file is byte-identical on disk — the day of work survived.
|
|
113
|
+
expect(readFileSync(paths().html, 'utf8')).toBe(NEWER_BODY);
|
|
114
|
+
// 2) Winner = local (newer mtime beats the hub's older stamp).
|
|
115
|
+
expect(conflicts).toHaveLength(1);
|
|
116
|
+
expect(conflicts[0].kind).toBe('cold-start-diverged');
|
|
117
|
+
expect(conflicts[0].winner).toBe('local');
|
|
118
|
+
// 3) BOTH versions snapshotted to _history/ui-maskot/ (real files).
|
|
119
|
+
expect(conflicts[0].snapshots?.local).toBeDefined();
|
|
120
|
+
expect(conflicts[0].snapshots?.hub).toBeDefined();
|
|
121
|
+
const snapDir = join(dir, '_history', 'ui-maskot');
|
|
122
|
+
const blobs = readdirSync(snapDir).filter((f) => f.endsWith('.html'));
|
|
123
|
+
const contents = blobs.map((f) => readFileSync(join(snapDir, f), 'utf8'));
|
|
124
|
+
expect(contents).toContain(NEWER_BODY);
|
|
125
|
+
expect(contents).toContain(STALE_BODY);
|
|
126
|
+
// 4) The hub doc now carries B's body (seeded up, stamped).
|
|
127
|
+
expect(htmlFromDoc(hubDoc)).toBe(NEWER_BODY);
|
|
128
|
+
// 5) The journal checkpointed — the NEXT boot is a clean fast-forward.
|
|
129
|
+
expect(journal.get('ui-maskot')?.bodyHash).toBe(hashBytes(NEWER_BODY));
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('peer B with a CLEAN journal fast-forwards silently: no snapshot, no conflict', async () => {
|
|
133
|
+
// B previously synced STALE_BODY on this machine (journal checkpoint),
|
|
134
|
+
// disk still holds exactly that; the hub then moved ahead.
|
|
135
|
+
writeFileSync(paths().html, STALE_BODY);
|
|
136
|
+
journal.record('ui-maskot', { bodyHash: hashBytes(STALE_BODY) });
|
|
137
|
+
|
|
138
|
+
const hubDoc = new Y.Doc();
|
|
139
|
+
hubDoc.transact(() => {
|
|
140
|
+
applyHtmlToDoc(hubDoc, NEWER_BODY);
|
|
141
|
+
stampBodyEdit(hubDoc);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const conflicts: unknown[] = [];
|
|
145
|
+
const agent = makeAgentB(hubDoc, conflicts);
|
|
146
|
+
await agent.reconcile();
|
|
147
|
+
agent.stop();
|
|
148
|
+
|
|
149
|
+
// Disk fast-forwarded to hub state…
|
|
150
|
+
expect(readFileSync(paths().html, 'utf8')).toBe(NEWER_BODY);
|
|
151
|
+
// …with zero conflict noise and zero history spam.
|
|
152
|
+
expect(conflicts).toHaveLength(0);
|
|
153
|
+
const snapDir = join(dir, '_history', 'ui-maskot');
|
|
154
|
+
let blobs: string[] = [];
|
|
155
|
+
try {
|
|
156
|
+
blobs = readdirSync(snapDir);
|
|
157
|
+
} catch {
|
|
158
|
+
/* dir never created — exactly the point */
|
|
159
|
+
}
|
|
160
|
+
expect(blobs).toHaveLength(0);
|
|
161
|
+
// Journal advanced to the new state.
|
|
162
|
+
expect(journal.get('ui-maskot')?.bodyHash).toBe(hashBytes(NEWER_BODY));
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('boot order REVERSED (B first, then A): A fast-forwards to B state, nothing lost either way', async () => {
|
|
166
|
+
// B boots first against an empty hub → DDR-064 seed-local-up.
|
|
167
|
+
writeFileSync(paths().html, NEWER_BODY);
|
|
168
|
+
const hubDoc = new Y.Doc();
|
|
169
|
+
const conflictsB: unknown[] = [];
|
|
170
|
+
const agentB = makeAgentB(hubDoc, conflictsB);
|
|
171
|
+
await agentB.reconcile();
|
|
172
|
+
agentB.stop();
|
|
173
|
+
expect(htmlFromDoc(hubDoc)).toBe(NEWER_BODY);
|
|
174
|
+
expect(conflictsB).toHaveLength(0); // empty-hub seed is not a conflict
|
|
175
|
+
|
|
176
|
+
// Peer A (separate design root + journal) holds the stale body it synced
|
|
177
|
+
// before — its journal matches its disk → clean fast-forward to B's state.
|
|
178
|
+
const dirA = mkdtempSync(join(tmpdir(), 'incident-replay-A-'));
|
|
179
|
+
mkdirSync(join(dirA, 'ui'), { recursive: true });
|
|
180
|
+
try {
|
|
181
|
+
writeFileSync(join(dirA, 'ui', 'maskot.html'), STALE_BODY);
|
|
182
|
+
const journalA = loadJournal(dirA, { flushMs: 0 });
|
|
183
|
+
journalA.record('ui-maskot', { bodyHash: hashBytes(STALE_BODY) });
|
|
184
|
+
const conflictsA: unknown[] = [];
|
|
185
|
+
const agentA = createCanvasSyncAgent({
|
|
186
|
+
slug: 'ui-maskot',
|
|
187
|
+
doc: hubDoc,
|
|
188
|
+
paths: {
|
|
189
|
+
html: join(dirA, 'ui', 'maskot.html'),
|
|
190
|
+
comments: join(dirA, '_comments', 'ui-maskot.json'),
|
|
191
|
+
annotations: join(dirA, 'ui-maskot.annotations.svg'),
|
|
192
|
+
},
|
|
193
|
+
echoGuard: createEchoGuard(),
|
|
194
|
+
flushMs: 0,
|
|
195
|
+
journal: journalA,
|
|
196
|
+
onConflict: (info) => {
|
|
197
|
+
conflictsA.push(info);
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
agentA.start();
|
|
201
|
+
await agentA.reconcile();
|
|
202
|
+
agentA.stop();
|
|
203
|
+
journalA.stop();
|
|
204
|
+
|
|
205
|
+
expect(readFileSync(join(dirA, 'ui', 'maskot.html'), 'utf8')).toBe(NEWER_BODY);
|
|
206
|
+
expect(conflictsA).toHaveLength(0);
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(dirA, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|