@1agh/maude 1.0.9 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/apps/studio/acp/bridge.ts +18 -13
- package/apps/studio/acp/probe.ts +268 -16
- package/apps/studio/acp/transcript-io.ts +394 -0
- package/apps/studio/acp/transcript.ts +103 -38
- package/apps/studio/api.ts +139 -26
- package/apps/studio/bin/_import-figma.mjs +57 -11
- package/apps/studio/bin/_seed-fixture.mjs +163 -0
- package/apps/studio/canvas-imports.ts +89 -0
- package/apps/studio/client/app.jsx +257 -7
- package/apps/studio/client/export-center.jsx +15 -7
- package/apps/studio/client/panels/ReadinessList.jsx +7 -1
- package/apps/studio/client/panels/SyncPanel.jsx +125 -4
- package/apps/studio/client/report-bug.jsx +19 -1
- package/apps/studio/client/styles/3-shell-maude.css +34 -0
- package/apps/studio/client/styles/4-components.css +26 -0
- package/apps/studio/collab/persistence.ts +132 -16
- package/apps/studio/collab/registry.ts +10 -18
- package/apps/studio/debug-bundle.ts +13 -0
- package/apps/studio/dist/client.bundle.js +1128 -1128
- package/apps/studio/dist/styles.css +1 -1
- package/apps/studio/export-dialog.tsx +54 -0
- package/apps/studio/exporters/degraded.ts +19 -3
- package/apps/studio/exporters/ghostscript.ts +194 -0
- package/apps/studio/exporters/pdf-fonts.ts +261 -0
- package/apps/studio/exporters/pdf.ts +152 -3
- package/apps/studio/exporters/remote.ts +36 -1
- package/apps/studio/fs-watch.ts +45 -1
- package/apps/studio/http.ts +7 -1
- package/apps/studio/mem.ts +45 -9
- package/apps/studio/readiness.ts +42 -7
- package/apps/studio/server.ts +11 -4
- package/apps/studio/sync/agent.ts +136 -29
- package/apps/studio/sync/asset-push.ts +1 -47
- package/apps/studio/sync/codec.ts +122 -31
- package/apps/studio/sync/cold-start-apply.ts +7 -0
- package/apps/studio/sync/cold-start.ts +118 -22
- package/apps/studio/sync/comment-identity.ts +68 -0
- package/apps/studio/sync/connection-state.ts +15 -1
- package/apps/studio/sync/file-ledger.ts +117 -0
- package/apps/studio/sync/file-plane.ts +623 -27
- package/apps/studio/sync/index.ts +711 -20
- package/apps/studio/sync/journal-client.ts +13 -1
- package/apps/studio/sync/migrate-seed.ts +40 -3
- package/apps/studio/sync/presentation.ts +26 -3
- package/apps/studio/sync/retry-after.ts +169 -0
- package/apps/studio/sync/seed-progress.ts +218 -0
- package/apps/studio/sync/status.ts +122 -5
- package/apps/studio/sync/transport-error.ts +111 -0
- package/apps/studio/whats-new.json +91 -0
- package/cli/bin/maude.mjs +2 -0
- package/cli/commands/codex.mjs +191 -0
- package/cli/commands/harness.mjs +1051 -0
- package/cli/commands/help.mjs +13 -0
- package/cli/lib/design-link.mjs +190 -2
- package/cli/lib/harness/capabilities.mjs +302 -0
- package/cli/lib/harness/codex-runtime.mjs +889 -0
- package/cli/lib/harness/compatibility.mjs +27 -0
- package/cli/lib/harness/discover-claude.mjs +1393 -0
- package/cli/lib/harness/managed-state.mjs +1296 -0
- package/cli/lib/harness/model.mjs +86 -0
- package/cli/lib/harness/secrets.mjs +390 -0
- package/cli/lib/harness/targets/codex.mjs +1141 -0
- package/cli/lib/harness/targets/opencode.mjs +807 -0
- package/cli/lib/harness/transaction.mjs +1099 -0
- package/cli/lib/hubs-config.mjs +35 -8
- package/cli/templates/harness/opencode/maude-projector.ts +58 -0
- package/docs/harness-capability-matrix.md +125 -0
- package/docs/harness-environment-projection.md +95 -0
- package/package.json +32 -9
- package/plugins/flow/.claude-plugin/config.schema.json +8 -0
- package/apps/studio/.ai/cache/_stats.json +0 -7
- package/apps/studio/bin/_agent-browser-safe.test.mjs +0 -165
- package/apps/studio/bin/_curl-local.test.mjs +0 -280
- package/apps/studio/bin/_pw-launch.test.mjs +0 -90
- package/apps/studio/bin/_smart-frames.test.mjs +0 -195
- package/apps/studio/bin/_transcribe.test.mjs +0 -80
- package/apps/studio/bun.lock +0 -928
- package/apps/studio/draw/test/animate.test.ts +0 -172
- package/apps/studio/draw/test/brush.test.ts +0 -213
- package/apps/studio/draw/test/composition.test.ts +0 -141
- package/apps/studio/draw/test/geometry.test.ts +0 -199
- package/apps/studio/draw/test/gradient.test.ts +0 -167
- package/apps/studio/draw/test/layout.test.ts +0 -120
- package/apps/studio/draw/test/morph.test.ts +0 -107
- package/apps/studio/draw/test/optimize.test.ts +0 -40
- package/apps/studio/draw/test/palette.test.ts +0 -123
- package/apps/studio/draw/test/primitives.test.ts +0 -129
- package/apps/studio/draw/test/serialize-animate.test.ts +0 -142
- package/apps/studio/draw/test/serialize.test.ts +0 -105
- package/apps/studio/figma/assets.test.ts +0 -464
- package/apps/studio/figma/client.test.ts +0 -395
- package/apps/studio/figma/codegen-client.test.ts +0 -276
- package/apps/studio/figma/codegen-fonts.test.ts +0 -103
- package/apps/studio/figma/codegen-values.test.ts +0 -179
- package/apps/studio/figma/comments-to-strokes.test.ts +0 -194
- package/apps/studio/figma/fig-decode.test.ts +0 -788
- package/apps/studio/figma/fig-differential.test.ts +0 -182
- package/apps/studio/figma/fig-translator.test.ts +0 -192
- package/apps/studio/figma/fig-vector.test.ts +0 -113
- package/apps/studio/figma/from-codegen.test.ts +0 -408
- package/apps/studio/figma/sanitize.test.ts +0 -325
- package/apps/studio/figma/tailwind-map.test.ts +0 -142
- package/apps/studio/figma/to-artboard.test.ts +0 -808
- package/apps/studio/figma/to-render.test.ts +0 -180
- package/apps/studio/figma/to-strokes-roundtrip.test.ts +0 -152
- package/apps/studio/figma/to-strokes.test.ts +0 -705
- package/apps/studio/figma/to-tokens.test.ts +0 -321
- package/apps/studio/figma/url.test.ts +0 -167
- package/apps/studio/footage/schema.test.ts +0 -287
- package/apps/studio/generation/adapters/groq.test.ts +0 -80
- package/apps/studio/generation/audio-library.test.ts +0 -95
- package/apps/studio/generation/captions.test.ts +0 -78
- package/apps/studio/generation/elevenlabs.test.ts +0 -223
- package/apps/studio/generation/gemini.test.ts +0 -350
- package/apps/studio/generation/jobs.test.ts +0 -128
- package/apps/studio/generation/keys.test.ts +0 -75
- package/apps/studio/generation/prefs.test.ts +0 -95
- package/apps/studio/generation/types.test.ts +0 -123
- package/apps/studio/generation/whisper-models.test.ts +0 -179
- package/apps/studio/test/_helpers.ts +0 -108
- package/apps/studio/test/acp-activity-endpoint.test.ts +0 -183
- package/apps/studio/test/acp-activity.test.ts +0 -154
- package/apps/studio/test/acp-ai-activity.test.ts +0 -182
- package/apps/studio/test/acp-attachment-serve.test.ts +0 -74
- package/apps/studio/test/acp-bootstrap-brief.test.ts +0 -178
- package/apps/studio/test/acp-branch-guard.test.ts +0 -123
- package/apps/studio/test/acp-bridge-lifetime.test.ts +0 -475
- package/apps/studio/test/acp-bridge.test.ts +0 -301
- package/apps/studio/test/acp-capabilities.test.ts +0 -123
- package/apps/studio/test/acp-caps-bridge.test.ts +0 -279
- package/apps/studio/test/acp-commands.test.ts +0 -119
- package/apps/studio/test/acp-elicitation-bridge.test.ts +0 -480
- package/apps/studio/test/acp-elicitation.test.ts +0 -251
- package/apps/studio/test/acp-env.test.ts +0 -95
- package/apps/studio/test/acp-mode-banner.test.ts +0 -45
- package/apps/studio/test/acp-origin-gate.test.ts +0 -160
- package/apps/studio/test/acp-permission-prompt.test.ts +0 -251
- package/apps/studio/test/acp-permission.test.ts +0 -278
- package/apps/studio/test/acp-plugin-bootstrap.test.ts +0 -89
- package/apps/studio/test/acp-session-allowed-tools.test.ts +0 -235
- package/apps/studio/test/acp-session-plugins.test.ts +0 -209
- package/apps/studio/test/acp-toolgroup.test.ts +0 -76
- package/apps/studio/test/acp-transcript-view.test.ts +0 -71
- package/apps/studio/test/acp-transcript.test.ts +0 -238
- package/apps/studio/test/acp-usage-bridge.test.ts +0 -141
- package/apps/studio/test/acp-usage.test.ts +0 -143
- package/apps/studio/test/acp-write-gate.test.ts +0 -323
- package/apps/studio/test/acp-write-scope.test.ts +0 -533
- package/apps/studio/test/active-state.test.ts +0 -186
- package/apps/studio/test/activity.test.ts +0 -189
- package/apps/studio/test/ai-activity.test.ts +0 -113
- package/apps/studio/test/annotate-write.test.ts +0 -1074
- package/apps/studio/test/annotation-strokes-command.test.ts +0 -166
- package/apps/studio/test/annotations-align.test.ts +0 -88
- package/apps/studio/test/annotations-api.test.ts +0 -145
- package/apps/studio/test/annotations-bindings.test.ts +0 -262
- package/apps/studio/test/annotations-draw-modifiers.test.ts +0 -206
- package/apps/studio/test/annotations-groups.test.ts +0 -231
- package/apps/studio/test/annotations-layer.test.ts +0 -1011
- package/apps/studio/test/annotations-roundtrip.test.ts +0 -663
- package/apps/studio/test/annotations-snap.test.ts +0 -135
- package/apps/studio/test/annotations-text-format.test.ts +0 -396
- package/apps/studio/test/artboard-activity-overlay.test.tsx +0 -56
- package/apps/studio/test/artboard-guides-overlay.test.tsx +0 -152
- package/apps/studio/test/artboard-hug-commit.test.ts +0 -36
- package/apps/studio/test/artboard-kinds.test.tsx +0 -83
- package/apps/studio/test/artboard-selection-attrs.test.ts +0 -69
- package/apps/studio/test/asset-api.test.ts +0 -149
- package/apps/studio/test/assets-s3.test.ts +0 -249
- package/apps/studio/test/binary-smoke.test.ts +0 -47
- package/apps/studio/test/boot-self-heal.test.ts +0 -87
- package/apps/studio/test/browse-posture.test.tsx +0 -107
- package/apps/studio/test/bundle-smoke.test.ts +0 -46
- package/apps/studio/test/camera-reveal.test.tsx +0 -173
- package/apps/studio/test/canvas-artifacts.test.ts +0 -125
- package/apps/studio/test/canvas-build-sandbox.test.ts +0 -69
- package/apps/studio/test/canvas-build.test.ts +0 -141
- package/apps/studio/test/canvas-create-api.test.ts +0 -676
- package/apps/studio/test/canvas-cursors.test.ts +0 -164
- package/apps/studio/test/canvas-edit.test.ts +0 -470
- package/apps/studio/test/canvas-freshness-deps.test.ts +0 -64
- package/apps/studio/test/canvas-header.test.ts +0 -127
- package/apps/studio/test/canvas-hide-chrome.test.ts +0 -88
- package/apps/studio/test/canvas-hmr-runtime.test.tsx +0 -114
- package/apps/studio/test/canvas-lib-inline.test.ts +0 -146
- package/apps/studio/test/canvas-lib-motion.test.ts +0 -131
- package/apps/studio/test/canvas-lib-resolver.test.ts +0 -111
- package/apps/studio/test/canvas-list-watch.test.ts +0 -388
- package/apps/studio/test/canvas-media-drop.test.ts +0 -187
- package/apps/studio/test/canvas-meta-api.test.ts +0 -707
- package/apps/studio/test/canvas-move-api.test.ts +0 -587
- package/apps/studio/test/canvas-origin-gate.test.ts +0 -417
- package/apps/studio/test/canvas-pipeline.test.ts +0 -180
- package/apps/studio/test/canvas-rects.test.ts +0 -198
- package/apps/studio/test/canvas-reorder.test.ts +0 -211
- package/apps/studio/test/canvas-route.test.ts +0 -179
- package/apps/studio/test/canvas-shell-base.test.ts +0 -107
- package/apps/studio/test/canvas-shell-build-error.test.ts +0 -52
- package/apps/studio/test/canvas-url.test.ts +0 -138
- package/apps/studio/test/canvas-wheel-delta.test.ts +0 -42
- package/apps/studio/test/canvas-zoom-floor.test.ts +0 -73
- package/apps/studio/test/capture-core.test.ts +0 -32
- package/apps/studio/test/capture-determinism-shape.test.ts +0 -135
- package/apps/studio/test/chat-attachments.test.ts +0 -117
- package/apps/studio/test/chat-context.test.ts +0 -129
- package/apps/studio/test/chat-markdown.test.tsx +0 -58
- package/apps/studio/test/clip-addressing.test.ts +0 -738
- package/apps/studio/test/clip-ops.test.ts +0 -922
- package/apps/studio/test/cloud-build.test.ts +0 -95
- package/apps/studio/test/cloud-connect-note.test.ts +0 -135
- package/apps/studio/test/cloud-endpoints.test.ts +0 -590
- package/apps/studio/test/cloud-history-hardening.test.ts +0 -165
- package/apps/studio/test/cloud-history-posture.test.ts +0 -230
- package/apps/studio/test/cloud-managed-save-surfaces.test.ts +0 -90
- package/apps/studio/test/cloud-renew.test.ts +0 -205
- package/apps/studio/test/cloud-session-role.test.ts +0 -161
- package/apps/studio/test/cloud-shell-surfaces.test.ts +0 -170
- package/apps/studio/test/cold-start-apply.test.ts +0 -303
- package/apps/studio/test/collab-annotations-bridge.test.ts +0 -55
- package/apps/studio/test/collab-awareness-bridge.test.ts +0 -223
- package/apps/studio/test/collab-bridge.test.ts +0 -79
- package/apps/studio/test/collab-loopback.test.ts +0 -63
- package/apps/studio/test/collab-origin-gate.test.ts +0 -323
- package/apps/studio/test/collab-protocol.test.ts +0 -146
- package/apps/studio/test/collab-readonly-gate.test.ts +0 -194
- package/apps/studio/test/collab-reseed-guard.test.ts +0 -78
- package/apps/studio/test/collab-room.test.ts +0 -188
- package/apps/studio/test/collab-session-survival.test.tsx +0 -176
- package/apps/studio/test/collab-stress.test.ts +0 -129
- package/apps/studio/test/comment-mount.test.ts +0 -97
- package/apps/studio/test/comment-relay-origin-gate.test.ts +0 -117
- package/apps/studio/test/comments-api.test.ts +0 -277
- package/apps/studio/test/comments-fs-rebroadcast.test.ts +0 -155
- package/apps/studio/test/comments-overlay.test.ts +0 -117
- package/apps/studio/test/compile-entry.test.ts +0 -133
- package/apps/studio/test/component-map.test.ts +0 -48
- package/apps/studio/test/config-projection.test.ts +0 -104
- package/apps/studio/test/config-reload.test.ts +0 -230
- package/apps/studio/test/config-version.test.ts +0 -88
- package/apps/studio/test/context-resolve-tokens.test.ts +0 -97
- package/apps/studio/test/convert-to-absolute.test.ts +0 -333
- package/apps/studio/test/credential-grammar.test.ts +0 -107
- package/apps/studio/test/csp-canvas-shell.test.ts +0 -62
- package/apps/studio/test/csrf-write-guard.test.ts +0 -141
- package/apps/studio/test/debug-bundle.test.ts +0 -105
- package/apps/studio/test/design-setup-readiness.test.ts +0 -155
- package/apps/studio/test/detach-component.test.ts +0 -94
- package/apps/studio/test/dns-rebinding-guard.test.ts +0 -101
- package/apps/studio/test/dom-selection.test.ts +0 -198
- package/apps/studio/test/dynamic-text-edit.test.ts +0 -162
- package/apps/studio/test/edit-css-occurrence.test.ts +0 -81
- package/apps/studio/test/edit-persistence.test.ts +0 -91
- package/apps/studio/test/edit-scope-api.test.ts +0 -119
- package/apps/studio/test/edit-source-command.test.ts +0 -142
- package/apps/studio/test/edit-suppress.test.ts +0 -170
- package/apps/studio/test/editing-presence.test.ts +0 -103
- package/apps/studio/test/element-resize.test.ts +0 -136
- package/apps/studio/test/element-structural-api.test.ts +0 -434
- package/apps/studio/test/element-structural-edit.test.ts +0 -750
- package/apps/studio/test/ensure-browser.test.ts +0 -92
- package/apps/studio/test/equal-spacing-detector.test.ts +0 -257
- package/apps/studio/test/export-assemble.test.ts +0 -53
- package/apps/studio/test/export-browser-lane.test.ts +0 -126
- package/apps/studio/test/export-capture-fidelity.test.ts +0 -146
- package/apps/studio/test/export-capture-hygiene.test.ts +0 -675
- package/apps/studio/test/export-center.test.tsx +0 -287
- package/apps/studio/test/export-e2e-lanes.test.ts +0 -857
- package/apps/studio/test/export-format-scope-coherence.test.ts +0 -89
- package/apps/studio/test/export-lane.test.ts +0 -245
- package/apps/studio/test/export-shim-multi-capture.test.ts +0 -83
- package/apps/studio/test/exporters/canva.test.ts +0 -63
- package/apps/studio/test/exporters/degraded-propagation.test.ts +0 -123
- package/apps/studio/test/exporters/endpoint.test.ts +0 -121
- package/apps/studio/test/exporters/history.test.ts +0 -107
- package/apps/studio/test/exporters/html.test.ts +0 -26
- package/apps/studio/test/exporters/jobs.test.ts +0 -342
- package/apps/studio/test/exporters/pdf.test.ts +0 -85
- package/apps/studio/test/exporters/png.test.ts +0 -103
- package/apps/studio/test/exporters/pptx-deck.test.ts +0 -112
- package/apps/studio/test/exporters/pptx.test.ts +0 -31
- package/apps/studio/test/exporters/pw-launch.test.ts +0 -49
- package/apps/studio/test/exporters/runtime.test.ts +0 -59
- package/apps/studio/test/exporters/scope.test.ts +0 -292
- package/apps/studio/test/exporters/svg.test.ts +0 -29
- package/apps/studio/test/exporters/unsupported-media.test.ts +0 -123
- package/apps/studio/test/exporters/zip.test.ts +0 -104
- package/apps/studio/test/fetch-asset-gate.test.ts +0 -189
- package/apps/studio/test/figjam-v3-model.test.ts +0 -342
- package/apps/studio/test/figma-explode.test.ts +0 -438
- package/apps/studio/test/figma-provenance.test.ts +0 -108
- package/apps/studio/test/figma-routes.test.ts +0 -294
- package/apps/studio/test/file-lock.test.ts +0 -84
- package/apps/studio/test/fixtures/fake-claude-auth.mjs +0 -13
- package/apps/studio/test/fixtures/figjam-v3-groups-bindings.svg +0 -1
- package/apps/studio/test/fixtures/mock-acp-agent-caps.mjs +0 -158
- package/apps/studio/test/fixtures/mock-acp-agent-commands.mjs +0 -44
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-flood.mjs +0 -46
- package/apps/studio/test/fixtures/mock-acp-agent-elicit-url.mjs +0 -42
- package/apps/studio/test/fixtures/mock-acp-agent-elicit.mjs +0 -63
- package/apps/studio/test/fixtures/mock-acp-agent-permission-flood.mjs +0 -51
- package/apps/studio/test/fixtures/mock-acp-agent-permission.mjs +0 -50
- package/apps/studio/test/fixtures/mock-acp-agent-slow.mjs +0 -45
- package/apps/studio/test/fixtures/mock-acp-agent-usage.mjs +0 -56
- package/apps/studio/test/fixtures/mock-acp-agent-wedged.mjs +0 -37
- package/apps/studio/test/fixtures/mock-acp-agent-write.mjs +0 -118
- package/apps/studio/test/fixtures/mock-acp-agent.mjs +0 -69
- package/apps/studio/test/fixtures/perf-canvas.mjs +0 -201
- package/apps/studio/test/fixtures/phase-20-annotations.svg +0 -1
- package/apps/studio/test/fixtures/phase-21-annotations.svg +0 -1
- package/apps/studio/test/fixtures/tiny-subset.woff2 +0 -0
- package/apps/studio/test/fixtures/video-comp-fixture.tsx +0 -82
- package/apps/studio/test/footage-store.test.ts +0 -100
- package/apps/studio/test/fs-mkdir-api.test.ts +0 -248
- package/apps/studio/test/fs-watch.test.ts +0 -41
- package/apps/studio/test/gemma-models.test.ts +0 -245
- package/apps/studio/test/generate-route.test.ts +0 -106
- package/apps/studio/test/git-api.test.ts +0 -0
- package/apps/studio/test/git-branches.test.ts +0 -341
- package/apps/studio/test/git-cloud-posture.test.ts +0 -59
- package/apps/studio/test/git-grouping.test.ts +0 -106
- package/apps/studio/test/git-lifecycle.test.ts +0 -100
- package/apps/studio/test/git-log-format.test.ts +0 -95
- package/apps/studio/test/git-safe-rel.test.ts +0 -132
- package/apps/studio/test/git-system-engine.test.ts +0 -135
- package/apps/studio/test/git-watch.test.ts +0 -97
- package/apps/studio/test/github-api.test.ts +0 -577
- package/apps/studio/test/grid-track-handles.test.ts +0 -160
- package/apps/studio/test/handoff-static-frames.test.ts +0 -215
- package/apps/studio/test/handoff.test.ts +0 -329
- package/apps/studio/test/history-rollback.test.ts +0 -88
- package/apps/studio/test/hmr-broadcast.test.ts +0 -236
- package/apps/studio/test/hmr-classify.test.ts +0 -87
- package/apps/studio/test/hub-link.test.ts +0 -80
- package/apps/studio/test/identity-cache.test.ts +0 -83
- package/apps/studio/test/import-asset-browser.test.ts +0 -64
- package/apps/studio/test/import-asset-route.test.ts +0 -71
- package/apps/studio/test/import-asset.test.ts +0 -427
- package/apps/studio/test/import-brand-browser.test.ts +0 -88
- package/apps/studio/test/import-brand-route.test.ts +0 -120
- package/apps/studio/test/import-brand.test.ts +0 -206
- package/apps/studio/test/import-figma.test.ts +0 -667
- package/apps/studio/test/import-tokens.test.ts +0 -722
- package/apps/studio/test/index-data-canvas-ds.test.ts +0 -97
- package/apps/studio/test/input-router.test.ts +0 -757
- package/apps/studio/test/inspect-script-syntax.test.ts +0 -53
- package/apps/studio/test/inspect-selections.test.ts +0 -219
- package/apps/studio/test/knob-props-authored.test.ts +0 -87
- package/apps/studio/test/layers-synthetic-groups.test.ts +0 -96
- package/apps/studio/test/locator.test.ts +0 -214
- package/apps/studio/test/marquee-overlay.test.ts +0 -94
- package/apps/studio/test/media-commit-chain.test.ts +0 -210
- package/apps/studio/test/mode-toggle.test.tsx +0 -226
- package/apps/studio/test/motion-sample.test.ts +0 -35
- package/apps/studio/test/move-artboards-command.test.ts +0 -108
- package/apps/studio/test/participants-chrome.test.ts +0 -65
- package/apps/studio/test/paths.test.ts +0 -88
- package/apps/studio/test/pdf-print-boxes.test.ts +0 -359
- package/apps/studio/test/peer-selection-follows-camera.test.tsx +0 -131
- package/apps/studio/test/perf-harness.ts +0 -193
- package/apps/studio/test/phase-3.6-smoke.test.ts +0 -79
- package/apps/studio/test/photo-bg-remove-validation.test.ts +0 -71
- package/apps/studio/test/photo-canvas-bundle.test.ts +0 -79
- package/apps/studio/test/photo-edit-api.test.ts +0 -201
- package/apps/studio/test/photo-filters.test.ts +0 -149
- package/apps/studio/test/photo-pipeline.test.ts +0 -106
- package/apps/studio/test/photo-taxonomy.test.ts +0 -96
- package/apps/studio/test/print-marks.test.ts +0 -113
- package/apps/studio/test/print-units.test.ts +0 -173
- package/apps/studio/test/read-annotations.test.ts +0 -727
- package/apps/studio/test/read-only-gate.test.ts +0 -188
- package/apps/studio/test/read-only-role.test.ts +0 -95
- package/apps/studio/test/readiness.test.ts +0 -226
- package/apps/studio/test/remote-ahead-behind-cache.test.ts +0 -89
- package/apps/studio/test/reorder-api.test.ts +0 -210
- package/apps/studio/test/repo-concurrency.test.ts +0 -146
- package/apps/studio/test/repo-lock.test.ts +0 -350
- package/apps/studio/test/report-proxy.test.ts +0 -125
- package/apps/studio/test/ripple.test.ts +0 -197
- package/apps/studio/test/runtime-bundle-error-mapping.test.ts +0 -43
- package/apps/studio/test/runtime-bundle.test.ts +0 -69
- package/apps/studio/test/sanitize-annotation-svg.test.ts +0 -238
- package/apps/studio/test/scaffold-design.test.ts +0 -122
- package/apps/studio/test/server-lifecycle.test.ts +0 -38
- package/apps/studio/test/session-runtime-state.test.ts +0 -226
- package/apps/studio/test/session-scope.test.ts +0 -100
- package/apps/studio/test/shared-doc-cell-pairing.test.ts +0 -642
- package/apps/studio/test/shared-doc-convergence.test.ts +0 -265
- package/apps/studio/test/shared-doc-foundation.test.ts +0 -63
- package/apps/studio/test/shared-doc-migrate.test.ts +0 -369
- package/apps/studio/test/shared-doc-projection.test.ts +0 -238
- package/apps/studio/test/shell-importmap.test.ts +0 -56
- package/apps/studio/test/sizing-mode.test.ts +0 -102
- package/apps/studio/test/slash-commands.test.ts +0 -117
- package/apps/studio/test/snap-distance-pill.test.ts +0 -68
- package/apps/studio/test/spacing-handles.test.ts +0 -138
- package/apps/studio/test/specimen-select.test.ts +0 -88
- package/apps/studio/test/sync-agent.test.ts +0 -660
- package/apps/studio/test/sync-annotations-cold-start.test.ts +0 -302
- package/apps/studio/test/sync-asset-push.test.ts +0 -931
- package/apps/studio/test/sync-atomic-write.test.ts +0 -62
- package/apps/studio/test/sync-attach-incremental.test.ts +0 -527
- package/apps/studio/test/sync-autocommit.test.ts +0 -537
- package/apps/studio/test/sync-canvas-path.test.ts +0 -200
- package/apps/studio/test/sync-cell-write-nudge.test.ts +0 -346
- package/apps/studio/test/sync-codec.test.ts +0 -230
- package/apps/studio/test/sync-cold-seed-dedup.test.ts +0 -187
- package/apps/studio/test/sync-cold-start.test.ts +0 -304
- package/apps/studio/test/sync-connect-honesty.test.ts +0 -114
- package/apps/studio/test/sync-connection-state.test.ts +0 -337
- package/apps/studio/test/sync-ctl-channel.test.ts +0 -508
- package/apps/studio/test/sync-decide-file.test.ts +0 -420
- package/apps/studio/test/sync-doc-name.test.ts +0 -281
- package/apps/studio/test/sync-echo-guard.test.ts +0 -96
- package/apps/studio/test/sync-file-ledger.test.ts +0 -334
- package/apps/studio/test/sync-file-membership.test.ts +0 -347
- package/apps/studio/test/sync-file-plane.test.ts +0 -976
- package/apps/studio/test/sync-file-pull.test.ts +0 -333
- package/apps/studio/test/sync-fresh-link-parity.test.ts +0 -267
- package/apps/studio/test/sync-fs-mirror.test.ts +0 -181
- package/apps/studio/test/sync-hardening.test.ts +0 -586
- package/apps/studio/test/sync-hub-listing.test.ts +0 -46
- package/apps/studio/test/sync-hubs-config.test.ts +0 -133
- package/apps/studio/test/sync-incident-replay.test.ts +0 -211
- package/apps/studio/test/sync-journal.test.ts +0 -176
- package/apps/studio/test/sync-meta-codec.test.ts +0 -199
- package/apps/studio/test/sync-migrate-flat-fallback.test.ts +0 -98
- package/apps/studio/test/sync-move-retirement.test.ts +0 -231
- package/apps/studio/test/sync-panel-surface.test.ts +0 -153
- package/apps/studio/test/sync-path-pull.test.ts +0 -531
- package/apps/studio/test/sync-presentation.test.ts +0 -262
- package/apps/studio/test/sync-pull-budget.test.ts +0 -169
- package/apps/studio/test/sync-remote-docs.test.ts +0 -333
- package/apps/studio/test/sync-resync-routes.test.ts +0 -168
- package/apps/studio/test/sync-runtime.test.ts +0 -1675
- package/apps/studio/test/sync-seed-defers-to-hub.test.ts +0 -83
- package/apps/studio/test/sync-settings-routes.test.ts +0 -195
- package/apps/studio/test/sync-settings.test.ts +0 -151
- package/apps/studio/test/sync-status.test.ts +0 -261
- package/apps/studio/test/sync-supervisor.test.ts +0 -262
- package/apps/studio/test/sync-tombstone-apply.test.ts +0 -111
- package/apps/studio/test/sync-trash.test.ts +0 -132
- package/apps/studio/test/sync-two-peer-discovery.test.ts +0 -343
- package/apps/studio/test/sync-untrusted.test.ts +0 -103
- package/apps/studio/test/system-endpoint.test.ts +0 -144
- package/apps/studio/test/text-editability-stamp.test.ts +0 -131
- package/apps/studio/test/timeline-comp-target.test.ts +0 -139
- package/apps/studio/test/timeline-media-cache.test.ts +0 -41
- package/apps/studio/test/timeline-parse.test.ts +0 -184
- package/apps/studio/test/timeline-scale.test.ts +0 -65
- package/apps/studio/test/timeline-snap.test.ts +0 -85
- package/apps/studio/test/tool-palette-insert-anchor.test.ts +0 -86
- package/apps/studio/test/tool-palette.test.tsx +0 -55
- package/apps/studio/test/tour-overlay.test.tsx +0 -90
- package/apps/studio/test/trusted-request-host.test.ts +0 -66
- package/apps/studio/test/undo-sequence-byte-compare.test.ts +0 -211
- package/apps/studio/test/undo-stack.test.ts +0 -244
- package/apps/studio/test/use-agent-presence.test.tsx +0 -117
- package/apps/studio/test/use-annotation-resize.test.ts +0 -402
- package/apps/studio/test/use-annotation-selection.test.tsx +0 -77
- package/apps/studio/test/use-artboard-drag.test.ts +0 -324
- package/apps/studio/test/use-canvas-activity.test.tsx +0 -206
- package/apps/studio/test/use-collab.test.ts +0 -0
- package/apps/studio/test/use-cursor-modifiers.test.ts +0 -59
- package/apps/studio/test/use-keyboard-discipline.test.ts +0 -27
- package/apps/studio/test/use-selection-set.test.tsx +0 -166
- package/apps/studio/test/use-snap-guides.test.ts +0 -271
- package/apps/studio/test/use-tool-mode.test.tsx +0 -186
- package/apps/studio/test/use-undo-stack.test.tsx +0 -223
- package/apps/studio/test/video-asset.test.ts +0 -163
- package/apps/studio/test/video-comp-fixture.test.ts +0 -50
- package/apps/studio/test/video-comp.test.ts +0 -270
- package/apps/studio/test/video-encode-lib.test.ts +0 -63
- package/apps/studio/test/video-render-bridge.test.ts +0 -171
- package/apps/studio/test/whats-new.test.ts +0 -113
- package/apps/studio/test/whisper-setup.test.ts +0 -97
- package/apps/studio/test/workspace-containment.test.ts +0 -382
- package/apps/studio/test/workspace-signin.test.ts +0 -292
- package/apps/studio/test/ws-handshake.test.ts +0 -33
- package/cli/commands/cache.test.mjs +0 -166
- package/cli/commands/design-link.test.mjs +0 -419
- package/cli/commands/design.test.mjs +0 -240
- package/cli/commands/doctor.test.mjs +0 -237
- package/cli/commands/hub-workspace.test.mjs +0 -275
- package/cli/commands/hub.test.mjs +0 -225
- package/cli/commands/kg.test.mjs +0 -142
- package/cli/commands/preflight.test.mjs +0 -46
- package/cli/lib/argv.test.mjs +0 -46
- package/cli/lib/cache.test.mjs +0 -303
- package/cli/lib/cell-plan.test.mjs +0 -225
- package/cli/lib/config-lint.test.mjs +0 -117
- package/cli/lib/ddr-to-kgai.test.mjs +0 -118
- package/cli/lib/design-ownership.test.mjs +0 -329
- package/cli/lib/fetch-asset.test.mjs +0 -213
- package/cli/lib/figma-codegen-reachability.test.mjs +0 -104
- package/cli/lib/figma-import-controls.test.mjs +0 -70
- package/cli/lib/flow-design-integration.test.mjs +0 -165
- package/cli/lib/gitignore-block.test.mjs +0 -136
- package/cli/lib/gitignore-drift.test.mjs +0 -156
- package/cli/lib/hubs-config.test.mjs +0 -165
- package/cli/lib/pkg-root.test.mjs +0 -111
- package/cli/lib/plugin-cli-reachability.test.mjs +0 -57
- package/cli/lib/plugin-name-namespace.test.mjs +0 -71
- package/cli/lib/reconstruct-toolset.test.mjs +0 -194
- package/cli/lib/stack-detect.test.mjs +0 -121
- package/cli/lib/update-check.test.mjs +0 -32
- package/cli/lib/workspace-plan.test.mjs +0 -699
|
@@ -42,6 +42,8 @@ import { readArtboardPrintProp } from '../canvas-edit.ts';
|
|
|
42
42
|
import { computeMarksGeometry, MARK_STROKE_PT, requiredSlugPt } from '../print/marks.ts';
|
|
43
43
|
import { CSS_DPI, getPaperPreset, mmToPt, resolveBleedMm, trimSizeMm } from '../print/units.ts';
|
|
44
44
|
import { exportShimPath, runShim } from './_runtime.ts';
|
|
45
|
+
import type { ExportDegradation } from './degraded.ts';
|
|
46
|
+
import { outlinePdf } from './ghostscript.ts';
|
|
45
47
|
import {
|
|
46
48
|
canvasShellUrl,
|
|
47
49
|
type ExportContext,
|
|
@@ -49,6 +51,7 @@ import {
|
|
|
49
51
|
type ExportOptions,
|
|
50
52
|
type ExportResult,
|
|
51
53
|
} from './index.ts';
|
|
54
|
+
import { analyzePdfFonts, describeFontProblem, unprintableFonts } from './pdf-fonts.ts';
|
|
52
55
|
import { clampDpi } from './png.ts';
|
|
53
56
|
import type { Target } from './scope.ts';
|
|
54
57
|
|
|
@@ -205,6 +208,121 @@ export function parsePageFit(raw: unknown): string | null {
|
|
|
205
208
|
return getPaperPreset(raw) ? raw : null;
|
|
206
209
|
}
|
|
207
210
|
|
|
211
|
+
/**
|
|
212
|
+
* How the export should treat text (issue #116).
|
|
213
|
+
*
|
|
214
|
+
* - `keep` — today's behaviour. Text stays selectable and searchable; the
|
|
215
|
+
* fonts are whatever Chromium emitted. A preflight NOTICE is
|
|
216
|
+
* attached when some of those are unprintable.
|
|
217
|
+
* - `embed` — assert every font is a real embedded font program. Type 3 is
|
|
218
|
+
* a hard failure, named — never a silent pass.
|
|
219
|
+
* - `outline` — every glyph becomes a vector path (Ghostscript). Print-safe
|
|
220
|
+
* by construction; text is no longer selectable and the file
|
|
221
|
+
* can grow substantially.
|
|
222
|
+
*/
|
|
223
|
+
export type PdfTextMode = 'keep' | 'embed' | 'outline';
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* `options.text` — TOP-LEVEL, not nested under `pdfPrint`, deliberately.
|
|
227
|
+
* `pdfPrint`'s presence is what switches the boxes/marks post-pass on, and
|
|
228
|
+
* text handling is orthogonal to that: a plain non-print PDF can carry Type 3
|
|
229
|
+
* fonts just as easily as a leaflet can.
|
|
230
|
+
*
|
|
231
|
+
* Degrades to `keep` on anything unrecognised rather than throwing, per this
|
|
232
|
+
* adapter's standing rule that a malformed options bag must never break an
|
|
233
|
+
* export (`ExportOptions` is free-form with no schema gate upstream).
|
|
234
|
+
*/
|
|
235
|
+
export function parsePdfText(raw: unknown): PdfTextMode {
|
|
236
|
+
return raw === 'embed' || raw === 'outline' ? raw : 'keep';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Classify the fonts in the bytes we are about to hand back.
|
|
241
|
+
*
|
|
242
|
+
* On the SAVED BYTES, not the in-memory `PDFDocument`, and that is load-bearing:
|
|
243
|
+
* pdf-lib DEFERS `embedPage` until flush, so before a save the `pageFit` path's
|
|
244
|
+
* page XObject is a dangling ref and a font walk finds nothing at all. Analysing
|
|
245
|
+
* the document would silently report "no fonts" for exactly the scale-to-paper
|
|
246
|
+
* exports print shops receive. `test/pdf-font-preflight.test.ts` pins both
|
|
247
|
+
* halves of that.
|
|
248
|
+
*
|
|
249
|
+
* Returns null when pdf-lib cannot read the bytes. A preflight is a warning
|
|
250
|
+
* mechanism; it must never become a new way for a successful export to fail.
|
|
251
|
+
*/
|
|
252
|
+
async function analyzeFinalBytes(bytes: Uint8Array) {
|
|
253
|
+
try {
|
|
254
|
+
const doc = await PDFDocument.load(bytes, { updateMetadata: false });
|
|
255
|
+
return analyzePdfFonts(doc);
|
|
256
|
+
} catch {
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Apply `options.text` to the assembled bytes — the one place all three modes
|
|
263
|
+
* are decided, so the fast path and the post-pass path can never disagree about
|
|
264
|
+
* what `embed` means.
|
|
265
|
+
*/
|
|
266
|
+
export async function applyTextMode(
|
|
267
|
+
bytes: Uint8Array,
|
|
268
|
+
mode: PdfTextMode,
|
|
269
|
+
hooks?: ExportHooks
|
|
270
|
+
): Promise<{ body: Uint8Array; degraded?: ExportDegradation }> {
|
|
271
|
+
if (mode === 'outline') {
|
|
272
|
+
const outlined = await outlinePdf(bytes, { signal: hooks?.signal });
|
|
273
|
+
// Ghostscript exited 0 — but "it ran" is not "it worked". Re-run the same
|
|
274
|
+
// preflight over its output: if a font survived, the file LOOKS converted
|
|
275
|
+
// and is not, which is the failure this whole feature exists to make
|
|
276
|
+
// impossible. Better to fail than to hand back a plausible lie.
|
|
277
|
+
const after = await analyzeFinalBytes(outlined);
|
|
278
|
+
if (after && after.length) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Ghostscript reported success but ${after.length} font(s) survived outlining ` +
|
|
281
|
+
`(${after.map((f) => f.name).join(', ')}). The PDF was not converted to curves; ` +
|
|
282
|
+
'do not send it to print.'
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
assertOutlinedSizeOk(outlined.byteLength);
|
|
286
|
+
return { body: outlined };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const fonts = await analyzeFinalBytes(bytes);
|
|
290
|
+
if (!fonts) return { body: bytes };
|
|
291
|
+
const bad = unprintableFonts(fonts);
|
|
292
|
+
if (!bad.length) return { body: bytes };
|
|
293
|
+
|
|
294
|
+
const { reason, remedy } = describeFontProblem(bad);
|
|
295
|
+
if (mode === 'embed') {
|
|
296
|
+
throw new Error(`text=embed: ${reason} ${remedy}`);
|
|
297
|
+
}
|
|
298
|
+
return {
|
|
299
|
+
body: bytes,
|
|
300
|
+
degraded: { fontsNotEmbedded: bad.map((f) => f.name), reason, remedy },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Outlining trades file size for print safety — glyphs become path data, and a
|
|
306
|
+
* photo-heavy piece can grow by an order of magnitude. That is a legitimate
|
|
307
|
+
* print deliverable, not a bug, so this reuses the SAME generous ceiling
|
|
308
|
+
* `assertTotalSizeOk` applies to captures rather than a tighter one; it exists
|
|
309
|
+
* to catch a runaway, not to cap ordinary print output.
|
|
310
|
+
*
|
|
311
|
+
* Checked separately because `assertTotalSizeOk` runs on the CAPTURED files,
|
|
312
|
+
* before assembly and long before outlining — the pass that actually creates
|
|
313
|
+
* the growth was downstream of every existing guard.
|
|
314
|
+
*/
|
|
315
|
+
export function assertOutlinedSizeOk(byteLength: number): void {
|
|
316
|
+
if (byteLength <= MAX_TOTAL_OUTPUT_BYTES) return;
|
|
317
|
+
const mb = Math.round(byteLength / 1024 / 1024);
|
|
318
|
+
const maxMb = Math.round(MAX_TOTAL_OUTPUT_BYTES / 1024 / 1024);
|
|
319
|
+
throw new Error(
|
|
320
|
+
`Outlined PDF is too large (~${mb}MB, limit ~${maxMb}MB) — converting text to curves ` +
|
|
321
|
+
'grows the file. Try exporting fewer artboards per file, a lower DPI, or ' +
|
|
322
|
+
'text=embed (which verifies the fonts without converting them).'
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
208
326
|
/**
|
|
209
327
|
* PDF's own device-scale resolver — deliberately NOT `png.ts`'s
|
|
210
328
|
* `resolveDeviceScale`, which defaults an absent `dpi` to `clampScale(undefined)`
|
|
@@ -361,6 +479,7 @@ export async function run(
|
|
|
361
479
|
const timeoutSec = (options.timeoutSec as number | undefined) ?? 12;
|
|
362
480
|
const printOpts = parsePdfPrintOptions(options.pdfPrint);
|
|
363
481
|
const pageFit = parsePageFit(options.pageFit);
|
|
482
|
+
const textMode = parsePdfText(options.text);
|
|
364
483
|
// Dogfood follow-up — raster CONTENT on the artboard (a dropped photo, a
|
|
365
484
|
// large-format piece authored at a fraction of its physical size) needs a
|
|
366
485
|
// real deviceScaleFactor to embed at print density; the page itself stays
|
|
@@ -407,11 +526,25 @@ export async function run(
|
|
|
407
526
|
// Fast path — no print/pageFit options given: pass Chromium's vector PDF
|
|
408
527
|
// straight through, exactly as before this feature (zero pdf-lib
|
|
409
528
|
// round-trip overhead for the common "just export a PDF" case).
|
|
529
|
+
//
|
|
530
|
+
// The font preflight (`applyTextMode`) DOES parse here, and that is a
|
|
531
|
+
// deliberate cost. This path is what `/design:export pdf` with no options
|
|
532
|
+
// takes — i.e. the most likely way someone exports a leaflet — and a PDF
|
|
533
|
+
// that a print shop will reject while saying nothing is precisely the bug
|
|
534
|
+
// being fixed. The parse is a tokenize + object-table build (raw streams
|
|
535
|
+
// are copied by length, never decoded), and it degrades to silence rather
|
|
536
|
+
// than an error on a PDF pdf-lib cannot read.
|
|
410
537
|
if (!needsPostPass) {
|
|
411
538
|
const [only] = written;
|
|
412
539
|
if (written.length === 1 && only) {
|
|
413
540
|
const bytes = new Uint8Array(readFileSync(only.path));
|
|
414
|
-
|
|
541
|
+
const finished = await applyTextMode(bytes, textMode, hooks);
|
|
542
|
+
return {
|
|
543
|
+
filename: `${baseSlug}.pdf`,
|
|
544
|
+
contentType: 'application/pdf',
|
|
545
|
+
body: finished.body,
|
|
546
|
+
...(finished.degraded ? { degraded: finished.degraded } : {}),
|
|
547
|
+
};
|
|
415
548
|
}
|
|
416
549
|
const out = await PDFDocument.create();
|
|
417
550
|
for (const w of written) {
|
|
@@ -420,7 +553,13 @@ export async function run(
|
|
|
420
553
|
for (const page of pages) out.addPage(page);
|
|
421
554
|
}
|
|
422
555
|
const bytes = await out.save();
|
|
423
|
-
|
|
556
|
+
const finished = await applyTextMode(bytes, textMode, hooks);
|
|
557
|
+
return {
|
|
558
|
+
filename: `${baseSlug}.pdf`,
|
|
559
|
+
contentType: 'application/pdf',
|
|
560
|
+
body: finished.body,
|
|
561
|
+
...(finished.degraded ? { degraded: finished.degraded } : {}),
|
|
562
|
+
};
|
|
424
563
|
}
|
|
425
564
|
|
|
426
565
|
// Post-pass path — every page goes through pdf-lib (single-page case now
|
|
@@ -473,7 +612,17 @@ export async function run(
|
|
|
473
612
|
}
|
|
474
613
|
}
|
|
475
614
|
const bytes = await out.save();
|
|
476
|
-
|
|
615
|
+
// Text handling runs LAST, on the fully assembled document: the boxes are
|
|
616
|
+
// set and the vector crop/registration marks are drawn, so they are part of
|
|
617
|
+
// what Ghostscript preserves rather than something applied to an already
|
|
618
|
+
// outlined file (pdf-lib could not re-open and re-mark it as cleanly).
|
|
619
|
+
const finished = await applyTextMode(bytes, textMode, hooks);
|
|
620
|
+
return {
|
|
621
|
+
filename: `${baseSlug}.pdf`,
|
|
622
|
+
contentType: 'application/pdf',
|
|
623
|
+
body: finished.body,
|
|
624
|
+
...(finished.degraded ? { degraded: finished.degraded } : {}),
|
|
625
|
+
};
|
|
477
626
|
} finally {
|
|
478
627
|
rmSync(tmp, { recursive: true, force: true });
|
|
479
628
|
}
|
|
@@ -54,6 +54,41 @@ function safeArtifactName(raw: string | null, format: Format): string {
|
|
|
54
54
|
return base.slice(0, 255);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Reduce whatever the render service put in `x-maude-degraded` to a value the
|
|
59
|
+
* rest of the system can hold, mirroring `safeArtifactName` above.
|
|
60
|
+
*
|
|
61
|
+
* The header was previously `JSON.parse`d and cast straight to
|
|
62
|
+
* `ExportDegradation` — the one field on this boundary that skipped the file's
|
|
63
|
+
* own "everything crossing back is untrusted" rule. It mattered less while the
|
|
64
|
+
* only consumer read a boolean; it matters now that a degradation can carry a
|
|
65
|
+
* `fontsNotEmbedded` ARRAY which `client/export-center.jsx` renders with
|
|
66
|
+
* `.join(', ')`. A hostile or simply mixed-version worker sending an object
|
|
67
|
+
* there turns the export panel into a TypeError, and unbounded `reason` /
|
|
68
|
+
* `remedy` strings land verbatim in the on-disk history ledger.
|
|
69
|
+
*
|
|
70
|
+
* Returns undefined for anything unusable — a malformed note must never cost a
|
|
71
|
+
* real artifact, which is the same reason the caller swallows parse errors.
|
|
72
|
+
*/
|
|
73
|
+
function safeDegradation(raw: unknown): ExportDegradation | undefined {
|
|
74
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
75
|
+
const o = raw as Record<string, unknown>;
|
|
76
|
+
const text = (v: unknown, max: number): string | undefined =>
|
|
77
|
+
typeof v === 'string' && v ? v.slice(0, max) : undefined;
|
|
78
|
+
|
|
79
|
+
const reason = text(o.reason, 2000);
|
|
80
|
+
if (!reason) return undefined; // `reason` is the only required field
|
|
81
|
+
const fonts = Array.isArray(o.fontsNotEmbedded)
|
|
82
|
+
? o.fontsNotEmbedded.flatMap((f) => text(f, 200) ?? []).slice(0, 50)
|
|
83
|
+
: undefined;
|
|
84
|
+
return {
|
|
85
|
+
reason,
|
|
86
|
+
...(o.audioDropped === true ? { audioDropped: true } : {}),
|
|
87
|
+
...(fonts?.length ? { fontsNotEmbedded: fonts } : {}),
|
|
88
|
+
...(text(o.remedy, 2000) ? { remedy: text(o.remedy, 2000) } : {}),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
/** Read a response body with a hard byte ceiling — `arrayBuffer()` has none. */
|
|
58
93
|
async function readCapped(res: Response, max: number): Promise<Uint8Array> {
|
|
59
94
|
const declared = Number(res.headers.get('content-length'));
|
|
@@ -274,7 +309,7 @@ export async function renderRemotely(args: {
|
|
|
274
309
|
const raw = /^[A-Za-z0-9+/=]+$/.test(degradedHeader)
|
|
275
310
|
? Buffer.from(degradedHeader, 'base64').toString('utf8')
|
|
276
311
|
: degradedHeader;
|
|
277
|
-
degraded = JSON.parse(raw)
|
|
312
|
+
degraded = safeDegradation(JSON.parse(raw));
|
|
278
313
|
} catch {
|
|
279
314
|
/* a malformed degradation note must not fail a real artifact */
|
|
280
315
|
}
|
package/apps/studio/fs-watch.ts
CHANGED
|
@@ -15,6 +15,34 @@ export interface FsWatch {
|
|
|
15
15
|
|
|
16
16
|
const DEBOUNCE_MS = 50;
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Runtime-state DIRECTORIES with no `fs:any` subscriber — see the note at the
|
|
20
|
+
* skip site for why this is deliberately NOT the sync taxonomy.
|
|
21
|
+
*
|
|
22
|
+
* `_comments/`, `_canvas-state/` and `_state/` are POINTEDLY absent: the first
|
|
23
|
+
* drives comment collab, and the other two are small and per-user, so there is
|
|
24
|
+
* no volume argument for suppressing them and a real risk in guessing. Add to
|
|
25
|
+
* this list only after confirming no `bus.on('fs:any' | 'fs:json' | …)`
|
|
26
|
+
* subscriber acts on the path.
|
|
27
|
+
*/
|
|
28
|
+
const RUNTIME_DIRS = ['_history', '_chat', '_trash', '_smoke'] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* True when `rel` is one of `RUNTIME_DIRS` or a path inside one, on either
|
|
32
|
+
* separator (`fs.watch` hands back platform-native separators on Windows).
|
|
33
|
+
*
|
|
34
|
+
* Matching the BARE directory name is load-bearing, not tidiness: macOS raises
|
|
35
|
+
* an event for the containing directory as well as the file, so a write to
|
|
36
|
+
* `_chat/c-1.jsonl` arrives as BOTH `_chat/c-1.jsonl` and a bare `_chat`.
|
|
37
|
+
* Suppressing only the prefixed form would leave every streamed ACP update
|
|
38
|
+
* still waking every `fs:any` subscriber — the wakeup this list exists to stop.
|
|
39
|
+
*/
|
|
40
|
+
function isUnderRuntimeDir(rel: string): boolean {
|
|
41
|
+
return RUNTIME_DIRS.some(
|
|
42
|
+
(d) => rel === d || rel.startsWith(`${d}/`) || rel.startsWith(`${d}${path.sep}`)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
export function createFsWatch(ctx: Context): FsWatch {
|
|
19
47
|
let watcher: ReturnType<typeof watch> | null = null;
|
|
20
48
|
const seen = new Map<string, number>();
|
|
@@ -48,10 +76,26 @@ export function createFsWatch(ctx: Context): FsWatch {
|
|
|
48
76
|
watcher = watch(ctx.paths.designRoot, { recursive: true }, (_event, filename) => {
|
|
49
77
|
if (!filename) return;
|
|
50
78
|
// Skip our own runtime artifacts.
|
|
79
|
+
//
|
|
80
|
+
// NOT `isRuntimeStateRel` (sync/file-membership.ts), even though this
|
|
81
|
+
// list looks like a narrower copy of it. That classifier answers "may
|
|
82
|
+
// this file travel to a peer", and it excludes `_comments/` — but
|
|
83
|
+
// `_comments/` IS needed on this bus: collab/index.ts matches
|
|
84
|
+
// `^_comments/(.+)\.json$` on `fs:any`, and git/watch.ts treats it as
|
|
85
|
+
// versionable. Routing this list through the sync taxonomy would
|
|
86
|
+
// silently kill comment collaboration. The two lists answer different
|
|
87
|
+
// questions and are deliberately not the same list.
|
|
88
|
+
//
|
|
89
|
+
// What IS added here (#119) are the high-volume runtime directories
|
|
90
|
+
// with no `fs:any` subscriber. `_chat/` matters most: the bridge
|
|
91
|
+
// appends to it on every streamed update, so each chunk of an agent
|
|
92
|
+
// turn was raising an `fs:any` that woke canvas-list-watch, the
|
|
93
|
+
// activity tracker and gitWatch (the last running git status against
|
|
94
|
+
// the project's whole repo) for a file none of them can act on.
|
|
51
95
|
if (filename.startsWith('_server.json')) return;
|
|
52
96
|
if (filename.startsWith('_active.json')) return;
|
|
53
97
|
if (filename.startsWith('_sync.json')) return;
|
|
54
|
-
if (
|
|
98
|
+
if (isUnderRuntimeDir(filename)) return;
|
|
55
99
|
emit(filename);
|
|
56
100
|
});
|
|
57
101
|
} catch (err) {
|
package/apps/studio/http.ts
CHANGED
|
@@ -4509,7 +4509,13 @@ export function createHttp(
|
|
|
4509
4509
|
const serviceUrl = process.env.MAUDE_RENDER_URL;
|
|
4510
4510
|
if (!serviceUrl) return Response.json({ ok: false, lane: resolveRenderLane() });
|
|
4511
4511
|
try {
|
|
4512
|
-
|
|
4512
|
+
// `globalThis.fetch`, never a bare `fetch(` — this module scope has
|
|
4513
|
+
// carried a same-named local handler before, and a bare call silently
|
|
4514
|
+
// resolved to it instead of the network (RCA
|
|
4515
|
+
// issue-report-a-bug-http-500). The handler is `handleFallthrough`
|
|
4516
|
+
// today; the rule stands so a rename can never reintroduce that, and
|
|
4517
|
+
// `report-proxy.test.ts` fails the build on any bare call site here.
|
|
4518
|
+
const r = await globalThis.fetch(`${serviceUrl.replace(/\/+$/, '')}/_health`, {
|
|
4513
4519
|
signal: AbortSignal.timeout(8_000),
|
|
4514
4520
|
});
|
|
4515
4521
|
const body = (await r.json().catch(() => null)) as { ok?: boolean } | null;
|
package/apps/studio/mem.ts
CHANGED
|
@@ -5,8 +5,30 @@
|
|
|
5
5
|
// not the primary path. The primary path is the explicit `iframe:closed` event
|
|
6
6
|
// emitted on the bus, which calls cleanupFn synchronously.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
// #119 — these thresholds are read against `rss`, NOT `heapTotal`.
|
|
9
|
+
//
|
|
10
|
+
// The watch originally polled `process.memoryUsage().heapTotal`, which is a V8
|
|
11
|
+
// intuition that does not carry over: under Bun/JSC it barely moves for the
|
|
12
|
+
// allocations that actually grow this process. Measured while the server read
|
|
13
|
+
// 554 MB of chat transcripts, `rss` peaked at 1.29 GB while `heapTotal` peaked
|
|
14
|
+
// at 5 MB — three orders of magnitude apart. Neither threshold below was
|
|
15
|
+
// reachable, so the one guard written for a runaway-memory failure could never
|
|
16
|
+
// fire, and the `[mem]` line that would have named the problem never reached
|
|
17
|
+
// the log ring (and therefore never reached a bug report's `serverLogTail`).
|
|
18
|
+
// The bug it was meant to catch shipped past it in silence.
|
|
19
|
+
//
|
|
20
|
+
// `rss` is the number the user sees in Activity Monitor, which is also the
|
|
21
|
+
// number they report. Sized for a server whose steady state is a few hundred
|
|
22
|
+
// MB: WARN is "something is wrong", PANIC is "intervene before the machine
|
|
23
|
+
// starts swapping".
|
|
24
|
+
const RSS_WARN_BYTES = 1024 * 1024 * 1024; // 1 GB — log only
|
|
25
|
+
const RSS_PANIC_BYTES = 2 * 1024 * 1024 * 1024; // 2 GB — log + force GC
|
|
26
|
+
|
|
27
|
+
/** Once past PANIC, back off exponentially instead of forcing a full GC on
|
|
28
|
+
* every tick forever. `Bun.gc(true)` is synchronous — it runs on the same
|
|
29
|
+
* single thread that serves the ACP socket and every HTTP route — so a
|
|
30
|
+
* permanently-crossed threshold must not become a permanent stutter. */
|
|
31
|
+
const PANIC_BACKOFF_MAX = 16;
|
|
10
32
|
|
|
11
33
|
let registry: FinalizationRegistry<{ id: string; cleanupFn: () => void }> | null = null;
|
|
12
34
|
|
|
@@ -73,17 +95,31 @@ let heapTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
73
95
|
|
|
74
96
|
export function startHeapWatch(intervalMs = 60_000) {
|
|
75
97
|
if (heapTimer) return;
|
|
98
|
+
let ticksUntilPanic = 0;
|
|
99
|
+
let panicBackoff = 1;
|
|
76
100
|
heapTimer = setInterval(() => {
|
|
77
101
|
const u = process.memoryUsage();
|
|
78
|
-
|
|
102
|
+
const mb = (n: number) => (n / 1024 / 1024).toFixed(0);
|
|
103
|
+
if (u.rss > RSS_PANIC_BYTES) {
|
|
104
|
+
// ALWAYS log — the log line is the diagnostic that survives into a bug
|
|
105
|
+
// report, and it must not be suppressed by the GC backoff below.
|
|
79
106
|
console.warn(
|
|
80
|
-
`[mem]
|
|
107
|
+
`[mem] rss ${mb(u.rss)}MB > panic threshold (heapUsed ${mb(u.heapUsed)}MB) — memory is not being reclaimed`
|
|
81
108
|
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
if (ticksUntilPanic > 0) {
|
|
110
|
+
ticksUntilPanic--;
|
|
111
|
+
} else {
|
|
112
|
+
const gc = (globalThis as { Bun?: { gc?: (sync: boolean) => void } }).Bun?.gc;
|
|
113
|
+
if (gc) gc(true);
|
|
114
|
+
ticksUntilPanic = panicBackoff;
|
|
115
|
+
panicBackoff = Math.min(panicBackoff * 2, PANIC_BACKOFF_MAX);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
ticksUntilPanic = 0;
|
|
119
|
+
panicBackoff = 1;
|
|
120
|
+
if (u.rss > RSS_WARN_BYTES) {
|
|
121
|
+
console.warn(`[mem] rss ${mb(u.rss)}MB > warn threshold (heapUsed ${mb(u.heapUsed)}MB)`);
|
|
122
|
+
}
|
|
87
123
|
}
|
|
88
124
|
}, intervalMs);
|
|
89
125
|
// Don't keep the event loop alive just for the heap watch.
|
package/apps/studio/readiness.ts
CHANGED
|
@@ -214,8 +214,22 @@ export async function probeReadiness(): Promise<ReadinessReport> {
|
|
|
214
214
|
const authStatus =
|
|
215
215
|
e2eForce === 'signed-out' ? { loggedIn: false } : claude ? await getClaudeAuthStatus() : null;
|
|
216
216
|
const signedIn = !!authStatus?.loggedIn;
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
// Issue #107 — a resolvable CLI whose auth status we could not READ is NOT
|
|
218
|
+
// the same as a signed-out one, and must not be reported as one. Claiming
|
|
219
|
+
// "not signed in" to a user who is signed in is a false accusation they
|
|
220
|
+
// can't argue with, and it routes them to a Sign-in button that can only
|
|
221
|
+
// dead-end (the poll re-runs the same failing probe until it times out).
|
|
222
|
+
// The probe's own tolerance for wrapper stdout noise now lives in probe.ts;
|
|
223
|
+
// this is the honest fallback for whatever noise that still can't read.
|
|
224
|
+
const authUnreadable = !!claude && e2eForce !== 'signed-out' && authStatus === null;
|
|
225
|
+
// Anything that is not an explicit `firstParty` counts as off-subscription,
|
|
226
|
+
// ABSENT included (attacker R2-3). The old `authStatus?.apiProvider &&`
|
|
227
|
+
// truthiness guard meant a metered user could suppress DDR-123's billing
|
|
228
|
+
// warning by OMITTING the field rather than forging `firstParty` — the exact
|
|
229
|
+
// outcome `narrowStatusTag`'s sentinel was written to prevent, reached through
|
|
230
|
+
// a different input shape. Over-warning is the safe direction here.
|
|
231
|
+
const offSubscription = signedIn && authStatus?.apiProvider !== 'firstParty';
|
|
232
|
+
const providerLabel = authStatus?.apiProvider ?? 'provider not reported';
|
|
219
233
|
// DDR-166 Decision 5 — the settings-UI opt-out (prefs.rs `claude_auto_setup`,
|
|
220
234
|
// DEFAULT ON) reaches the dev-server as an env var set fresh at each sidecar
|
|
221
235
|
// spawn. Disabled → fall back to guide-only: no automated action offered,
|
|
@@ -226,21 +240,42 @@ export async function probeReadiness(): Promise<ReadinessReport> {
|
|
|
226
240
|
id: 'claude',
|
|
227
241
|
label: 'Claude Code (the `claude` CLI)',
|
|
228
242
|
required: true,
|
|
229
|
-
status: claude && signedIn ? 'present' : 'missing',
|
|
243
|
+
status: claude && signedIn ? 'present' : authUnreadable ? 'unknown' : 'missing',
|
|
230
244
|
detail: !claude
|
|
231
245
|
? 'Not found on PATH.'
|
|
232
246
|
: signedIn
|
|
233
247
|
? offSubscription
|
|
234
|
-
? `Signed in, but not on a Claude subscription (${
|
|
248
|
+
? `Signed in, but not on a Claude subscription (${providerLabel}) — AI editing may bill metered API usage.`
|
|
235
249
|
: `Signed in${authStatus?.subscriptionType ? ` · ${authStatus.subscriptionType}` : ''} — AI editing runs on your own Pro/Max subscription.`
|
|
236
|
-
:
|
|
250
|
+
: authUnreadable
|
|
251
|
+
? "Installed, but Maude couldn't read its sign-in state."
|
|
252
|
+
: 'Installed, but not signed in.',
|
|
237
253
|
remediation: !claude
|
|
238
254
|
? 'Install Claude Code, then sign in. AI editing runs on your own Pro/Max subscription — never API billing.'
|
|
239
255
|
: signedIn
|
|
240
256
|
? undefined
|
|
241
|
-
:
|
|
257
|
+
: authUnreadable
|
|
258
|
+
? // Deliberately does NOT interpolate the resolved path (attacker
|
|
259
|
+
// R2-2). This row carries a Copy affordance, and `copyFix` strips
|
|
260
|
+
// the backticks — so a path containing shell metacharacters would
|
|
261
|
+
// land on the clipboard inside a sentence the user is being told to
|
|
262
|
+
// paste. Worse, it would turn the possibly-hijacked binary that
|
|
263
|
+
// DDR-166 Decision 3 shows them precisely so they can DOUBT it into
|
|
264
|
+
// an instruction to run it. The path renders on its own line below.
|
|
265
|
+
'Maude ran `claude auth status --json` and could not read a status back. The usual cause is a version-manager shim or wrapper (mise, asdf, volta) fronting the real binary — check the path below, fix or quiet the wrapper, then Re-check.'
|
|
266
|
+
: 'Sign in to connect it to your Pro/Max subscription.',
|
|
242
267
|
installCommand: !claude ? 'curl -fsSL https://claude.ai/install.sh | bash' : undefined,
|
|
243
|
-
|
|
268
|
+
// No Sign-in button when the status is unreadable: `pollForSignin` in
|
|
269
|
+
// ReadinessList.jsx polls THIS probe, so a button whose success signal we
|
|
270
|
+
// can't read can only ever end in "Sign-in timed out" (issue #107). The
|
|
271
|
+
// remediation text above is the actionable path in that state.
|
|
272
|
+
action: !autoSetupEnabled
|
|
273
|
+
? undefined
|
|
274
|
+
: !claude
|
|
275
|
+
? 'install'
|
|
276
|
+
: !signedIn && !authUnreadable
|
|
277
|
+
? 'signin'
|
|
278
|
+
: undefined,
|
|
244
279
|
resolvedPath: claude ?? undefined,
|
|
245
280
|
resolvedViaMaude: !!claude && claude === process.env.MAUDE_CLAUDE_BIN,
|
|
246
281
|
});
|
package/apps/studio/server.ts
CHANGED
|
@@ -78,16 +78,23 @@ let collab: ReturnType<typeof createCollab> | null = null;
|
|
|
78
78
|
let inspectHandle: ReturnType<typeof createInspectRegistry> | null = null;
|
|
79
79
|
|
|
80
80
|
const api = createApi(ctx, {
|
|
81
|
-
onCommentsChanged:
|
|
82
|
-
// After every comments mutation, re-broadcast the updated list.
|
|
83
|
-
const comments = await api.loadCommentsForFile(file);
|
|
84
|
-
ctx.bus.emit('comments', { file, comments });
|
|
81
|
+
onCommentsChanged: (file, comments) => {
|
|
85
82
|
// Phase 8 Task 3 — bridge into the live Y.Array so collab peers see the
|
|
86
83
|
// change without waiting for cold-open re-seeding. No-op when no room is
|
|
87
84
|
// live for this canvas slug.
|
|
85
|
+
//
|
|
86
|
+
// Issue #111 — this runs BEFORE the mutation reaches disk (see
|
|
87
|
+
// `publishComments`), and `comments` is the post-mutation list handed over
|
|
88
|
+
// in memory. It used to be `await api.loadCommentsForFile(file)`: a disk
|
|
89
|
+
// round-trip sitting between the file write and this publish, wide enough
|
|
90
|
+
// for a `Room.flush()` to project the pre-mutation doc back over the file.
|
|
91
|
+
// Nothing here touches disk any more, and the doc is updated synchronously,
|
|
92
|
+
// so there is no window for a flush to be the stale writer.
|
|
88
93
|
if (collab) {
|
|
89
94
|
collab.registry.syncRoomFromComments(api.fileSlug(file), comments);
|
|
90
95
|
}
|
|
96
|
+
// After every comments mutation, re-broadcast the updated list.
|
|
97
|
+
ctx.bus.emit('comments', { file, comments });
|
|
91
98
|
},
|
|
92
99
|
// Phase 8 Task 5 — same bridge for annotations. PUT /_api/annotations writes
|
|
93
100
|
// the SVG blob to disk; we mirror it into the live Y.Map for collab peers.
|