@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,152 @@
1
+ #!/usr/bin/env bash
2
+ # canvas-rects.sh — the whiteboard AI toolkit's geometry manifest
3
+ # (feature-whiteboard-ai-toolkit). Emits `{ artboards, elements,
4
+ # elementsTruncated }` in WORLD coordinates for a canvas — the file
5
+ # `read-annotations --rects` and `annotate --in/--pin/--board` consume so
6
+ # neither verb ever hand-computes a coordinate. Reached via
7
+ # `maude design canvas-rects` (DDR-062), never a raw bin path.
8
+ #
9
+ # Usage:
10
+ # canvas-rects.sh <rel-path> [--root <repo>] [--port N] [--timeout 8]
11
+ # [--engine auto|agent-browser|playwright]
12
+ #
13
+ # Two lanes:
14
+ # - LIVE (preferred) — when a dev server is reachable (_server.json or
15
+ # --port), navigates the canvas-shell URL and reads
16
+ # `window.__maudeCanvasRects()` (canvas-lib.tsx) so element rects reflect
17
+ # actual CSS/content. Engine: agent-browser (default, matches
18
+ # screenshot.sh) or playwright (--engine playwright, or auto-fallback when
19
+ # agent-browser isn't on PATH).
20
+ # - STATIC fallback — no server reachable: artboard positions/sizes only
21
+ # (meta.json + JSX width/height, DDR-027), `elements: []`. No browser
22
+ # needed. A stderr note explains the degraded result.
23
+ #
24
+ # Unlike screenshot.sh this has no port-bounce retry (accepted v1 simplification
25
+ # — read-annotations/annotate don't have one either); a stale port just falls
26
+ # through to the static lane.
27
+ #
28
+ # Stdout: the manifest JSON (one line). Stderr: diagnostics/engine choice.
29
+ # Exit: 0 on success (either lane) / 2 on bad args.
30
+
31
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
32
+ ROOT=""
33
+ PORT=""
34
+ TIMEOUT=8
35
+ ENGINE="auto"
36
+ REL=""
37
+
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --root) ROOT="$2"; shift 2 ;;
41
+ --port) PORT="$2"; shift 2 ;;
42
+ --timeout) TIMEOUT="$2"; shift 2 ;;
43
+ --engine) ENGINE="$2"; shift 2 ;;
44
+ --help|-h) sed -n '2,24p' "$0" | sed 's/^# \?//'; exit 0 ;;
45
+ -*) echo "canvas-rects.sh: unknown arg '$1' (try --help)" >&2; exit 2 ;;
46
+ *) REL="$1"; shift ;;
47
+ esac
48
+ done
49
+
50
+ [ -n "$REL" ] || { echo "canvas-rects.sh: missing <rel-path>" >&2; exit 2; }
51
+
52
+ REPO="${ROOT:-${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}}"
53
+ STATE="$REPO/.design/_server.json"
54
+
55
+ emit_static() {
56
+ node "$SCRIPT_DIR/_canvas-rects-static.mjs" --rel "$REL" --root "$REPO"
57
+ }
58
+
59
+ # ---------- server detection ----------
60
+ if [ -z "$PORT" ] && [ -f "$STATE" ]; then
61
+ if command -v jq >/dev/null 2>&1; then
62
+ PORT=$(jq -r .port "$STATE" 2>/dev/null)
63
+ else
64
+ PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
65
+ fi
66
+ fi
67
+
68
+ if [ -z "$PORT" ] || ! curl -fsS -o /dev/null -m 2 "http://localhost:$PORT/_health" 2>/dev/null; then
69
+ echo "→ no live dev server reachable — static artboard-only fallback" >&2
70
+ emit_static
71
+ exit 0
72
+ fi
73
+
74
+ # ---------- designRel (for the canvas= query param) ----------
75
+ DESIGN_REL=".design"
76
+ CFG="$REPO/.design/config.json"
77
+ if [ -f "$CFG" ] && command -v jq >/dev/null 2>&1; then
78
+ CAND=$(jq -r '.designRoot // empty' "$CFG" 2>/dev/null)
79
+ [ -n "$CAND" ] && DESIGN_REL="$CAND"
80
+ fi
81
+
82
+ # Canvases mount through the canvas shell; the bare `/<rel>` route 404s when
83
+ # the canvas-origin sandbox is on (default since phase-9.1) — mirrors
84
+ # screenshot.sh's URL construction.
85
+ REL_ENC=$(printf '%s' "$DESIGN_REL/$REL" | sed 's/ /%20/g')
86
+ URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${REL_ENC}"
87
+
88
+ # ---------- engine resolution (mirrors screenshot.sh) ----------
89
+ AB="${MAUDE_AGENT_BROWSER:-agent-browser}"
90
+ if [ "$ENGINE" = "auto" ]; then
91
+ if command -v "$AB" >/dev/null 2>&1; then
92
+ ENGINE="agent-browser"
93
+ else
94
+ ENGINE="playwright"
95
+ fi
96
+ fi
97
+ echo "→ canvas-rects engine: $ENGINE | url: $URL" >&2
98
+
99
+ # ---------- agent-browser lane ----------
100
+ if [ "$ENGINE" = "agent-browser" ]; then
101
+ if ! command -v "$AB" >/dev/null 2>&1; then
102
+ echo "→ agent-browser not on PATH — static artboard-only fallback" >&2
103
+ emit_static
104
+ exit 0
105
+ fi
106
+ "$AB" open "$URL" >&2
107
+
108
+ # Poll for the hook itself (more precise than screenshot.sh's generic
109
+ # [data-dc-screen] mount check — this is exactly what we're about to call).
110
+ poll=0
111
+ ready=0
112
+ while [ $poll -lt "$TIMEOUT" ]; do
113
+ sleep 1
114
+ poll=$((poll + 1))
115
+ raw=$("$AB" eval "typeof window.__maudeCanvasRects" 2>/dev/null)
116
+ case "$raw" in
117
+ *function*) ready=1; break ;;
118
+ esac
119
+ done
120
+ if [ $ready -eq 0 ]; then
121
+ echo "→ hook never installed after ${TIMEOUT}s (non-canvas page?) — static artboard-only fallback" >&2
122
+ emit_static
123
+ exit 0
124
+ fi
125
+
126
+ RAW_JSON=$("$AB" eval "JSON.stringify(window.__maudeCanvasRects())" 2>/dev/null)
127
+ if [ -z "$RAW_JSON" ]; then
128
+ echo "→ eval returned nothing — static artboard-only fallback" >&2
129
+ emit_static
130
+ exit 0
131
+ fi
132
+ if command -v jq >/dev/null 2>&1; then
133
+ printf '%s\n' "$RAW_JSON" | jq -r .
134
+ else
135
+ # No jq — agent-browser's output is a JSON-encoded STRING (quoted,
136
+ # escaped); node unwraps it the same way `jq -r .` would.
137
+ printf '%s' "$RAW_JSON" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>process.stdout.write(JSON.parse(s)+'\n'))"
138
+ fi
139
+ exit 0
140
+ fi
141
+
142
+ # ---------- playwright lane ----------
143
+ if [ "$ENGINE" = "playwright" ]; then
144
+ if ! node "$SCRIPT_DIR/_canvas-rects-playwright.mjs" --url "$URL" --timeout "$TIMEOUT"; then
145
+ echo "→ playwright capture failed — static artboard-only fallback" >&2
146
+ emit_static
147
+ fi
148
+ exit 0
149
+ fi
150
+
151
+ echo "canvas-rects.sh: unknown --engine '$ENGINE'" >&2
152
+ exit 2
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # fetch-asset.sh — hardened download-first image fetch. Thin shim over
3
+ # _fetch-asset.mjs; reached via `maude design fetch-asset` (DDR-062), never a
4
+ # raw bin path. See _fetch-asset.mjs for the full security rationale
5
+ # (DDR-147 § Security follow-up item 1).
6
+ #
7
+ # Usage:
8
+ # fetch-asset.sh <https-url> --root <repo> [--design-root .design]
9
+ # [--max-bytes N] [--max-time S] [--json]
10
+ #
11
+ # stdout on success = the canvas reference path (e.g. /assets/a44d3d60.png).
12
+ # Exit: 0 ok · 2 usage · 3 SSRF/validation reject · 4 download/http error ·
13
+ # 5 unsupported media type · 6 write/containment error · 1 other.
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+
16
+ case "$1" in
17
+ --help|-h) sed -n '2,17p' "$0" | sed 's/^# \?//'; exit 0 ;;
18
+ esac
19
+
20
+ if ! command -v curl >/dev/null 2>&1; then
21
+ echo "fetch-asset.sh: curl is required (hardened download uses fixed curl args)." >&2
22
+ exit 1
23
+ fi
24
+
25
+ # Prefer node (always present with a maude install); fall back to bun in a dev
26
+ # tree that has bun but a shimmed node. The module is pure Node ESM — no .ts.
27
+ if command -v node >/dev/null 2>&1; then
28
+ exec node "$SCRIPT_DIR/_fetch-asset.mjs" "$@"
29
+ elif command -v bun >/dev/null 2>&1; then
30
+ exec bun run "$SCRIPT_DIR/_fetch-asset.mjs" "$@"
31
+ else
32
+ echo "fetch-asset.sh: node (or bun) is required." >&2
33
+ exit 1
34
+ fi
@@ -77,6 +77,7 @@ DEFAULT_DS="project"
77
77
  KNOWN_DS=""
78
78
  ACCENT_STRATEGY="single"
79
79
  COLOR_SPACE="oklch"
80
+ MOODBOARD_VARIANTS="3"
80
81
 
81
82
  if [ "$CONFIG_PRESENT" = "true" ] && [ "$HAVE_JQ" = "1" ]; then
82
83
  NAME=$(jq -r '.name // "Project"' "$CFG")
@@ -91,6 +92,9 @@ if [ "$CONFIG_PRESENT" = "true" ] && [ "$HAVE_JQ" = "1" ]; then
91
92
  KNOWN_DS=$(jq -r '.designSystems[]?.name // empty' "$CFG" 2>/dev/null | tr '\n' ' ' | sed 's/ $//')
92
93
  ACCENT_STRATEGY=$(jq -r '.accentStrategy // "single"' "$CFG")
93
94
  COLOR_SPACE=$(jq -r '.colorSpace // "oklch"' "$CFG")
95
+ # Type-coerce + clamp in jq: config.json is untrusted input (a poisoned repo
96
+ # could put a string here) and --shell-export output gets eval'd by callers.
97
+ MOODBOARD_VARIANTS=$(jq -r '.moodboard.variants // 3 | if type=="number" then floor else 3 end | if . < 1 then 1 elif . > 3 then 3 else . end' "$CFG")
94
98
  fi
95
99
 
96
100
  DR_ABS="$REPO/$DESIGN_ROOT"
@@ -159,6 +163,7 @@ if [ "$MODE" = "shell" ]; then
159
163
  printf 'export KNOWN_DS=%q\n' "$KNOWN_DS"
160
164
  printf 'export ACCENT_STRATEGY=%q\n' "$ACCENT_STRATEGY"
161
165
  printf 'export COLOR_SPACE=%q\n' "$COLOR_SPACE"
166
+ printf 'export MOODBOARD_VARIANTS=%q\n' "$MOODBOARD_VARIANTS"
162
167
  printf 'export DEPS_OK=%s\n' "$DEPS_OK"
163
168
  printf 'export DEPS_MISSING=%q\n' "$DEPS_MISSING"
164
169
  printf 'export SERVER_UP=%s\n' "$SERVER_UP"
@@ -181,6 +186,7 @@ if [ "$HAVE_JQ" = "1" ]; then
181
186
  --arg tok "$TOKENS_REL" --arg ncd "$NEW_CANVAS_DIR" --arg ncm "$NEW_COMPONENT_DIR" \
182
187
  --arg acc "$TEAM_ACCENT" --arg dds "$DEFAULT_DS" --arg kds "$KNOWN_DS" \
183
188
  --arg astrat "$ACCENT_STRATEGY" --arg cspace "$COLOR_SPACE" \
189
+ --arg mbvar "$MOODBOARD_VARIANTS" \
184
190
  --arg depsok "$DEPS_OK" --arg depsmiss "$DEPS_MISSING" \
185
191
  --arg sup "$SERVER_UP" --arg sport "$SERVER_PORT" \
186
192
  --arg act "$ACTIVE_CANVAS" --arg selfile "$SELECTED_FILE" \
@@ -194,7 +200,8 @@ if [ "$HAVE_JQ" = "1" ]; then
194
200
  tokens_rel: $tok, new_canvas_dir: $ncd, new_component_dir: $ncm,
195
201
  team_accent: $acc, default_ds: $dds,
196
202
  known_ds: ($kds | if . == "" then [] else split(" ") end),
197
- accent_strategy: $astrat, color_space: $cspace
203
+ accent_strategy: $astrat, color_space: $cspace,
204
+ moodboard_variants: ($mbvar | tonumber? // 3)
198
205
  },
199
206
  active: {
200
207
  canvas: $act, selected: $sel, selected_file: $selfile,
@@ -37,7 +37,14 @@ import { pathToFileURL } from 'node:url';
37
37
  // Argv
38
38
 
39
39
  function parseArgv(argv) {
40
- const out = { positional: [], root: null, canvasState: null, help: false, graph: false };
40
+ const out = {
41
+ positional: [],
42
+ root: null,
43
+ canvasState: null,
44
+ rects: null,
45
+ help: false,
46
+ graph: false,
47
+ };
41
48
  for (let i = 0; i < argv.length; i += 1) {
42
49
  const a = argv[i];
43
50
  if (a === '--help' || a === '-h') {
@@ -56,6 +63,11 @@ function parseArgv(argv) {
56
63
  out.canvasState = argv[i];
57
64
  } else if (a.startsWith('--canvas-state=')) {
58
65
  out.canvasState = a.slice('--canvas-state='.length);
66
+ } else if (a === '--rects') {
67
+ i += 1;
68
+ out.rects = argv[i];
69
+ } else if (a.startsWith('--rects=')) {
70
+ out.rects = a.slice('--rects='.length);
59
71
  } else {
60
72
  out.positional.push(a);
61
73
  }
@@ -67,6 +79,7 @@ const HELP = `read-annotations.mjs — headless SVG → JSON annotation reader (
67
79
 
68
80
  Usage:
69
81
  maude design read-annotations <rel-path> [--root <repo>] [--canvas-state <path>]
82
+ [--rects <path>]
70
83
 
71
84
  Args:
72
85
  <rel-path> Canvas path relative to the design root (e.g. "ui/Foo.tsx").
@@ -75,6 +88,16 @@ Args:
75
88
  each annotation is tagged with the artboard it overlaps,
76
89
  plus artboard-relative coords (rel) and a W3C-style
77
90
  target block { source, selector, geometry }.
91
+ --rects <p> A \`maude design canvas-rects\` geometry manifest
92
+ ({ artboards, elements }) — feature-whiteboard-ai-toolkit.
93
+ Adds ELEMENT-level context: an annotation whose center
94
+ falls inside an element's world rect gets
95
+ element: { cdId, selector, index, artboard, rect, tag,
96
+ text } (or element: null). Also supplies artboard
97
+ tagging (as --canvas-state does) when --canvas-state is
98
+ not separately given. When an element resolves, the W3C
99
+ target.selector upgrades from AnnotationIdSelector to
100
+ { type: "CssSelector", value: <element.selector> }.
78
101
  --graph Emit { annotations, graph } instead of the bare array:
79
102
  graph.edges = bound arrows (from/to host ids), graph.nodes
80
103
  = the strokes those arrows connect (with labels) — a
@@ -495,23 +518,103 @@ function tagArtboard(ann, artboards) {
495
518
  * FigJam v3 — anchor an annotation to its artboard for AI consumers: the
496
519
  * overlapping artboard id, artboard-RELATIVE coords (what survives an artboard
497
520
  * move), and a W3C Web-Annotation-style target (anchor by stable id first,
498
- * geometry as the refinement/fallback).
521
+ * geometry as the refinement/fallback). feature-whiteboard-ai-toolkit — when
522
+ * `anchorToElement` (below) already resolved `ann.element`, the target
523
+ * selector upgrades to a CssSelector naming that element instead of the bare
524
+ * AnnotationIdSelector.
499
525
  */
500
526
  function anchorToArtboard(ann, artboards) {
501
527
  const r = findArtboard(ann, artboards);
502
528
  if (!r) return { ...ann, artboard: null };
529
+ const selector = ann.element
530
+ ? { type: 'CssSelector', value: ann.element.selector }
531
+ : { type: 'AnnotationIdSelector', value: ann.id };
503
532
  return {
504
533
  ...ann,
505
534
  artboard: r.id,
506
535
  rel: ann.x != null ? { x: ann.x - r.x, y: ann.y - r.y } : null,
507
536
  target: {
508
537
  source: r.id,
509
- selector: { type: 'AnnotationIdSelector', value: ann.id },
538
+ selector,
510
539
  geometry: ann.x != null ? { x: ann.x, y: ann.y, w: ann.w ?? 0, h: ann.h ?? 0 } : null,
511
540
  },
512
541
  };
513
542
  }
514
543
 
544
+ // ─────────────────────────────────────────────────────────────────────────────
545
+ // --rects element-level context (feature-whiteboard-ai-toolkit). Reads a
546
+ // `maude design canvas-rects` geometry manifest ({ artboards, elements }) —
547
+ // `loadArtboards` above already understands `raw.artboards`, so only the
548
+ // elements side needs a dedicated loader.
549
+
550
+ function loadElements(p) {
551
+ try {
552
+ const raw = JSON.parse(readFileSync(p, 'utf8'));
553
+ const arr = Array.isArray(raw?.elements) ? raw.elements : [];
554
+ return arr.filter(
555
+ (e) =>
556
+ e &&
557
+ typeof e.selector === 'string' &&
558
+ e.selector &&
559
+ [e.x, e.y, e.w, e.h].every((n) => Number.isFinite(n))
560
+ );
561
+ } catch {
562
+ return [];
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Deepest-element approximation over a FLAT rect list (no live DOM to walk,
568
+ * unlike `resolveHoverTarget` in the browser): the smallest-area element whose
569
+ * world rect contains the annotation's CENTER point. Smallest-containing-rect
570
+ * is the standard proxy for "innermost" when only bounding boxes are known —
571
+ * a wrapping card and the button inside it both contain the point, but the
572
+ * button's rect is smaller.
573
+ */
574
+ function findElement(ann, elements) {
575
+ if (ann.x == null || ann.y == null) return null;
576
+ const cx = ann.x + (ann.w || 0) / 2;
577
+ const cy = ann.y + (ann.h || 0) / 2;
578
+ let best = null;
579
+ let bestArea = Number.POSITIVE_INFINITY;
580
+ for (const el of elements) {
581
+ if (cx < el.x || cx > el.x + el.w || cy < el.y || cy > el.y + el.h) continue;
582
+ const area = Math.max(0, el.w) * Math.max(0, el.h);
583
+ if (area < bestArea) {
584
+ bestArea = area;
585
+ best = el;
586
+ }
587
+ }
588
+ return best;
589
+ }
590
+
591
+ /**
592
+ * Look up a manifest element BY IDENTITY (its `cdId` or its full `selector`
593
+ * string) rather than by point — what `annotate --pin <cdId|selector>` needs
594
+ * to resolve a placement target. First match in manifest order.
595
+ */
596
+ function findElementById(elements, query) {
597
+ if (!query) return null;
598
+ return elements.find((el) => el.cdId === query || el.selector === query) ?? null;
599
+ }
600
+
601
+ function anchorToElement(ann, elements) {
602
+ const el = findElement(ann, elements);
603
+ if (!el) return { ...ann, element: null };
604
+ return {
605
+ ...ann,
606
+ element: {
607
+ cdId: el.cdId ?? null,
608
+ selector: el.selector,
609
+ index: el.index ?? 0,
610
+ artboard: el.artboard ?? null,
611
+ rect: { x: el.x, y: el.y, w: el.w, h: el.h },
612
+ tag: el.tag ?? '',
613
+ text: el.text ?? '',
614
+ },
615
+ };
616
+ }
617
+
515
618
  // ─────────────────────────────────────────────────────────────────────────────
516
619
  // Main
517
620
 
@@ -548,14 +651,28 @@ function main() {
548
651
 
549
652
  let annotations = parseAnnotations(svg);
550
653
 
654
+ // feature-whiteboard-ai-toolkit — --rects supplies element-level context
655
+ // (and, absent a separate --canvas-state, artboards too: loadArtboards
656
+ // already understands a canvas-rects manifest's `{ artboards, elements }`
657
+ // shape). Elements are resolved BEFORE artboards so anchorToArtboard can see
658
+ // the already-attached `element` and upgrade its W3C target.selector.
659
+ let artboards = [];
551
660
  if (args.canvasState) {
552
661
  const csPath = isAbsolute(args.canvasState)
553
662
  ? args.canvasState
554
663
  : resolve(process.cwd(), args.canvasState);
555
- const artboards = loadArtboards(csPath);
556
- if (artboards.length) {
557
- annotations = annotations.map((a) => anchorToArtboard(a, artboards));
664
+ artboards = loadArtboards(csPath);
665
+ }
666
+ if (args.rects) {
667
+ const rectsPath = isAbsolute(args.rects) ? args.rects : resolve(process.cwd(), args.rects);
668
+ const elements = loadElements(rectsPath);
669
+ if (elements.length) {
670
+ annotations = annotations.map((a) => anchorToElement(a, elements));
558
671
  }
672
+ if (!artboards.length) artboards = loadArtboards(rectsPath);
673
+ }
674
+ if (artboards.length) {
675
+ annotations = annotations.map((a) => anchorToArtboard(a, artboards));
559
676
  }
560
677
 
561
678
  if (args.graph) {
@@ -567,7 +684,21 @@ function main() {
567
684
 
568
685
  // FigJam v3 — the parsing core is importable (the `annotate` write verb reuses
569
686
  // it for host-geometry lookups); main() runs only when invoked as a script.
570
- export { buildGraph, fileSlug, loadArtboards, parseAnnotations, resolveDesignRoot, tagArtboard };
687
+ // feature-whiteboard-ai-toolkit findElement/loadElements are exported too so
688
+ // `annotate --pin` (Task 4) can resolve an element's rect from the same
689
+ // canvas-rects manifest without a second implementation.
690
+ export {
691
+ anchorToElement,
692
+ buildGraph,
693
+ fileSlug,
694
+ findElement,
695
+ findElementById,
696
+ loadArtboards,
697
+ loadElements,
698
+ parseAnnotations,
699
+ resolveDesignRoot,
700
+ tagArtboard,
701
+ };
571
702
 
572
703
  const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
573
704
  if (isMain) main();
@@ -40,7 +40,11 @@
40
40
  # Stdout: one line per canvas, tab-separated:
41
41
  # STATUS \t FILE \t SCREENSHOT \t DETAIL
42
42
  # Stderr: diagnostic / progress.
43
- # Exit: 0 = all green / 3 = at least one blank/error/unstyled/lint-fail / 1 = missing deps / 2 = bad args.
43
+ # Exit: 0 = all green / 3 = at least one blank/error/unstyled/broken-img/lint-fail / 1 = missing deps / 2 = bad args.
44
+ #
45
+ # BROKEN-IMG (agent-browser engine only) flags a VISIBLE <img> that finished
46
+ # loading with naturalWidth 0 — a CSP-blocked hotlink or a 404'd local asset the
47
+ # user sees as an empty thumbnail. onError-hidden scrap fallbacks are excluded.
44
48
 
45
49
  REPO=""
46
50
  OUT_DIR=""
@@ -356,6 +360,37 @@ probe_agent_browser() {
356
360
  return
357
361
  fi
358
362
 
363
+ # Broken-image gate (DDR-147 / 2026-07-06 RCA) — the moodboard + DS specimens
364
+ # fill photo slots with web imagery; a hotlink blocked by the canvas CSP
365
+ # (`img-src 'self'`, default-on split origin) or a 404'd local path renders as
366
+ # an empty "broken image" thumbnail the user sees but every check above sails
367
+ # past (it has text, no error, a >2 KB screenshot). Flag any <img> that
368
+ # FINISHED loading with naturalWidth 0 AND is actually VISIBLE — a slot whose
369
+ # `onError` hid the img and swapped in a scrap (display:none / hidden / opacity
370
+ # 0) is a graceful degrade, NOT a failure, so it's excluded. Catches the class
371
+ # of regression where download-first silently stopped working.
372
+ local broken
373
+ broken=$(agent-browser eval "(() => {
374
+ const bad = [...document.querySelectorAll('img')].filter((im) => {
375
+ if (!im.complete || im.naturalWidth > 0) return false;
376
+ const src = (im.getAttribute('src') || '').trim();
377
+ if (!src || src.startsWith('data:')) return false;
378
+ const cs = getComputedStyle(im);
379
+ if (cs.display === 'none' || cs.visibility === 'hidden' || cs.opacity === '0') return false;
380
+ return true;
381
+ });
382
+ if (!bad.length) return '';
383
+ const sample = bad.slice(0, 2).map((im) => (im.currentSrc || im.getAttribute('src') || '').slice(0, 60)).join(', ');
384
+ return bad.length + ' broken img(s): ' + sample;
385
+ })()" 2>/dev/null)
386
+ broken=$(printf '%s' "$broken" | sed 's/^\"//; s/\"$//')
387
+ if [ -n "$broken" ] && [ "$broken" != "null" ] && [ "$broken" != "undefined" ]; then
388
+ echo "BROKEN-IMG"
389
+ echo "$broken"
390
+ echo "$out_png"
391
+ return
392
+ fi
393
+
359
394
  # Computed-style gate (DDR-068) — "mounts with content" ≠ "rendered styled". A
360
395
  # specimen whose import graph dropped the token CSS passes every check above
361
396
  # (it has text, no error, a >2 KB screenshot) yet every var()-driven rule is
@@ -456,11 +491,12 @@ while IFS= read -r CANVAS; do
456
491
  SHOT=$(printf '%s\n' "$RESULT" | sed -n '3p')
457
492
 
458
493
  case "$STATUS" in
459
- OK) SYM="✓"; echo "$SYM" >&2 ;;
460
- BLANK) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM blank ($DETAIL)" >&2 ;;
461
- ERROR) SYM="⚠"; FAILED=$((FAILED + 1)); echo "$SYM error ($DETAIL)" >&2 ;;
462
- UNSTYLED) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM unstyled ($DETAIL)" >&2 ;;
463
- *) SYM="?"; FAILED=$((FAILED + 1)); echo "? unknown ($STATUS)" >&2 ;;
494
+ OK) SYM="✓"; echo "$SYM" >&2 ;;
495
+ BLANK) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM blank ($DETAIL)" >&2 ;;
496
+ ERROR) SYM="⚠"; FAILED=$((FAILED + 1)); echo "$SYM error ($DETAIL)" >&2 ;;
497
+ UNSTYLED) SYM="✗"; FAILED=$((FAILED + 1)); echo "$SYM unstyled ($DETAIL)" >&2 ;;
498
+ BROKEN-IMG) SYM=""; FAILED=$((FAILED + 1)); echo "$SYM broken-img ($DETAIL)" >&2 ;;
499
+ *) SYM="?"; FAILED=$((FAILED + 1)); echo "? unknown ($STATUS)" >&2 ;;
464
500
  esac
465
501
 
466
502
  printf '%s\t%s\t%s\t%s\n' "$STATUS" "$REL" "$SHOT" "$DETAIL" >> "$TSV"
@@ -197,6 +197,19 @@ async function buildCss(): Promise<{ outBytes: number; outPath: string }> {
197
197
  // Pre-building all 6 sub-bundles at release time + shipping in dist/runtime/
198
198
  // eliminates the runtime dependency on disk node_modules entirely.
199
199
 
200
+ // Dev builds skip the runtime-bundle regen whenever every bundle is already
201
+ // on disk: the committed dist/runtime/*.js are authoritative for releases
202
+ // (CI builds with MAUDE_SKIP_RUNTIME_BUILD=1), and a dev-mode regen writes
203
+ // UNMINIFIED output that dirties the tree at ~2× the shipped size. Runtime
204
+ // bundles depend only on dependency versions, never on client/server source,
205
+ // so after a dep bump regen deliberately:
206
+ // MAUDE_FORCE_RUNTIME_BUILD=1 bun run build.ts --release
207
+ async function runtimeBundlesComplete(): Promise<boolean> {
208
+ const { RUNTIME_PACKAGES, slugFor } = await import('./runtime-bundle.ts');
209
+ const outDir = join(DIST, 'runtime');
210
+ return RUNTIME_PACKAGES.every((pkg) => existsSync(join(outDir, `${slugFor(pkg)}.js`)));
211
+ }
212
+
200
213
  async function buildRuntimeBundles(): Promise<{ outDir: string; count: number; bytes: number }> {
201
214
  // Defer import so build.ts can run in --dry-run without pulling react.
202
215
  const { RUNTIME_PACKAGES, getRuntimeBundle, slugFor } = await import('./runtime-bundle.ts');
@@ -478,6 +491,14 @@ async function main() {
478
491
  console.log(
479
492
  '[build] dist/runtime/*.js SKIPPED (MAUDE_SKIP_RUNTIME_BUILD=1 — using committed pre-built)'
480
493
  );
494
+ } else if (
495
+ MODE === 'dev' &&
496
+ process.env.MAUDE_FORCE_RUNTIME_BUILD !== '1' &&
497
+ (await runtimeBundlesComplete())
498
+ ) {
499
+ console.log(
500
+ '[build] dist/runtime/*.js SKIPPED (all present; committed bundles are authoritative — MAUDE_FORCE_RUNTIME_BUILD=1 to regen after a dep bump)'
501
+ );
481
502
  } else {
482
503
  const runtime = await buildRuntimeBundles();
483
504
  const t2b = performance.now();