@1agh/maude 0.22.2 → 0.24.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 (125) hide show
  1. package/README.md +34 -1
  2. package/cli/bin/maude.mjs +3 -0
  3. package/cli/commands/cache.mjs +181 -0
  4. package/cli/commands/cache.test.mjs +166 -0
  5. package/cli/commands/design-link.test.mjs +84 -2
  6. package/cli/commands/design.mjs +95 -4
  7. package/cli/commands/design.test.mjs +56 -0
  8. package/cli/commands/help.mjs +34 -0
  9. package/cli/commands/hub.mjs +255 -30
  10. package/cli/commands/hub.test.mjs +135 -10
  11. package/cli/commands/init.mjs +3 -0
  12. package/cli/commands/preflight.mjs +11 -0
  13. package/cli/commands/preflight.test.mjs +46 -0
  14. package/cli/commands/scenario-report.mjs +45 -0
  15. package/cli/lib/cache.mjs +407 -0
  16. package/cli/lib/cache.test.mjs +303 -0
  17. package/cli/lib/design-link.mjs +222 -11
  18. package/cli/lib/flow-design-integration.test.mjs +165 -0
  19. package/cli/lib/gitignore-block.mjs +90 -0
  20. package/cli/lib/gitignore-block.test.mjs +123 -0
  21. package/cli/lib/hubs-config.mjs +42 -4
  22. package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
  23. package/cli/lib/preflight.mjs +41 -10
  24. package/package.json +8 -8
  25. package/plugins/design/dev-server/ai-banner.tsx +2 -2
  26. package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
  27. package/plugins/design/dev-server/annotations-layer.tsx +906 -137
  28. package/plugins/design/dev-server/api.ts +109 -4
  29. package/plugins/design/dev-server/bin/preflight.sh +21 -10
  30. package/plugins/design/dev-server/bin/prep.sh +211 -0
  31. package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
  32. package/plugins/design/dev-server/bin/screenshot.sh +18 -1
  33. package/plugins/design/dev-server/bin/smoke.sh +94 -11
  34. package/plugins/design/dev-server/build.ts +69 -3
  35. package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
  36. package/plugins/design/dev-server/canvas-cursors.ts +125 -0
  37. package/plugins/design/dev-server/canvas-icons.tsx +130 -0
  38. package/plugins/design/dev-server/canvas-lib.tsx +47 -27
  39. package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
  40. package/plugins/design/dev-server/canvas-shell.tsx +358 -245
  41. package/plugins/design/dev-server/client/app.jsx +191 -21
  42. package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
  43. package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
  44. package/plugins/design/dev-server/collab/index.ts +87 -9
  45. package/plugins/design/dev-server/collab/persistence.ts +34 -3
  46. package/plugins/design/dev-server/collab/registry.ts +131 -2
  47. package/plugins/design/dev-server/collab/room.ts +21 -8
  48. package/plugins/design/dev-server/config.schema.json +20 -0
  49. package/plugins/design/dev-server/context-menu.tsx +167 -23
  50. package/plugins/design/dev-server/context.ts +31 -0
  51. package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
  52. package/plugins/design/dev-server/dist/client.bundle.js +144 -22
  53. package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
  54. package/plugins/design/dev-server/dist/styles.css +16 -0
  55. package/plugins/design/dev-server/dom-selection.ts +156 -0
  56. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  57. package/plugins/design/dev-server/export-dialog.tsx +19 -17
  58. package/plugins/design/dev-server/fs-watch.ts +1 -0
  59. package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
  60. package/plugins/design/dev-server/http.ts +260 -20
  61. package/plugins/design/dev-server/input-router.tsx +125 -64
  62. package/plugins/design/dev-server/participants-chrome.tsx +10 -10
  63. package/plugins/design/dev-server/server.ts +141 -1
  64. package/plugins/design/dev-server/sync/agent.ts +418 -0
  65. package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
  66. package/plugins/design/dev-server/sync/codec.ts +324 -0
  67. package/plugins/design/dev-server/sync/connection-state.ts +203 -0
  68. package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
  69. package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
  70. package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
  71. package/plugins/design/dev-server/sync/index.ts +918 -0
  72. package/plugins/design/dev-server/sync/materialize.ts +62 -0
  73. package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
  74. package/plugins/design/dev-server/sync/origins.ts +57 -0
  75. package/plugins/design/dev-server/sync/projection.ts +368 -0
  76. package/plugins/design/dev-server/sync/status.ts +115 -0
  77. package/plugins/design/dev-server/sync/untrusted.ts +153 -0
  78. package/plugins/design/dev-server/test/_helpers.ts +6 -2
  79. package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
  80. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
  81. package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
  82. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
  83. package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
  84. package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
  85. package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
  86. package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
  87. package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
  88. package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
  89. package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +21 -0
  91. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
  92. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
  93. package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
  94. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
  95. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
  96. package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
  97. package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
  98. package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
  99. package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
  100. package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
  101. package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
  102. package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
  103. package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
  104. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
  105. package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
  106. package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
  107. package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
  108. package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
  109. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
  110. package/plugins/design/dev-server/tool-palette.tsx +18 -16
  111. package/plugins/design/dev-server/undo-hud.tsx +4 -4
  112. package/plugins/design/dev-server/undo-stack.ts +20 -4
  113. package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
  114. package/plugins/design/dev-server/use-collab.tsx +157 -13
  115. package/plugins/design/dev-server/use-selection-set.tsx +12 -0
  116. package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
  117. package/plugins/design/dev-server/ws.ts +40 -1
  118. package/plugins/design/templates/_shell.html +63 -5
  119. package/plugins/design/templates/canvas.tsx.template +13 -0
  120. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
  121. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  122. package/plugins/flow/templates/ai-skeleton/README.md +1 -1
  123. package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
  124. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
  125. package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
@@ -189,6 +189,18 @@ export function SelectionSetProvider({
189
189
  return <SelectionSetContext.Provider value={value}>{children}</SelectionSetContext.Provider>;
190
190
  }
191
191
 
192
+ /**
193
+ * Mount a `SelectionSetProvider` only when none exists above us. The shell-
194
+ * owned comment mount layer provides one so both the lite comment router and
195
+ * `CanvasShell` share a single selection set. Hook called unconditionally;
196
+ * only the returned tree branches (hook rules).
197
+ */
198
+ export function MaybeSelectionSetProvider({ children }: { children: ReactNode }) {
199
+ const outer = useContext(SelectionSetContext);
200
+ if (outer) return <>{children}</>;
201
+ return <SelectionSetProvider>{children}</SelectionSetProvider>;
202
+ }
203
+
192
204
  // ─────────────────────────────────────────────────────────────────────────────
193
205
  // Hooks
194
206
 
@@ -21,6 +21,7 @@ import {
21
21
  useState,
22
22
  } from 'react';
23
23
 
24
+ import { TOOL_CURSORS } from './canvas-cursors.ts';
24
25
  import type { Tool } from './input-router.tsx';
25
26
 
26
27
  // ─────────────────────────────────────────────────────────────────────────────
@@ -35,19 +36,21 @@ export interface ToolDescriptor {
35
36
  cursor: string;
36
37
  }
37
38
 
39
+ // Phase 21 — every tool ships a custom 32×32 SVG cursor (canvas-cursors.ts)
40
+ // with a white outline halo so the glyph reads on any background. The native
41
+ // crosshair/text/cell were thin + tiny ("pen almost invisible"); these mirror
42
+ // the tool-palette icons. `move` keeps the system arrow on purpose.
38
43
  export const DEFAULT_TOOLS: readonly ToolDescriptor[] = Object.freeze([
39
- { id: 'move', label: 'Move', shortcut: 'V', cursor: 'default' },
40
- { id: 'hand', label: 'Hand', shortcut: 'H', cursor: 'grab' },
41
- { id: 'comment', label: 'Comment', shortcut: 'C', cursor: 'crosshair' },
42
- // Phase 5 draw / annotation tools. Cursors stay as `crosshair` for pen /
43
- // rect / arrow (the pen-tip glyph is reserved for the system text caret).
44
- // Eraser uses `cell` as the closest cross-browser substitute for a rubber
45
- // affordance (no native rubber cursor exists).
46
- { id: 'pen', label: 'Pen', shortcut: 'B', cursor: 'crosshair' },
47
- { id: 'rect', label: 'Rect', shortcut: 'R', cursor: 'crosshair' },
48
- { id: 'ellipse', label: 'Ellipse', shortcut: 'O', cursor: 'crosshair' },
49
- { id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: 'crosshair' },
50
- { id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: 'cell' },
44
+ { id: 'move', label: 'Move', shortcut: 'V', cursor: TOOL_CURSORS.move },
45
+ { id: 'hand', label: 'Hand', shortcut: 'H', cursor: TOOL_CURSORS.hand },
46
+ { id: 'comment', label: 'Comment', shortcut: 'C', cursor: TOOL_CURSORS.comment },
47
+ { id: 'pen', label: 'Pen', shortcut: 'B', cursor: TOOL_CURSORS.pen },
48
+ { id: 'rect', label: 'Rect', shortcut: 'R', cursor: TOOL_CURSORS.rect },
49
+ { id: 'ellipse', label: 'Ellipse', shortcut: 'O', cursor: TOOL_CURSORS.ellipse },
50
+ { id: 'sticky', label: 'Sticky', shortcut: 'N', cursor: TOOL_CURSORS.sticky },
51
+ { id: 'arrow', label: 'Arrow', shortcut: 'A', cursor: TOOL_CURSORS.arrow },
52
+ { id: 'text', label: 'Text', shortcut: 'T', cursor: TOOL_CURSORS.text },
53
+ { id: 'eraser', label: 'Eraser', shortcut: 'E', cursor: TOOL_CURSORS.eraser },
51
54
  ]);
52
55
 
53
56
  interface ToolContextValue {
@@ -122,6 +125,18 @@ export function ToolProvider({
122
125
  return <ToolContext.Provider value={value}>{children}</ToolContext.Provider>;
123
126
  }
124
127
 
128
+ /**
129
+ * Mount a `ToolProvider` only when none exists above us. When the shell-owned
130
+ * comment mount layer (canvas-comment-mount.tsx) already provides one,
131
+ * `DesignCanvas` consumes that instance instead of double-mounting. The hook
132
+ * is called unconditionally; only the returned tree branches (hook rules).
133
+ */
134
+ export function MaybeToolProvider({ children }: { children: ReactNode }) {
135
+ const outer = useContext(ToolContext);
136
+ if (outer) return <>{children}</>;
137
+ return <ToolProvider>{children}</ToolProvider>;
138
+ }
139
+
125
140
  // ─────────────────────────────────────────────────────────────────────────────
126
141
  // Hook
127
142
 
@@ -25,6 +25,16 @@ export type WsData =
25
25
  remote: string;
26
26
  kind: 'collab';
27
27
  slug: string;
28
+ }
29
+ | {
30
+ // T2 (9.1-A) — HMR-only socket for the segregated canvas origin. Receives
31
+ // ONLY `canvas-hmr` broadcasts; never the privileged inspector feed
32
+ // (comments / ai-activity / git-lifecycle / sync:status / selection) and
33
+ // ignores all inbound messages. Hub-pushed canvas code on the canvas
34
+ // origin can open this, but it leaks nothing and mutates nothing.
35
+ id: string;
36
+ remote: string;
37
+ kind: 'canvas-hmr';
28
38
  };
29
39
 
30
40
  /**
@@ -77,6 +87,8 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
77
87
  }
78
88
  }
79
89
 
90
+ // Privileged inspector feed — comments, selection, ai-activity, git-lifecycle,
91
+ // sync:status, fs:*. ONLY the same-origin inspector clients (the shell) get it.
80
92
  function broadcast(payload: unknown) {
81
93
  const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
82
94
  for (const ws of clients) {
@@ -85,6 +97,17 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
85
97
  }
86
98
  }
87
99
 
100
+ // HMR feed — `canvas-hmr` reload signals. Safe to deliver to the segregated
101
+ // canvas origin, so both inspector (shell) AND canvas-hmr (canvas iframe)
102
+ // sockets receive it. T2 (9.1-A).
103
+ function broadcastHmr(payload: unknown) {
104
+ const msg = typeof payload === 'string' ? payload : JSON.stringify(payload);
105
+ for (const ws of clients) {
106
+ if (ws.data.kind !== 'inspector' && ws.data.kind !== 'canvas-hmr') continue;
107
+ send(ws, msg);
108
+ }
109
+ }
110
+
88
111
  // Wire bus -> WS broadcasts. inspect.ts emits 'selected' / 'active' after every
89
112
  // state write; fs-watch.ts emits 'fs:*' on every save.
90
113
  ctx.bus.on('selected', (sel) => broadcast({ type: 'selected', selected: sel }));
@@ -106,9 +129,17 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
106
129
  // canvas iframes both subscribe.
107
130
  ctx.bus.on('git-lifecycle', (payload: unknown) => broadcast({ type: 'git-lifecycle', payload }));
108
131
 
132
+ // Phase 9 Task 8 — hub-down offline mode. The linked-mode sync runtime emits
133
+ // 'sync:status' on every connection-state change (online / connecting /
134
+ // offline / offline-long, queued-op count, conflict notices). Browser tabs
135
+ // render the offline/synced/escalation banner from this. Solo mode never
136
+ // emits, so this is a no-op for unlinked projects.
137
+ ctx.bus.on('sync:status', (payload: unknown) => broadcast({ type: 'sync:status', payload }));
138
+
109
139
  // HMR broadcaster — turns fs:any change events into `canvas-hmr` messages.
110
140
  // The iframe-side client (in _shell.html) decides reload strategy from `mode`.
111
- createHmrBroadcaster(ctx, (msg) => broadcast(msg));
141
+ // Uses broadcastHmr so the segregated canvas origin's HMR-only sockets get it.
142
+ createHmrBroadcaster(ctx, (msg) => broadcastHmr(msg));
112
143
 
113
144
  // Bind a connection to its room. Stored per-socket so close() can find the
114
145
  // right room to disconnect from. Multiplexed via ws.data.id.
@@ -138,6 +169,11 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
138
169
  await room.connect(conn);
139
170
  return;
140
171
  }
172
+ if (ws.data.kind === 'canvas-hmr') {
173
+ // HMR-only: join the broadcast set but get NO inspector snapshot.
174
+ clients.add(ws);
175
+ return;
176
+ }
141
177
  clients.add(ws);
142
178
  send(ws, { type: 'snapshot', state: inspect.state });
143
179
  },
@@ -159,6 +195,9 @@ export function createWs(ctx: Context, api: Api, inspect: Inspect, collab: Colla
159
195
  clients.delete(ws);
160
196
  },
161
197
  async message(ws, raw) {
198
+ // HMR-only canvas-origin socket: never accepts inbound messages (the
199
+ // canvas iframe only listens for canvas-hmr; it never sends).
200
+ if (ws.data.kind === 'canvas-hmr') return;
162
201
  if (ws.data.kind === 'collab') {
163
202
  const binding = collabConns.get(ws.data.id);
164
203
  if (!binding) return;
@@ -28,6 +28,26 @@
28
28
  <meta charset="utf-8" />
29
29
  <meta name="viewport" content="width=device-width,initial-scale=1" />
30
30
  <title>Canvas</title>
31
+ <script>
32
+ // T2 (9.1 / DDR-060 F1 re-audit) — best-effort WebRTC denial in the canvas
33
+ // iframe. CSP `webrtc 'block'` is specified but UNIMPLEMENTED in shipping
34
+ // Chrome/Firefox as of 2026 (Chromium #40188662, Firefox bug 1783489), so
35
+ // it is currently a no-op, and `connect-src 'self'` does NOT cover WebRTC
36
+ // (it is not Fetch). Hub-pushed canvas JS could otherwise exfil via
37
+ // ICE/STUN. Locking these to undefined raises the bar against the naive
38
+ // case before any canvas module loads. NOT bulletproof — a nested
39
+ // same-origin (about:blank) frame can re-acquire them — so WebRTC exfil
40
+ // stays a DOCUMENTED residual of opt-in linked mode (see the linked-mode
41
+ // README banner). `webrtc 'block'` stays in the CSP for when browsers
42
+ // enforce it. The canvas runtime itself uses zero WebRTC.
43
+ for (var __k of ['RTCPeerConnection', 'webkitRTCPeerConnection', 'RTCDataChannel']) {
44
+ try {
45
+ Object.defineProperty(window, __k, { value: undefined, configurable: false, writable: false });
46
+ } catch (_) {
47
+ /* already locked / not present */
48
+ }
49
+ }
50
+ </script>
31
51
  <style>
32
52
  :root {
33
53
  color-scheme: light dark;
@@ -122,6 +142,10 @@
122
142
  const styleEl = document.getElementById('canvas-hide-chrome');
123
143
  if (styleEl) styleEl.media = 'all';
124
144
  }
145
+ // Shell-owned comment layer toggle. Gallery thumbnails pass `?comments=0`
146
+ // so the comment chrome is suppressed in previews; opened tabs omit it
147
+ // (comments on). hide-chrome exports also suppress comments.
148
+ const commentsEnabled = params.get('comments') !== '0' && params.get('hide-chrome') !== '1';
125
149
 
126
150
  function showError(msg) {
127
151
  const el = document.getElementById('canvas-mount-error');
@@ -146,13 +170,28 @@
146
170
  const v = msg.version || Date.now();
147
171
  // Match by exact filename when we have one; otherwise refresh all.
148
172
  const targetFile = (msg.file || '').split('/').pop();
173
+ let matched = false;
149
174
  for (const link of document.querySelectorAll('link[rel="stylesheet"]')) {
150
175
  const href = link.getAttribute('href') || '';
151
176
  if (!targetFile || href.includes(targetFile)) {
152
177
  const base = href.split('?')[0];
153
178
  link.setAttribute('href', base + '?v=' + v);
179
+ matched = true;
154
180
  }
155
181
  }
182
+ // A per-canvas `<slug>.css` imported by the TSX is INLINED into the
183
+ // canvas module as a <style> by Bun.build — there is no <link> to
184
+ // swap, so the loop above matches nothing and the edit is silently
185
+ // dropped until a manual hard-reload (RCA hmr-inlined-css-dropped).
186
+ // When nothing matched AND the changed CSS lives in THIS canvas's
187
+ // directory, reload so the rebuilt module re-inlines the fresh CSS.
188
+ // Scoped to same-dir so an unrelated specimen's CSS write doesn't
189
+ // reload every open tab.
190
+ if (!matched && msg.file && canvasRel) {
191
+ const cssDir = msg.file.replace(/[^/]+$/, '');
192
+ const canvasDir = canvasRel.replace(/[^/]+$/, '');
193
+ if (cssDir === canvasDir) location.reload();
194
+ }
156
195
  } else if (msg.mode === 'module' || msg.mode === 'hard') {
157
196
  // Only reload when the change touches *this* canvas (or `_lib`).
158
197
  if (msg.mode === 'hard' || !msg.file || (canvasRel && msg.file === canvasRel)) {
@@ -234,10 +273,13 @@
234
273
  window.__canvas_meta_file__ = designRel + '/' + canvasRel;
235
274
  });
236
275
  try {
237
- const [{ createRoot }, mod, react] = await Promise.all([
238
- import('react-dom/client'),
276
+ // mountCanvas (dist/comment-mount.js) owns the React root + the lite
277
+ // comment provider tree. It externalizes react / react-dom/client to
278
+ // the importmap so the canvas module and the comment layer share one
279
+ // React singleton (inlining a second React would break hooks).
280
+ const [{ mountCanvas }, mod] = await Promise.all([
281
+ import('/_client/comment-mount.js'),
239
282
  import(canvasUrl),
240
- import('react'),
241
283
  metaFetch,
242
284
  ]);
243
285
  const Canvas = mod.default;
@@ -246,8 +288,24 @@
246
288
  'Canvas module at ' + canvasUrl + ' has no default export (or it is not a component).'
247
289
  );
248
290
  }
249
- const root = createRoot(document.getElementById('canvas-root'));
250
- root.render(react.createElement(Canvas));
291
+ const rootEl = document.getElementById('canvas-root');
292
+ mountCanvas(Canvas, {
293
+ rootEl,
294
+ file: designRel + '/' + canvasRel,
295
+ commentsEnabled,
296
+ });
297
+ // T2 (9.1-A) diagnostic — tell the parent the canvas mounted + how many
298
+ // root children it rendered. Lets the shell (and tooling) confirm a
299
+ // cross-origin canvas actually rendered, which a parent-page screenshot
300
+ // of an OOPIF cannot show. Harmless heartbeat; ignored if unhandled.
301
+ setTimeout(() => {
302
+ try {
303
+ window.parent.postMessage(
304
+ { dgn: 'canvas-mounted', file: designRel + '/' + canvasRel, children: rootEl ? rootEl.children.length : 0 },
305
+ '*'
306
+ );
307
+ } catch (e) { /* parent gone */ }
308
+ }, 800);
251
309
  } catch (err) {
252
310
  showError((err && err.stack) || String(err));
253
311
  }
@@ -29,6 +29,19 @@
29
29
  * server (canvas-pipeline.ts pass 1). Don't author them by hand — they'd be
30
30
  * stripped + replaced. `/design:handoff` strips them again before the registry
31
31
  * sidecar leaves dev (production has no business with dev-time scaffolding).
32
+ *
33
+ * Theme model (two ISOLATED layers — system-review D9):
34
+ * • Canvas-shell CHROME (workspace plane, floating toolbar, minimap, zoom
35
+ * HUD, halos) auto-follows the Maude dev-server theme via `data-maude-theme`
36
+ * on the iframe <html> — the dev-server sets it; never theme the chrome
37
+ * from canvas code.
38
+ * • ARTBOARDS keep THEIR design system's theme. The `data-theme` on the
39
+ * `{{ROOT_CLASS}}` wrapper pins this artboard to a specific DS theme block;
40
+ * leave it at the DS default ({{THEME_DEFAULT}}). Do NOT hardcode a theme
41
+ * the DS doesn't intend — a reviewer flips an individual artboard at will
42
+ * via right-click → Theme ▸ Light / Dark / Follow chrome (enabled when the
43
+ * DS ships both light + dark token blocks). Only pin a non-default theme
44
+ * when the canvas is intentionally single-theme.
32
45
  */
33
46
 
34
47
  import { useState } from "react";
@@ -14,6 +14,7 @@ The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEH
14
14
 
15
15
  ### ANIMATION SAFETY (mandatory — applies to motion specimen + any canvas with @keyframes / transitions)
16
16
 
17
+ - **Tooling contract first (DDR-049).** The authoritative rule for WHICH tool to use is the "Animation tooling contract" in `skills/design-system/SKILL.md`: the **default for both specimens and canvases is `<MotionDemo role>` / the canvas-lib vocabulary from `@maude/canvas-lib`** (wraps `motion/react`), NOT hand-rolled `@keyframes`. Pure-CSS `.motion-*` role classes are a justified-only zero-JS escape hatch. The CSS rules below apply to that escape hatch and to the bounded-geometry constraints the vocabulary already satisfies — they do NOT license reaching for `@keyframes` by default.
17
18
  - **Bounded geometry.** Every tile that hosts a rotating / scaling animation MUST have `overflow: hidden`. Otherwise the bounding box extends ~√2× at 45°/135° rotations and overflows adjacent rows. **The motion specimen tiles all set `overflow: hidden` explicitly** — codify this with a CSS comment so future agents reading the file see the rationale.
18
19
  - **Sparkle / pulse / twinkle = small only.** Keyframes that scale from `0 → 1 → 0` (or similar "appear-and-disappear" patterns) are for elements ≤56px. **Never apply to full-width tiles.** Demo sparkle on a 32×32 chip inside a tile, never on the tile itself. The studyfi imprint retro D-3 caught a sparkle-on-full-tile that exploded the whole row.
19
20
  - **Loop motion = `infinite alternate`.** Specimens meant for continuous display use `animation: <kf> <dur> infinite alternate <easing>`. Single-shot animations finish in 150-200ms and leave the demo invisible on the second look — "looks dead until you hover" is the regression mode.
@@ -24,9 +25,9 @@ The three MANDATORY safety blocks (ANIMATION SAFETY, RELATIVE-URL SAFETY, PLACEH
24
25
  ### RELATIVE-URL SAFETY (mandatory — applies to any specimen referencing assets, logos, glyphs)
25
26
 
26
27
  - The dev-server serves canvases via `/_canvas-shell.html?canvas=<rel-path>`. **Relative URLs (`../foo.svg`, `./assets/logo.svg`) resolve against the SHELL's URL, not the canvas file's location.** Result: 404 / broken-image icon. Studyfi imprint retro D-4 was caught by the user mid-flow because of exactly this.
27
- - **Always inline SVGs** in JSX. Use `useId()` for any `<filter id>` / `<linearGradient id>` so multiple instances of the same component don't collide on a single page. OR
28
- - **Always use absolute paths rooted at `/assets/...`** served from the dev-server's static mount. The `system/<ds>/assets/` directory is mounted at `/assets/<ds>/` in production-handoff mode.
29
- - **Forbidden:** `<img src="../foo.svg">`, `<image href="./logo.svg">`, `url('../bg.png')` inside inline `<style>` blocks. Any of these surviving in a specimen is a graphic-design-critic blocker.
28
+ - **Always inline SVGs** in JSX. Use `useId()` for any `<filter id>` / `<linearGradient id>` so multiple instances of the same component don't collide on a single page. This is the **strongly-preferred** default. OR
29
+ - **Use an absolute path that mirrors the real on-disk location under the repo root** — the ONE correct form is `/<designRoot>/system/<ds>/assets/<file>`, e.g. `/.design/system/<ds>/assets/logo.svg` (with the default `designRoot` of `.design`). The dev-server's fall-through serves any file by its real path under the repo root (`http.ts` `safePathUnderRoot`); there is **no `/assets/<ds>/` alias** that earlier wording was wrong and 404s. Substitute the project's actual `designRoot` (read `config.json` `designRoot`) if it isn't `.design`.
30
+ - **Forbidden:** `<img src="../foo.svg">`, `<image href="./logo.svg">`, `url('../bg.png')` inside inline `<style>` blocks, AND the bogus `/assets/<ds>/…` alias. Any of these surviving in a specimen is a graphic-design-critic blocker.
30
31
 
31
32
  ### PLACEHOLDER POLICY (mandatory — applies to logo / mark / wordmark / mascot / glyph / illustration claims)
32
33
 
@@ -133,7 +134,8 @@ SAFETY BLOCKS — apply to every file in your slice:
133
134
  + compositor-only + reduced-motion + no-bouncy-springs.
134
135
  - **RELATIVE-URL SAFETY** (see SUB-AGENT-PROMPTS.md). Mandatory if your slice
135
136
  references ANY asset (logo, glyph, illustration, background image). Inline
136
- SVG with `useId()`, OR absolute `/assets/<ds>/...` path. NEVER relative.
137
+ SVG with `useId()`, OR absolute `/<designRoot>/system/<ds>/assets/...` path
138
+ (e.g. `/.design/system/<ds>/assets/...`). NEVER relative, NEVER `/assets/<ds>/`.
137
139
  - **PLACEHOLDER POLICY** (see SUB-AGENT-PROMPTS.md). Mandatory if your slice
138
140
  scaffolds the `logo` / `empty-state` specimen or any brand-asset surface.
139
141
  Read the roster `assets:` block FIRST. If `source:` is populated, copy the
@@ -211,13 +213,17 @@ The base template above applies to every slice. Each slice gets a small addendum
211
213
 
212
214
  **Apply RELATIVE-URL SAFETY in full.** The `iconography` specimen tends to reference glyph SVGs; the `focus` specimen sometimes references focus-ring fixtures. Inline ALL SVGs.
213
215
 
216
+ **Restraint-default type ladder (D-8).** The `type-scale` specimen demonstrates the scale the tokens already encode — do NOT reintroduce drama the tokens deliberately held back. The default ceiling is **type-scale ratio ≤ 1.2, optical-size ≤ 72, display weight ≤ semibold, tracking ≥ -0.02em**. If `colors_and_type.css` already carries a more dramatic ladder (a high-confidence research recommendation explicitly asked for maximalism), mirror what the tokens declare — never push *beyond* the tokens on your own initiative. Restraint is the default; the user opts UP via `/design:edit`, not the sub-agent.
217
+
218
+ **Research type-fidelity (D-7).** Read the research payload's type recommendation (in the discovery payload / DS README). **Mirror its PRIMARY display-face ROLE exactly.** A "grotesque" direction yields a grotesque display face even when an open-source serif is more convenient to wire up — do NOT let font availability flip the role. Distinguish the **display role** from the **body-accent role**: a recommendation like `display-grotesque-editorial-serif` means a grotesque sans for the DISPLAY face *with* an editorial serif for BODY accents — it does NOT mean the serif is the display face. If the named face is unavailable, substitute within the SAME classification (grotesque → grotesque, not grotesque → serif) and note the substitution so it lands in the bypass-log. (Studyfi read "serif" off `display-grotesque-editorial-serif` and picked Fraunces as the display face — inverting the roles.)
219
+
214
220
  ### Slice: `brand + voice` (empty-state, logo)
215
221
 
216
222
  **Apply PLACEHOLDER POLICY in full.** This slice is the highest-risk for placeholder-bleed. The `logo` specimen MUST start by reading the roster's `assets:` block:
217
223
  - If `assets.logo.source` is a real path, the specimen `<img>`s / inlines the real asset.
218
224
  - If `assets.logo.source` is empty, fall back to a `-placeholder` SVG with the rationale in the JSX comment block. Set `source: placeholder` on roster update.
219
225
 
220
- **Apply RELATIVE-URL SAFETY in full.** Logo specimens are 100% asset-bound. Inline SVG is the default; absolute `/assets/<ds>/logo.svg` is the alternative.
226
+ **Apply RELATIVE-URL SAFETY in full.** Logo specimens are 100% asset-bound. Inline SVG is the default; absolute `/<designRoot>/system/<ds>/assets/logo.svg` (e.g. `/.design/system/<ds>/assets/logo.svg`) is the alternative — never the bogus `/assets/<ds>/` alias.
221
227
 
222
228
  ### Slice: `core components` (components-buttons, components-cards, components-inputs)
223
229
 
@@ -229,6 +235,8 @@ No slice-specific addenda — the base template covers them.
229
235
 
230
236
  ### Slice: `ui_kits-*-showcase` (main agent OR signature sub-agent)
231
237
 
238
+ **Showcase-from-real-app (D-6) — read the real layout BEFORE composing, for existing products.** If the prompt injects real layout paths (the orchestrator passes the app's `AppLayout` + primary-nav component paths when the DS is for a shipped product), you MUST **read those files first and mirror that UX, restyling only** — apply the DS tokens + signature treatment to the *real* screen anatomy. Do NOT invent a plausible-but-fictional product UX when a real one was handed to you; inventing it is the studyfi D-6 regression that forced a ~5500-LOC rebuild. If NO real layout paths are injected (greenfield DS), compose a project-specific mock from `domain_nouns` per the creativity rubric — that path is unchanged.
239
+
232
240
  **Apply ANIMATION SAFETY.** Showcase is the highest-density "DS in use" artifact; presence cursors, route transitions, and panel slides all show up here. Every animation must be bounded + reduced-motion-safe + token-derived.
233
241
 
234
242
  **Apply RELATIVE-URL SAFETY.** Showcase often references domain product imagery. Inline SVG mocks, never `<img src="../assets/...">`.
@@ -241,5 +249,5 @@ No slice-specific addenda — the base template covers them.
241
249
  - Discovery probe templates (sibling): `plugins/design/skills/design-system/_pastier-probe-templates.md`
242
250
  - Motion library decision: `.ai/decisions/DDR-049-motion-one-as-canonical-motion-library.md`
243
251
  - Imprint-bootstrap retro (source of D-1 … D-5): `.ai/logs/system-reviews/imprint-bootstrap-review-2026-05-26.md`
244
- - Asset sweep helper: `plugins/design/dev-server/bin/asset-sweep.sh`
245
- - Visual sanity helper: `plugins/design/dev-server/bin/visual-sanity.sh`
252
+ - Asset sweep helper: `maude design asset-sweep` (on-PATH `maude` dispatches to the bundled helper — DDR-062)
253
+ - Visual sanity helper: `maude design visual-sanity` (on-PATH `maude` dispatches to the bundled helper — DDR-062)
@@ -42,6 +42,11 @@
42
42
  "type": "string",
43
43
  "description": "Living codebase snapshot. Default: `.ai/context/codebase-map.md`.",
44
44
  "default": ".ai/context/codebase-map.md"
45
+ },
46
+ "designRoot": {
47
+ "type": "string",
48
+ "description": "Root of the design plugin's canvas workspace, relative to repo root. Where flow looks for `.meta.json` sidecars + canvases when bridging to design (`/flow:plan` canvas detection, `/flow:done` handoff sweep, `codebase-intelligence` design-artifacts section). Mirrors the design plugin's own default. Flow reads this directory read-only — it never writes into the design root except the `/flow:done` handoff sweep's meta status update. Default: `.design`.",
49
+ "default": ".design"
45
50
  }
46
51
  }
47
52
  },
@@ -27,7 +27,7 @@ These live at the root of `.ai/` (not in subfolders) and are referenced by `work
27
27
 
28
28
  - `<name>-prd.md` — product requirements / brief
29
29
  - `<name>-design-system.md` — design system reference (look & feel, tokens, density)
30
- - `workflows.config.json` — config consumed by all flow commands and skills
30
+ - `workflows.config.json` — config consumed by all flow commands and skills. The optional top-level `quality` map (lint / format / typecheck / tests / build → shell command) is **not** scaffolded here — run `maude doctor --fix` once your `package.json` scripts exist and it detects + adds them (additive, never overwrites your edits).
31
31
 
32
32
  ## Conventions
33
33
 
@@ -0,0 +1,10 @@
1
+ # Sidecar cache — Phase C / DDR-061 (cli/lib/cache.mjs).
2
+ # Commit the shareable, content-addressed layers (research/domain + codebase-
3
+ # intelligence are SHA-keyed and reusable across collaborators); keep the
4
+ # brief/run/HEAD-specific layers (research/project, design-context, scenario,
5
+ # security) and the local telemetry (_stats.json, README.md) out of git.
6
+ cache/*
7
+ !cache/codebase-intelligence/
8
+ !cache/research/
9
+ cache/research/*
10
+ !cache/research/domain/
@@ -9,6 +9,7 @@ scenarios/
9
9
  ├── _lib/ — shared helpers (selectors, fixtures, env)
10
10
  └── <scenario-slug>/
11
11
  ├── spec.md — what the flow does, success criteria, platform matrix
12
+ ├── covers.json — OPTIONAL: { web/native/shared git pathspecs } the scenario exercises
12
13
  └── <YYYY-MM-DD-HHMM>/
13
14
  ├── report.md — TL;DR + per-platform pivot table + counter deltas
14
15
  ├── web-desktop/ — screenshots, logs
@@ -21,6 +22,7 @@ scenarios/
21
22
 
22
23
  - **Scenario slug:** kebab-case, action-oriented (`coach-creates-recurring-practice`, `signup-via-invite`).
23
24
  - **Run timestamp:** local time of the run start. Latest run = lexicographically last folder.
24
- - **Report format:** owned by the `scenario` skill — see plugin docs.
25
+ - **Report format:** owned by the `scenario` skill — `maude scenario-report <run-dir>` generates the mechanical tables; the LLM authors only the prose sections.
26
+ - **`covers.json`** (optional): declares the source globs the scenario exercises, split into `web` / `native` / `shared` git pathspecs. Enables the route-aware skip (re-run only when covered files changed) and the web-only scope skip (no sim boot for web-only diffs). Example: `{"web":["app/(video)/**"],"native":["expo-app/**"],"shared":["packages/api-client/**"]}`. Absent → the scenario always runs the full platform matrix.
25
27
 
26
28
  Run with `/flow:scenario <slug>` (registered scenarios) or `/flow:scenario <ad-hoc description>` (one-off).
@@ -6,7 +6,8 @@
6
6
  "paths": {
7
7
  "prd": ".ai/PROJECT_NAME-prd.md",
8
8
  "designSystem": ".ai/PROJECT_NAME-design-system.md",
9
- "codebaseMap": ".ai/context/codebase-map.md"
9
+ "codebaseMap": ".ai/context/codebase-map.md",
10
+ "designRoot": ".design"
10
11
  },
11
12
  "platforms": ["web-desktop"],
12
13
  "stack": {