@clearlysh/beehaven 0.8.27 → 0.8.30

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.
@@ -22,6 +22,10 @@ stored. A rendered image you have inspected is the evidence for visual quality.
22
22
  For a small correction, keep the existing direction and make the smallest useful edit.
23
23
  For a new composition or substantial redesign, use the workflow below.
24
24
 
25
+ Keep the requested scope. A landing concept with a logo needs its page layouts and a usable
26
+ logo; add application boards or a full guideline system only when the brief calls for them.
27
+ Read only relevant references. Save large tool responses once and print compact summaries.
28
+
25
29
  ## The studio bar — what "done" means, every time
26
30
 
27
31
  Load **`design-craft`** before any new visual work: grid, type scale, colour roles,
@@ -33,8 +37,9 @@ A composition is done when every requested artboard meets all four conditions:
33
37
  1. **`canvas.review` verdict is `clean` or `look`.** `fix` means a probable defect
34
38
  remains: text colliding or covered, contrast under WCAG (4.5:1 body, 3:1 display),
35
39
  type below the artboard's legible floor, missing media, text overflowing its box,
36
- placeholder copy. Fix it. When it is deliberate, pass the check name or layer in
37
- `ignore` and say why in the handoff.
40
+ placeholder copy. Fix it. For an intentional finding, use
41
+ `accept:[{check,ids:[node IDs],reason}]`. Accepted findings stay visible and return `look`.
42
+ Legacy `ignore` can suppress multiple checks and is not an unconditional pass.
38
43
  2. **Every `look` finding was looked at**: type-size and palette sprawl, near-miss
39
44
  alignment, copy against the trim, text over imagery. Act on it or say why not.
40
45
  3. **You inspected the rendered PNG** against design-craft's studio pass, covering what no
@@ -54,8 +59,9 @@ build from them — redrawing a button the library already has is how designs dr
54
59
  product they describe.
55
60
 
56
61
  ```sh
57
- beehaven call brand-tokens '{}' # colours (light + dark), type roles, radius, logos, voice
58
- beehaven call component-list '{"query":"<brand or kind>"}' # parts, with props, variants and the code they mirror
62
+ beehaven call brand-tokens '{"compact":true}' # roles without repeated CSS/prose
63
+ beehaven call brand-import '{"url":"theirsite.com","dryRun":true}' # no brand yet? read it off their website
64
+ beehaven call component-list '{"query":"<kind>","compact":true}' # IDs, dimensions and prop names
59
65
  beehaven call component-instantiate '{"compositionId":"<id>","instances":[
60
66
  {"id":"<component>","x":96,"y":200,"variant":"Light"},
61
67
  {"id":"<component>","x":96,"y":280,"variant":"Light","props":{"label":"Save changes"}}]}'
@@ -68,8 +74,9 @@ beehaven call component-instantiate '{"compositionId":"<id>","instances":[
68
74
  `component-create` (or `{ fromCompositionId, nodeId }` to save one you drew).
69
75
  - Component node coordinates are relative to the component ROOT, even under an inner
70
76
  frame — the opposite of `canvas.create-node`, where x/y follow `parentId`.
71
- - If the workspace has a brand kit, it IS the system for the brief: use its roles, faces
72
- and voice rather than inventing a palette. Change the brand itself only when asked.
77
+ - For Clearly product UI, use its brand kit. For a separate client, use that client's kit
78
+ and supplied assets. A requested new client identity is scoped to that composition;
79
+ it does not change the workspace's default brand.
73
80
 
74
81
  **UX design before implementation.** When the work is a change to a product's UI, the
75
82
  composition is the design the code will answer to: show the real states (empty, loading,
@@ -103,10 +110,46 @@ can read and edit artifact records, while canvas-specific actions use the CLI.
103
110
  beehaven env
104
111
  beehaven connect home
105
112
  beehaven agent login <name> --label "Design <artifact>" --client cli
106
- beehaven call canvas-perceive '{"compositionId":"<id>","format":"json"}'
113
+ beehaven call canvas-perceive '{"compositionId":"<id>","outline":true}'
107
114
  beehaven call canvas-catalog '{"query":"create-node"}'
108
115
  ```
109
116
 
117
+ Read a canvas the way you read a repository: `canvas-perceive {outline:true}` is `tree` (artboards,
118
+ sections, layer counts, first copy, EMPTY sections — a few hundred bytes); `{frame:"<id or name>"}`
119
+ opens one frame like a file (its layers indented, positions relative to their parent);
120
+ `{find:"Start free"}` is `grep` (copy, layer names or `#hex`, with each match's frame path). Use
121
+ `format:"json"` only when you need every field of every node. Every `canvas-act` write returns
122
+ `diagnostics` — a lint of the artboards it touched (text overflow, collisions, covered text,
123
+ contrast) — treat them like compiler errors and fix them in the next batch. `canvas.review` is still
124
+ the pixel check and the ticket gate.
125
+
126
+ A canvas is planned like a git repository: its own plan board, a WORK TICKET for the request, and
127
+ COMMITS — one ticket per coherent layer or section of a larger change.
128
+ `design-start {title, brandId?}` makes a PRIVATE DRAFT canvas, bound to the brand, with its private
129
+ plan board and the work ticket, and returns `{ compositionId, ticketId, ticketKey, boardId, brand }`.
130
+ Same title again returns the same draft. Nothing is shared until it is filed:
131
+ `design-file {compositionId, projectId | projectTitle}` moves the canvas — plan board and every
132
+ ticket with it — into the project the person chose and sends the work ticket to in-review. Ask which
133
+ project unless they named one; a close-but-different title answers `similar-projects` with
134
+ candidates. Repeat-safe.
135
+ Plan a larger change before drawing, the way you would split a big code change into commits:
136
+ `design-plan {compositionId, commits: ["Grid, type and colour styles", "Nav", "Hero", …]}` (drafts
137
+ and filed canvases alike) adds ordered commit tickets under the work ticket. `sections: true` uses
138
+ the landing-page set (set up frames and styles, nav bar, hero, highlights, offer, story, call to
139
+ action, footer, mobile, states); `phases: true` and a `brief` add the studio method (brief, research,
140
+ structure, direction, design, review) — load the `design-process` skill. Order commits so each
141
+ builds on the last and can be reviewed alone. Build ONE commit per canvas-act batch that ends with
142
+ `canvas.review` of the artboard and carries
143
+ `"ticket":{"id":"<key>","status":"done","resolution":"<what changed>"}` — it lands only if that
144
+ review is clean or look; `result.ticket` says whether it did, and the ticket logs the canvas
145
+ revision and the layers the commit changed. Auto-layout, gradients, shadows, drawing and placeholders:
146
+ see "Technique" in [authoring.md](authoring.md).
147
+
148
+ `composition-board-get {compositionId}` reads the plan back. `design-start` with a project, ticket or
149
+ `phases` still sets up planned work directly (kept for one release).
150
+ For an existing task, reuse its ticket/project. Check one unknown action with
151
+ `beehaven explain <action>` or a filtered catalog; do not download the entire registry.
152
+
110
153
  Use the composition supplied by the user. If none is named, inspect recent titles and
111
154
  context; create a clearly titled composition for new work instead of modifying an
112
155
  arbitrary recent canvas. File shared work into its project.
@@ -161,8 +204,11 @@ hand-author hundreds of coordinates in a chat message.
161
204
  Check contrast on the actual ground. Use the user's real brand assets.
162
205
  - Choose relevant imagery, a meaningful diagram, a product view or a deliberate
163
206
  typographic treatment. Decorative blobs and random stock photos do not prove a
164
- product claim. Native SVG/paths suit diagrams and vector art; raster generation
165
- suits imagery when an image tool is available.
207
+ product claim. Draw marks, icons, illustrations and diagrams yourself from native
208
+ layers (shapes, vector.create pen paths, text, groups), the way a designer works in
209
+ Figma, so every part stays named and editable. Raster generation suits photographic
210
+ imagery when an image tool is available. Raw SVG (an svg node, or a generated SVG)
211
+ is for importing an existing file or a requested SVG, never the default.
166
212
  - Define repeated components once. Vary composition where the content changes.
167
213
  A deck of identical cards has little rhythm even when every card aligns.
168
214
  - Keep important copy and layers editable. Do not flatten the whole composition
@@ -188,6 +234,10 @@ For a single page, establish the complete hierarchy before polishing small detai
188
234
  Use a batch for each coherent build or revision, with descriptive unique layer names.
189
235
  Parent content to its artboard. Inspect every operation result and rollback status.
190
236
  Save your builder and returned IDs so revisions are targeted and reproducible.
237
+ Check `ignoredFields`, `rejectedFields`, `warnings`, `outcome` and `canvasChanges` as well as
238
+ `ok/count`. `matched` is not `updated`. An unchanged or partly ignored request does not prove
239
+ the intended edit happened. Read `composition-detail {includeScene:true}` from its top-level
240
+ `nodes`; never treat a missing response field as an empty scene. Keep executed payloads immutable.
191
241
 
192
242
  ⚠ **Batch, never loop.** One `canvas-act` batch of hundreds of steps is one scene read,
193
243
  persisted every 50 steps, and the workspace stays responsive for everyone in it while it
@@ -225,6 +275,12 @@ It invokes `canvas.review` on one persisted snapshot and returns:
225
275
  type and palette sprawl, near-miss edges, trim crowding, text over imagery;
226
276
  - the placeholder/overflow audit, measured text fit, and a preview PNG.
227
277
 
278
+ For several artboards, write their IDs/names as a JSON array and use:
279
+ `node <skill-directory>/review.mjs --many <composition-id> <ids.json> <output-directory>`.
280
+ This uses `canvas.review-many`: one scene read, separate findings per artboard and compact
281
+ preview handles. Review only artboards changed since their verified snapshot. The helper versions
282
+ existing output paths so before/after evidence is retained.
283
+
228
284
  The helper prints the verdict and every finding. Work until the verdict is `clean` or
229
285
  `look`, as described in the studio bar above. If the installed worker lacks `canvas.review`,
230
286
  use a real frame ID with `--perceive` for a preview-only check and run the text measurement
@@ -232,9 +288,16 @@ and audit separately. A failed or missing render means visual verification is in
232
288
 
233
289
  ```sh
234
290
  # A deliberate finding — say why in the handoff:
235
- beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame>"],"ignore":["text-over-image"]}}'
291
+ beehaven call canvas-act '{"compositionId":"<id>","action":"canvas.review","args":{"ids":["<frame>"],"accept":[{"check":"text-over-image","ids":["<text-id>","<image-id>"],"reason":"Intentional type over this photograph; rendered contrast was inspected."}]}}'
236
292
  ```
237
293
 
294
+ Asset storage is a reference-aware pipeline: an SVG may have `svgRef` instead of inline markup.
295
+ Do not shrink or recreate it just because the inline field is absent. Replace it with
296
+ `canvas.update-nodes {ids, patch:{svg:<complete markup>}}`; references are managed by the action.
297
+ Use `canvas.fonts` before choosing an unfamiliar family/weight. Fallback metrics are not exact.
298
+ Use `canvas.editor-state {requireLive:true,includePixels:true}` to verify the responding editor.
299
+ An agent camera and a navigation delivery count do not prove the person's view was framed.
300
+
238
301
  Inspect the actual image and identify concrete defects, not generic praise:
239
302
 
240
303
  | Lens | What to judge |
@@ -49,6 +49,12 @@ do not blindly resend only skipped operations. Mixed live/headless actions may h
49
49
  different rollback behavior. Use deterministic layer names and returned IDs to avoid
50
50
  duplicate content during retries.
51
51
 
52
+ Also check ignored/rejected fields, warnings, mutation outcome and actual changed IDs. A matched
53
+ node or `ok:true` receipt alone is not proof of an update. The bundled `assertReceipt` helper can
54
+ reject partial drops; `requireChange:true` rejects an unexpected no-op. A deliberate replay may
55
+ be unchanged and should reuse its previous evidence instead of recreating nodes.
56
+ Keep executed payloads and returned-ID manifests separate from regenerated plans.
57
+
52
58
  ## Node shape and coordinates
53
59
 
54
60
  Fields are flat: `{name,type,x,y,w,h,text,size,family,fontWeight,fill,...}`.
@@ -71,6 +77,13 @@ Create normalizes hex colours; raw stored scenes use RGBA arrays in 0..1.
71
77
  moves/resizes use the catalog's frame/arrangement/scale actions; do not assume a patch
72
78
  reflows children.
73
79
  - Build backgrounds before foregrounds. Use `canvas.order` to change stacking.
80
+
81
+ For create/update geometry, `coordinateSpace:"world"` or `"parent"` makes the intent explicit.
82
+ Default behavior remains compatible with the rules above. Receipts include normalized world
83
+ bounds and parent offsets where available. Template coordinates remain relative to their root.
84
+ An SVG's bytes may be stored in `svgRef` once it exceeds the inline limit; this is valid artwork,
85
+ not a missing node. Replacing `svg` clears and rebuilds its reference automatically. Never patch
86
+ `svgRef` manually or guess its storage URL. Image `fit` is explicitly `contain` or `cover`.
74
87
  - Name nodes uniquely; use returned IDs where names collide. Parent content to its
75
88
  frame so movement, selection, clipping and reviews can treat it as one artifact.
76
89
  - Give an intended stroke both `stroke` and `strokeWidth`. Zero explicitly removes it.
@@ -106,8 +119,19 @@ and tokens when relevant. Constraints reflow children on frame resize; they do n
106
119
  a static canvas into a responsive website. Design explicit mobile artboards when the
107
120
  deliverable requires them, with content order and hierarchy reconsidered for the width.
108
121
 
122
+ ## Technique
123
+
124
+ The same rules the in-app assistant works to (canvas-catalog returns them as `technique`).
125
+
126
+ - AUTO-LAYOUT: rows and stacks (nav links, button groups, card rows, form fields) are a frame whose children you create in reading order, then canvas.auto-layout { id, direction, gap, padding, align, justify, hug }. hug:false keeps the frame size, which a full-width bar needs with justify:"space-between"; set a child to layoutSizingH/V "fill" | "hug" | "fixed" with canvas.update-nodes. The frame stays live: create more children into it (parentId) and they flow in; layoutPositioning:"absolute" takes one out of the flow.
127
+ - GRADIENTS: gradient { kind, angle, stops: [{ offset, color }] } replaces the solid fill. angle is the direction the colour travels: 0 = left to right, 90 = top to bottom, 45 = top-left to bottom-right (not CSS degrees). Use two or three stops from the brand palette, and keep text off a busy gradient or check its contrast in canvas.review.
128
+ - SHADOWS AND BLUR: effects on any node. A soft card shadow is { type: "drop-shadow", x: 0, y: 4, blur: 16, spread: 0, color: "#0000001F" }; frosted glass is { type: "background-blur", blur: 16 }. Never fake a shadow with an extra rectangle.
129
+ - DRAWING: build every mark, icon, illustration and diagram shape from native layers, as a designer does in Figma, never as one generated SVG file. Primitives are canvas.create-node rect and ellipse; curves are pen paths, vector.create { name, parentId, points: [{ x, y, hIn?: { x, y }, hOut?: { x, y } }], closed, fill or stroke, strokeWidth }, where hIn/hOut are absolute control points: use few anchors with smooth handles, and refine with vector.read, vector.vertex.move and vector.handle.set. Round caps and joins: canvas.update-nodes patch { strokeCap: "round", strokeJoin: "round" }. Work back to front: ground, big silhouettes in flat brand colours, then features, then details and highlights; check the silhouette in canvas.review before detailing. Name layers by part ("fox/ear-left"), group each figure with canvas.group { ids, name } and set overlap with canvas.order { ids, op }. Make a cut-out by layering a ground-colour shape on top; vector.boolean { op } on selected paths needs an open editor. Keep one stroke weight per drawing. Photographic or painterly imagery is a placed raster. Raw SVG stays supported but is never the default: when an SVG is asked for, write the markup yourself as an svg node, canvas.create-node { type: "svg", name, w, h, svg } with <svg viewBox="0 0 W H"> matching w and h, or import existing markup the same way; never an SVG generation pipeline.
130
+ - PLACEHOLDERS: until real imagery exists, draw a native placeholder at the final size in a muted brand tone, never a flat accent block: a rect with the slot radius plus a simple motif (an ellipse sun, a vector.create mountain), grouped with canvas.group and named "<section> image placeholder", with any label in its own text node.
131
+ - SECTIONS: one frame per page section, named exactly as its ticket says, inside the page artboard. Build a section in one canvas-act batch that ends with canvas.review of the artboard and carries ticket: { id: "<its key>", status: "done", resolution: "<what changed>" }. The ticket moves only when that review is clean or look; result.ticket says whether it did. Then start the next section.
132
+
109
133
  Use `arrow.create {from,to,routing,label}` for connected diagrams, so arrows remain bound
110
- when nodes move. Use native shapes and SVG for custom vector details. Keep labels as
134
+ when nodes move. Draw custom vector details natively (shapes, vector.create pen paths); raw SVG only imports an existing file. Keep labels as
111
135
  native text so they remain editable and measurable.
112
136
 
113
137
  Use `canvas.place {kind,id,x,y,w,h,name,parentId?}` to put a workspace object (document,
@@ -1,20 +1,69 @@
1
1
  #!/usr/bin/env node
2
2
  /** Save review pixels outside the model context. No shell interpolation, no workspace mutations. */
3
3
  import { execFileSync } from 'node:child_process';
4
- import { mkdirSync, writeFileSync } from 'node:fs';
4
+ import { createHash } from 'node:crypto';
5
+ import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
5
6
  import { dirname, resolve } from 'node:path';
6
7
  import { fileURLToPath } from 'node:url';
7
8
 
8
- export function rpc(action, payload) {
9
+ export function assertReceipt(result, { requireChange = false } = {}) {
10
+ if (result?.ok === false || result?.error || result?.results?.some(r => r.ok === false)) {
11
+ throw new Error(result.error || result.results?.find(r => !r.ok)?.error || 'Canvas request failed');
12
+ }
13
+ for (const row of result?.results ?? [result]) {
14
+ const value = row.value ?? row;
15
+ if (value.ignoredFields?.length || value.rejectedFields?.length) {
16
+ throw new Error(`Fields were not applied: ${[...(value.ignoredFields ?? []), ...(value.rejectedFields ?? [])].join(', ')}. Some work may have landed; inspect the saved scene before retrying.`);
17
+ }
18
+ if (requireChange) {
19
+ const delta = value.canvasChanges;
20
+ const changed = ['changed', 'partial'].includes(value.outcome) || (Array.isArray(delta) ? delta.length > 0 : delta && typeof delta === 'object'
21
+ ? Number(delta.added || 0) + Number(delta.changed || 0) + Number(delta.removed || 0) > 0
22
+ : value.placed === true || /\.create(-node)?$/.test(String(row.action || '')) && !!value.id && !value.duplicate);
23
+ if (!changed) throw new Error('No nodes changed, or the receipt does not prove a change. Inspect the saved scene; do not treat a match as an update.');
24
+ }
25
+ }
26
+ return result;
27
+ }
28
+
29
+ export function rpc(action, payload, options) {
9
30
  const stdout = execFileSync('beehaven', ['call', action, '-'], {
10
31
  input: JSON.stringify(payload), encoding: 'utf8', maxBuffer: 32 * 1024 * 1024,
11
32
  stdio: ['pipe', 'pipe', 'pipe'], timeout: 120_000,
12
33
  });
13
34
  const result = JSON.parse(stdout);
14
- if (result.ok === false || result.error || result.results?.some((r) => r.ok === false)) {
15
- throw new Error(result.error || result.results.find((r) => !r.ok)?.error || 'Canvas request failed');
35
+ return assertReceipt(result, options);
36
+ }
37
+
38
+ export function sceneNodes(result) {
39
+ if (!Array.isArray(result?.nodes)) throw new Error('Expected composition-detail includeScene:true to return top-level nodes. Refusing to interpret a missing field as an empty canvas.');
40
+ return result.nodes;
41
+ }
42
+
43
+ /** Resume creates only. Existing layers are preserved, including the person's later edits. */
44
+ export function resumeCanvasPlan(plan, snapshot, idManifest = {}) {
45
+ const nodes = sceneNodes(snapshot), byId = new Map(nodes.map(n => [n.id, n]));
46
+ const names = new Map();
47
+ for (const n of nodes) { const list = names.get(n.name) ?? []; list.push(n); names.set(n.name, list); }
48
+ const roots = new Map(), batch = [], skipped = [];
49
+ for (const step of plan.batch ?? []) {
50
+ const args = { ...(step.args ?? {}) };
51
+ if (!['frame.create', 'canvas.create-node'].includes(step.action)) { skipped.push({ action: step.action, reason: 'Resume preserves existing mutations; revise explicitly.' }); continue; }
52
+ const type = step.action === 'frame.create' ? 'frame' : args.type;
53
+ const pinned = args.id || idManifest[args.name];
54
+ const matches = names.get(args.name) ?? [];
55
+ if (!pinned && matches.length > 1) throw new Error(`Ambiguous layer name ${args.name}; use its recorded ID.`);
56
+ const existing = pinned ? byId.get(pinned) : matches[0];
57
+ if (existing) {
58
+ if (existing.type !== type) throw new Error(`Layer ${args.name} has type ${existing.type}, expected ${type}. No automatic replacement.`);
59
+ roots.set(args.name, existing.id); skipped.push({ id: existing.id, name: args.name, reason: 'Already exists; preserved.' }); continue;
60
+ }
61
+ args.id = pinned || `plan-${createHash('sha256').update(`${plan.compositionId}:${args.name}`).digest('hex').slice(0, 24)}`;
62
+ if (args.parentId && roots.has(args.parentId)) args.parentId = roots.get(args.parentId);
63
+ roots.set(args.name, args.id);
64
+ batch.push({ ...step, args });
16
65
  }
17
- return result;
66
+ return { compositionId: plan.compositionId, expectedCanvasNodesRev: snapshot.canvasNodesRev, batch, skipped };
18
67
  }
19
68
 
20
69
  export function saveReview(report, prefix) {
@@ -24,7 +73,21 @@ export function saveReview(report, prefix) {
24
73
  }
25
74
  const bytes = Buffer.from(preview.dataUrl.slice('data:image/png;base64,'.length), 'base64');
26
75
  if (!bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) throw new Error('Invalid PNG response');
27
- const base = resolve(prefix);
76
+ if (bytes.length < 45 || bytes.readUInt32BE(8) !== 13 || bytes.subarray(12,16).toString() !== 'IHDR'
77
+ || !bytes.readUInt32BE(16) || !bytes.readUInt32BE(20)) throw new Error('Invalid PNG response: missing image header/body');
78
+ let offset = 8, imageData = false, end = false;
79
+ while (offset + 12 <= bytes.length) {
80
+ const length = bytes.readUInt32BE(offset), tag = bytes.subarray(offset+4,offset+8).toString();
81
+ if (offset + length + 12 > bytes.length) throw new Error('Invalid PNG response: truncated image chunk');
82
+ if (tag === 'IDAT' && length > 0) imageData = true;
83
+ offset += length + 12;
84
+ if (tag === 'IEND') { end = length === 0; break; }
85
+ }
86
+ if (!imageData || !end) throw new Error('Invalid PNG response: missing image data/end');
87
+ let base = resolve(prefix);
88
+ const original = base;
89
+ let version = 0;
90
+ while (existsSync(`${base}.png`) || existsSync(`${base}.json`)) base = `${original}-r${report.canvasNodesRev ?? 'unknown'}-${++version}`;
28
91
  mkdirSync(dirname(base), { recursive: true });
29
92
  const { dataUrl, url, ...metadata } = preview;
30
93
  const saved = { ...report, preview: { ...metadata, path: `${base}.png` } };
@@ -34,16 +97,47 @@ export function saveReview(report, prefix) {
34
97
  return { png: `${base}.png`, report: `${base}.json`,
35
98
  // `fix` = a probable defect remains: fix it, or pass its check/id in `ignore` with a reason.
36
99
  // `look` = only things worth a second look. `clean` = nothing mechanical found — NOT "good".
37
- verdict: report.verdict ?? 'unknown', blocking: report.blocking,
100
+ verdict: report.verdict ?? 'unknown', acceptanceStatus: report.acceptanceStatus,
101
+ acceptedFindings: report.acceptedFindings ?? [], blocking: report.blocking,
38
102
  craft: craft.map((f) => `${f.severity} ${f.check}: ${f.detail}`),
103
+ audit: (report.audit?.violations ?? []).map(v => ({check:v.kind,layer:v.name || v.id,detail:v.detail || v.message || v.reason || `${v.kind} on ${v.name || v.id}`})),
104
+ scopeWarnings: report.scopeWarnings ?? [],
105
+ assets: (report.assets ?? []).filter(a => a.status !== 'ready'),
106
+ typography: (report.typography?.text ?? []).filter(t => t.issue || t.horizontalOverflow > 1 || !t.autoHeight && t.verticalOverflow > 1 || t.exact === false)
107
+ .map(t => ({layer:t.name || t.id,issue:t.issue,horizontalOverflow:t.horizontalOverflow,verticalOverflow:t.verticalOverflow,
108
+ requestedFont:t.font?.requestedFamily,requestedWeight:t.font?.requestedWeight,resolvedFile:t.font?.file,exact:t.exact})),
39
109
  nodes: report.scope?.nodeCount, findings: report.audit?.total, textChecked: report.typography?.checked,
40
110
  textOmitted: report.typography?.omitted, visualReviewRequired: true,
41
111
  note: report.verdict === 'fix'
42
- ? 'Fix every warn (or justify it with ignore), then review again. Then open the PNG and critique it.'
112
+ ? 'Fix every warn (or accept an exact finding with a reason), then review changed artboards. Then open the PNG and critique it.'
43
113
  : 'Open the PNG with your image-viewing tool, critique it against the studio bar, revise and render again.' };
44
114
  }
45
115
 
46
- export function main(argv = process.argv.slice(2)) {
116
+ export async function saveReviewPixels(report, prefix, fetchImage = fetch) {
117
+ if (report.preview?.dataUrl) return saveReview(report, prefix);
118
+ const url = report.preview?.url;
119
+ if (!url || !/^https?:\/\//.test(url)) throw new Error(report.previewError || 'No downloadable preview. Visual verification is incomplete.');
120
+ const response = await fetchImage(url);
121
+ if (!response.ok) throw new Error(`Preview download failed (HTTP ${response.status}); request a fresh review.`);
122
+ const bytes = Buffer.from(await response.arrayBuffer());
123
+ return saveReview({ ...report, preview: { ...report.preview, dataUrl: `data:image/png;base64,${bytes.toString('base64')}` } }, prefix);
124
+ }
125
+
126
+ export async function main(argv = process.argv.slice(2)) {
127
+ if (argv.includes('--many')) {
128
+ const args = argv.filter(a => a !== '--many');
129
+ if (args.length !== 3) throw new Error('Usage: node review.mjs --many <compositionId> <artboard-ids.json> <output-directory>');
130
+ const [compositionId, manifest, directory] = args;
131
+ const ids = JSON.parse(readFileSync(manifest, 'utf8'));
132
+ if (!Array.isArray(ids) || !ids.length) throw new Error('The manifest must be a non-empty artboard ID/name array.');
133
+ const response = rpc('canvas-act', { compositionId, action: 'canvas.review-many', args: { ids, preview: 'files' } });
134
+ const reports = response.results?.[0]?.value?.reviews;
135
+ if (!Array.isArray(reports) || reports.length !== ids.length) throw new Error('Incomplete review-many result; do not claim all artboards passed.');
136
+ const saved = [];
137
+ for (let i = 0; i < reports.length; i++) saved.push(await saveReviewPixels(reports[i], resolve(directory, `${String(i + 1).padStart(2, '0')}-${String(ids[i]).replace(/[^a-z0-9_-]/gi, '-')}`)));
138
+ process.stdout.write(JSON.stringify(saved, null, 2) + '\n');
139
+ return;
140
+ }
47
141
  const legacy = argv.includes('--perceive');
48
142
  const args = argv.filter((a) => a !== '--perceive');
49
143
  if (args.length !== 3) throw new Error('Usage: node review.mjs <compositionId> <frame-id-or-name> <output-prefix> [--perceive]\n--perceive: older workers, preview only; pass a frame ID.');
@@ -60,9 +154,9 @@ export function main(argv = process.argv.slice(2)) {
60
154
  report = response.results?.[0]?.value;
61
155
  if (!report?.scope) throw new Error('canvas.review unavailable. Check canvas-catalog; use --perceive for an explicitly preview-only check on an older worker.');
62
156
  }
63
- process.stdout.write(JSON.stringify(saveReview(report, prefix), null, 2) + '\n');
157
+ process.stdout.write(JSON.stringify(await saveReviewPixels(report, prefix), null, 2) + '\n');
64
158
  }
65
159
 
66
- if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
67
- try { main(); } catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 1; }
160
+ if (process.argv[1] && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
161
+ main().catch(error => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
68
162
  }
@@ -298,6 +298,11 @@ fewer elements, tighter type, one accent*. Every ornament added is a step away f
298
298
 
299
299
  ## Sources
300
300
 
301
+ The full shelf — about fifty books across grid, type, print production, colour, graphic form,
302
+ brand identity, information and interface design, each reduced to one rule — is in
303
+ [references.md](references.md). In Research pick two or three whose lens fits the brief, and name
304
+ the one behind each direction in its rationale.
305
+
301
306
  Josef Müller-Brockmann, *Grid Systems in Graphic Design* (1981) · Massimo Vignelli, *The
302
307
  Vignelli Canon* (semantics / syntactics / pragmatics; "Design is one") · Michael Bierut,
303
308
  *How to* (Pentagram; brief → rationale → trade-offs) · Paula Scher / Pentagram, The Public
@@ -0,0 +1,95 @@
1
+ # The design shelf
2
+
3
+ <!-- Generated from apps/cloudflare/src/agent/design-references.ts (the design-references and brand-references house skills).
4
+ Edit the list there, then run: WRITE_DESIGN_REFERENCES=1 npx vitest run src/__tests__/verify-design-references.test.ts -->
5
+
6
+ Books a studio designer works from, each reduced to one rule you can act on. The in-app
7
+ assistant gets the same list as the `design-references` and `brand-references` workspace skills.
8
+
9
+ ## How to use it
10
+
11
+ - In Research, pick two or three references whose lens fits the brief: a print piece or poster → grid, type, print; an identity → brand; a chart or report → information; a screen or flow → interface. Write what each one changes in this design.
12
+ - In Direction, name the reference behind each direction's idea in one line ("a Müller-Brockmann modular grid, type as the image after Scher").
13
+ - In Review, critique against the same rules you chose.
14
+ - These are paraphrased principles, not quotations. Never invent a quote or page number; cite author and title only.
15
+
16
+ ## Method — deciding before drawing
17
+
18
+ - Massimo Vignelli, *The Vignelli Canon* (2010): Answer semantics (what it means, to whom), syntactics (its visual grammar) and pragmatics (will they get it) before placing a node; few typefaces, a grid, discipline.
19
+ - Michael Bierut, *How to* (2015): Present brief → idea → trade-offs; the idea comes out of the problem, and the rationale is part of the deliverable.
20
+ - Paul Rand, *Thoughts on Design* (1947): Form and content are one: simplify until the idea is legible, and allow wit — play is a way of making it memorable.
21
+ - Bruno Munari, *Design as Art* (1966): Solve the problem with the simplest means; decoration that solves nothing is noise.
22
+ - Karl Gerstner, *Designing Programmes* (1964): Design the rules, not one layout: define the variables (size, position, colour) and generate the set from them.
23
+ - Dieter Rams, *Less but Better* (1995): As little design as possible: every element must earn its place by use; remove what does not.
24
+ - Alan Fletcher, *The Art of Looking Sideways* (2001): Look for the idea sideways — a visual pun or found connection beats a literal illustration of the brief.
25
+
26
+ ## Grid and layout
27
+
28
+ - Josef Müller-Brockmann, *Grid Systems in Graphic Design* (1981): Construct margins, columns, gutter and baseline first; every element lands on the grid; break it once, on purpose.
29
+ - Timothy Samara, *Making and Breaking the Grid* (2002): Pick the grid for the content (manuscript, column, modular, hierarchical); a deliberate break needs a grid to break.
30
+ - Kimberly Elam, *Geometry of Design* (2001): Place the focal point on a proportional line (golden section, root rectangles, thirds), not at dead centre.
31
+ - Beth Tondreau, *Layout Essentials* (2009): Grids by job: a single column for reading, multi-column for mixed content, modular for catalogues and dashboards.
32
+
33
+ ## Typography
34
+
35
+ - Robert Bringhurst, *The Elements of Typographic Style* (1992): Measure 45–75 characters, leading to suit the measure, honour the content: type exists to be read.
36
+ - Ellen Lupton, *Thinking with Type* (2004): Build hierarchy by changing one variable at a time — size, weight, colour or space — and track capitals open.
37
+ - Jan Tschichold, *The New Typography* (1928): Asymmetric, functional layout: hierarchy by size, weight and position, never ornament.
38
+ - Emil Ruder, *Typographie* (1967): White space is active form: the counter, the gap and the margin are composed like the type itself.
39
+ - Jost Hochuli, *Detail in Typography* (1987): Letter, word and line spacing decide readability: tighten display type, keep word spaces even, open the leading for long lines.
40
+ - Erik Spiekermann and E. M. Ginger, *Stop Stealing Sheep & Find Out How Type Works* (1993): A typeface is a voice: choose it for the message and audience, then set it with care before anything else.
41
+ - Ellen Lupton, *Type on Screen* (2014): On screen, type is motion and interaction too: sizes, scales and states are a system, not one setting.
42
+
43
+ ## Print and editorial production
44
+
45
+ - Jan Tschichold, *The Form of the Book*: Classical page proportions: inner margin smallest, foot margin largest; the text block sits high and toward the spine.
46
+ - Richard Hendel, *On Book Design* (1998): A book design serves the reading; decide trim, margins, type and running heads together, and stay invisible.
47
+ - Andrew Haslam, *Book Design* (2006): Plan the whole object — format, grid, pagination, binding — before designing a spread.
48
+ - Jan V. White, *Editing by Design* (1974): Editorial layout starts from what the story says; headline, picture and caption are one unit.
49
+ - International Paper, *Pocket Pal*: Production rules: CMYK for press, 300 dpi images at final size, 3 mm (0.125 in) bleed, a safe area inside the trim.
50
+ - Alan Pipes, *Production for Graphic Designers*: Specify for the press: paper stock, ink coverage, rich black for large areas only, overprint and trap small type.
51
+
52
+ ## Colour
53
+
54
+ - Josef Albers, *Interaction of Color* (1963): Colour is relative: judge it beside its neighbours on the real ground, never as a swatch alone.
55
+ - Johannes Itten, *The Art of Color* (1961): Choose the one contrast that leads (light–dark, warm–cool, complementary, saturation, extent) and let the rest support it.
56
+
57
+ ## Graphic form and visual perception
58
+
59
+ - Armin Hofmann, *Graphic Design Manual* (1965): Build with elementary contrasts — point, line, plane, black and white, large and small — before colour.
60
+ - Ellen Lupton and Jennifer Cole Phillips, *Graphic Design: The New Basics* (2008): Point, line, plane, rhythm, scale, texture, figure/ground and framing are variables; change one deliberately.
61
+ - Wucius Wong, *Principles of Two-Dimensional Design* (1972): Repetition, gradation and radiation give a field structure; an anomaly in that structure is the focal point.
62
+ - Rudolf Arnheim, *Art and Visual Perception* (1954): Visual weight balances on the frame's hidden axes; size, value, isolation and position all add weight.
63
+ - Robin Williams, *The Non-Designer's Design Book* (1994): Contrast, repetition, alignment, proximity: if two things differ, make them clearly different.
64
+ - Alex W. White, *The Elements of Graphic Design* (2002): Treat space as an element: emptiness gives the content its shape and its importance.
65
+ - Kenya Hara, *Designing Design* (2007): Emptiness invites the viewer in: leave room for the reader's imagination instead of filling the frame.
66
+ - Paula Scher, *Make It Bigger* (2002): Type can be the image: scale and crop a word until it carries the identity on its own.
67
+ - Philip B. Meggs, *A History of Graphic Design* (1983): Know the lineage of a style so a reference is chosen, not a pastiche of the first thing found.
68
+ - Adrian Frutiger, *Signs and Symbols*: A sign reads by its simplest form; test a mark small, at a distance and in one colour.
69
+
70
+ ## Brand and identity
71
+
72
+ - Alina Wheeler, *Designing Brand Identity* (2003): Identity is a process — research, strategy, design, touchpoints, asset management; design the touchpoints, not just the logo.
73
+ - Marty Neumeier, *The Brand Gap* (2003): A brand is the customer's gut feeling; close the gap between strategy and the look by testing what people actually perceive.
74
+ - Marty Neumeier, *Zag* (2006): When everyone zigs, zag: state what makes this the only one, then design to that difference.
75
+ - Wally Olins, *On Brand* (2003): A brand shows in product, environment, communication and behaviour; keep all four saying the same thing.
76
+ - Byron Sharp, *How Brands Grow* (2010): Growth comes from being easy to notice and buy: protect distinctive assets and stay consistent.
77
+ - Jenni Romaniuk, *Building Distinctive Brand Assets* (2018): An asset (colour, shape, character) works once it is famous and unique to the brand; use it everywhere, change it rarely.
78
+ - Tom Geismar, Ivan Chermayeff and Sagi Haviv, *Identify* (2011): A mark must be appropriate, distinctive and simple; it earns meaning through use over time.
79
+ - David Airey, *Logo Design Love* (2009): Test a logo at 16 px, in one colour and in reverse; if it needs detail or colour to survive, simplify it.
80
+ - Danne & Blackburn, *NASA Graphics Standards Manual* (1975): A standards manual fixes the mark, clear space, colours, type, grid and applications so anyone can apply the system.
81
+ - Unimark International (Vignelli, Noorda), *New York City Transit Authority Graphics Standards Manual* (1970): A system scales by rules: one typeface, fixed modules, colour coding and consistent placement.
82
+
83
+ ## Information design
84
+
85
+ - Edward R. Tufte, *The Visual Display of Quantitative Information* (1983): Maximise data-ink: remove chart junk, label directly, show the comparison; use small multiples for change.
86
+ - Edward R. Tufte, *Envisioning Information* (1990): Layer and separate: quiet the grid and background so the data reads first.
87
+ - Otto Neurath, *International Picture Language* (1936): Isotype: count with repeated identical pictograms, never by enlarging one.
88
+
89
+ ## Interface and product design
90
+
91
+ - Don Norman, *The Design of Everyday Things* (1988): Make the action visible: signifiers, feedback, mapping and constraints; a control must say how it is used.
92
+ - Steve Krug, *Don't Make Me Think* (2000): People scan, they do not read: obvious hierarchy, conventions, one clear next action per page.
93
+ - Adam Wathan and Steve Schoger, *Refactoring UI* (2018): Hierarchy by weight and colour before size, de-emphasise secondary text, use a spacing and shadow scale.
94
+ - Jenifer Tidwell, *Designing Interfaces* (2005): Use the known pattern for the job (navigation, forms, lists, dashboards) before inventing one.
95
+ - William Lidwell, Kritina Holden and Jill Butler, *Universal Principles of Design* (2003): Check the design against named principles (Fitts, Hick, proximity, progressive disclosure) and say which one a choice serves.
package/skills/index.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "files": [
5
5
  "SKILL.md"
6
6
  ],
7
- "sha": "2d33d2ecfb166fa6"
7
+ "sha": "6233057bd1a44b51"
8
8
  },
9
9
  {
10
10
  "name": "clearly-canvas",
@@ -17,7 +17,7 @@
17
17
  "project-plans.md",
18
18
  "review.mjs"
19
19
  ],
20
- "sha": "8b1890ca7588af6c"
20
+ "sha": "b942eebe6f0397fb"
21
21
  },
22
22
  {
23
23
  "name": "agent-home",
@@ -36,9 +36,10 @@
36
36
  {
37
37
  "name": "design-craft",
38
38
  "files": [
39
- "SKILL.md"
39
+ "SKILL.md",
40
+ "references.md"
40
41
  ],
41
- "sha": "260c3a6967b08a8b"
42
+ "sha": "bf2f4fac6f57ea46"
42
43
  },
43
44
  {
44
45
  "name": "layout-systems",
@@ -52,7 +53,7 @@
52
53
  "files": [
53
54
  "SKILL.md"
54
55
  ],
55
- "sha": "50587f531bfab1c6"
56
+ "sha": "755db2d1ab8c759f"
56
57
  },
57
58
  {
58
59
  "name": "pair-on-canvas",
@@ -81,6 +82,6 @@
81
82
  "files": [
82
83
  "SKILL.md"
83
84
  ],
84
- "sha": "c16e1b5d08dbd669"
85
+ "sha": "b3f47988cb9235b9"
85
86
  }
86
87
  ]