pmx-canvas 0.3.0 → 0.3.1

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 (156) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/canvas/index.js +2 -2
  3. package/dist/types/cli/daemon.d.ts +74 -0
  4. package/dist/types/cli/watch.d.ts +2 -2
  5. package/dist/types/client/canvas/CanvasViewport.d.ts +1 -1
  6. package/dist/types/client/canvas/CommandPalette.d.ts +1 -1
  7. package/dist/types/client/canvas/Minimap.d.ts +1 -1
  8. package/dist/types/client/nodes/FileNode.d.ts +1 -1
  9. package/dist/types/client/nodes/ImageNode.d.ts +1 -1
  10. package/dist/types/client/nodes/InlineFormatBar.d.ts +1 -1
  11. package/dist/types/client/nodes/MarkdownNode.d.ts +1 -1
  12. package/dist/types/client/nodes/PromptNode.d.ts +1 -1
  13. package/dist/types/client/nodes/ResponseNode.d.ts +1 -1
  14. package/dist/types/server/canvas-schema.d.ts +1 -1
  15. package/dist/types/server/html-primitives.d.ts +1 -1
  16. package/dist/types/server/index.d.ts +4 -4
  17. package/dist/types/server/operations/index.d.ts +1 -1
  18. package/dist/types/server/operations/ops/ax-read.d.ts +2 -0
  19. package/dist/types/server/operations/ops/canvas-wire.d.ts +2 -0
  20. package/dist/types/server/operations/ops/ext-app.d.ts +2 -0
  21. package/docs/http-api.md +28 -0
  22. package/docs/plans/plan-009-tech-debt-backlog.md +5 -5
  23. package/docs/tech-debt-assessment-2026-07.md +2 -2
  24. package/package.json +5 -2
  25. package/skills/pmx-canvas/SKILL.md +3 -1
  26. package/skills/pmx-canvas/references/full-reference.md +10 -3
  27. package/src/cli/agent.ts +1861 -1548
  28. package/src/cli/daemon.ts +460 -0
  29. package/src/cli/index.ts +63 -326
  30. package/src/cli/watch.ts +2 -10
  31. package/src/client/App.tsx +48 -46
  32. package/src/client/canvas/AttentionHistory.tsx +11 -1
  33. package/src/client/canvas/CanvasNode.tsx +41 -29
  34. package/src/client/canvas/CanvasViewport.tsx +101 -66
  35. package/src/client/canvas/CommandPalette.tsx +61 -27
  36. package/src/client/canvas/ContextMenu.tsx +13 -20
  37. package/src/client/canvas/ContextPinBar.tsx +1 -5
  38. package/src/client/canvas/ContextPinHud.tsx +1 -6
  39. package/src/client/canvas/DockedNode.tsx +4 -4
  40. package/src/client/canvas/EdgeLayer.tsx +37 -36
  41. package/src/client/canvas/ExpandedNodeOverlay.tsx +69 -45
  42. package/src/client/canvas/FocusFieldLayer.tsx +20 -22
  43. package/src/client/canvas/IntentLayer.tsx +31 -14
  44. package/src/client/canvas/Minimap.tsx +11 -16
  45. package/src/client/canvas/SelectionBar.tsx +4 -11
  46. package/src/client/canvas/ShortcutOverlay.tsx +3 -1
  47. package/src/client/canvas/SnapshotPanel.tsx +77 -95
  48. package/src/client/canvas/auto-fit.ts +15 -14
  49. package/src/client/canvas/snap-guides.ts +12 -12
  50. package/src/client/canvas/use-node-resize.ts +1 -5
  51. package/src/client/canvas/use-pan-zoom.ts +25 -26
  52. package/src/client/ext-app/bridge.ts +3 -12
  53. package/src/client/icons.tsx +63 -20
  54. package/src/client/nodes/ContextNode.tsx +14 -25
  55. package/src/client/nodes/ExtAppFrame.tsx +60 -39
  56. package/src/client/nodes/FileNode.tsx +74 -62
  57. package/src/client/nodes/GroupNode.tsx +4 -6
  58. package/src/client/nodes/HtmlNode.tsx +76 -46
  59. package/src/client/nodes/ImageNode.tsx +18 -27
  60. package/src/client/nodes/InlineFormatBar.tsx +4 -21
  61. package/src/client/nodes/InlineMarkdownEditor.tsx +0 -1
  62. package/src/client/nodes/LedgerNode.tsx +10 -4
  63. package/src/client/nodes/MarkdownNode.tsx +3 -10
  64. package/src/client/nodes/McpAppNode.tsx +26 -22
  65. package/src/client/nodes/MdFormatBar.tsx +10 -7
  66. package/src/client/nodes/PromptNode.tsx +23 -51
  67. package/src/client/nodes/ResponseNode.tsx +3 -13
  68. package/src/client/nodes/StatusNode.tsx +5 -9
  69. package/src/client/nodes/StatusSummary.tsx +2 -8
  70. package/src/client/nodes/WebpageNode.tsx +20 -14
  71. package/src/client/nodes/iframe-document-url.ts +25 -16
  72. package/src/client/nodes/image-warnings.ts +1 -7
  73. package/src/client/nodes/md-format.ts +20 -5
  74. package/src/client/state/attention-bridge.ts +4 -9
  75. package/src/client/state/attention-store.ts +1 -7
  76. package/src/client/state/canvas-store.ts +52 -36
  77. package/src/client/state/intent-bridge.ts +176 -112
  78. package/src/client/state/intent-store.ts +4 -1
  79. package/src/client/state/sse-bridge.ts +53 -70
  80. package/src/json-render/catalog.ts +12 -16
  81. package/src/json-render/charts/components.tsx +16 -20
  82. package/src/json-render/charts/extra-components.tsx +8 -16
  83. package/src/json-render/charts/extra-definitions.ts +1 -2
  84. package/src/json-render/charts/tufte-components.tsx +37 -20
  85. package/src/json-render/charts/tufte-definitions.ts +8 -2
  86. package/src/json-render/renderer/index.tsx +42 -22
  87. package/src/json-render/schema.ts +6 -3
  88. package/src/json-render/server.ts +33 -39
  89. package/src/mcp/canvas-access.ts +35 -21
  90. package/src/mcp/server.ts +132 -70
  91. package/src/server/agent-context.ts +63 -36
  92. package/src/server/ax-context.ts +7 -5
  93. package/src/server/ax-interaction.ts +176 -43
  94. package/src/server/ax-state-manager.ts +182 -39
  95. package/src/server/ax-state.ts +142 -47
  96. package/src/server/canvas-db.ts +213 -95
  97. package/src/server/canvas-operations.ts +177 -120
  98. package/src/server/canvas-provenance.ts +1 -4
  99. package/src/server/canvas-schema.ts +454 -73
  100. package/src/server/canvas-serialization.ts +27 -35
  101. package/src/server/canvas-state.ts +150 -58
  102. package/src/server/chart-template.ts +4 -5
  103. package/src/server/code-graph.ts +19 -6
  104. package/src/server/diagram-presets.ts +28 -29
  105. package/src/server/ext-app-lookup.ts +3 -12
  106. package/src/server/html-node-summary.ts +19 -10
  107. package/src/server/html-primitives.ts +326 -97
  108. package/src/server/html-surface.ts +6 -9
  109. package/src/server/image-source.ts +6 -4
  110. package/src/server/index.ts +320 -217
  111. package/src/server/intent-registry.ts +2 -5
  112. package/src/server/mcp-app-candidate.ts +5 -10
  113. package/src/server/mcp-app-host.ts +14 -38
  114. package/src/server/mcp-app-runtime.ts +12 -20
  115. package/src/server/mutation-history.ts +15 -5
  116. package/src/server/operations/composites.ts +1 -3
  117. package/src/server/operations/http.ts +2 -3
  118. package/src/server/operations/index.ts +7 -1
  119. package/src/server/operations/invoker.ts +4 -3
  120. package/src/server/operations/mcp.ts +22 -30
  121. package/src/server/operations/ops/annotation.ts +122 -10
  122. package/src/server/operations/ops/app.ts +98 -73
  123. package/src/server/operations/ops/ax-await.ts +17 -10
  124. package/src/server/operations/ops/ax-read.ts +347 -0
  125. package/src/server/operations/ops/ax-shared.ts +2 -7
  126. package/src/server/operations/ops/ax-state.ts +32 -14
  127. package/src/server/operations/ops/ax-timeline.ts +32 -19
  128. package/src/server/operations/ops/ax-work.ts +54 -37
  129. package/src/server/operations/ops/batch.ts +39 -14
  130. package/src/server/operations/ops/canvas-wire.ts +91 -0
  131. package/src/server/operations/ops/edges.ts +37 -25
  132. package/src/server/operations/ops/ext-app.ts +346 -0
  133. package/src/server/operations/ops/groups.ts +49 -20
  134. package/src/server/operations/ops/intent.ts +18 -12
  135. package/src/server/operations/ops/json-render.ts +239 -98
  136. package/src/server/operations/ops/nodes.ts +298 -109
  137. package/src/server/operations/ops/query.ts +46 -28
  138. package/src/server/operations/ops/snapshots.ts +35 -26
  139. package/src/server/operations/ops/validate.ts +2 -1
  140. package/src/server/operations/ops/viewport.ts +60 -16
  141. package/src/server/operations/ops/webview.ts +44 -18
  142. package/src/server/operations/registry.ts +2 -3
  143. package/src/server/operations/types.ts +7 -5
  144. package/src/server/operations/webview-runner.ts +1 -3
  145. package/src/server/placement.ts +8 -18
  146. package/src/server/server.ts +108 -1027
  147. package/src/server/spatial-analysis.ts +39 -25
  148. package/src/server/trace-manager.ts +3 -8
  149. package/src/server/web-artifacts.ts +23 -27
  150. package/src/server/webpage-node.ts +5 -13
  151. package/src/shared/auto-arrange.ts +12 -5
  152. package/src/shared/content-height-reporter.ts +8 -6
  153. package/src/shared/ext-app-tool-result.ts +2 -6
  154. package/src/shared/placement.ts +1 -4
  155. package/src/shared/semantic-attention.ts +39 -37
  156. package/src/shared/surface.ts +8 -4
@@ -20,7 +20,7 @@ export const HTML_PRIMITIVE_KINDS = [
20
20
  'prompt-tuner',
21
21
  ] as const;
22
22
 
23
- export type HtmlPrimitiveKind = typeof HTML_PRIMITIVE_KINDS[number];
23
+ export type HtmlPrimitiveKind = (typeof HTML_PRIMITIVE_KINDS)[number];
24
24
 
25
25
  export interface HtmlPrimitiveDescriptor {
26
26
  kind: HtmlPrimitiveKind;
@@ -55,7 +55,11 @@ export interface HtmlPrimitiveSemanticMetadata {
55
55
  presentationTheme?: string | Record<string, string>;
56
56
  }
57
57
 
58
- type PrimitiveRenderer = (input: { title: string; data: Record<string, unknown>; descriptor: HtmlPrimitiveDescriptor }) => string;
58
+ type PrimitiveRenderer = (input: {
59
+ title: string;
60
+ data: Record<string, unknown>;
61
+ descriptor: HtmlPrimitiveDescriptor;
62
+ }) => string;
59
63
 
60
64
  const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
61
65
  {
@@ -70,7 +74,13 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
70
74
  title: 'Implementation Options',
71
75
  data: {
72
76
  items: [
73
- { title: 'Small patch', summary: 'Least disruption.', tradeoff: 'Limited future flexibility.', pros: ['Fast'], cons: ['May need follow-up'] },
77
+ {
78
+ title: 'Small patch',
79
+ summary: 'Least disruption.',
80
+ tradeoff: 'Limited future flexibility.',
81
+ pros: ['Fast'],
82
+ cons: ['May need follow-up'],
83
+ },
74
84
  ],
75
85
  },
76
86
  },
@@ -81,7 +91,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
81
91
  description: 'Implementation plan with milestones, data flow, code checkpoints, and risks.',
82
92
  useWhen: 'Use when a markdown plan would be long and the human needs sequence, dependencies, and risk at a glance.',
83
93
  defaultSize: { width: 1040, height: 760 },
84
- dataShape: '{ milestones: [{ title, detail, status }], flow: [{ from, to, label }], risks: [{ risk, mitigation }], snippets: [{ label, code }] }',
94
+ dataShape:
95
+ '{ milestones: [{ title, detail, status }], flow: [{ from, to, label }], risks: [{ risk, mitigation }], snippets: [{ label, code }] }',
85
96
  example: {
86
97
  kind: 'plan-timeline',
87
98
  title: 'Feature Plan',
@@ -102,7 +113,15 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
102
113
  kind: 'review-sheet',
103
114
  title: 'Streaming Review',
104
115
  data: {
105
- findings: [{ severity: 'warning', title: 'Backpressure boundary', file: 'src/server.ts', line: 42, detail: 'Confirm queue flush before close.' }],
116
+ findings: [
117
+ {
118
+ severity: 'warning',
119
+ title: 'Backpressure boundary',
120
+ file: 'src/server.ts',
121
+ line: 42,
122
+ detail: 'Confirm queue flush before close.',
123
+ },
124
+ ],
106
125
  },
107
126
  },
108
127
  },
@@ -112,13 +131,20 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
112
131
  description: 'Reviewer-ready PR narrative with motivation, before/after, file tour, test plan, and rollout notes.',
113
132
  useWhen: 'Use when a change needs a high-signal review guide rather than a flat pull request description.',
114
133
  defaultSize: { width: 1040, height: 760 },
115
- dataShape: '{ summary, why, before: string[], after: string[], files: [{ path, why, focus }], reviewFocus: string[], tests: string[], rollout: string[] }',
134
+ dataShape:
135
+ '{ summary, why, before: string[], after: string[], files: [{ path, why, focus }], reviewFocus: string[], tests: string[], rollout: string[] }',
116
136
  example: {
117
137
  kind: 'pr-writeup',
118
138
  title: 'HTML Primitive PR',
119
139
  data: {
120
140
  summary: 'Adds generated HTML primitives across HTTP, MCP, CLI, and SDK.',
121
- files: [{ path: 'src/server/html-primitives.ts', why: 'Primitive catalog and renderers.', focus: 'Escaping and export behavior.' }],
141
+ files: [
142
+ {
143
+ path: 'src/server/html-primitives.ts',
144
+ why: 'Primitive catalog and renderers.',
145
+ focus: 'Escaping and export behavior.',
146
+ },
147
+ ],
122
148
  },
123
149
  },
124
150
  },
@@ -142,15 +168,19 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
142
168
  kind: 'code-walkthrough',
143
169
  title: 'Code Walkthrough',
144
170
  description: 'Guided code-path map with modules, ordered file steps, snippets, key files, and gotchas.',
145
- useWhen: 'Use to explain an unfamiliar package, request path, or implementation slice after inspecting source files.',
171
+ useWhen:
172
+ 'Use to explain an unfamiliar package, request path, or implementation slice after inspecting source files.',
146
173
  defaultSize: { width: 1040, height: 760 },
147
- dataShape: '{ summary, modules: [{ id, title, detail, role }], edges: [{ from, to, label }], steps: [{ title, file, detail, code }], keyFiles: [{ path, description }], gotchas: string[] }',
174
+ dataShape:
175
+ '{ summary, modules: [{ id, title, detail, role }], edges: [{ from, to, label }], steps: [{ title, file, detail, code }], keyFiles: [{ path, description }], gotchas: string[] }',
148
176
  example: {
149
177
  kind: 'code-walkthrough',
150
178
  title: 'Canvas Creation Path',
151
179
  data: {
152
180
  modules: [{ id: 'api', title: 'HTTP API', detail: 'Receives node create requests.', role: 'entry' }],
153
- steps: [{ title: 'Route request', file: 'src/server/server.ts', detail: 'Normalize input before mutating state.' }],
181
+ steps: [
182
+ { title: 'Route request', file: 'src/server/server.ts', detail: 'Normalize input before mutating state.' },
183
+ ],
154
184
  },
155
185
  },
156
186
  },
@@ -165,7 +195,14 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
165
195
  kind: 'design-sheet',
166
196
  title: 'Visual Directions',
167
197
  data: {
168
- directions: [{ title: 'Editorial', tone: 'calm, high contrast', palette: ['#f8f1e7', '#16120f', '#d65a31'], rationale: 'Readable and opinionated.' }],
198
+ directions: [
199
+ {
200
+ title: 'Editorial',
201
+ tone: 'calm, high contrast',
202
+ palette: ['#f8f1e7', '#16120f', '#d65a31'],
203
+ rationale: 'Readable and opinionated.',
204
+ },
205
+ ],
169
206
  },
170
207
  },
171
208
  },
@@ -188,10 +225,13 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
188
225
  {
189
226
  kind: 'interaction-prototype',
190
227
  title: 'Interaction Prototype',
191
- description: 'Throwaway interaction or motion sandbox with a live stage, controls, annotations, and copyable config.',
192
- useWhen: 'Use for animation tuning, click-through sketches, draggable behavior studies, and interaction questions that prose cannot answer.',
228
+ description:
229
+ 'Throwaway interaction or motion sandbox with a live stage, controls, annotations, and copyable config.',
230
+ useWhen:
231
+ 'Use for animation tuning, click-through sketches, draggable behavior studies, and interaction questions that prose cannot answer.',
193
232
  defaultSize: { width: 1040, height: 760 },
194
- dataShape: '{ scenario, controls: [{ key, label, value, min?, max?, unit? }], screens: [{ title, detail }], annotations: [{ title, detail }], questions: string[], snippet? }',
233
+ dataShape:
234
+ '{ scenario, controls: [{ key, label, value, min?, max?, unit? }], screens: [{ title, detail }], annotations: [{ title, detail }], questions: string[], snippet? }',
195
235
  example: {
196
236
  kind: 'interaction-prototype',
197
237
  title: 'Sidebar Motion Study',
@@ -234,17 +274,25 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
234
274
  {
235
275
  kind: 'presentation',
236
276
  title: 'HTML Presentation',
237
- description: 'PowerPoint-style fullscreen-ready HTML presentation with slide navigation, progress, speaker notes, and presentation metadata.',
238
- useWhen: 'Use when the human asks for a presentation, pitch deck, briefing, workshop walkthrough, or PowerPoint-like deliverable.',
277
+ description:
278
+ 'PowerPoint-style fullscreen-ready HTML presentation with slide navigation, progress, speaker notes, and presentation metadata.',
279
+ useWhen:
280
+ 'Use when the human asks for a presentation, pitch deck, briefing, workshop walkthrough, or PowerPoint-like deliverable.',
239
281
  defaultSize: { width: 1120, height: 700 },
240
- dataShape: '{ subtitle?, theme?: "canvas"|"midnight"|"paper"|"aurora"|{ bg?, panel?, surface?, border?, text?, textSecondary?, textMuted?, accent? }, slides: [{ title, kicker?, body?, bullets?: string[], metrics?: [{ label, value, detail? }], note? }] }',
282
+ dataShape:
283
+ '{ subtitle?, theme?: "canvas"|"midnight"|"paper"|"aurora"|{ bg?, panel?, surface?, border?, text?, textSecondary?, textMuted?, accent? }, slides: [{ title, kicker?, body?, bullets?: string[], metrics?: [{ label, value, detail? }], note? }] }',
241
284
  example: {
242
285
  kind: 'presentation',
243
286
  title: 'Project Briefing',
244
287
  data: {
245
288
  subtitle: 'A meeting-ready narrative for review.',
246
289
  slides: [
247
- { title: 'Why this matters', kicker: '01', body: 'Frame the decision and outcome.', bullets: ['Human-readable', 'Fullscreen-ready'] },
290
+ {
291
+ title: 'Why this matters',
292
+ kicker: '01',
293
+ body: 'Frame the decision and outcome.',
294
+ bullets: ['Human-readable', 'Fullscreen-ready'],
295
+ },
248
296
  { title: 'What changes', kicker: '02', bullets: ['Show the before/after', 'End with clear next steps'] },
249
297
  ],
250
298
  },
@@ -254,14 +302,22 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
254
302
  kind: 'illustration-set',
255
303
  title: 'Illustration Set',
256
304
  description: 'Inline SVG figure sheet with captions and per-figure SVG copy/export controls.',
257
- useWhen: 'Use for blog figures, architecture illustrations, conceptual diagrams, and vector sketches that should be tweakable or pasteable.',
305
+ useWhen:
306
+ 'Use for blog figures, architecture illustrations, conceptual diagrams, and vector sketches that should be tweakable or pasteable.',
258
307
  defaultSize: { width: 1040, height: 760 },
259
- dataShape: '{ figures: [{ title, caption, shapes: [{ type, x, y, width, height, cx, cy, r, x1, y1, x2, y2, text, color }] }] }',
308
+ dataShape:
309
+ '{ figures: [{ title, caption, shapes: [{ type, x, y, width, height, cx, cy, r, x1, y1, x2, y2, text, color }] }] }',
260
310
  example: {
261
311
  kind: 'illustration-set',
262
312
  title: 'Article Figures',
263
313
  data: {
264
- figures: [{ title: 'Feedback Loop', caption: 'Human and agent exchange context.', shapes: [{ type: 'rect', x: 40, y: 50, width: 130, height: 70, text: 'Human' }] }],
314
+ figures: [
315
+ {
316
+ title: 'Feedback Loop',
317
+ caption: 'Human and agent exchange context.',
318
+ shapes: [{ type: 'rect', x: 40, y: 50, width: 130, height: 70, text: 'Human' }],
319
+ },
320
+ ],
265
321
  },
266
322
  },
267
323
  },
@@ -271,7 +327,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
271
327
  description: 'Readable explainer with TLDR, collapsible steps, annotated snippets, FAQ, and glossary.',
272
328
  useWhen: 'Use to teach how a feature, algorithm, or code path works after inspecting repo context.',
273
329
  defaultSize: { width: 980, height: 760 },
274
- dataShape: '{ summary, steps: [{ title, detail }], snippets: [{ label, code, note }], faq: [{ q, a }], glossary: [{ term, definition }] }',
330
+ dataShape:
331
+ '{ summary, steps: [{ title, detail }], snippets: [{ label, code, note }], faq: [{ q, a }], glossary: [{ term, definition }] }',
275
332
  example: {
276
333
  kind: 'explainer',
277
334
  title: 'Rate Limiter Explainer',
@@ -284,10 +341,12 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
284
341
  {
285
342
  kind: 'incident-report',
286
343
  title: 'Incident Report',
287
- description: 'Post-incident report with impact metrics, minute-by-minute timeline, root cause, log excerpts, and action checklist.',
344
+ description:
345
+ 'Post-incident report with impact metrics, minute-by-minute timeline, root cause, log excerpts, and action checklist.',
288
346
  useWhen: 'Use for incident summaries, post-mortems, reliability reviews, and follow-up tracking.',
289
347
  defaultSize: { width: 1040, height: 760 },
290
- dataShape: '{ severity, status, duration, summary, impact: [{ label, value, tone }], timeline: [{ time, event, detail, tone }], rootCause, logs?: string, actions: [{ done, owner, description, due }] }',
348
+ dataShape:
349
+ '{ severity, status, duration, summary, impact: [{ label, value, tone }], timeline: [{ time, event, detail, tone }], rootCause, logs?: string, actions: [{ done, owner, description, due }] }',
291
350
  example: {
292
351
  kind: 'incident-report',
293
352
  title: 'API Latency Incident',
@@ -304,7 +363,8 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
304
363
  description: 'Skimmable report with metrics, shipped/slipped lists, blockers, and next actions.',
305
364
  useWhen: 'Use for weekly updates, project health, incident summaries, and leadership-ready status.',
306
365
  defaultSize: { width: 980, height: 720 },
307
- dataShape: '{ metrics: [{ label, value, tone }], shipped: string[], slipped: string[], risks: string[], next: string[] }',
366
+ dataShape:
367
+ '{ metrics: [{ label, value, tone }], shipped: string[], slipped: string[], risks: string[], next: string[] }',
308
368
  example: {
309
369
  kind: 'status-report',
310
370
  title: 'Weekly Canvas Status',
@@ -325,7 +385,14 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
325
385
  kind: 'triage-board',
326
386
  title: 'Ticket Triage',
327
387
  data: {
328
- items: [{ title: 'Fix flaky smoke test', detail: 'Fails on CI timeout.', column: 'Now', rationale: 'Blocks release.' }],
388
+ items: [
389
+ {
390
+ title: 'Fix flaky smoke test',
391
+ detail: 'Fails on CI timeout.',
392
+ column: 'Now',
393
+ rationale: 'Blocks release.',
394
+ },
395
+ ],
329
396
  },
330
397
  },
331
398
  },
@@ -340,7 +407,9 @@ const DESCRIPTORS: HtmlPrimitiveDescriptor[] = [
340
407
  kind: 'config-editor',
341
408
  title: 'Feature Flags',
342
409
  data: {
343
- flags: [{ key: 'newCheckout', label: 'New checkout', area: 'Checkout', enabled: false, requires: ['paymentsV2'] }],
410
+ flags: [
411
+ { key: 'newCheckout', label: 'New checkout', area: 'Checkout', enabled: false, requires: ['paymentsV2'] },
412
+ ],
344
413
  },
345
414
  },
346
415
  },
@@ -381,36 +450,57 @@ function strings(value: unknown): string[] {
381
450
  return value.map((item) => text(item).trim()).filter(Boolean);
382
451
  }
383
452
 
384
- function fieldRecords(data: Record<string, unknown>, key: string, fallback: Record<string, unknown>[]): Record<string, unknown>[] {
453
+ function fieldRecords(
454
+ data: Record<string, unknown>,
455
+ key: string,
456
+ fallback: Record<string, unknown>[],
457
+ ): Record<string, unknown>[] {
385
458
  const found = records(data[key]);
386
459
  return found.length > 0 ? found : fallback;
387
460
  }
388
461
 
389
462
  const DEFAULT_DECK_SLIDES: Record<string, unknown>[] = [
390
- { title: 'HTML keeps humans in the loop', kicker: 'Thesis', bullets: ['Higher information density', 'Visual clarity', 'Two-way interaction'] },
391
- { title: 'Use the lightest tier that works', bullets: ['json-render for structured UI', 'html primitives for rich documents', 'web artifacts for full React apps'] },
463
+ {
464
+ title: 'HTML keeps humans in the loop',
465
+ kicker: 'Thesis',
466
+ bullets: ['Higher information density', 'Visual clarity', 'Two-way interaction'],
467
+ },
468
+ {
469
+ title: 'Use the lightest tier that works',
470
+ bullets: [
471
+ 'json-render for structured UI',
472
+ 'html primitives for rich documents',
473
+ 'web artifacts for full React apps',
474
+ ],
475
+ },
392
476
  ];
393
477
 
394
478
  const DEFAULT_PRESENTATION_SLIDES: Record<string, unknown>[] = [
395
- { title: 'Set the frame', kicker: '01', body: 'Open with the decision, audience, and outcome this presentation supports.' },
396
- { title: 'Show the evidence', kicker: '02', bullets: ['Use concrete facts', 'Keep one idea per slide', 'Make risks visible'] },
479
+ {
480
+ title: 'Set the frame',
481
+ kicker: '01',
482
+ body: 'Open with the decision, audience, and outcome this presentation supports.',
483
+ },
484
+ {
485
+ title: 'Show the evidence',
486
+ kicker: '02',
487
+ bullets: ['Use concrete facts', 'Keep one idea per slide', 'Make risks visible'],
488
+ },
397
489
  { title: 'Close with action', kicker: '03', bullets: ['Decision needed', 'Owner and next step', 'Timing'] },
398
490
  ];
399
491
 
400
- function presentationSlides(data: Record<string, unknown>, fallback = DEFAULT_PRESENTATION_SLIDES): Record<string, unknown>[] {
492
+ function presentationSlides(
493
+ data: Record<string, unknown>,
494
+ fallback = DEFAULT_PRESENTATION_SLIDES,
495
+ ): Record<string, unknown>[] {
401
496
  return fieldRecords(data, 'slides', fallback);
402
497
  }
403
498
 
404
- function enrichPresentationData(
405
- kind: HtmlPrimitiveKind,
406
- data: Record<string, unknown>,
407
- ): Record<string, unknown> {
499
+ function enrichPresentationData(kind: HtmlPrimitiveKind, data: Record<string, unknown>): Record<string, unknown> {
408
500
  if (kind !== 'deck' && kind !== 'presentation') return data;
409
501
  const slides = presentationSlides(data, kind === 'deck' ? DEFAULT_DECK_SLIDES : DEFAULT_PRESENTATION_SLIDES);
410
502
  const slideTitles = slides.map((slide, index) => itemTitle(slide, `Slide ${index + 1}`));
411
- const speakerNotes = slides
412
- .map((slide) => text(slide.note).trim())
413
- .filter(Boolean);
503
+ const speakerNotes = slides.map((slide) => text(slide.note).trim()).filter(Boolean);
414
504
  const theme = kind === 'presentation' ? presentationThemeMetadata(data) : undefined;
415
505
  return {
416
506
  ...data,
@@ -517,7 +607,9 @@ function isPresentationThemeName(value: string): value is PresentationThemeName
517
607
 
518
608
  function parsePresentationThemeName(value: string, field = 'theme'): PresentationThemeName {
519
609
  if (isPresentationThemeName(value)) return value;
520
- throw new Error(`Invalid presentation ${field} "${value}". Use canvas, midnight, paper, aurora, or a custom theme object.`);
610
+ throw new Error(
611
+ `Invalid presentation ${field} "${value}". Use canvas, midnight, paper, aurora, or a custom theme object.`,
612
+ );
521
613
  }
522
614
 
523
615
  function presentationTheme(data: Record<string, unknown>): PresentationThemeTokens {
@@ -526,9 +618,7 @@ function presentationTheme(data: Record<string, unknown>): PresentationThemeToke
526
618
  return PRESENTATION_THEMES[parsePresentationThemeName(raw)];
527
619
  }
528
620
  if (!isRecord(raw)) return PRESENTATION_THEMES.canvas;
529
- const baseName = typeof raw.base === 'string'
530
- ? parsePresentationThemeName(raw.base, 'theme base')
531
- : 'canvas';
621
+ const baseName = typeof raw.base === 'string' ? parsePresentationThemeName(raw.base, 'theme base') : 'canvas';
532
622
  const base = PRESENTATION_THEMES[baseName];
533
623
  const readColor = (key: string, fallback: string): string => {
534
624
  const value = text(raw[key]);
@@ -570,7 +660,9 @@ function number(value: unknown, fallback = 0): number {
570
660
  function inlineSvgShape(shape: Record<string, unknown>, index: number): string {
571
661
  const color = safeCssColor(text(shape.color, '#4a9eff'));
572
662
  const label = text(shape.text, text(shape.label));
573
- const labelText = label ? `<text x="${number(shape.x, number(shape.cx, 80)) + 12}" y="${number(shape.y, number(shape.cy, 80)) + 28}" fill="var(--color-text, #e8edf2)" font-size="13" font-weight="700">${escapeHtml(label)}</text>` : '';
663
+ const labelText = label
664
+ ? `<text x="${number(shape.x, number(shape.cx, 80)) + 12}" y="${number(shape.y, number(shape.cy, 80)) + 28}" fill="var(--color-text, #e8edf2)" font-size="13" font-weight="700">${escapeHtml(label)}</text>`
665
+ : '';
574
666
  switch (text(shape.type, 'rect')) {
575
667
  case 'circle':
576
668
  return `<circle cx="${number(shape.cx, 80 + index * 40)}" cy="${number(shape.cy, 80)}" r="${number(shape.r, 32)}" fill="${color}" opacity="0.22" stroke="${color}" stroke-width="2" />${label ? `<text x="${number(shape.cx, 80 + index * 40)}" y="${number(shape.cy, 80) + 4}" text-anchor="middle" fill="var(--color-text, #e8edf2)" font-size="13" font-weight="700">${escapeHtml(label)}</text>` : ''}`;
@@ -597,13 +689,23 @@ function itemTitle(item: Record<string, unknown>, fallback: string): string {
597
689
 
598
690
  function badge(value: string): string {
599
691
  const normalized = value.toLowerCase();
600
- const tone = normalized.includes('block') || normalized.includes('fail') || normalized.includes('danger') || normalized.includes('critical')
601
- ? 'danger'
602
- : normalized.includes('warn') || normalized.includes('risk') || normalized.includes('progress') || normalized.includes('later')
603
- ? 'warn'
604
- : normalized.includes('ok') || normalized.includes('done') || normalized.includes('ship') || normalized.includes('green')
605
- ? 'ok'
606
- : 'info';
692
+ const tone =
693
+ normalized.includes('block') ||
694
+ normalized.includes('fail') ||
695
+ normalized.includes('danger') ||
696
+ normalized.includes('critical')
697
+ ? 'danger'
698
+ : normalized.includes('warn') ||
699
+ normalized.includes('risk') ||
700
+ normalized.includes('progress') ||
701
+ normalized.includes('later')
702
+ ? 'warn'
703
+ : normalized.includes('ok') ||
704
+ normalized.includes('done') ||
705
+ normalized.includes('ship') ||
706
+ normalized.includes('green')
707
+ ? 'ok'
708
+ : 'info';
607
709
  return `<span class="badge ${tone}">${escapeHtml(value)}</span>`;
608
710
  }
609
711
 
@@ -727,11 +829,31 @@ ${input.script ?? ''}
727
829
 
728
830
  function renderChoiceGrid({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
729
831
  const items = fieldRecords(data, 'items', [
730
- { title: 'Option A', summary: 'Conservative path with minimal code churn.', tradeoff: 'Lower upside, lower risk.', pros: ['Quick to ship', 'Easy to review'], cons: ['Less flexible'] },
731
- { title: 'Option B', summary: 'Balanced refactor with clearer seams.', tradeoff: 'More code touched.', pros: ['Better long-term shape'], cons: ['Requires more tests'] },
732
- { title: 'Option C', summary: 'Purpose-built new layer.', tradeoff: 'Best UX, highest implementation cost.', pros: ['Distinctive output'], cons: ['More maintenance'] },
832
+ {
833
+ title: 'Option A',
834
+ summary: 'Conservative path with minimal code churn.',
835
+ tradeoff: 'Lower upside, lower risk.',
836
+ pros: ['Quick to ship', 'Easy to review'],
837
+ cons: ['Less flexible'],
838
+ },
839
+ {
840
+ title: 'Option B',
841
+ summary: 'Balanced refactor with clearer seams.',
842
+ tradeoff: 'More code touched.',
843
+ pros: ['Better long-term shape'],
844
+ cons: ['Requires more tests'],
845
+ },
846
+ {
847
+ title: 'Option C',
848
+ summary: 'Purpose-built new layer.',
849
+ tradeoff: 'Best UX, highest implementation cost.',
850
+ pros: ['Distinctive output'],
851
+ cons: ['More maintenance'],
852
+ },
733
853
  ]);
734
- const body = `<section class="grid">${items.map((item, index) => `
854
+ const body = `<section class="grid">${items
855
+ .map(
856
+ (item, index) => `
735
857
  <article class="card ${index === 0 ? 'emphasis' : ''}">
736
858
  <div class="small">Choice ${index + 1}</div>
737
859
  <h2>${escapeHtml(itemTitle(item, `Option ${index + 1}`))}</h2>
@@ -739,7 +861,9 @@ function renderChoiceGrid({ title, data, descriptor }: Parameters<PrimitiveRende
739
861
  ${text(item.tradeoff) ? `<p><strong>Tradeoff:</strong> ${escapeHtml(text(item.tradeoff))}</p>` : ''}
740
862
  <div class="two"><div><h3>Pros</h3>${list(strings(item.pros))}</div><div><h3>Cons</h3>${list(strings(item.cons))}</div></div>
741
863
  ${codeBlock(item.code)}
742
- </article>`).join('')}</section>`;
864
+ </article>`,
865
+ )
866
+ .join('')}</section>`;
743
867
  return page({ title, kind: 'choice-grid', summary: descriptor.description, data, body });
744
868
  }
745
869
 
@@ -755,10 +879,16 @@ function renderPlanTimeline({ title, data, descriptor }: Parameters<PrimitiveRen
755
879
  { from: 'Primitive catalog', to: 'HTML node', label: 'renders iframe HTML' },
756
880
  { from: 'Human', to: 'Agent', label: 'exports edited JSON/prompt' },
757
881
  ]);
758
- const risks = fieldRecords(data, 'risks', [{ risk: 'Overly generic output', mitigation: 'Use named primitives with clear use cases.' }]);
882
+ const risks = fieldRecords(data, 'risks', [
883
+ { risk: 'Overly generic output', mitigation: 'Use named primitives with clear use cases.' },
884
+ ]);
759
885
  const snippets = fieldRecords(data, 'snippets', []);
760
- const body = `<section class="two"><div class="panel"><h2>Milestones</h2><div class="timeline">${milestones.map((item, index) => `
761
- <div class="step"><div class="dot">${index + 1}</div><div class="card"><h3>${escapeHtml(itemTitle(item, `Milestone ${index + 1}`))} ${badge(text(item.status, 'planned'))}</h3><p>${escapeHtml(text(item.detail, 'Add implementation detail.'))}</p></div></div>`).join('')}</div></div>
886
+ const body = `<section class="two"><div class="panel"><h2>Milestones</h2><div class="timeline">${milestones
887
+ .map(
888
+ (item, index) => `
889
+ <div class="step"><div class="dot">${index + 1}</div><div class="card"><h3>${escapeHtml(itemTitle(item, `Milestone ${index + 1}`))} ${badge(text(item.status, 'planned'))}</h3><p>${escapeHtml(text(item.detail, 'Add implementation detail.'))}</p></div></div>`,
890
+ )
891
+ .join('')}</div></div>
762
892
  <div class="panel"><h2>Data Flow</h2><div class="flow">${flow.map((item, index) => `<div class="flow-node"><h3>${escapeHtml(text(item.from, `Step ${index + 1}`))}</h3><p>${escapeHtml(text(item.label, 'flows to'))}</p><strong>${escapeHtml(text(item.to, 'Next'))}</strong></div>${index < flow.length - 1 ? '<div class="arrow">-></div>' : ''}`).join('')}</div><h2 style="margin-top:18px">Risks</h2>${risks.map((item) => `<div class="card"><strong>${escapeHtml(text(item.risk, 'Risk'))}</strong><p>${escapeHtml(text(item.mitigation, 'Mitigation'))}</p></div>`).join('')}</div></section>
763
893
  ${snippets.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Code Checkpoints</h2>${snippets.map((item) => `<h3>${escapeHtml(itemTitle(item, 'Snippet'))}</h3>${codeBlock(item.code)}`).join('')}</section>` : ''}`;
764
894
  return page({ title, kind: 'plan-timeline', summary: descriptor.description, data, body });
@@ -766,19 +896,31 @@ function renderPlanTimeline({ title, data, descriptor }: Parameters<PrimitiveRen
766
896
 
767
897
  function renderReviewSheet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
768
898
  const findings = fieldRecords(data, 'findings', [
769
- { severity: 'warning', title: 'Review focus', file: 'src/example.ts', line: 1, detail: 'Explain the risky behavior and what to inspect.' },
899
+ {
900
+ severity: 'warning',
901
+ title: 'Review focus',
902
+ file: 'src/example.ts',
903
+ line: 1,
904
+ detail: 'Explain the risky behavior and what to inspect.',
905
+ },
770
906
  ]);
771
907
  const files = fieldRecords(data, 'files', []);
772
908
  const diff = text(data.diff);
773
- const body = `<section class="two"><div class="panel"><h2>Findings</h2>${findings.map((item) => `
774
- <article class="card"><h3>${badge(text(item.severity, 'info'))} ${escapeHtml(itemTitle(item, 'Finding'))}</h3><p class="small">${escapeHtml([text(item.file), text(item.line)].filter(Boolean).join(':'))}</p><p>${escapeHtml(text(item.detail, 'Add review note.'))}</p></article>`).join('')}</div>
909
+ const body = `<section class="two"><div class="panel"><h2>Findings</h2>${findings
910
+ .map(
911
+ (item) => `
912
+ <article class="card"><h3>${badge(text(item.severity, 'info'))} ${escapeHtml(itemTitle(item, 'Finding'))}</h3><p class="small">${escapeHtml([text(item.file), text(item.line)].filter(Boolean).join(':'))}</p><p>${escapeHtml(text(item.detail, 'Add review note.'))}</p></article>`,
913
+ )
914
+ .join('')}</div>
775
915
  <div class="panel"><h2>Review Tour</h2>${files.length > 0 ? files.map((item) => `<article class="card"><h3>${escapeHtml(text(item.path, 'File'))}</h3><p>${escapeHtml(text(item.why, 'Why this file matters.'))}</p></article>`).join('') : '<p class="muted">Add files with path and why fields for a guided review.</p>'}</div></section>
776
916
  ${diff ? `<section class="panel" style="margin-top:14px"><h2>Diff Excerpt</h2>${codeBlock(diff)}</section>` : ''}`;
777
917
  return page({ title, kind: 'review-sheet', summary: descriptor.description, data, body });
778
918
  }
779
919
 
780
920
  function renderPrWriteup({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
781
- const files = fieldRecords(data, 'files', [{ path: 'src/example.ts', why: 'Core behavior changed here.', focus: 'Review edge cases and tests.' }]);
921
+ const files = fieldRecords(data, 'files', [
922
+ { path: 'src/example.ts', why: 'Core behavior changed here.', focus: 'Review edge cases and tests.' },
923
+ ]);
782
924
  const body = `<section class="panel"><h2>Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize the change in one reviewer-friendly paragraph.'))}</p><p>${escapeHtml(text(data.why, 'Explain why this change matters now.'))}</p></section>
783
925
  <section class="two" style="margin-top:14px"><div class="card"><h2>Before</h2>${list(fieldStrings(data, 'before', ['Current behavior or pain point.']))}</div><div class="card emphasis"><h2>After</h2>${list(fieldStrings(data, 'after', ['New behavior or reviewer-visible outcome.']))}</div></section>
784
926
  <section class="three" style="margin-top:14px"><div class="panel"><h2>File Tour</h2>${files.map((file) => `<details open><summary><strong>${escapeHtml(text(file.path, 'File'))}</strong></summary><p>${escapeHtml(text(file.why, 'Why this file matters.'))}</p><p class="small">Focus: ${escapeHtml(text(file.focus, 'Review behavior and tests.'))}</p></details>`).join('')}</div>
@@ -831,7 +973,9 @@ function renderCodeWalkthrough({ title, data, descriptor }: Parameters<Primitive
831
973
  { id: 'core', title: 'Core Logic', detail: 'State change or main computation.', role: 'core' },
832
974
  { id: 'view', title: 'Renderer', detail: 'User-visible result.', role: 'view' },
833
975
  ]);
834
- const steps = fieldRecords(data, 'steps', [{ title: 'Trace the path', file: 'src/example.ts', detail: 'Explain the first important hop.', code: '' }]);
976
+ const steps = fieldRecords(data, 'steps', [
977
+ { title: 'Trace the path', file: 'src/example.ts', detail: 'Explain the first important hop.', code: '' },
978
+ ]);
835
979
  const keyFiles = fieldRecords(data, 'keyFiles', []);
836
980
  const edges = fieldRecords(data, 'edges', []);
837
981
  const body = `<section class="panel"><h2>Path Summary</h2><p>${escapeHtml(text(data.summary, 'Explain the code path this walkthrough covers.'))}</p><div class="flow" style="margin-top:12px">${modules.map((module) => `<div class="flow-node"><div class="small">${escapeHtml(text(module.role, text(module.id, 'module')))}</div><h3>${escapeHtml(itemTitle(module, 'Module'))}</h3><p>${escapeHtml(text(module.detail, ''))}</p></div>`).join('<div class="arrow">-></div>')}</div>${edges.length > 0 ? `<p class="small" style="margin-top:10px">Edges: ${escapeHtml(edges.map((edge) => `${text(edge.from)} -> ${text(edge.to)}${text(edge.label) ? ` (${text(edge.label)})` : ''}`).join(', '))}</p>` : ''}</section>
@@ -843,14 +987,26 @@ function renderCodeWalkthrough({ title, data, descriptor }: Parameters<Primitive
843
987
 
844
988
  function renderDesignSheet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
845
989
  const directions = fieldRecords(data, 'directions', [
846
- { title: 'Editorial dense', tone: 'serious, information-rich', palette: ['#f4efe5', '#17120f', '#c84f2f'], rationale: 'Good when humans need to compare many details quickly.' },
847
- { title: 'Control-room dark', tone: 'operational, high contrast', palette: ['#07111d', '#dce8f2', '#4a9eff'], rationale: 'Good for dashboards and incident views.' },
990
+ {
991
+ title: 'Editorial dense',
992
+ tone: 'serious, information-rich',
993
+ palette: ['#f4efe5', '#17120f', '#c84f2f'],
994
+ rationale: 'Good when humans need to compare many details quickly.',
995
+ },
996
+ {
997
+ title: 'Control-room dark',
998
+ tone: 'operational, high contrast',
999
+ palette: ['#07111d', '#dce8f2', '#4a9eff'],
1000
+ rationale: 'Good for dashboards and incident views.',
1001
+ },
848
1002
  ]);
849
1003
  const tokens = fieldRecords(data, 'tokens', []);
850
- const body = `<section class="grid">${directions.map((item) => {
851
- const palette = strings(item.palette);
852
- return `<article class="card"><h2>${escapeHtml(itemTitle(item, 'Direction'))}</h2><p>${escapeHtml(text(item.tone, 'Tone'))}</p><div class="swatches">${palette.map((color) => `<span class="swatch" title="${escapeHtml(color)}" style="background:${safeCssColor(color)}"></span>`).join('')}</div><p>${escapeHtml(text(item.rationale, 'Rationale'))}</p></article>`;
853
- }).join('')}</section>
1004
+ const body = `<section class="grid">${directions
1005
+ .map((item) => {
1006
+ const palette = strings(item.palette);
1007
+ return `<article class="card"><h2>${escapeHtml(itemTitle(item, 'Direction'))}</h2><p>${escapeHtml(text(item.tone, 'Tone'))}</p><div class="swatches">${palette.map((color) => `<span class="swatch" title="${escapeHtml(color)}" style="background:${safeCssColor(color)}"></span>`).join('')}</div><p>${escapeHtml(text(item.rationale, 'Rationale'))}</p></article>`;
1008
+ })
1009
+ .join('')}</section>
854
1010
  ${tokens.length > 0 ? `<section class="panel" style="margin-top:14px"><h2>Tokens</h2><div class="grid">${tokens.map((item) => `<div class="card"><strong>${escapeHtml(itemTitle(item, 'Token'))}</strong><p>${escapeHtml(text(item.value, ''))}</p></div>`).join('')}</div></section>` : ''}`;
855
1011
  return page({ title, kind: 'design-sheet', summary: descriptor.description, data, body });
856
1012
  }
@@ -860,7 +1016,13 @@ function renderComponentGallery({ title, data, descriptor }: Parameters<Primitiv
860
1016
  const variants = fieldRecords(data, 'variants', [
861
1017
  { label: 'Primary', state: 'default', intent: 'Main action', example: 'Continue', note: 'High emphasis.' },
862
1018
  { label: 'Secondary', state: 'hover', intent: 'Alternative action', example: 'Back', note: 'Lower emphasis.' },
863
- { label: 'Destructive', state: 'disabled', intent: 'Danger zone', example: 'Delete', note: 'Requires confirmation.' },
1019
+ {
1020
+ label: 'Destructive',
1021
+ state: 'disabled',
1022
+ intent: 'Danger zone',
1023
+ example: 'Delete',
1024
+ note: 'Requires confirmation.',
1025
+ },
864
1026
  ]);
865
1027
  const body = `<section class="panel"><h2>${escapeHtml(component)}</h2><p>Variant contact sheet for fast visual review.</p></section>
866
1028
  <section class="grid" style="margin-top:14px">${variants.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.state, 'state'))} / ${escapeHtml(text(item.intent, 'intent'))}</div><h2>${escapeHtml(itemTitle(item, 'Variant'))}</h2><button type="button">${escapeHtml(text(item.example, itemTitle(item, 'Example')))}</button><p>${escapeHtml(text(item.note, ''))}</p></article>`).join('')}</section>`;
@@ -868,13 +1030,17 @@ function renderComponentGallery({ title, data, descriptor }: Parameters<Primitiv
868
1030
  }
869
1031
 
870
1032
  function renderInteractionPrototype({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
871
- const controls = fieldRecords(data, 'controls', [{ key: 'duration', label: 'Duration', value: 280, min: 100, max: 900, unit: 'ms' }]);
1033
+ const controls = fieldRecords(data, 'controls', [
1034
+ { key: 'duration', label: 'Duration', value: 280, min: 100, max: 900, unit: 'ms' },
1035
+ ]);
872
1036
  const screens = fieldRecords(data, 'screens', [
873
1037
  { title: 'Start', detail: 'Initial state before interaction.' },
874
1038
  { title: 'Active', detail: 'The user has started the interaction.' },
875
1039
  { title: 'Done', detail: 'Final state after completion.' },
876
1040
  ]);
877
- const annotations = fieldRecords(data, 'annotations', [{ title: 'Decision', detail: 'Tune the values until this feels right.' }]);
1041
+ const annotations = fieldRecords(data, 'annotations', [
1042
+ { title: 'Decision', detail: 'Tune the values until this feels right.' },
1043
+ ]);
878
1044
  const body = `<section class="three"><div class="panel"><h2>Stage</h2><p>${escapeHtml(text(data.scenario, 'Describe the interaction this prototype evaluates.'))}</p><div class="flow" style="margin-top:16px">${screens.map((screen, index) => `<button class="flow-node" type="button" data-screen="${index}"><h3>${escapeHtml(itemTitle(screen, 'Screen'))}</h3><p>${escapeHtml(text(screen.detail, ''))}</p></button>`).join('<div class="arrow">-></div>')}</div><div class="card emphasis" id="prototype-readout" style="margin-top:14px">Select a screen to inspect it.</div></div>
879
1045
  <div class="panel"><h2>Controls</h2>${controls.map((control, index) => `<label class="card"><span class="small">${escapeHtml(text(control.key, `control${index}`))}</span><h3>${escapeHtml(text(control.label, 'Control'))}: <span data-control-value="${index}">${escapeHtml(text(control.value, '0'))}</span>${escapeHtml(text(control.unit, ''))}</h3><input type="range" data-control="${index}" min="${number(control.min, 0)}" max="${number(control.max, 1000)}" value="${number(control.value, 0)}"></label>`).join('')}</div>
880
1046
  <div class="panel sticky"><h2>Notes</h2>${annotations.map((item) => `<article class="card"><h3>${escapeHtml(itemTitle(item, 'Note'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></article>`).join('')}<h2 style="margin-top:18px">Questions</h2>${list(fieldStrings(data, 'questions', ['Does the timing feel responsive?', 'What should persist after completion?']))}${codeBlock(data.snippet)}</div></section>`;
@@ -924,11 +1090,23 @@ document.querySelectorAll('[data-step]').forEach((button) => button.addEventList
924
1090
  }
925
1091
 
926
1092
  function renderIllustrationSet({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
927
- const figures = fieldRecords(data, 'figures', [{ title: 'System Loop', caption: 'A small editable SVG-style figure.', shapes: [{ type: 'rect', x: 40, y: 60, width: 160, height: 72, text: 'Agent', color: '#4a9eff' }, { type: 'arrow', x1: 210, y1: 96, x2: 340, y2: 96, color: '#eab308' }, { type: 'circle', cx: 420, cy: 96, r: 42, text: 'Human', color: '#22c55e' }] }]);
928
- const body = `<section class="grid">${figures.map((figure, index) => {
929
- const shapes = records(figure.shapes);
930
- return `<article class="card figure"><h2>${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}</h2><svg viewBox="0 0 560 260" role="img" aria-label="${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}" data-figure="${index}"><defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"></path></marker></defs>${shapes.map(inlineSvgShape).join('')}</svg><p>${escapeHtml(text(figure.caption, ''))}</p><button type="button" data-copy-svg="${index}">Copy SVG</button></article>`;
931
- }).join('')}</section>`;
1093
+ const figures = fieldRecords(data, 'figures', [
1094
+ {
1095
+ title: 'System Loop',
1096
+ caption: 'A small editable SVG-style figure.',
1097
+ shapes: [
1098
+ { type: 'rect', x: 40, y: 60, width: 160, height: 72, text: 'Agent', color: '#4a9eff' },
1099
+ { type: 'arrow', x1: 210, y1: 96, x2: 340, y2: 96, color: '#eab308' },
1100
+ { type: 'circle', cx: 420, cy: 96, r: 42, text: 'Human', color: '#22c55e' },
1101
+ ],
1102
+ },
1103
+ ]);
1104
+ const body = `<section class="grid">${figures
1105
+ .map((figure, index) => {
1106
+ const shapes = records(figure.shapes);
1107
+ return `<article class="card figure"><h2>${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}</h2><svg viewBox="0 0 560 260" role="img" aria-label="${escapeHtml(itemTitle(figure, `Figure ${index + 1}`))}" data-figure="${index}"><defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" markerUnits="strokeWidth"><path d="M0,0 L0,6 L9,3 z" fill="currentColor"></path></marker></defs>${shapes.map(inlineSvgShape).join('')}</svg><p>${escapeHtml(text(figure.caption, ''))}</p><button type="button" data-copy-svg="${index}">Copy SVG</button></article>`;
1108
+ })
1109
+ .join('')}</section>`;
932
1110
  return page({
933
1111
  title,
934
1112
  kind: 'illustration-set',
@@ -973,9 +1151,10 @@ function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRen
973
1151
  const theme = presentationTheme(data);
974
1152
  const accentOverride = safeCssColor(text(data.accent, ''));
975
1153
  const accent = accentOverride === 'transparent' ? theme.accent : accentOverride;
976
- const slideMarkup = slides.map((item, index) => {
977
- const metrics = records(item.metrics);
978
- return `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}">
1154
+ const slideMarkup = slides
1155
+ .map((item, index) => {
1156
+ const metrics = records(item.metrics);
1157
+ return `<article class="slide ${index === 0 ? 'active' : ''}" data-slide="${index}">
979
1158
  <div class="slide-grid ${metrics.length > 0 ? 'with-metrics' : 'without-metrics'}">
980
1159
  <div class="slide-copy">
981
1160
  <div class="kicker">${escapeHtml(text(item.kicker, `Slide ${index + 1}`))}</div>
@@ -987,7 +1166,8 @@ function renderPresentation({ title, data, descriptor }: Parameters<PrimitiveRen
987
1166
  </div>
988
1167
  ${text(item.note) ? `<aside class="speaker-note"><span>Speaker note</span>${escapeHtml(text(item.note))}</aside>` : ''}
989
1168
  </article>`;
990
- }).join('');
1169
+ })
1170
+ .join('');
991
1171
 
992
1172
  return `<!doctype html>
993
1173
  <html lang="en">
@@ -1088,7 +1268,9 @@ showSlide(0);
1088
1268
  }
1089
1269
 
1090
1270
  function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
1091
- const steps = fieldRecords(data, 'steps', [{ title: 'Start here', detail: 'Add request path, data flow, or concept steps.' }]);
1271
+ const steps = fieldRecords(data, 'steps', [
1272
+ { title: 'Start here', detail: 'Add request path, data flow, or concept steps.' },
1273
+ ]);
1092
1274
  const snippets = fieldRecords(data, 'snippets', []);
1093
1275
  const faq = fieldRecords(data, 'faq', []);
1094
1276
  const glossary = fieldRecords(data, 'glossary', []);
@@ -1100,7 +1282,10 @@ function renderExplainer({ title, data, descriptor }: Parameters<PrimitiveRender
1100
1282
  }
1101
1283
 
1102
1284
  function renderStatusReport({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
1103
- const metrics = fieldRecords(data, 'metrics', [{ label: 'Health', value: 'on track', tone: 'ok' }, { label: 'Risk', value: 'medium', tone: 'warn' }]);
1285
+ const metrics = fieldRecords(data, 'metrics', [
1286
+ { label: 'Health', value: 'on track', tone: 'ok' },
1287
+ { label: 'Risk', value: 'medium', tone: 'warn' },
1288
+ ]);
1104
1289
  const body = `<section class="grid">${metrics.map((item) => `<article class="card"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><h2>${escapeHtml(text(item.value, 'Value'))}</h2>${badge(text(item.tone, 'info'))}</article>`).join('')}</section>
1105
1290
  <section class="grid" style="margin-top:14px"><article class="card"><h2>Shipped</h2>${list(fieldStrings(data, 'shipped', ['Add shipped items.']))}</article><article class="card"><h2>Slipped</h2>${list(fieldStrings(data, 'slipped', ['Add slipped items.']))}</article><article class="card"><h2>Risks</h2>${list(fieldStrings(data, 'risks', ['Add risks.']))}</article><article class="card"><h2>Next</h2>${list(fieldStrings(data, 'next', ['Add next actions.']))}</article></section>`;
1106
1291
  return page({ title, kind: 'status-report', summary: descriptor.description, data, body });
@@ -1112,8 +1297,12 @@ function renderIncidentReport({ title, data, descriptor }: Parameters<PrimitiveR
1112
1297
  { label: 'Status', value: text(data.status, 'resolved'), tone: 'ok' },
1113
1298
  { label: 'Duration', value: text(data.duration, 'unknown'), tone: 'info' },
1114
1299
  ]);
1115
- const timeline = fieldRecords(data, 'timeline', [{ time: '00:00', event: 'Incident started', detail: 'Add the first observed signal.', tone: 'warn' }]);
1116
- const actions = fieldRecords(data, 'actions', [{ done: false, owner: 'Unassigned', description: 'Add follow-up action.', due: 'TBD' }]);
1300
+ const timeline = fieldRecords(data, 'timeline', [
1301
+ { time: '00:00', event: 'Incident started', detail: 'Add the first observed signal.', tone: 'warn' },
1302
+ ]);
1303
+ const actions = fieldRecords(data, 'actions', [
1304
+ { done: false, owner: 'Unassigned', description: 'Add follow-up action.', due: 'TBD' },
1305
+ ]);
1117
1306
  const body = `<section class="grid">${impact.map((item) => `<article class="card metric"><div class="small">${escapeHtml(text(item.label, 'Metric'))}</div><strong>${escapeHtml(text(item.value, 'Value'))}</strong>${badge(text(item.tone, 'info'))}</article>`).join('')}</section>
1118
1307
  <section class="panel" style="margin-top:14px"><h2>Executive Summary</h2><p>${escapeHtml(text(data.summary, 'Summarize user impact, detection, and resolution.'))}</p></section>
1119
1308
  <section class="three" style="margin-top:14px"><div class="panel"><h2>Timeline</h2><div class="timeline">${timeline.map((item, index) => `<div class="step"><div class="dot">${escapeHtml(text(item.time, String(index + 1)))}</div><div class="card"><h3>${escapeHtml(text(item.event, 'Event'))} ${badge(text(item.tone, 'info'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p></div></div>`).join('')}</div></div>
@@ -1137,10 +1326,33 @@ document.querySelector('[data-copy-actions]')?.addEventListener('click', () => c
1137
1326
  function renderTriageBoard({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
1138
1327
  const columns = fieldStrings(data, 'columns', ['Now', 'Next', 'Later', 'Cut']);
1139
1328
  const items = fieldRecords(data, 'items', [
1140
- { title: 'Clarify requirements', detail: 'Human should decide scope boundary.', column: 'Now', rationale: 'Blocks accurate implementation.' },
1141
- { title: 'Polish visuals', detail: 'Improve hierarchy after behavior lands.', column: 'Next', rationale: 'Useful but not blocking.' },
1329
+ {
1330
+ title: 'Clarify requirements',
1331
+ detail: 'Human should decide scope boundary.',
1332
+ column: 'Now',
1333
+ rationale: 'Blocks accurate implementation.',
1334
+ },
1335
+ {
1336
+ title: 'Polish visuals',
1337
+ detail: 'Improve hierarchy after behavior lands.',
1338
+ column: 'Next',
1339
+ rationale: 'Useful but not blocking.',
1340
+ },
1142
1341
  ]);
1143
- const body = `<section class="columns">${columns.map((column) => `<div class="column" data-column="${escapeHtml(column)}"><h2>${escapeHtml(column)}</h2>${items.filter((item) => text(item.column, columns[0]) === column).map((item, index) => `<article class="card ticket" draggable="true" data-ticket="${index}"><h3>${escapeHtml(itemTitle(item, 'Item'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p><p class="small">${escapeHtml(text(item.rationale, ''))}</p></article>`).join('')}</div>`).join('')}</section><p class="small" style="margin-top:12px">Drag cards between columns, then copy JSON or markdown.</p><button type="button" data-copy-markdown>Copy markdown</button>`;
1342
+ const body = `<section class="columns">${columns
1343
+ .map(
1344
+ (column) =>
1345
+ `<div class="column" data-column="${escapeHtml(column)}"><h2>${escapeHtml(column)}</h2>${items
1346
+ .filter((item) => text(item.column, columns[0]) === column)
1347
+ .map(
1348
+ (item, index) =>
1349
+ `<article class="card ticket" draggable="true" data-ticket="${index}"><h3>${escapeHtml(itemTitle(item, 'Item'))}</h3><p>${escapeHtml(text(item.detail, ''))}</p><p class="small">${escapeHtml(text(item.rationale, ''))}</p></article>`,
1350
+ )
1351
+ .join('')}</div>`,
1352
+ )
1353
+ .join(
1354
+ '',
1355
+ )}</section><p class="small" style="margin-top:12px">Drag cards between columns, then copy JSON or markdown.</p><button type="button" data-copy-markdown>Copy markdown</button>`;
1144
1356
  return page({
1145
1357
  title,
1146
1358
  kind: 'triage-board',
@@ -1178,8 +1390,21 @@ document.querySelector('[data-copy-markdown]')?.addEventListener('click', () =>
1178
1390
 
1179
1391
  function renderConfigEditor({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
1180
1392
  const flags = fieldRecords(data, 'flags', [
1181
- { key: 'paymentsV2', label: 'Payments V2', area: 'Checkout', enabled: true, description: 'Required for the new checkout path.' },
1182
- { key: 'newCheckout', label: 'New checkout', area: 'Checkout', enabled: false, requires: ['paymentsV2'], description: 'Routes users through the new flow.' },
1393
+ {
1394
+ key: 'paymentsV2',
1395
+ label: 'Payments V2',
1396
+ area: 'Checkout',
1397
+ enabled: true,
1398
+ description: 'Required for the new checkout path.',
1399
+ },
1400
+ {
1401
+ key: 'newCheckout',
1402
+ label: 'New checkout',
1403
+ area: 'Checkout',
1404
+ enabled: false,
1405
+ requires: ['paymentsV2'],
1406
+ description: 'Routes users through the new flow.',
1407
+ },
1183
1408
  ]);
1184
1409
  const body = `<section class="grid">${flags.map((flag, index) => `<label class="card"><div class="small">${escapeHtml(text(flag.area, 'General'))}</div><h3><input type="checkbox" data-flag="${index}" ${flag.enabled === true ? 'checked' : ''}> ${escapeHtml(text(flag.label, text(flag.key, `Flag ${index + 1}`)))}</h3><p>${escapeHtml(text(flag.description, ''))}</p><p class="small">Key: ${escapeHtml(text(flag.key, 'unknown'))}${strings(flag.requires).length > 0 ? ` / requires: ${escapeHtml(strings(flag.requires).join(', '))}` : ''}</p><p class="small" data-warning="${index}"></p></label>`).join('')}</section><button type="button" data-copy-diff style="margin-top:12px">Copy diff</button>`;
1185
1410
  return page({
@@ -1212,8 +1437,13 @@ refreshWarnings();`,
1212
1437
  }
1213
1438
 
1214
1439
  function renderPromptTuner({ title, data, descriptor }: Parameters<PrimitiveRenderer>[0]): string {
1215
- const template = text(data.template, 'Explain {{feature}} for {{audience}}. Include the tradeoffs and one concrete example.');
1216
- const samples = fieldRecords(data, 'samples', [{ name: 'Default', variables: { feature: 'PMX Canvas pins', audience: 'coding agents' } }]);
1440
+ const template = text(
1441
+ data.template,
1442
+ 'Explain {{feature}} for {{audience}}. Include the tradeoffs and one concrete example.',
1443
+ );
1444
+ const samples = fieldRecords(data, 'samples', [
1445
+ { name: 'Default', variables: { feature: 'PMX Canvas pins', audience: 'coding agents' } },
1446
+ ]);
1217
1447
  const body = `<section class="two"><div class="panel"><h2>Template</h2><textarea id="template">${escapeHtml(template)}</textarea><p class="small"><span id="char-count">0</span> characters</p></div><div class="panel"><h2>Live Samples</h2><div id="previews"></div><button type="button" data-copy-template>Copy template</button></div></section>`;
1218
1448
  return page({
1219
1449
  title,
@@ -1296,9 +1526,8 @@ export function buildHtmlPrimitive(input: HtmlPrimitiveInput): HtmlPrimitiveBuil
1296
1526
  const data = enrichPresentationData(input.kind, input.data ?? {});
1297
1527
  const renderer = RENDERERS[input.kind];
1298
1528
  const slideTitles = strings(data.slideTitles);
1299
- const summary = slideTitles.length > 0
1300
- ? `${descriptor.description} Slides: ${slideTitles.join(', ')}.`
1301
- : descriptor.description;
1529
+ const summary =
1530
+ slideTitles.length > 0 ? `${descriptor.description} Slides: ${slideTitles.join(', ')}.` : descriptor.description;
1302
1531
  return {
1303
1532
  kind: input.kind,
1304
1533
  title,