@1agh/maude 0.45.2 → 0.47.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 (91) hide show
  1. package/apps/studio/acp/bootstrap-brief.ts +8 -0
  2. package/apps/studio/acp/bridge.ts +151 -1
  3. package/apps/studio/annotations-layer.tsx +42 -0
  4. package/apps/studio/annotations-snap.ts +50 -10
  5. package/apps/studio/api.ts +760 -15
  6. package/apps/studio/artboard-guides-overlay.tsx +270 -0
  7. package/apps/studio/bin/_agent-browser-safe-config.json +1 -0
  8. package/apps/studio/bin/_agent-browser-safe.mjs +228 -0
  9. package/apps/studio/bin/_agent-browser-safe.test.mjs +165 -0
  10. package/apps/studio/bin/_curl-local.mjs +349 -0
  11. package/apps/studio/bin/_curl-local.test.mjs +280 -0
  12. package/apps/studio/bin/_pdf-playwright.mjs +35 -4
  13. package/apps/studio/bin/_png-playwright.mjs +38 -4
  14. package/apps/studio/bin/_pw-launch.mjs +52 -0
  15. package/apps/studio/bin/_pw-launch.test.mjs +90 -0
  16. package/apps/studio/bin/_smart-frames.mjs +419 -0
  17. package/apps/studio/bin/_smart-frames.test.mjs +140 -0
  18. package/apps/studio/bin/agent-browser-safe.sh +29 -0
  19. package/apps/studio/bin/curl-local.sh +28 -0
  20. package/apps/studio/bin/smart-frames.sh +30 -0
  21. package/apps/studio/canvas-cursors.ts +6 -0
  22. package/apps/studio/canvas-edit.ts +972 -6
  23. package/apps/studio/canvas-icons.tsx +13 -0
  24. package/apps/studio/canvas-lib.tsx +206 -11
  25. package/apps/studio/canvas-shell.tsx +809 -50
  26. package/apps/studio/client/app.jsx +1554 -206
  27. package/apps/studio/client/panels/ChatPanel.jsx +92 -3
  28. package/apps/studio/client/panels/SettingsPanel.jsx +211 -0
  29. package/apps/studio/client/panels/acp-capabilities.js +11 -0
  30. package/apps/studio/client/styles/3-shell-maude.css +39 -0
  31. package/apps/studio/client/styles/6-acp-chat.css +41 -0
  32. package/apps/studio/contextual-toolbar.tsx +5 -3
  33. package/apps/studio/dist/client.bundle.js +1627 -1627
  34. package/apps/studio/dist/comment-mount.js +2 -2
  35. package/apps/studio/dist/styles.css +1 -1
  36. package/apps/studio/dom-selection.ts +20 -0
  37. package/apps/studio/export-dialog.tsx +138 -18
  38. package/apps/studio/exporters/pdf.ts +332 -18
  39. package/apps/studio/exporters/png.ts +45 -3
  40. package/apps/studio/footage/schema.ts +17 -1
  41. package/apps/studio/generation/gemma-models.ts +224 -0
  42. package/apps/studio/generation/prefs.ts +43 -0
  43. package/apps/studio/grid-track-handles.ts +179 -0
  44. package/apps/studio/handoff.ts +35 -0
  45. package/apps/studio/http.ts +331 -7
  46. package/apps/studio/input-router.tsx +73 -17
  47. package/apps/studio/print/marks.ts +113 -0
  48. package/apps/studio/print/units.ts +269 -0
  49. package/apps/studio/print-overlay-content.tsx +132 -0
  50. package/apps/studio/test/acp-mode-banner.test.ts +45 -0
  51. package/apps/studio/test/acp-session-allowed-tools.test.ts +187 -0
  52. package/apps/studio/test/annotations-snap.test.ts +56 -0
  53. package/apps/studio/test/artboard-guides-overlay.test.tsx +152 -0
  54. package/apps/studio/test/artboard-kinds.test.tsx +83 -0
  55. package/apps/studio/test/artboard-selection-attrs.test.ts +69 -0
  56. package/apps/studio/test/browse-posture.test.tsx +107 -0
  57. package/apps/studio/test/canvas-hide-chrome.test.ts +58 -0
  58. package/apps/studio/test/canvas-meta-api.test.ts +237 -0
  59. package/apps/studio/test/canvas-origin-gate.test.ts +4 -0
  60. package/apps/studio/test/comment-mount.test.ts +2 -1
  61. package/apps/studio/test/component-map.test.ts +48 -0
  62. package/apps/studio/test/convert-to-absolute.test.ts +333 -0
  63. package/apps/studio/test/detach-component.test.ts +94 -0
  64. package/apps/studio/test/edit-scope-api.test.ts +8 -4
  65. package/apps/studio/test/element-structural-api.test.ts +74 -0
  66. package/apps/studio/test/element-structural-edit.test.ts +363 -0
  67. package/apps/studio/test/exporters/png.test.ts +49 -1
  68. package/apps/studio/test/grid-track-handles.test.ts +160 -0
  69. package/apps/studio/test/handoff.test.ts +48 -0
  70. package/apps/studio/test/input-router.test.ts +82 -8
  71. package/apps/studio/test/layers-synthetic-groups.test.ts +96 -0
  72. package/apps/studio/test/pdf-print-boxes.test.ts +326 -0
  73. package/apps/studio/test/print-marks.test.ts +113 -0
  74. package/apps/studio/test/print-units.test.ts +173 -0
  75. package/apps/studio/test/use-snap-guides.test.ts +81 -0
  76. package/apps/studio/test/use-tool-mode.test.tsx +10 -2
  77. package/apps/studio/tool-palette.tsx +3 -1
  78. package/apps/studio/use-canvas-media-drop.tsx +126 -0
  79. package/apps/studio/use-chrome-visibility.tsx +19 -0
  80. package/apps/studio/use-element-resize.tsx +24 -3
  81. package/apps/studio/use-grid-track-handles.tsx +364 -0
  82. package/apps/studio/use-keyboard-discipline.tsx +15 -0
  83. package/apps/studio/use-snap-guides.tsx +73 -5
  84. package/apps/studio/use-spacing-handles.tsx +9 -5
  85. package/apps/studio/use-tool-mode.tsx +30 -3
  86. package/apps/studio/web-overlay-content.tsx +52 -0
  87. package/apps/studio/whats-new.json +55 -0
  88. package/cli/commands/design.mjs +25 -1
  89. package/package.json +8 -8
  90. package/plugins/design/dependencies.json +35 -0
  91. package/plugins/design/templates/_shell.html +4 -0
@@ -5,13 +5,26 @@
5
5
  // Per DDR-041 (PDF via page.pdf()): print-media emulation, font readiness
6
6
  // wait, explicit page size matching the artboard rect.
7
7
  //
8
+ // feature-2-print-artboards T5 (dogfood follow-up) — a "vector PDF" is only
9
+ // vector for TEXT and drawn shapes; any raster <img>/<canvas> content on the
10
+ // artboard (a photo dropped onto a flyer, a large-format billboard authored
11
+ // at a small on-screen scale) still embeds as a bitmap, and that bitmap's
12
+ // pixel density is set by the CAPTURING context's deviceScaleFactor — 1× by
13
+ // default, which looks fine on screen but is visibly soft once printed large
14
+ // (e.g. a design authored at 1:10 scale for a billboard). `--scale` sets the
15
+ // context's deviceScaleFactor before `page.pdf()` runs, exactly like
16
+ // _png-playwright.mjs, so embedded raster content captures at print density
17
+ // while vector text/shapes stay crisp regardless (they're never rasterized).
18
+ // Page SIZE (in pt) is unaffected — it always comes from the artboard's own
19
+ // rect, not from this scale.
20
+ //
8
21
  // Invocation (Bun.spawn from exporters/pdf.ts — not invoked directly):
9
22
  // node _pdf-playwright.mjs --url <url> --selector <css> --out <path>
10
- // [--multi] [--out-dir <dir>] [--timeout 12]
23
+ // [--multi] [--out-dir <dir>] [--timeout 12] [--scale 3.125]
11
24
 
12
25
  import { mkdirSync, writeFileSync } from 'node:fs';
13
26
  import { dirname, join } from 'node:path';
14
- import { launchChromium } from './_pw-launch.mjs';
27
+ import { assertRenderOutputSizeOk, launchChromium, safeArtboardFilename } from './_pw-launch.mjs';
15
28
 
16
29
  const args = Object.fromEntries(
17
30
  process.argv.slice(2).reduce((acc, cur, i, all) => {
@@ -28,6 +41,7 @@ const {
28
41
  'widen-to-artboard': widenFlag,
29
42
  multi: multiFlag,
30
43
  timeout = '12',
44
+ scale = '1',
31
45
  } = args;
32
46
 
33
47
  if (!url) {
@@ -38,10 +52,21 @@ if (!url) {
38
52
  const widen = widenFlag !== undefined;
39
53
  const multi = multiFlag !== undefined;
40
54
  const timeoutMs = Number(timeout) * 1000;
55
+ // Same ceiling as _png-playwright.mjs's raised T4 ceiling (600dpi = 6.25×).
56
+ // The PDF *page* stays vector-sized regardless of this factor (only raster
57
+ // CONTENT density scales with it — see the file header) — but Chromium's
58
+ // rendering/rasterization surface for `page.pdf()` scales with
59
+ // deviceScaleFactor exactly like a screenshot buffer does, so the same
60
+ // output-size guard as PNG's DOES apply here (see assertRenderOutputSizeOk
61
+ // call below); a huge artboard + high DPI is just as costly to render.
62
+ const deviceScaleFactor = Math.max(1, Math.min(8, Number(scale) || 1));
41
63
 
42
64
  const browser = await launchChromium();
43
65
  try {
44
- const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
66
+ const ctx = await browser.newContext({
67
+ viewport: { width: 1440, height: 900 },
68
+ deviceScaleFactor,
69
+ });
45
70
  const page = await ctx.newPage();
46
71
  await page.goto(url, { waitUntil: 'load', timeout: timeoutMs });
47
72
  // Wait for web fonts so `@font-face` glyphs land in the PDF instead of
@@ -102,9 +127,15 @@ try {
102
127
  const r = el.getBoundingClientRect();
103
128
  return { w: r.width, h: r.height, x: r.left, y: r.top };
104
129
  });
130
+ assertRenderOutputSizeOk(rect.w, rect.h, deviceScaleFactor, '_pdf-playwright');
131
+ // feature-2-print-artboards T5 — name multi output by the artboard's
132
+ // OWN id (mirrors _png-playwright.mjs's multi-write scheme) rather than
133
+ // a positional index, so the PDF post-pass can correlate each written
134
+ // file back to its artboard's `print` prop via the filename alone.
135
+ const screenId = multi ? await screens[i].getAttribute('data-dc-screen') : null;
105
136
  // Set the page size to the artboard's pixel dimensions so the resulting
106
137
  // PDF is exactly one artboard per page with no margin.
107
- const targetPath = multi ? join(outDir, `artboard-${i + 1}.pdf`) : out;
138
+ const targetPath = multi ? join(outDir, safeArtboardFilename(screenId, i, 'pdf')) : out;
108
139
  // Crop trick: set the viewport to the artboard rect, scroll it into the
109
140
  // top-left corner, then page.pdf() with matching width/height.
110
141
  await page.setViewportSize({
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { mkdirSync } from 'node:fs';
9
9
  import { dirname, join } from 'node:path';
10
- import { launchChromium } from './_pw-launch.mjs';
10
+ import { launchChromium, safeArtboardFilename } from './_pw-launch.mjs';
11
11
 
12
12
  const args = Object.fromEntries(
13
13
  process.argv.slice(2).reduce((acc, cur, i, all) => {
@@ -35,7 +35,38 @@ if (!url) {
35
35
  const widen = widenFlag !== undefined;
36
36
  const multi = multiFlag !== undefined;
37
37
  const timeoutMs = Number(timeout) * 1000;
38
- const deviceScaleFactor = Math.max(1, Math.min(4, Number(scale) || 1));
38
+ // feature-2-print-artboards T4 — ceiling raised 4→8 so a 300dpi export
39
+ // (3.125×) AND 600dpi (6.25×) both fit; exporters/png.ts resolveDeviceScale
40
+ // is the only caller that can reach beyond 3, via the `dpi` option.
41
+ const deviceScaleFactor = Math.max(1, Math.min(8, Number(scale) || 1));
42
+ // feature-2-print-artboards T4 — a raster export's actual pixel dimensions
43
+ // are only known once the target element's DOM rect is measured below (the
44
+ // adapter can't pre-compute them — see png.ts's own comment), so the "too
45
+ // big to render" guard lives here rather than in the Node-side adapter.
46
+ // 16,000px matches common canvas/texture limits across browser engines;
47
+ // ~600MB is a generous RGBA-buffer ceiling (width × height × 4 bytes) well
48
+ // under typical CI/desktop RAM, chosen so a legitimate 600dpi A0 poster still
49
+ // fits (9933×14043px × 4B ≈ 558MB) while a runaway value fails loud instead
50
+ // of OOM-killing the render process.
51
+ const MAX_OUTPUT_SIDE_PX = 16000;
52
+ const MAX_OUTPUT_BYTES = 600 * 1024 * 1024;
53
+ function assertOutputSizeOk(widthCss, heightCss) {
54
+ const outW = Math.ceil(widthCss * deviceScaleFactor);
55
+ const outH = Math.ceil(heightCss * deviceScaleFactor);
56
+ const estBytes = outW * outH * 4;
57
+ if (outW > MAX_OUTPUT_SIDE_PX || outH > MAX_OUTPUT_SIDE_PX || estBytes > MAX_OUTPUT_BYTES) {
58
+ const maxScaleForSide = MAX_OUTPUT_SIDE_PX / Math.max(widthCss, heightCss);
59
+ const maxScaleForBytes = Math.sqrt(MAX_OUTPUT_BYTES / 4 / (widthCss * heightCss));
60
+ const maxScale = Math.min(maxScaleForSide, maxScaleForBytes);
61
+ const maxDpi = Math.floor(maxScale * 96);
62
+ console.error(
63
+ `_png-playwright: requested output ${outW}×${outH}px at ${deviceScaleFactor}× exceeds the ` +
64
+ `render guard (max side ${MAX_OUTPUT_SIDE_PX}px / max ~${Math.round(MAX_OUTPUT_BYTES / 1024 / 1024)}MB). ` +
65
+ `For this artboard size (${Math.round(widthCss)}×${Math.round(heightCss)}px), the max supported DPI is ~${maxDpi}.`
66
+ );
67
+ process.exit(1);
68
+ }
69
+ }
39
70
 
40
71
  const browser = await launchChromium();
41
72
  try {
@@ -97,6 +128,9 @@ try {
97
128
  const r = el.getBoundingClientRect();
98
129
  return { x: r.left, y: r.top, width: r.width, height: r.height };
99
130
  });
131
+ // feature-2-print-artboards T4 — fail loud BEFORE resizing the viewport
132
+ // to a pathological size, not after.
133
+ assertOutputSizeOk(rect.width, rect.height);
100
134
  // Resize the viewport to fit the artboard so the screenshot doesn't
101
135
  // include the world plane's pan/zoom margin. The artboard then sits at
102
136
  // (0,0) with its native dimensions.
@@ -141,8 +175,8 @@ try {
141
175
  const screens = await page.locator(selector ?? '[data-dc-screen]').all();
142
176
  for (let i = 0; i < screens.length; i += 1) {
143
177
  const handle = screens[i];
144
- const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
145
- await captureHandle(handle, join(outDir, `${id}.png`));
178
+ const id = await handle.getAttribute('data-dc-screen');
179
+ await captureHandle(handle, join(outDir, safeArtboardFilename(id, i, 'png')));
146
180
  console.log(`MAUDE_PROGRESS {"current":${i + 1},"total":${screens.length}}`);
147
181
  }
148
182
  } else {
@@ -113,3 +113,55 @@ export async function launchChromium(opts) {
113
113
  throw err;
114
114
  }
115
115
  }
116
+
117
+ // Shared by every multi-artboard export shim (_png/_pdf-playwright.mjs) that
118
+ // names its output file after an artboard's `data-dc-screen` id for T5's
119
+ // filename-correlation convenience. That id comes straight off a live DOM
120
+ // attribute — i.e. from canvas-authored JSX, DDR-054 untrusted content — and
121
+ // feeds directly into `path.join(outDir, ...)`, which does NOT sandbox `..`
122
+ // segments. Re-validates against the SAME shape api.ts's write-side artboard-id
123
+ // ops already enforce (`/^[A-Za-z][\w-]{0,63}$/`) before it's ever used as a
124
+ // path segment; anything else (or a `false`y id) falls back to the safe
125
+ // positional name so a malformed/malicious id can never escape `outDir`.
126
+ const SAFE_ARTBOARD_ID_RE = /^[A-Za-z][\w-]{0,63}$/;
127
+ export function safeArtboardFilename(id, index, ext) {
128
+ const safe =
129
+ typeof id === 'string' && SAFE_ARTBOARD_ID_RE.test(id) ? id : `artboard-${index + 1}`;
130
+ return `${safe}.${ext}`;
131
+ }
132
+
133
+ // Shared by every raster-capture export shim (_png/_pdf-playwright.mjs) that
134
+ // scales its render surface by `deviceScaleFactor` (feature-2-print-artboards
135
+ // T4's DPI support). The requested pixel output is only knowable once the
136
+ // target element's DOM rect is measured (the Node-side adapter can't
137
+ // pre-compute it), so this guard lives here rather than in exporters/*.ts.
138
+ // Applies EQUALLY to the PDF shim: `deviceScaleFactor` scales Chromium's
139
+ // whole rendering/rasterization surface for `page.pdf()` too, not just PNG's
140
+ // screenshot buffer — the PDF *page* stays vector-sized regardless, but the
141
+ // backing store Chromium allocates to paint it does not, so a huge
142
+ // artboard + high DPI is exactly as costly to render as it is for PNG, even
143
+ // though the resulting PDF bytes are typically far smaller than a raw PNG
144
+ // buffer. 16,000px matches common canvas/texture limits across browser
145
+ // engines; ~600MB is a generous RGBA-buffer ceiling (width × height × 4
146
+ // bytes) chosen so a legitimate 600dpi A0 poster still fits (9933×14043px ×
147
+ // 4B ≈ 558MB) while a runaway value fails loud instead of OOM-killing the
148
+ // render process.
149
+ const MAX_OUTPUT_SIDE_PX = 16000;
150
+ const MAX_OUTPUT_BYTES = 600 * 1024 * 1024;
151
+ export function assertRenderOutputSizeOk(widthCss, heightCss, deviceScaleFactor, label) {
152
+ const outW = Math.ceil(widthCss * deviceScaleFactor);
153
+ const outH = Math.ceil(heightCss * deviceScaleFactor);
154
+ const estBytes = outW * outH * 4;
155
+ if (outW > MAX_OUTPUT_SIDE_PX || outH > MAX_OUTPUT_SIDE_PX || estBytes > MAX_OUTPUT_BYTES) {
156
+ const maxScaleForSide = MAX_OUTPUT_SIDE_PX / Math.max(widthCss, heightCss);
157
+ const maxScaleForBytes = Math.sqrt(MAX_OUTPUT_BYTES / 4 / (widthCss * heightCss));
158
+ const maxScale = Math.min(maxScaleForSide, maxScaleForBytes);
159
+ const maxDpi = Math.floor(maxScale * 96);
160
+ console.error(
161
+ `${label}: requested output ${outW}×${outH}px at ${deviceScaleFactor}× exceeds the ` +
162
+ `render guard (max side ${MAX_OUTPUT_SIDE_PX}px / max ~${Math.round(MAX_OUTPUT_BYTES / 1024 / 1024)}MB). ` +
163
+ `For this artboard size (${Math.round(widthCss)}×${Math.round(heightCss)}px), the max supported DPI is ~${maxDpi}.`
164
+ );
165
+ process.exit(1);
166
+ }
167
+ }
@@ -0,0 +1,90 @@
1
+ // _pw-launch.test.mjs — the pure, security-relevant helpers shared by the
2
+ // multi-artboard export shims (_png/_pdf-playwright.mjs). Regression coverage
3
+ // for the arbitrary-file-write finding fixed in the print-artboards dogfood
4
+ // pass (DDR-182 addendum): a `data-dc-screen` id is DDR-054 untrusted canvas
5
+ // content, and used to be trusted verbatim as a `path.join` segment.
6
+
7
+ import { describe, expect, test } from 'bun:test';
8
+ import { assertRenderOutputSizeOk, safeArtboardFilename } from './_pw-launch.mjs';
9
+
10
+ describe('safeArtboardFilename', () => {
11
+ test('passes a well-formed id through unchanged', () => {
12
+ expect(safeArtboardFilename('ak-alpha', 0, 'pdf')).toBe('ak-alpha.pdf');
13
+ expect(safeArtboardFilename('Beta_2', 3, 'png')).toBe('Beta_2.png');
14
+ });
15
+
16
+ test('falls back to the positional name for a path-traversal id', () => {
17
+ expect(safeArtboardFilename('../../../../tmp/pwned', 2, 'pdf')).toBe('artboard-3.pdf');
18
+ expect(safeArtboardFilename('/etc/passwd', 0, 'png')).toBe('artboard-1.png');
19
+ expect(safeArtboardFilename('..', 1, 'pdf')).toBe('artboard-2.pdf');
20
+ });
21
+
22
+ test('falls back for null/undefined/empty/non-string ids', () => {
23
+ expect(safeArtboardFilename(null, 0, 'pdf')).toBe('artboard-1.pdf');
24
+ expect(safeArtboardFilename(undefined, 4, 'png')).toBe('artboard-5.png');
25
+ expect(safeArtboardFilename('', 1, 'pdf')).toBe('artboard-2.pdf');
26
+ });
27
+
28
+ test('falls back for an id starting with a digit or containing unsafe chars', () => {
29
+ expect(safeArtboardFilename('1abc', 0, 'pdf')).toBe('artboard-1.pdf');
30
+ expect(safeArtboardFilename('a/b', 0, 'pdf')).toBe('artboard-1.pdf');
31
+ expect(safeArtboardFilename('a b', 0, 'pdf')).toBe('artboard-1.pdf');
32
+ });
33
+
34
+ test('falls back for an id over the 64-char cap', () => {
35
+ expect(safeArtboardFilename(`a${'x'.repeat(64)}`, 0, 'pdf')).toBe('artboard-1.pdf');
36
+ });
37
+ });
38
+
39
+ describe('assertRenderOutputSizeOk', () => {
40
+ test('does not exit for a reasonable A4-at-300dpi request', () => {
41
+ const origExit = process.exit;
42
+ let exited = false;
43
+ process.exit = () => {
44
+ exited = true;
45
+ };
46
+ try {
47
+ assertRenderOutputSizeOk(818, 1146, 3.125, '_test');
48
+ } finally {
49
+ process.exit = origExit;
50
+ }
51
+ expect(exited).toBe(false);
52
+ });
53
+
54
+ test('exits when the requested output exceeds the max side guard', () => {
55
+ const origExit = process.exit;
56
+ const origError = console.error;
57
+ let exitCode;
58
+ process.exit = (code) => {
59
+ exitCode = code;
60
+ throw new Error('exit'); // stop the (test-only) function from continuing past exit
61
+ };
62
+ console.error = () => {};
63
+ try {
64
+ expect(() => assertRenderOutputSizeOk(20000, 20000, 1, '_test')).toThrow();
65
+ } finally {
66
+ process.exit = origExit;
67
+ console.error = origError;
68
+ }
69
+ expect(exitCode).toBe(1);
70
+ });
71
+
72
+ test('exits when the requested output exceeds the byte-size guard even under the side cap', () => {
73
+ const origExit = process.exit;
74
+ const origError = console.error;
75
+ let exitCode;
76
+ process.exit = (code) => {
77
+ exitCode = code;
78
+ throw new Error('exit');
79
+ };
80
+ console.error = () => {};
81
+ try {
82
+ // 15000×15000 stays under the 16000px side cap but blows the ~600MB estimate.
83
+ expect(() => assertRenderOutputSizeOk(15000, 15000, 1, '_test')).toThrow();
84
+ } finally {
85
+ process.exit = origExit;
86
+ console.error = origError;
87
+ }
88
+ expect(exitCode).toBe(1);
89
+ });
90
+ });