@1agh/maude 0.39.1 → 0.41.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.
Files changed (137) hide show
  1. package/README.md +1 -1
  2. package/apps/studio/ai-banner.tsx +2 -2
  3. package/apps/studio/annotations-context-toolbar.tsx +44 -1
  4. package/apps/studio/annotations-layer.tsx +306 -5
  5. package/apps/studio/annotations-model.ts +107 -6
  6. package/apps/studio/api.ts +1316 -67
  7. package/apps/studio/artboard-marquee.tsx +1 -1
  8. package/apps/studio/bin/_canvas-rects-playwright.mjs +55 -0
  9. package/apps/studio/bin/_canvas-rects-static.mjs +166 -0
  10. package/apps/studio/bin/_fetch-asset.mjs +556 -0
  11. package/apps/studio/bin/_html-playwright.mjs +9 -1
  12. package/apps/studio/bin/_pdf-playwright.mjs +8 -1
  13. package/apps/studio/bin/_png-playwright.mjs +8 -1
  14. package/apps/studio/bin/_pw-launch.mjs +54 -0
  15. package/apps/studio/bin/_svg-playwright.mjs +8 -1
  16. package/apps/studio/bin/_video-playwright.mjs +452 -0
  17. package/apps/studio/bin/annotate.mjs +576 -34
  18. package/apps/studio/bin/canvas-rects.sh +152 -0
  19. package/apps/studio/bin/fetch-asset.sh +34 -0
  20. package/apps/studio/bin/prep.sh +8 -1
  21. package/apps/studio/bin/read-annotations.mjs +138 -7
  22. package/apps/studio/bin/smoke.sh +42 -6
  23. package/apps/studio/build.ts +21 -0
  24. package/apps/studio/canvas-comment-mount.tsx +138 -4
  25. package/apps/studio/canvas-edit.ts +2625 -111
  26. package/apps/studio/canvas-lib.tsx +238 -2
  27. package/apps/studio/canvas-list-watch.ts +6 -2
  28. package/apps/studio/canvas-shell.tsx +514 -20
  29. package/apps/studio/client/app.jsx +2480 -52
  30. package/apps/studio/client/comments-overlay.css +130 -126
  31. package/apps/studio/client/github.js +15 -0
  32. package/apps/studio/client/panels/TimelinePanel.jsx +860 -0
  33. package/apps/studio/client/panels/timeline-parse.js +229 -0
  34. package/apps/studio/client/panels/timeline-snap.js +55 -0
  35. package/apps/studio/client/styles/3-shell-maude.css +155 -0
  36. package/apps/studio/comments-overlay.tsx +148 -41
  37. package/apps/studio/config.schema.json +14 -0
  38. package/apps/studio/context-menu.tsx +16 -6
  39. package/apps/studio/context.ts +113 -1
  40. package/apps/studio/contextual-toolbar.tsx +262 -4
  41. package/apps/studio/cursors-overlay.tsx +4 -4
  42. package/apps/studio/dist/client.bundle.js +89 -17
  43. package/apps/studio/dist/comment-mount.js +59 -1
  44. package/apps/studio/dist/runtime/.min-sizes.json +11 -1
  45. package/apps/studio/dist/runtime/@remotion_media.js +491 -0
  46. package/apps/studio/dist/runtime/@remotion_player.js +56 -0
  47. package/apps/studio/dist/runtime/@remotion_transitions.js +300 -0
  48. package/apps/studio/dist/runtime/@remotion_transitions_clock-wipe.js +1 -0
  49. package/apps/studio/dist/runtime/@remotion_transitions_fade.js +1 -0
  50. package/apps/studio/dist/runtime/@remotion_transitions_flip.js +1 -0
  51. package/apps/studio/dist/runtime/@remotion_transitions_none.js +1 -0
  52. package/apps/studio/dist/runtime/@remotion_transitions_slide.js +1 -0
  53. package/apps/studio/dist/runtime/@remotion_transitions_wipe.js +1 -0
  54. package/apps/studio/dist/runtime/REMOTION-LICENSE.md +28 -0
  55. package/apps/studio/dist/runtime/remotion.js +42 -0
  56. package/apps/studio/dist/styles.css +1 -1
  57. package/apps/studio/dom-selection.ts +127 -1
  58. package/apps/studio/drag-state.ts +24 -0
  59. package/apps/studio/equal-spacing-detector.ts +205 -0
  60. package/apps/studio/export-dialog.tsx +1 -1
  61. package/apps/studio/exporters/_browser-bundles.ts +117 -0
  62. package/apps/studio/exporters/_runtime.ts +69 -0
  63. package/apps/studio/exporters/html.ts +4 -3
  64. package/apps/studio/exporters/index.ts +28 -2
  65. package/apps/studio/exporters/pdf.ts +4 -3
  66. package/apps/studio/exporters/png.ts +5 -3
  67. package/apps/studio/exporters/pptx.ts +6 -4
  68. package/apps/studio/exporters/svg.ts +9 -5
  69. package/apps/studio/exporters/video-encode-lib.ts +200 -0
  70. package/apps/studio/exporters/video-render-lib.ts +108 -0
  71. package/apps/studio/exporters/video.ts +184 -0
  72. package/apps/studio/history.ts +47 -1
  73. package/apps/studio/http.ts +758 -27
  74. package/apps/studio/input-router.tsx +19 -1
  75. package/apps/studio/marquee-overlay.tsx +1 -1
  76. package/apps/studio/measure-overlay.tsx +241 -0
  77. package/apps/studio/participants-chrome.tsx +3 -3
  78. package/apps/studio/runtime-bundle.ts +30 -0
  79. package/apps/studio/server.ts +34 -9
  80. package/apps/studio/sizing-mode.ts +117 -0
  81. package/apps/studio/spacing-handles.ts +166 -0
  82. package/apps/studio/test/annotate-write.test.ts +890 -0
  83. package/apps/studio/test/annotations-roundtrip.test.ts +47 -0
  84. package/apps/studio/test/camera-reveal.test.tsx +173 -0
  85. package/apps/studio/test/canvas-create-api.test.ts +76 -0
  86. package/apps/studio/test/canvas-edit.test.ts +140 -0
  87. package/apps/studio/test/canvas-list-watch.test.ts +49 -0
  88. package/apps/studio/test/canvas-media-drop.test.ts +30 -1
  89. package/apps/studio/test/canvas-origin-gate.test.ts +54 -0
  90. package/apps/studio/test/canvas-rects.test.ts +198 -0
  91. package/apps/studio/test/clip-addressing.test.ts +732 -0
  92. package/apps/studio/test/comments-overlay.test.ts +117 -0
  93. package/apps/studio/test/config-reload.test.ts +230 -0
  94. package/apps/studio/test/dns-rebinding-guard.test.ts +74 -0
  95. package/apps/studio/test/dom-selection.test.ts +130 -0
  96. package/apps/studio/test/edit-css-occurrence.test.ts +81 -0
  97. package/apps/studio/test/edit-persistence.test.ts +91 -0
  98. package/apps/studio/test/edit-scope-api.test.ts +115 -0
  99. package/apps/studio/test/element-resize.test.ts +136 -0
  100. package/apps/studio/test/element-structural-api.test.ts +360 -0
  101. package/apps/studio/test/element-structural-edit.test.ts +233 -0
  102. package/apps/studio/test/equal-spacing-detector.test.ts +165 -1
  103. package/apps/studio/test/exporters/runtime.test.ts +59 -0
  104. package/apps/studio/test/file-lock.test.ts +84 -0
  105. package/apps/studio/test/fixtures/video-comp-fixture.tsx +82 -0
  106. package/apps/studio/test/history-rollback.test.ts +26 -0
  107. package/apps/studio/test/knob-props-authored.test.ts +87 -0
  108. package/apps/studio/test/read-annotations.test.ts +154 -0
  109. package/apps/studio/test/sizing-mode.test.ts +102 -0
  110. package/apps/studio/test/spacing-handles.test.ts +138 -0
  111. package/apps/studio/test/specimen-select.test.ts +88 -0
  112. package/apps/studio/test/timeline-parse.test.ts +127 -0
  113. package/apps/studio/test/timeline-snap.test.ts +85 -0
  114. package/apps/studio/test/tool-palette-insert-anchor.test.ts +84 -0
  115. package/apps/studio/test/undo-sequence-byte-compare.test.ts +211 -0
  116. package/apps/studio/test/video-asset.test.ts +163 -0
  117. package/apps/studio/test/video-comp-fixture.test.ts +50 -0
  118. package/apps/studio/test/video-comp.test.ts +168 -0
  119. package/apps/studio/test/video-render-bridge.test.ts +149 -0
  120. package/apps/studio/tool-palette.tsx +122 -2
  121. package/apps/studio/undo-hud.tsx +2 -2
  122. package/apps/studio/use-annotation-resize.tsx +6 -3
  123. package/apps/studio/use-canvas-media-drop.tsx +66 -4
  124. package/apps/studio/use-element-resize.tsx +732 -0
  125. package/apps/studio/use-keyboard-discipline.tsx +205 -15
  126. package/apps/studio/use-selection-set.tsx +14 -0
  127. package/apps/studio/use-spacing-handles.tsx +388 -0
  128. package/apps/studio/video-comp.tsx +444 -0
  129. package/apps/studio/whats-new.json +55 -0
  130. package/apps/studio/ws.ts +5 -0
  131. package/cli/commands/design.mjs +6 -1
  132. package/cli/commands/design.test.mjs +49 -1
  133. package/cli/lib/fetch-asset.test.mjs +213 -0
  134. package/package.json +8 -8
  135. package/plugins/design/dependencies.json +10 -2
  136. package/plugins/design/templates/_shell.html +25 -2
  137. package/plugins/design/templates/design-system-inspiration/_MAPPING.md +1 -1
@@ -0,0 +1,213 @@
1
+ // DDR-147 § Security follow-up item 1 — the hardened `maude design fetch-asset`
2
+ // helper (apps/studio/bin/_fetch-asset.mjs). This suite locks the SECURITY CORE
3
+ // that the untrusted-fetch rails promise: the resolved-IP SSRF classifier
4
+ // (reject loopback / link-local / RFC-1918 / CGNAT / multicast / reserved, v4 +
5
+ // v6 incl. IPv4-mapped + NAT64), https-only URL parsing, the png/jpg/gif/webp
6
+ // magic sniff (SVG/HTML rejected), the content-addressed name contract, and the
7
+ // realpath containment assertion. Runs under `pnpm test` (node --test cli/**).
8
+ //
9
+ // The network path (curl) is exercised by the manual smoke in the RCA/PR; these
10
+ // are the offline, deterministic guards a regression would trip.
11
+
12
+ import assert from 'node:assert/strict';
13
+ import { existsSync, readFileSync } from 'node:fs';
14
+ import { dirname, resolve } from 'node:path';
15
+ import { test } from 'node:test';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const HERE = dirname(fileURLToPath(import.meta.url));
19
+ const REPO = resolve(HERE, '..', '..');
20
+ const MOD = resolve(REPO, 'apps', 'studio', 'bin', '_fetch-asset.mjs');
21
+
22
+ const {
23
+ classifyAddress,
24
+ parseIPv4,
25
+ parseIPv6,
26
+ parseHttpsTarget,
27
+ sniffImageExt,
28
+ assetName,
29
+ containedAssetPath,
30
+ FetchAssetError,
31
+ } = await import(MOD);
32
+
33
+ // ── SSRF classifier: BLOCKED addresses ───────────────────────────────────────
34
+ test('classifyAddress blocks private / loopback / link-local / reserved IPv4', () => {
35
+ const blocked = [
36
+ '127.0.0.1',
37
+ '127.9.9.9',
38
+ '10.0.0.1',
39
+ '10.255.255.255',
40
+ '172.16.0.1',
41
+ '172.31.255.255',
42
+ '192.168.1.1',
43
+ '169.254.169.254', // AWS/GCP IMDS
44
+ '169.254.0.1',
45
+ '100.64.0.1', // CGNAT
46
+ '100.127.255.255',
47
+ '0.0.0.0',
48
+ '224.0.0.1', // multicast
49
+ '239.255.255.255',
50
+ '240.0.0.1', // reserved
51
+ '255.255.255.255',
52
+ '198.18.0.1', // benchmark
53
+ '192.0.2.5', // TEST-NET-1
54
+ '192.0.0.1', // IETF
55
+ ];
56
+ for (const ip of blocked) {
57
+ assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
58
+ }
59
+ });
60
+
61
+ test('classifyAddress allows genuine public IPv4 (incl. just-outside-range boundaries)', () => {
62
+ const allowed = [
63
+ '8.8.8.8',
64
+ '1.1.1.1',
65
+ '93.184.216.34', // example.com
66
+ '172.15.255.255', // just below 172.16/12
67
+ '172.32.0.1', // just above 172.31
68
+ '100.63.255.255', // just below CGNAT
69
+ '100.128.0.1', // just above CGNAT
70
+ '169.253.255.255', // just below link-local
71
+ '11.0.0.1',
72
+ '192.167.255.255',
73
+ '192.169.0.1',
74
+ '223.255.255.255', // just below multicast
75
+ ];
76
+ for (const ip of allowed) {
77
+ assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
78
+ }
79
+ });
80
+
81
+ test('classifyAddress blocks loopback / link-local / ULA / multicast + embedded-v4 IPv6', () => {
82
+ const blocked = [
83
+ '::1', // loopback
84
+ '::', // unspecified
85
+ 'fe80::1', // link-local
86
+ 'fe80::abcd:1234',
87
+ 'fc00::1', // ULA
88
+ 'fd12:3456::1', // ULA
89
+ 'ff02::1', // multicast
90
+ '::ffff:127.0.0.1', // IPv4-mapped loopback
91
+ '::ffff:10.0.0.1', // IPv4-mapped private
92
+ '::ffff:169.254.169.254', // IPv4-mapped IMDS
93
+ '64:ff9b::127.0.0.1', // NAT64 → loopback
94
+ '64:ff9b::192.168.0.1', // NAT64 → private
95
+ ];
96
+ for (const ip of blocked) {
97
+ assert.ok(classifyAddress(ip), `expected ${ip} to be BLOCKED but it was allowed`);
98
+ }
99
+ });
100
+
101
+ test('classifyAddress allows genuine public IPv6 (incl. IPv4-mapped public)', () => {
102
+ const allowed = [
103
+ '2001:4860:4860::8888', // Google DNS
104
+ '2606:4700:4700::1111', // Cloudflare DNS
105
+ '::ffff:8.8.8.8', // IPv4-mapped PUBLIC → allowed via embedded classify
106
+ '2a00:1450:4001:81b::200e',
107
+ ];
108
+ for (const ip of allowed) {
109
+ assert.equal(classifyAddress(ip), null, `expected ${ip} to be ALLOWED but it was blocked`);
110
+ }
111
+ });
112
+
113
+ test('classifyAddress fail-closes on non-IP / garbage input', () => {
114
+ for (const bad of ['not-an-ip', 'example.com', '', '999.999.999.999', 'gggg::1']) {
115
+ assert.ok(classifyAddress(bad), `expected ${JSON.stringify(bad)} to be treated as blocked`);
116
+ }
117
+ });
118
+
119
+ // ── IP parsers ───────────────────────────────────────────────────────────────
120
+ test('parseIPv4 rejects malformed dotted-quads', () => {
121
+ for (const bad of ['1.2.3', '1.2.3.4.5', '256.1.1.1', '01.2.3.4', '1.2.3.a', '', '-1.2.3.4']) {
122
+ assert.equal(parseIPv4(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
123
+ }
124
+ assert.deepEqual([...parseIPv4('1.2.3.4')], [1, 2, 3, 4]);
125
+ assert.deepEqual([...parseIPv4('255.255.255.255')], [255, 255, 255, 255]);
126
+ });
127
+
128
+ test('parseIPv6 handles ::, embedded IPv4, zone id; rejects malformed', () => {
129
+ assert.ok(parseIPv6('::1'));
130
+ assert.ok(parseIPv6('::'));
131
+ assert.ok(parseIPv6('fe80::1%en0')); // zone id stripped
132
+ assert.ok(parseIPv6('2001:db8::1'));
133
+ assert.ok(parseIPv6('::ffff:1.2.3.4'));
134
+ for (const bad of ['gggg::', '1::2::3', '12345::', '1.2.3.4', '']) {
135
+ assert.equal(parseIPv6(bad), null, `expected ${JSON.stringify(bad)} to fail parse`);
136
+ }
137
+ });
138
+
139
+ // ── URL target ───────────────────────────────────────────────────────────────
140
+ test('parseHttpsTarget accepts https and extracts host/port', () => {
141
+ assert.deepEqual(parseHttpsTarget('https://example.com/a.png'), {
142
+ host: 'example.com',
143
+ port: 443,
144
+ });
145
+ assert.deepEqual(parseHttpsTarget('https://cdn.example.com:8443/x'), {
146
+ host: 'cdn.example.com',
147
+ port: 8443,
148
+ });
149
+ });
150
+
151
+ test('parseHttpsTarget rejects non-https, credentials, and hostless URLs', () => {
152
+ const bad = [
153
+ 'http://example.com/a.png',
154
+ 'ftp://example.com/a.png',
155
+ 'file:///etc/passwd',
156
+ 'https://user:pass@example.com/a.png',
157
+ 'not a url',
158
+ ];
159
+ for (const u of bad) {
160
+ assert.throws(() => parseHttpsTarget(u), FetchAssetError, `expected ${u} to throw`);
161
+ }
162
+ });
163
+
164
+ // ── image sniff ──────────────────────────────────────────────────────────────
165
+ test('sniffImageExt recognises png/jpg/gif/webp and rejects SVG/HTML/empty', () => {
166
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0, 0]);
167
+ const jpg = Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0, 0]);
168
+ const gif = Buffer.from([0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0, 0]);
169
+ const webp = Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50]);
170
+ assert.equal(sniffImageExt(png), 'png');
171
+ assert.equal(sniffImageExt(jpg), 'jpg');
172
+ assert.equal(sniffImageExt(gif), 'gif');
173
+ assert.equal(sniffImageExt(webp), 'webp');
174
+ assert.equal(sniffImageExt(Buffer.from('<svg xmlns="...')), null);
175
+ assert.equal(sniffImageExt(Buffer.from('<!DOCTYPE html>')), null);
176
+ assert.equal(sniffImageExt(Buffer.from([])), null);
177
+ // RIFF that is NOT webp (e.g. wav) must not sniff as an image.
178
+ assert.equal(
179
+ sniffImageExt(Buffer.from([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x41, 0x56, 0x45])),
180
+ null
181
+ );
182
+ });
183
+
184
+ // ── naming + containment ─────────────────────────────────────────────────────
185
+ test('assetName is content-addressed <sha8>.<ext> and deterministic', () => {
186
+ const bytes = Buffer.from('hello world');
187
+ const n1 = assetName(bytes, 'png');
188
+ const n2 = assetName(bytes, 'png');
189
+ assert.equal(n1, n2);
190
+ assert.match(n1, /^[a-f0-9]{8}\.png$/);
191
+ assert.notEqual(assetName(Buffer.from('other'), 'jpg'), n1);
192
+ });
193
+
194
+ test('containedAssetPath resolves under <root>/<designRoot>/assets and rejects escape', () => {
195
+ const { assetsDir, fileAbs } = containedAssetPath('/repo', '.design', 'a1b2c3d4.png');
196
+ assert.equal(assetsDir, resolve('/repo/.design/assets'));
197
+ assert.equal(fileAbs, resolve('/repo/.design/assets/a1b2c3d4.png'));
198
+ // A traversal name escapes → throws.
199
+ assert.throws(() => containedAssetPath('/repo', '.design', '../../etc/passwd'), FetchAssetError);
200
+ // A design-root that climbs out of the repo → throws.
201
+ assert.throws(() => containedAssetPath('/repo', '../../etc', 'a1b2c3d4.png'), FetchAssetError);
202
+ });
203
+
204
+ // ── wiring: verb registered + helper files ship ──────────────────────────────
205
+ test('fetch-asset is a registered `maude design` bin verb with both helper files present', () => {
206
+ assert.ok(existsSync(resolve(REPO, 'apps/studio/bin/fetch-asset.sh')), 'fetch-asset.sh missing');
207
+ assert.ok(
208
+ existsSync(resolve(REPO, 'apps/studio/bin/_fetch-asset.mjs')),
209
+ '_fetch-asset.mjs missing'
210
+ );
211
+ const design = readFileSync(resolve(REPO, 'cli/commands/design.mjs'), 'utf8');
212
+ assert.match(design, /'fetch-asset'/, "'fetch-asset' not in design.mjs BIN_VERBS");
213
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.39.1",
3
+ "version": "0.41.0",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -50,13 +50,13 @@
50
50
  "test:e2e:desktop:onboarding": "pnpm --filter @maude/desktop-e2e e2e:onboarding"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@1agh/maude-darwin-arm64": "0.39.1",
54
- "@1agh/maude-darwin-x64": "0.39.1",
55
- "@1agh/maude-linux-arm64": "0.39.1",
56
- "@1agh/maude-linux-arm64-musl": "0.39.1",
57
- "@1agh/maude-linux-x64": "0.39.1",
58
- "@1agh/maude-linux-x64-musl": "0.39.1",
59
- "@1agh/maude-win32-x64": "0.39.1"
53
+ "@1agh/maude-darwin-arm64": "0.41.0",
54
+ "@1agh/maude-darwin-x64": "0.41.0",
55
+ "@1agh/maude-linux-arm64": "0.41.0",
56
+ "@1agh/maude-linux-arm64-musl": "0.41.0",
57
+ "@1agh/maude-linux-x64": "0.41.0",
58
+ "@1agh/maude-linux-x64-musl": "0.41.0",
59
+ "@1agh/maude-win32-x64": "0.41.0"
60
60
  },
61
61
  "files": [
62
62
  "cli",
@@ -74,8 +74,10 @@
74
74
  "commands/new.md",
75
75
  "commands/edit.md",
76
76
  "commands/smoke.md",
77
+ "commands/board.md",
77
78
  "dev-server/bin/screenshot.sh",
78
79
  "dev-server/bin/smoke.sh",
80
+ "dev-server/bin/canvas-rects.sh",
79
81
  "agents/design-critic.md",
80
82
  "agents/graphic-design-critic.md",
81
83
  "agents/a11y-critic.md"
@@ -104,7 +106,9 @@
104
106
  "dev-server/bin/_html-playwright.mjs",
105
107
  "dev-server/bin/_pptx-playwright.mjs",
106
108
  "dev-server/bin/_enumerate-artboards-playwright.mjs",
107
- "dev-server/bin/screenshot.sh"
109
+ "dev-server/bin/screenshot.sh",
110
+ "dev-server/bin/_canvas-rects-playwright.mjs",
111
+ "dev-server/bin/canvas-rects.sh"
108
112
  ],
109
113
  "docsUrl": "https://playwright.dev"
110
114
  },
@@ -157,7 +161,11 @@
157
161
  },
158
162
  "autoInstall": false,
159
163
  "fallbackBehavior": "Slash commands' bash blocks that read .ai/workflows.config.json via jq degrade to inline node one-liners; harder to read but functional.",
160
- "usedBy": ["dev-server/bin/bootstrap-check.sh", "dev-server/bin/server-up.sh"]
164
+ "usedBy": [
165
+ "dev-server/bin/bootstrap-check.sh",
166
+ "dev-server/bin/server-up.sh",
167
+ "dev-server/bin/canvas-rects.sh"
168
+ ]
161
169
  },
162
170
  {
163
171
  "id": "askuserquestion-mcp",
@@ -141,7 +141,17 @@
141
141
  "y-protocols/sync": "/_canvas-runtime/y-protocols_sync.js",
142
142
  "y-protocols/awareness": "/_canvas-runtime/y-protocols_awareness.js",
143
143
  "lib0/decoding": "/_canvas-runtime/lib0_decoding.js",
144
- "lib0/encoding": "/_canvas-runtime/lib0_encoding.js"
144
+ "lib0/encoding": "/_canvas-runtime/lib0_encoding.js",
145
+ "remotion": "/_canvas-runtime/remotion.js",
146
+ "@remotion/media": "/_canvas-runtime/@remotion_media.js",
147
+ "@remotion/player": "/_canvas-runtime/@remotion_player.js",
148
+ "@remotion/transitions": "/_canvas-runtime/@remotion_transitions.js",
149
+ "@remotion/transitions/fade": "/_canvas-runtime/@remotion_transitions_fade.js",
150
+ "@remotion/transitions/slide": "/_canvas-runtime/@remotion_transitions_slide.js",
151
+ "@remotion/transitions/wipe": "/_canvas-runtime/@remotion_transitions_wipe.js",
152
+ "@remotion/transitions/flip": "/_canvas-runtime/@remotion_transitions_flip.js",
153
+ "@remotion/transitions/clock-wipe": "/_canvas-runtime/@remotion_transitions_clock-wipe.js",
154
+ "@remotion/transitions/none": "/_canvas-runtime/@remotion_transitions_none.js"
145
155
  }
146
156
  }
147
157
  </script>
@@ -350,7 +360,20 @@
350
360
  // skipped. Mirrors the meta echo-guard below.
351
361
  if (msg.mode === 'module') {
352
362
  // read the parent-verified local (NOT a canvas-writable global)
353
- if (Date.now() - lastCssOptimisticAt < 1500) return;
363
+ //
364
+ // VIDEO-COMP EXCEPTION (DDR-150 dogfood): on a canvas with a
365
+ // mounted Remotion comp the optimistic DOM style CANNOT hold —
366
+ // the Player re-renders styles from the (old) module on every
367
+ // frame, wiping the edit at the next play-through. Skipping the
368
+ // module reload here left the OLD color playing until a manual
369
+ // Cmd+R even though the new value was on disk. So: skip the
370
+ // redundant reload only on comp-less canvases. (Reading the
371
+ // comp registry off `window` is canvas-influenced, but a canvas
372
+ // lying "I have comps" can only FORCE a reload — the safe
373
+ // direction; it can never suppress one.)
374
+ var hasComps = false;
375
+ try { hasComps = !!(window.__maude_comps__ && window.__maude_comps__().length); } catch (err) { hasComps = false; }
376
+ if (!hasComps && Date.now() - lastCssOptimisticAt < 1500) return;
354
377
  }
355
378
  // Phase 30 (F4) — prefer an in-place hot-swap for ANY canvas-body
356
379
  // (`module`) change whenever the canvas runtime is mounted: the
@@ -170,7 +170,7 @@ The agent may also propose treatments outside this catalog — when it does, it
170
170
  | `expressive` | `paired \| chromatic-3` | accent-tinted, gradients OK | `gradient-mesh`, `chromatic-blocks`, `frosted-blur` | `aesthetic` |
171
171
  | `maximalist` | `chromatic-N` | bold, colour-as-structure | `chromatic-blocks`, `gradient-mesh`, `body-pattern` | `full` |
172
172
 
173
- **Anti-funnel invariant:** absence of a clear brand-character signal does NOT mean `restrained` — it means low confidence, so Stage 3 ASKS across the full scale (incl. a multi-colour palette option). High-confidence skip is legitimate only when the character is unambiguous, at **both** ends of the scale. Q10 hard-NOs still override (e.g. "no gradients" disables `gradient-mesh`).
173
+ **Anti-funnel invariant:** absence of a clear brand-character signal does NOT mean `restrained` — it means low confidence, so the flow surfaces the full scale to the user (on the multi-tile path the Stage-3 direction pick shows the ambition poles rendered; on the degraded-to-1 path the Stage-4 refinement ASKS across the full scale, incl. a multi-colour palette option — DDR-147). High-confidence skip is legitimate only when the character is unambiguous, at **both** ends of the scale. Q10 hard-NOs still override (e.g. "no gradients" disables `gradient-mesh`).
174
174
 
175
175
  ### Q10 hard NOs — sub-agent guardrails
176
176