Skip to content

feat(venn): native Venn diagrams across every surface - #557

Merged
graphite-app[bot] merged 1 commit into
mainfrom
feat/venn-diagrams
Oct 29, 2026
Merged

graphite-app[bot] merged 1 commit into
mainfrom
feat/venn-diagrams

Conversation

@anandpant

@anandpant anandpant commented Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

What changes

Native Venn diagrams (#556) across every surface. Until now "Venn diagram of what our teams share" went through createCanvas with model-placed circles, or was refused.

  • Contract. VennDiagram has sets and regions.
    • A set is { id, label, color? }; two or three per diagram. color is a palette name (blue, orange, green, yellow, pink, violet, red, gray) or #rrggbb; sets without one take blue, orange and green.
    • A region { sets, label?, items?, count? } holds what lies in exactly its sets and in no other: ["a"] is only a, ["a", "b"] is a and b but not c, in any key order. Items (≤ 60 characters, ≤ 12 per region, ≤ 40 in all) or a count, not both; label names the overlap itself. Empty regions are omitted; items: [], and a count of 0 in a diagram of items, draw nothing.
    • Semantic validation returns 8 typed issues with repair hints: one set or four, a repeated set id or label, an unknown color, a region naming an unknown set or a set twice, two regions for one combination, items with a count, text over its limit, and too many items.
    • Code Mode and generation also read a set without an id (it takes its label), regions naming sets by label, a lone set name, in and members, and a set listing its whole membership as items (each item lands in the region of exactly the sets that list it).
  • Layout (venn-layout.ts, venn.ts), off the graph path; scene.ts and other families are unchanged.
    • The standard symmetric arrangement of equal circles: two side by side, three in an equilateral triangle. Area-proportional is out of scope.
    • Text sizes the diagram. For each centre separation (0.6–1.4 radii, canonical 1.0 first) the radius grows, to the pixel, until every region's text sits inside its region at least 10 px from every outline; the separation that keeps the diagram narrowest wins, with a cost per step away from canonical. A crowded centre pulls the circles together; crowded sides push them apart.
    • Items are 15 px, one per line, centred and 7 px apart, under a 17 px region label or a 26 px count. An item wraps at word boundaries only when its region would otherwise need circles over a fifth larger, and a wrap that would split a Latin word is never taken.
    • Each block sits where it is farthest from the outlines around it, then centres within the stretch where it stays nearly that far, so a lone item sits on its crescent's middle line.
    • Set labels (20 px) sit outside their circles: the upper two above, over the part only their set covers; a third below. Each takes its set's color, darkened to read on the surface.
    • Fills are 22% translucent so overlaps blend. A dark set color fills with a lighter tint of itself, so ink stays ≥ 7:1 on every blend.
  • Excalidraw. Each set is a group: a translucent filled ellipse, an outline ellipse drawn over every fill (both with the new venn-set renderer role, so neither grows a generated label and patches keep the role), and its label. Each region's items are free, centred text elements grouped per region, one element per item. Exports validate before and after a save and reload.
  • Code Mode and MCP. buildVennDiagram, docs({ topic: "buildVennDiagram" }), three issue codes (venn_too_large, invalid_venn_set, invalid_venn_region), one compact line on the code parameter. Docs cost (o200k):
    • execute description head: +18 characters (+5 tokens), only buildVennDiagram on its Builders line (1,540 characters);
    • code parameter: +336 characters (+96 tokens), a builder summary and one line (10,884; budget 10,650 → 10,900);
    • compact tools/list JSON 13,849 (budget 13,650 → 13,870);
    • the topic, served on demand: 1,289 characters (334 tokens);
    • generation system prompt: +168 tokens (Venn IR rules and the sets/items/set/item requirement targets).
  • Every other family surface: generation IR rules, an example and requirement targets; CLI create and generate --type venn with a changeset; the generate API; Studio build_venn_diagram and POST /api/v1/venn-diagrams/build; Storybook stories; both llms.txt; both READMEs; docs/venn-diagrams.md.

Scenarios

From Frontierclimb's eval draft (§6), prompts verbatim:

  • team-skills-venn (standard): Design, Engineering and Product, all seven regions, 13 items.
  • plan-features-venn (challenge): 20 features, seven in the centre, long items, Personal ∩ Education stated empty.
  • ios-android-venn (smoke): two sets, 10 items.

--input checks score the model's own spec: set:<key>, item:<key>, region:<key> (exactly the stated combination, any key order), sets-exact, items-exact (no unstated item, none in two regions) and empty-region:<combo> (no items, a count of 0, or no entry). Labels compare without case, spacing, punctuation or a leading bullet; set order, ids and colors are free. venn-drawn (output) reads the scene back: every outline with its label outside and nearest its own outline, every item, region label and count wholly inside its own sets and outside the others. venn-layout (hint): no text overlaps, items ≥ 4 px clear of outlines, set labels clear of every circle.

Verification

  • Tests that fail on main: core contract, issues and shorthand; renderer (symmetric arrangement, labels over each set's own part, whole lines where there is room, crescent centring, whole Latin tokens, growth for heavy regions, translucent groups and z-order, contrast on every blend for dark colors, key order, empty regions, work guard); Excalidraw export and reload; Code Mode buildVennDiagram (documented spec, aliases and memberships, patch keeps roles, round-trip, every repair code); generation; scenarios (expected diagrams pass, each mutation fails its own check, equivalent forms pass, swapped labels and an item across an outline fail venn-drawn, overlap fails venn-layout, fixture model, MCP receipt); CLI offline failures and PNG export; MCP docs and tool bounds; route lists.
  • Layout work guard: deterministic placement counts, not wall clock: the fixture under 400k, a twelve-item centre under 600k. Renders take 0.2–75 ms.
  • Fuzz (pnpm nx run diagram-excalidraw:venn-fuzz, CI runs 12): 900 random diagrams over three seeds, two or three sets with short, long and CJK labels and colors, regions empty, sparse or packed to the caps with phrases up to 60 characters and a 33-character token, sometimes labeled or counted. Zero defects: every export valid before and after reload, every item drawn once, whole, inside exactly its region, no text overlap, set labels outside every circle, aspect between 1:1.4 and 2.45:1. A mutation that ignores the outlines a region excludes fails 805 checks.
  • Local: pnpm nx affected -t typecheck,test,build --base=origin/main (18 projects), pnpm run check, pnpm run test:tools, pnpm nx build-storybook diagram-ui, pnpm run test:wrangler-dry-runs.
  • PNGs inspected (CLI create + export --format png): every scenario and the stress cases (two sets, twelve-item centre, twelve-item overlap, long items with a dark custom color, counts under 40-character labels, sets alone).
  • Preview MCP replay at 80bac7d9 (studio preview /mcp, one buildVennDiagram per scenario written from its prompt, each accepted on the first call, 0.7–1.4 s warm): team-skills-venn 35/35, plan-features-venn 50/50 (seven centre items at full size, Personal ∩ Education empty), ios-android-venn 28/28. Hosted PNG of the challenge inspected.
  • Real page (agent-browser -p browserbase, preview): the challenge's artifact page and edit page render with no page or console errors; dragging the Personal circle moves its fill, outline and label as one group while every region's text stays put.

Review fixes

Codex review on 80bac7d, each with a failing-first test (core, renderer and Code Mode):

  • An item or region label with an authored newline now renders: each authored line wraps on its own, and the natural lines are always an option (it threw render_failed before).
  • regions given as a lone object reads as a list of one; a string, number or null reaches the schema and is rejected (it was silently replaced with []).
  • A diagram of counts alone draws every count, 0 included. A 0 is empty only in a diagram that lists items and no positive count.
  • Palette names match the palette's own keys, so constructor or __proto__ get the invalid-color hint instead of crashing the render.

Renders are byte-identical to 80bac7d for every input without these cases: 920 scene and Excalidraw hashes (fixture, stress cases, the three scenario specs, the three preview-replay specs and the 900 fuzz diagrams).

The two CI flakes this PR hit (the rollback flowchart timeout and the render pool launch pacing) are fixed on main by #559; this branch is restacked onto it.

Known limits

  • Four sets (the ellipse arrangement) are not in this PR; the contract rejects them with a repair hint.
  • A crowded centre makes a large diagram: twelve centre items need circles of radius ~285 px, so the export is ~820 × 870.
  • Items in a narrow crescent wrap onto two lines when keeping them whole would need circles more than a fifth larger.

Copy link
Copy Markdown
Collaborator Author

How to use the Graphite Merge Queue

Add either label to this PR to merge it via the merge queue:

  • merge - adds this PR to the back of the merge queue
  • fast - for urgent changes, fast-track this PR to the front of the merge queue

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@anandpant anandpant changed the title feat(venn): native Venn diagrams across every surface (#556) feat(venn): native Venn diagrams across every surface Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Icons Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Web Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Eval Harness Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Excalidraw Workspace Preview

Status: deleted

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Sketchi Playground / Studio Preview

Status: deleted

anandpant added a commit that referenced this pull request Oct 29, 2026
…all-zero counts, own palette keys

Code review on #557:
- An item or region label with an authored newline rendered nothing valid:
  every wrap option failed and layout threw. Each authored line now wraps on
  its own; the natural lines always remain an option.
- A non-list regions value was replaced with [] before validation. A lone
  region object now reads as a list of one; anything else reaches the schema.
- A diagram of counts alone dropped every 0. A 0 is empty only in a diagram
  that lists items and no positive count.
- Palette validation used 'in', so 'constructor' passed and rendering threw.
  Names now match the palette's own keys.
anandpant added a commit that referenced this pull request Oct 29, 2026
…imeout

It runs in about 1 s locally on main and on this branch alike, but near
vitest's 5 s default on a loaded CI runner, where it timed out twice on #557.
anandpant added a commit that referenced this pull request Oct 29, 2026
…ting them

The pool's retry-pacing tests counted launches after a fixed 20 ms of real
time. Under a loaded runner the forked renders had not reached the mock yet
(0 of 3 launches), failing #557's CI once and a local affected run once.
Positive counts now poll until they hold; the 'still 3' checks keep the
fixed settle.
graphite-app Bot pushed a commit that referenced this pull request Oct 29, 2026
…its in the render pool tests (#559)

## Why

Two tests timed out or flaked in CI under load from sibling runs (PR #557), and both pass locally:

- **diagram-agent** `runtime.test.ts` › "exports production release rollback flows without dense route collisions" timed out at Vitest's 5000 ms default (5049 ms on Oct 25, 5564 ms on #557). It takes about 400 ms idle and 1.2 s with the machine loaded.
- **playground** `browser-render-pool.server.test.ts` › "paces launch retries with every other launch" (from #525) failed with `expected "vi.fn()" to be called 3 times, but got 0 times`. It already used `TestClock`, but between clock steps it waited a real 20 ms (`settle`). The first launch goes through `await import("@cloudflare/playwright")`, so on a loaded runner the forked renders hadn't reached `launch` within those 20 ms.

## What changed

- `packages/diagram/agent/vitest.config.mts`: package-level `testTimeout: 60_000`, a hang guard rather than a speed budget, the same as #413 added for renderer, excalidraw, cli and tools. Those guards, and #413's suite-level guards, are all still in place. No other unguarded package runs heavy deterministic renders: per-test durations with the machine loaded show only jsdom first renders above 1 s, which #413 deliberately left on the default timeout.
- Render pool tests: the wall-clock `settle` is gone. A thin wrapper around the test clock records the wake time of each pending sleep. Each step now waits for the pool state its assertion needs:
  - three 250 ms backoff sleeps pending
  - then one launch-window sleep due at 1 s
  - then the launch count

  Before each check that no launch happened (after `adjust(999)`, `adjust(749)` and each second of the per-second loop), forked renders get a few macrotask turns, not wall time, to act. The loop then waits until every launched browser has opened its page or the remaining launchers are parked on the next window. The same change applies to the two sibling tests that used `settle`: queue time for a busy page, and three launches per second. The assertions are unchanged: no retry launches before the 1 s window, three per window.

No algorithm, input, assertion-target or sweep-count changes. No wall-clock assertions. No changeset (test-only).

## Verification

- `pnpm nx affected -t typecheck,test --base=origin/main`: 12 tasks across 6 projects passed
- `pnpm run check` passed
- Render pool file: 3/3 sequential runs and 10/10 concurrent runs passed with all 24 cores pinned by CPU burners.
- Mutation check: shrinking the launch window to 999 ms fails both pacing tests on their negative assertions ("expected 3 … got 6"), not on a timeout. The old version also passed locally under that load, so the CI flake couldn't be reproduced here; the fix removes the wall-clock wait it depended on.
anandpant added a commit that referenced this pull request Oct 29, 2026
…all-zero counts, own palette keys

Code review on #557:
- An item or region label with an authored newline rendered nothing valid:
  every wrap option failed and layout threw. Each authored line now wraps on
  its own; the natural lines always remain an option.
- A non-list regions value was replaced with [] before validation. A lone
  region object now reads as a list of one; anything else reaches the schema.
- A diagram of counts alone dropped every 0. A 0 is empty only in a diagram
  that lists items and no positive count.
- Palette validation used 'in', so 'constructor' passed and rendering threw.
  Names now match the palette's own keys.
@graphite-app

graphite-app Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Merge activity

## What changes

Native Venn diagrams (#556) across every surface. Until now "Venn diagram of what our teams share" went through `createCanvas` with model-placed circles, or was refused.

- **Contract.** `VennDiagram` has `sets` and `regions`.
  - A set is `{ id, label, color? }`; two or three per diagram. `color` is a palette name (blue, orange, green, yellow, pink, violet, red, gray) or `#rrggbb`; sets without one take blue, orange and green.
  - A region `{ sets, label?, items?, count? }` holds what lies in exactly its sets and in no other: `["a"]` is only a, `["a", "b"]` is a and b but not c, in any key order. Items (≤ 60 characters, ≤ 12 per region, ≤ 40 in all) or a count, not both; `label` names the overlap itself. Empty regions are omitted; `items: []`, and a `count` of 0 in a diagram of items, draw nothing.
  - Semantic validation returns 8 typed issues with repair hints: one set or four, a repeated set id or label, an unknown color, a region naming an unknown set or a set twice, two regions for one combination, items with a count, text over its limit, and too many items.
  - Code Mode and generation also read a set without an id (it takes its label), regions naming sets by label, a lone set name, `in` and `members`, and a set listing its whole membership as `items` (each item lands in the region of exactly the sets that list it).
- **Layout** (`venn-layout.ts`, `venn.ts`), off the graph path; `scene.ts` and other families are unchanged.
  - The standard symmetric arrangement of equal circles: two side by side, three in an equilateral triangle. Area-proportional is out of scope.
  - Text sizes the diagram. For each centre separation (0.6–1.4 radii, canonical 1.0 first) the radius grows, to the pixel, until every region's text sits inside its region at least 10 px from every outline; the separation that keeps the diagram narrowest wins, with a cost per step away from canonical. A crowded centre pulls the circles together; crowded sides push them apart.
  - Items are 15 px, one per line, centred and 7 px apart, under a 17 px region label or a 26 px count. An item wraps at word boundaries only when its region would otherwise need circles over a fifth larger, and a wrap that would split a Latin word is never taken.
  - Each block sits where it is farthest from the outlines around it, then centres within the stretch where it stays nearly that far, so a lone item sits on its crescent's middle line.
  - Set labels (20 px) sit outside their circles: the upper two above, over the part only their set covers; a third below. Each takes its set's color, darkened to read on the surface.
  - Fills are 22% translucent so overlaps blend. A dark set color fills with a lighter tint of itself, so ink stays ≥ 7:1 on every blend.
- **Excalidraw.** Each set is a group: a translucent filled ellipse, an outline ellipse drawn over every fill (both with the new `venn-set` renderer role, so neither grows a generated label and patches keep the role), and its label. Each region's items are free, centred text elements grouped per region, one element per item. Exports validate before and after a save and reload.
- **Code Mode and MCP.** `buildVennDiagram`, `docs({ topic: "buildVennDiagram" })`, three issue codes (`venn_too_large`, `invalid_venn_set`, `invalid_venn_region`), one compact line on the `code` parameter. Docs cost (o200k):
  - execute description head: +18 characters (+5 tokens), only `buildVennDiagram` on its Builders line (1,540 characters);
  - `code` parameter: +336 characters (+96 tokens), a builder summary and one line (10,884; budget 10,650 → 10,900);
  - compact tools/list JSON 13,849 (budget 13,650 → 13,870);
  - the topic, served on demand: 1,289 characters (334 tokens);
  - generation system prompt: +168 tokens (Venn IR rules and the `sets`/`items`/`set`/`item` requirement targets).
- **Every other family surface:** generation IR rules, an example and requirement targets; CLI `create` and `generate --type venn` with a changeset; the generate API; Studio `build_venn_diagram` and `POST /api/v1/venn-diagrams/build`; Storybook stories; both `llms.txt`; both READMEs; `docs/venn-diagrams.md`.

## Scenarios

From Frontierclimb's eval draft (§6), prompts verbatim:

- `team-skills-venn` (standard): Design, Engineering and Product, all seven regions, 13 items.
- `plan-features-venn` (challenge): 20 features, seven in the centre, long items, Personal ∩ Education stated empty.
- `ios-android-venn` (smoke): two sets, 10 items.

`--input` checks score the model's own spec: `set:<key>`, `item:<key>`, `region:<key>` (exactly the stated combination, any key order), `sets-exact`, `items-exact` (no unstated item, none in two regions) and `empty-region:<combo>` (no items, a count of 0, or no entry). Labels compare without case, spacing, punctuation or a leading bullet; set order, ids and colors are free. `venn-drawn` (output) reads the scene back: every outline with its label outside and nearest its own outline, every item, region label and count wholly inside its own sets and outside the others. `venn-layout` (hint): no text overlaps, items ≥ 4 px clear of outlines, set labels clear of every circle.

## Verification

- **Tests that fail on main:** core contract, issues and shorthand; renderer (symmetric arrangement, labels over each set's own part, whole lines where there is room, crescent centring, whole Latin tokens, growth for heavy regions, translucent groups and z-order, contrast on every blend for dark colors, key order, empty regions, work guard); Excalidraw export and reload; Code Mode `buildVennDiagram` (documented spec, aliases and memberships, patch keeps roles, round-trip, every repair code); generation; scenarios (expected diagrams pass, each mutation fails its own check, equivalent forms pass, swapped labels and an item across an outline fail `venn-drawn`, overlap fails `venn-layout`, fixture model, MCP receipt); CLI offline failures and PNG export; MCP docs and tool bounds; route lists.
- **Layout work guard:** deterministic placement counts, not wall clock: the fixture under 400k, a twelve-item centre under 600k. Renders take 0.2–75 ms.
- **Fuzz** (`pnpm nx run diagram-excalidraw:venn-fuzz`, CI runs 12): 900 random diagrams over three seeds, two or three sets with short, long and CJK labels and colors, regions empty, sparse or packed to the caps with phrases up to 60 characters and a 33-character token, sometimes labeled or counted. Zero defects: every export valid before and after reload, every item drawn once, whole, inside exactly its region, no text overlap, set labels outside every circle, aspect between 1:1.4 and 2.45:1. A mutation that ignores the outlines a region excludes fails 805 checks.
- **Local:** `pnpm nx affected -t typecheck,test,build --base=origin/main` (18 projects), `pnpm run check`, `pnpm run test:tools`, `pnpm nx build-storybook diagram-ui`, `pnpm run test:wrangler-dry-runs`.
- **PNGs inspected** (CLI `create` + `export --format png`): every scenario and the stress cases (two sets, twelve-item centre, twelve-item overlap, long items with a dark custom color, counts under 40-character labels, sets alone).
- **Preview MCP replay** at `80bac7d9` (studio preview `/mcp`, one `buildVennDiagram` per scenario written from its prompt, each accepted on the first call, 0.7–1.4 s warm): `team-skills-venn` 35/35, `plan-features-venn` 50/50 (seven centre items at full size, Personal ∩ Education empty), `ios-android-venn` 28/28. Hosted PNG of the challenge inspected.
- **Real page** (`agent-browser -p browserbase`, preview): the challenge's artifact page and edit page render with no page or console errors; dragging the Personal circle moves its fill, outline and label as one group while every region's text stays put.

## Review fixes

Codex review on 80bac7d, each with a failing-first test (core, renderer and Code Mode):

- An item or region label with an authored newline now renders: each authored line wraps on its own, and the natural lines are always an option (it threw `render_failed` before).
- `regions` given as a lone object reads as a list of one; a string, number or `null` reaches the schema and is rejected (it was silently replaced with `[]`).
- A diagram of counts alone draws every count, 0 included. A 0 is empty only in a diagram that lists items and no positive count.
- Palette names match the palette's own keys, so `constructor` or `__proto__` get the invalid-color hint instead of crashing the render.

Renders are byte-identical to 80bac7d for every input without these cases: 920 scene and Excalidraw hashes (fixture, stress cases, the three scenario specs, the three preview-replay specs and the 900 fuzz diagrams).

The two CI flakes this PR hit (the rollback flowchart timeout and the render pool launch pacing) are fixed on main by #559; this branch is restacked onto it.

## Known limits

- Four sets (the ellipse arrangement) are not in this PR; the contract rejects them with a repair hint.
- A crowded centre makes a large diagram: twelve centre items need circles of radius ~285 px, so the export is ~820 × 870.
- Items in a narrow crescent wrap onto two lines when keeping them whole would need circles more than a fifth larger.
@graphite-app
graphite-app Bot force-pushed the feat/venn-diagrams branch from 66d4f7d to bc303df Compare October 29, 2026 15:33
@graphite-app
graphite-app Bot merged commit bc303df into main Oct 29, 2026
14 checks passed
@graphite-app
graphite-app Bot deployed to production October 29, 2026 15:35 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 15:35 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 15:35 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 15:35 Active
@graphite-app
graphite-app Bot deployed to production October 29, 2026 15:35 Active
@graphite-app
graphite-app Bot deleted the feat/venn-diagrams branch October 29, 2026 15:35
@github-actions github-actions Bot mentioned this pull request Oct 29, 2026
graphite-app Bot pushed a commit that referenced this pull request Oct 29, 2026
## What changes

Native user journey maps (#554) across every surface. Until now a journey-map prompt went through `createCanvas` with model-placed coordinates.

- **Contract** (`journey`). Ordered `stages`, each `{ name, steps, painPoints?, opportunities? }`; a step is `{ label, score, actor?, touchpoint? }` (a step several actors share names them all, `"Customer, Agent"`); optional `persona` and `actors`.
  - `score` uses Mermaid's journey scale: a whole number from 1 (very negative) to 5 (very positive), 3 neutral.
  - Validation returns 7 typed issues with repair hints: a score off the scale or not whole, an empty or repeated stage, an actor outside `actors`, repeated actors, text over its limit (step labels 90 characters, notes 100), and size (10 stages, 40 steps, 8 per stage, 4 notes per stage per kind, 8 actors).
  - Code Mode and generation also read Mermaid's `sections` of `tasks` (and `Task: 5: Actor` strings), `title`/`task`/`sentiment`/`emotion`/`channel` aliases, `pains`/`ideas`, a persona written as `{ name, description }`, and flat `steps` and notes that name their `stage`. A flat note naming a stage the journey lacks, or none, is rejected with the stages it may name; nothing is dropped.
- **Layout** (`journey.ts`). Its own placement; `scene.ts` and every other family's renderer are unchanged.
  - Stages are colored headers across the top (blue, violet, pink, so none reads as a mood or a note), each step's card under its stage in reading order, dividers between stages, and a header column naming each row.
  - An experience row is a labeled 1–5 scale: `5`, `4`, `3 · Neutral`, `2`, `1` at its left, a faint rule at every level but neutral's dashed baseline, and a face per step centered under its card, exactly on its score's level, with **the score printed just above it**, clear of the curve, the face and the level above. A face's mouth smiles, sets flat or frowns with its score and its color runs red → gray → green, so color never works alone. One rounded line runs through the faces.
  - Pain points and opportunities stack as notes spanning their stage's columns, in rows beneath the curve.
  - A stage's column is never narrower than the widest word of its name or notes; a stage wider than its steps centers them.
  - Cards share one width (the narrowest at which no label takes more lines than on the widest card) and one height per band; labels wrap whole, at the card's top, with `actor · touchpoint` at its foot.
  - **Wide journeys.** Past 2.45:1 a journey narrows its cards (while every label keeps to four lines), then raises its curve, then wraps into bands of whole stages (narrowest widest band, then the most even, earlier bands fuller). The curve runs on across a wrap: dashed out of a band toward the next step's score and into the next band from the last one's, with a "continued" label. A stage too long for any band splits and repeats its header as "(continued)". Every placement fits the CLI's PNG ceiling.
- **Excalidraw.** Each card is a group (rounded rectangle, the label bound to its top, details at its foot); each face is a group (ellipse, two eyes, a rounded mouth line) under a new `journey-mark` renderer role, kept through `applyDiagramPatch`; the curve and baseline are Excalidraw lines. The CLI PNG adds no title banner, since the scene draws its own title and persona.
- **Code Mode and MCP.** `buildJourneyMap`, `docs({ topic: "buildJourneyMap" })`, three issue codes (`journey_too_large`, `invalid_journey_stage`, `invalid_journey_step`). Docs cost (o200k):
  - +17 characters (+4 tokens) on the execute description head: `buildJourneyMap` on its Builders line (1,539 of 2,048).
  - +367 characters (+94 tokens) on the `code` parameter: one index line (16 tokens) and one compact spec line (78 tokens); budget 10,650 → 11,000 (now 10,915).
  - Compact tools/list JSON 13,872 (budget 13,650 → 13,950).
  - The topic, served on demand: 1,382 characters (337 tokens).
- **Every other family surface:** generation IR rules and an example (count `tasks` and label `task`/`phase` targets, as Mermaid's tasks and sections, so the shared requirement lists are unchanged); CLI `create` and `generate --type journey` with a changeset; the generate API; Studio `build_journey_map` and `POST /api/v1/journey-maps/build`; Storybook stories; both `llms.txt`; both READMEs; `docs/journey-maps.md`.

## Scenarios

From Frontierclimb's eval draft (§4), with its check IDs:

- `online-purchase-journey` (smoke): four stages, eight steps with touchpoints, a pain point and an opportunity under two stages.
- `saas-onboarding-journey` (challenge): six uneven stages (2, 2, 2, 2, 1, 1), long labels (one of 85 characters), five actors, notes under four stages.
- `support-ticket-journey` (standard): four stages, seven steps, one pain point and one opportunity.

`--input` checks score the model's own spec: `persona`, `stage:<key>`, `stage-order`, `step:<key>` (under its stage), `step-order:<stage>`, `score:<key>`, `actor:<key>`/`touchpoint:<key>` (the field, or a label opening with it), `pain-point:<stage>:<n>`, `opportunity:<stage>:<n>`, `steps-exact`, `pain-points-exact`, `opportunities-exact`, and `actors-stated` where the prompt names actors. A label may drop the actor or persona its step names, or a leading article; the persona may live in the title. `journey-drawn` (output) reads the scene back: every stage header in order, each card under its stage, each face at its score's height inside its step's column with the matching expression, the curve through the faces, and each note in its stage's column. `journey-layout` (hint): nothing overlaps, every text fits, rows keep to their stages.

## Verification

- **Tests that fail on main:** core contract, validation and shorthand (`journey.test.ts`); renderer (stages, cards, faces at their scores and expressions, curve, notes, details lines, whole-word wraps, narrowing, banding, a split stage, every stress fixture, and the readout catching a moved face, a sad face on a 5 and a broken curve); Excalidraw conversion and reload; generation (prompt, Mermaid aliases, repair hints, `tasks`/`phase` targets); Code Mode `buildJourneyMap` (documented example, aliases, flat steps, patch keeping faces, round-trip, every repair code); scenarios (expected diagrams pass, each mutation fails its own check only, equivalent forms pass, drawn and layout checks, fixture client, MCP receipt); CLI offline failures and PNG export with no second title banner; MCP docs, tool bounds and route lists.
- **Fuzz** (`pnpm nx run diagram-excalidraw:journey-fuzz`, CI runs 8): 900 random journeys across three seeds (1–40 steps, 1–10 stages, uneven stages, 90-character labels, long header and note words, shared actors, hyphenated and Japanese text, touchpoints, empty and full note rows): **0 defects**. Each exports and reloads clean, stays under 600 elements, passes every layout rule (no text crossed by the curve or a rule; each printed score matches its face and sits on its level) and reads at 2.45:1 or less. 6 of 900 (37–40 steps with full note stacks) exceed the CLI's PNG area ceiling, capped at 1 in 100. ~10 s locally.
- **Render time:** 4 ms for the fixture, 16 ms for 40 steps (the placement search caches word advances; a 40,000-case local check matched the shared wrap line for line).
- **PNGs inspected:** the three scenarios, the fixture and every stress fixture (40 steps in three bands, ten stages, an 8-step single stage split across bands, the wordiest text, a single step).
- **Preview MCP replay** at `aeca9e1d` (studio preview `/mcp`, `execute` with each scenario's `buildJourneyMap` written from its prompt, graded with the scenario CLI `--input`): each accepted on the first call in 2–8 s; online purchase 45/45, SaaS onboarding 59/59 (two bands, curve running on), support ticket 40/40. The hosted PNGs match the local renders.
- **Real page** (`agent-browser -p browserbase`, preview): the SaaS onboarding artifact's review and edit pages render with no page or console errors; dragging a card moves it with its label and details, dragging a face moves its eyes and mouth; a reload restores the stored artifact.
- **Frontierclimb controls** (§7), rendered with this branch's CLI and handed to the lead: a step moved to the next stage and a changed score (`data-accuracy`), a face shifted between two columns (`ambiguous-value`), a frowning face at the score-5 height (`misleading-scale`), and the wrapped SaaS journey (should pass).
- **Local:** `pnpm nx affected -t typecheck,test,build --base=origin/main` (18 projects), `pnpm run check`, `pnpm run test:tools`, `pnpm nx build-storybook diagram-ui`, `pnpm run test:wrangler-dry-runs`; scenario CLI `--fixture` passes all three (44, 58 and 39 checks).

## Fix after the A/B and review (364f4f5)

- **Scores readable from the image** (A/B: judge could not tell a 4 from a 5): every face's score printed above it, and a labeled 1–5 level axis per band with faint rules; `journey-drawn` now also fails a missing or wrong printed score, and the readout checks each face against its level's label. The least level pitch (34) leaves the printed score clear of the level above, so no rule breaks around it.
- **Codex P2s:** a one-step stage named `Acknowledgement` (or holding a long note word) no longer fails to render; `Review request: 4: Customer, Agent` and `actors: ["Customer", "Agent"]` keep both actors as `"Customer, Agent"`, each checked against `actors`; a flat note naming `Revieew` is rejected (`invalid_journey_stage`) with the stage names it may use. Each has a failing-first test.
- Placement also keeps to the canvas's 600-element limit, and a fallback placement no longer runs to a column of bands.
- Preview replay at `364f4f51`: online 45/45, SaaS 59/59, support 40/40, each accepted on the first call.

## Restack onto Venn and delta-review fixes (d256f92)

- Restacked onto `5d43d4d7` (Venn #557): shared registries resolved additively (both families everywhere); MCP budgets now count both (code parameter 11,251 < 11,300; tools/list 14,253 < 14,300).
- Delta review P2s, each with a failing-first test: flat steps group under their stages one by one, and a step naming no stage is rejected (`unplaced_journey_entry` → `invalid_journey_stage`) instead of dropping every flat step; a shared step's actors resolve against declared actors, so `"Smith, Jane"` stays whole.
- Scenario and fixture scenes are byte-identical to `364f4f51`; preview replay at `d256f921` passes 45/45, 59/59, 40/40 and its hosted scenes are identical to the `364f4f51` replay.

## Known limits

- A 30-plus-step journey with full note stacks may exceed the CLI's PNG area ceiling (6 of 900 in the fuzz); Excalidraw and hosted PNGs are unaffected.

- The CLI's headless PNG cannot draw Japanese text (the bundled Excalifont lacks CJK glyphs), as for every family; Excalidraw and hosted PNGs are unaffected.
- A face's score is its height and expression; the score number is not written on the map.
- The curve is an Excalidraw line through the faces, not bound to them: dragging a face in the editor leaves the curve where it was.
graphite-app Bot pushed a commit that referenced this pull request Oct 29, 2026
## What changes

Sankey diagrams become a native family (#553). Until now "show our budget as a Sankey" went through `createCanvas` with model-placed coordinates, or was refused. Now the model sends nodes and weighted links, and Sketchi lays the diagram out.

- **Contract** (`sankey`): `nodes: [{ id, label, color? }]`, `links: [{ source, target, value, label? }]`, optional `unit` formatted as charts format it. No field takes coordinates.
  - Semantic validation returns 11 typed issues with repair hints: cycles (named in flow order, with the closing link and a "send it to `X (returning)`" hint), self-links, unknown or unlinked nodes, repeated ids or pairs, a colon in an id, values of zero or less (a negative one is told to swap its ends), blank or over-long labels, and more than 40 nodes, 100 links or 8 columns.
  - A node's inflow and outflow may differ; it draws at the larger.
  - Shorthand for Code Mode and generation: `flows`/`edges`, `from`/`to`, `amount`/`weight`/`count`, nodes as bare names or with `name`, links that name nodes by label, and links alone with no node list. A malformed node or link list (null, an object, a name) is never replaced; it is left for the decoder to reject with the field named.
- **Layout** (`sankey-layout.ts`, off the graph path; `scene.ts` is unchanged):
  - **Columns.** Each node sits one column past the longest run of links into it. A sink fed by several nodes (a shared drop-off, an outcome) aligns with the last column; one fed by a single node stays beside it, so per-stage drop-offs read stage by stage. A link that spans columns passes each one through a slot between its bars, kept as straight as the room allows.
  - **Bars and bands.** Bars are as tall as their fuller side; bands are as wide as their values, the same at both ends; on each side of a bar the bands stack and the fuller side fills it. The documented minimum: a band narrower than 3 px draws at 3 px and keeps its exact value. That is the only width that departs from scale.
  - **Order.** Each column is ordered so that as little band *weight* crosses as possible: a crossing weighs the product of its two flows' values, and crossing count breaks ties. The search runs barycenter sweeps, then moves each node to its best place in its column, starting from several orders (authored, the sweeps' best, and the sweeps from the reversed authored order). Band slots follow their other ends. The authored order is kept unless something beats it, so crossings never weigh more than the authored baseline. Bars then relax toward the bands they join, with a 22 px gap (more where labels need it).
  - **Text.** Every node shows its label and total: left of the first column, right of the others, riding its own bands' straight leads in a middle column. Every flow shows its value:
    - inside its band where it enters its target when the band is thick enough;
    - otherwise just beside the band at either end or along its curve;
    - otherwise the diagram first grows taller (capped), and only then does the value sit at the nearest clear spot on a short leader that crosses no label or bar;
    - if some value still has no clear spot, the layout reflows with wider column gaps and room to grow taller. A diagram that still can't place every label clear is rejected by `buildSankey` (`quality_failed`, with a hint to shorten node and link labels, leave link labels out, or split the flows). Overlaps are never accepted.
    - Words wrap whole; authored newlines are line breaks; a word unit (`visits`) reads on totals only.
  - **Color.** Nodes with outflow take the charts' CVD-checked palette column by column, so meeting sources differ. Bands take their source's color at 45% opacity. Pure sinks draw in neutral gray.
- **Excalidraw.** Bands are closed, filled, roughness-0 lines grouped with their values; bars are square-cornered rectangles grouped with their labels. A new `sankey-mark` renderer role keeps both through patches. Exports validate and survive a save and reload.
- **Code Mode and MCP.** `buildSankey`, `docs({ topic: "buildSankey" })`, four issue codes (`sankey_too_large`, `invalid_sankey_node`, `invalid_sankey_flow`, `sankey_cycle`) and a compact line on the `code` parameter. Docs cost (o200k):
  - execute description head: +13 characters (+5 tokens), just `buildSankey` on its Builders line (1,535 of 2,048);
  - `code` parameter: +304 characters (+86 tokens). With Venn and journey also on main, it is 11,555; budget 11,300 → 11,600;
  - compact tools/list JSON: 14,590 with all three; budget 14,300 → 14,600;
  - the topic, on demand: 1,336 characters (360 tokens);
  - generation system prompt: +457 characters (~116 tokens) of Sankey rules, plus `"links"` as a count target.
- **Every other surface:**
  - generation rules and example, with `links` count and `node` label targets;
  - CLI `create` and `generate --type sankey`, with a changeset; the CLI PNG skips its banner, since the scene draws its own title;
  - the generate API; Studio `build_sankey`; `POST /api/v1/sankey-diagrams/build`;
  - the Studio policy line; Storybook stories; both `llms.txt`; both READMEs; `docs/sankey.md`.

Rebased onto main after Venn (#557) and journey maps (#560); shared registries keep all three families, in the order Venn, journey, Sankey. Shared files: additive edits to `DIAGRAM_TYPES`, `diagram.ts`, `documents.ts`, the MCP `catalog.ts`, `messages.ts`, `intent.ts`, CLI generation types and both `llms.txt`. `CANVAS_RENDERER_ROLES` gains `sankey-mark`; other families' scenes are byte-identical.

## Scenarios

The prompts and check IDs follow Frontierclimb's eval draft (§3):

- `cloud-budget-sankey` (challenge): 5 departments → 7 services, flows from 2 to 310, long service names.
- `website-traffic-sankey`: 3 sources → 3 landing pages → 3 outcomes, 15 crossing flows.
- `hiring-funnel-sankey`: rejections and withdrawals at every stage.
- `city-energy-sankey`: five columns, with gas imports skipping to the sectors.

Checks:

- `--input` checks score the model's own spec: `node:<key>`, `flow:<source>-><target>` with the exact value, `unit`, `nodes-exact` and `flows-exact`.
  - The same amounts in another accepted unit pass (310000 in `$` is 310 `$k`); a reversed flow fails.
  - A drop-off the prompt doesn't pin down passes as one shared node or one per stage.
  - An "Unallocated" node or a split flow fails.
- `sankey-drawn` (output): every bar with its label and total, every band between its bars with its value, widths in proportion apart from the floor, and each bar as tall as its fuller side.
- `sankey-layout` (hint): no label overlaps, bands stay in their gaps, and crossings weigh no more than the authored order's (each crossing weighs the product of its two flows' values).

## Verification

- **Tests that fail on main:**
  - core contract, shorthand and edge inputs (authored newlines, blank labels, zero vs missing values, `__proto__`/`constructor` ids and units, malformed lists);
  - renderer: columns and sink alignment, proportional widths and the floor, exact sliver labels beside their bands, unbalanced nodes, label sides, colors, whole-word wraps, every stress shape, work bound;
  - Excalidraw export, roles and reload;
  - Code Mode `buildSankey`: documented spec, shorthand, patch keeping roles, round-trip, every repair code, malformed lists;
  - generation; scenarios (each passes its own diagram, each mutation fails its check, drawn and layout checks, fixture model, MCP receipt);
  - CLI offline failures and PNG export; MCP docs, protocol and budgets; the frozen issue-code corpus; route lists.
- **Fuzz** (`pnpm nx run diagram-excalidraw:sankey-fuzz`, CI runs 10): 1,200 seeded random flows across three seeds, with zero defects and zero aspect shortfalls.
  - Shapes: two-column budgets, funnels with shared or per-stage drop-offs, layered flows with skipped columns, splitting trees and merges.
  - Inputs: values from 0.01 to ~16,000, slivers, long and CJK labels, units, link labels.
  - Invariants checked on every flow:
    - every export validates;
    - on each side of every bar the bands sum to its height;
    - no band leaves its gap or crosses another bar;
    - no label overlaps a label, a bar, or another band;
    - every width follows its value at one scale, apart from the 3 px floor;
    - crossings weigh no more than the authored order's (Σ products of crossing flows' values);
    - every node label reads whole.
- **Local:**
  - `pnpm nx affected -t typecheck,test,build --base=origin/main` (18 projects), `pnpm run check`, `pnpm run test:tools`;
  - `pnpm nx build-storybook diagram-ui`, `pnpm run test:wrangler-dry-runs`.
- **Weighted crossings**, before → after the weighted ordering (Σ products of crossing flows' values; authored order in parentheses):
  - cloud-budget-sankey: 86,848 → 8,300 (34,848). Brute-force optimum is 7,314. Engineering → Compute no longer crosses either Data Science band; the only crossings left are Eng → Object storage × DS → Compute (45 × 140) and slivers.
  - website-traffic-sankey: 7,260,000 → 7,260,000 (23,240,000), the brute-force optimum.
  - hiring-funnel-sankey: 0 → 0 (250,500).
  - city-energy-sankey: 73,950 → 64,350 (225,000).
  - energy stress: 977.6 → 760.6 (2,184.4); dense 8×8: 1,449,698 → 1,422,326 (2,119,562); cloud-budget fixture: 10,900 → 10,200 (20,525).
  - slivers 80 → 80, eight stages 0 → 0, per-stage drop-offs 0 → 0, long labels 0 → 0.
- **Render time** (median, warm Node): 0.3–2.8 ms for the fixtures and scenarios (cloud budget 1.4 → 1.8 ms, energy stress 1.7 → 2.8 ms), and 102 → 110 ms for the 64-flow 8×8 stress case. Ordering work is a deterministic pair-comparison count with a 4M budget.
- **PNGs inspected** for every scenario and the stress shapes: slivers, long labels, eight stages, per-stage drop-offs, a 26-flow energy flow, and the 64-flow dense budget.
- **Preview MCP replay** at `142b5b8b` (restacked on #559): each scenario's `buildSankey`, written from its prompt, was accepted on the first call (0.8–1.4 s warm; the first, cold call took 8–10 s). Scores: cloud budget 31/31, traffic 30/30, hiring 24/24, city energy 34/34. The hosted PNGs match the local renders.
- **Real page** (`agent-browser -p browserbase`, preview): the cloud budget review page and edit page render with no page or console errors. Dragging a band moves it and its value as one group, and reloading the review page restores the stored artifact unchanged.

## Known limits

- In dense all-to-all flows, values that find no spot beside their band sit on leader lines. About 5% of values do across adversarial random flows; the scenarios need none. Where even leaders can't keep labels clear (64 flows each carrying a 40-character link label), the build is rejected with a repair hint rather than drawn overlapping.
- Excalidraw bands are not bound to bars, so dragging a bar leaves its bands where they were.
- Columns follow longest paths: a node fed only by sources sits in the second column even when its peers sit further right (Transportation in the energy stress case sits beside Electricity).

## Review fixes (Codex review of 142b5b8, plus the lead's crossing note)

- **Weighted crossings.** Ordering minimizes Σ(product of crossing flows' values), with count as the tie-break. The fuzz and `sankey-layout` hold crossings to the authored order's weight.
- **Exact ids win.** In shorthand, an end resolves to the node with that exact id before any label or case-insensitive alias. Nodes `{a, label "b"}` and `{b}` keep an authored `a → b`.
- **No accepted overlaps.** The layout reflows when a value has no clear spot (wider gaps, taller). `buildSankey` rejects any scene with layout issues as `quality_failed`, with a repair hint. The last-resort spot search is bounded (about 2,500 places, skipped once a drawing is already headed for reflow or rejection): Codex's 64-labeled-flow case lays out in 436 ms instead of 4.6 s and is still rejected.
- **Authored precision.** Totals round at the finest precision among their values with no fixed cap, so `1e-13` totals `1e-13` and `0.1234567890123` keeps every digit.
- **Scale-relative ties** (Codex delta review of ba3d35b). Crossing scores now compare within 1e-9 of their own magnitude rather than an absolute 1e-9, so a unit change (the cloud budget ×1e-8) can't turn a heavier ordering into a tie. Every scenario, replay spec, stress fixture and fuzz render (1,218 scenes and exports) is byte-identical to ba3d35b.

This branch was successfully deployed

1 active and 1 inactive deployments
staging — bc303df7 Deployed Oct 29, 2026 by graphite-app[bot] via cleanup-preview (icons) #1021
production — bc303df7 Deployed Oct 29, 2026 by graphite-app[bot] via deploy (eval-harness) #231
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant