Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/venn-diagrams.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@sketchi/cli": minor
---

Add native Venn diagrams: `sketchi create` builds `{ "type": "venn", "spec": { "title", "sets", "regions" } }` documents offline, and `sketchi generate --type venn` generates them. Two or three equal, overlapping translucent circles each carry their set's label outside, and each region's items (or a count) sit centred inside exactly that region; the circles grow until every region's text fits, so a crowded centre or long items never cross an outline.
3 changes: 2 additions & 1 deletion .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@
"packages/diagram/core/src/types/gantt.ts",
"packages/diagram/core/src/types/er.ts",
"packages/diagram/core/src/types/class.ts",
"packages/diagram/core/src/types/org-chart.ts"
"packages/diagram/core/src/types/org-chart.ts",
"packages/diagram/core/src/types/venn.ts"
],
"rules": {
"eslint/no-restricted-imports": [
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ local proof when their inputs change:
- `pnpm nx run diagram-excalidraw:er-fuzz` (240 schemas; CI runs 16)
- `pnpm nx run diagram-excalidraw:class-fuzz` (240 models and 120 text-fit rounds; CI runs 16 and 12)
- `pnpm nx run diagram-excalidraw:org-chart-fuzz` (900 random charts across three seeds; CI runs 6)
- `pnpm nx run diagram-excalidraw:venn-fuzz` (900 random Venn diagrams across three seeds; CI runs 12)
- `pnpm nx run diagram-excalidraw:sequence-fuzz` (400 sequences with self-messages; CI runs 12)
- `pnpm run test:wrangler-dry-runs` after app builds

Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ scene representations.

## What Sketchi Does

- Creates hosted flowcharts, mindmaps, sequence diagrams, timelines, state machines, Gantt charts, ER diagrams, UML class diagrams, org charts, and bar, line, and pie charts from natural-language prompts.
- Creates hosted flowcharts, mindmaps, sequence diagrams, timelines, state machines, Gantt charts, ER diagrams, UML class diagrams, org charts, Venn diagrams, and bar, line, and pie charts from natural-language prompts.
- Produces validated, editable Excalidraw scenes instead of opaque images.
- Exposes Code Mode through MCP and versioned HTTP artifact endpoints.
- Keeps layout, bindings, wrapping, and scene conversion deterministic in code.
Expand Down Expand Up @@ -68,7 +68,7 @@ Explicit `--type` and file `--dest` values preset their wizard questions;
interactive generation remains PNG-only, so other formats and `--dest -` also
require `--prompt`.
Without `--type`, the model selects a supported native type. Explicit flowchart,
mindmap, sequence, timeline, chart, state-machine, gantt, er, class, or org-chart selection is authoritative; architecture and
mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart, or venn selection is authoritative; architecture and
swimlane requests fail clearly instead of being coerced.
The default file is `<generated-id>.png` in the current directory. Use
`--format excalidraw` or `--format scene` for another artifact, and `--dest`
Expand Down Expand Up @@ -149,7 +149,7 @@ model candidate → typed diagram IR → deterministic scene → Excalidraw arti
| `packages/svg-excalidraw` | Native SVG-to-Excalidraw conversion and library serialization |

The first high-reliability target is decision-heavy flowchart generation.
`flowchart`, `mindmap`, `sequence`, `timeline`, `chart`, `state-machine`, `gantt`, `er`, `class`, and `org-chart` are explicit diagram families; unsupported families do
`flowchart`, `mindmap`, `sequence`, `timeline`, `chart`, `state-machine`, `gantt`, `er`, `class`, `org-chart`, and `venn` are explicit diagram families; unsupported families do
not silently fall back to a generic template. See the full
[architecture guide](docs/architecture.md) and the
[MCP-first generation boundary](docs/mcp-first-generation.md).
Expand Down Expand Up @@ -335,3 +335,5 @@ Native ER diagrams: use `buildErDiagram` (Code Mode) or `generate --type er` (CL
Native class diagrams: use `buildClassDiagram` (Code Mode) or `generate --type class` (CLI). Supply classes with unique ids (no colons or spaces), an optional `kind` (`abstract`, `interface` or `enum`) and members written as UML does (`"- radius: double"`, `"+ area(): double {abstract}"`), and relationships with a `kind`: `inheritance` or `realization` (`from` extends or implements `to`), `composition` or `aggregation` (`from` is the whole), `association` (`navigable: true` adds an arrow) or `dependency` (`from` uses `to`). Associations, compositions and aggregations take multiplicities and roles at each end. The renderer draws three-compartment boxes with superclasses above their subclasses and each relationship's UML head; it owns every coordinate.

Native org charts: use `buildOrgChart` (Code Mode) or `generate --type org-chart` (CLI). Supply people with unique ids (no colons), a `name`, an optional `title` and `department`, and `manager`: the id of the person they report to (only the top person has none; `forest: true` allows several). `assistant: true` places an assistant beside their manager, `avatar: true` draws an initials placeholder, `dottedLines: [{ report, manager, label? }]` adds dotted-line or matrix reports (a stated scope or reason, such as "for clinical systems", goes in `label`), and `groupByDepartment: true` draws each department below the top as a labeled region. The renderer draws a card per person top-down, a bus line from each manager to their reports, long lists of reports stacked in one or two columns, and dotted lines dashed with a head at the second manager and their label beside them; it owns every coordinate.

Native Venn diagrams: use `buildVennDiagram` (Code Mode) or `generate --type venn` (CLI). Supply two or three `sets: [{ id, label, color? }]` and `regions: [{ sets, items }]`, one region per overlap: `["a"]` holds what is only in a, `["a", "b"]` what is in a and b but not c. A region holds `items` (short phrases) or a `count`, and an optional `label` naming the overlap; omit empty regions. A set may instead list its whole membership as `items`, and each item then lands in the region of exactly the sets that list it. The renderer draws equal, overlapping translucent circles in the standard symmetric arrangement, each set's label outside its circle, and each region's text centred inside exactly that region; it sizes the circles until every region fits and owns every coordinate.
10 changes: 6 additions & 4 deletions apps/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<p align="center">
<strong>Typed, deterministic diagrams from your terminal.</strong><br />
Create, inspect, revise, and export local flowcharts, mindmaps, sequence diagrams, timelines, charts, state machines, Gantt charts, ER diagrams, class diagrams, and org charts—or turn one prompt into validated, editable Excalidraw.
Create, inspect, revise, and export local flowcharts, mindmaps, sequence diagrams, timelines, charts, state machines, Gantt charts, ER diagrams, class diagrams, org charts, and Venn diagrams—or turn one prompt into validated, editable Excalidraw.
</p>

<p align="center">
Expand Down Expand Up @@ -53,7 +53,7 @@ sketchi generate --prompt "Map release approval with pass and revise branches"

With no `--prompt`, Sketchi opens a short wizard only when both standard input
and standard output are human terminals, output is text, and CI is absent. It
asks for prompt text, flowchart (the default), mind map, sequence diagram, timeline, chart, state machine, Gantt chart, ER diagram, class diagram, or org chart, and where to save the
asks for prompt text, flowchart (the default), mind map, sequence diagram, timeline, chart, state machine, Gantt chart, ER diagram, class diagram, org chart, or Venn diagram, and where to save the
PNG. The choices are the current directory, a `diagrams/` folder under exactly
the directory where Sketchi was run, or a custom path with the same semantics as
`--dest`. Sketchi does not inspect Git, and it creates `diagrams/` only after
Expand All @@ -64,7 +64,7 @@ that wizard question. Explicit `--format png` may proceed; another format or
Scripts, pipes, redirected streams, CI, and `--output json` never prompt or
block. They must pass `--prompt`; omitting it retains the deterministic usage
error. Passing `--prompt` is always direct and noninteractive.
`--type` accepts only flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, or org-chart and is authoritative.
`--type` accepts only flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart, or venn and is authoritative.
Without it, the generation model selects one of those from the request; a
request for any other kind of diagram returns a typed unsupported-type error
instead of being coerced into a supported one.
Expand Down Expand Up @@ -103,7 +103,7 @@ The endpoint defaults to
`https://playground.sketchi.app/api/v1/canvases/create`; use `--endpoint URL` or
`SKETCHI_CANVAS_ENDPOINT` only for preview and local testing. The existing
`create` command remains the separate, strictly offline command for accepted
flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, and org-chart documents.
flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart, and venn documents.

## Create and work with a diagram offline

Expand Down Expand Up @@ -332,3 +332,5 @@ Native ER diagrams: use `buildErDiagram` (Code Mode) or `generate --type er` (CL
Native class diagrams: use `buildClassDiagram` (Code Mode) or `generate --type class` (CLI). Supply classes with unique ids (no colons or spaces), an optional `kind` (`abstract`, `interface` or `enum`) and members written as UML does (`"- radius: double"`, `"+ area(): double {abstract}"`), and relationships with a `kind`: `inheritance` or `realization` (`from` extends or implements `to`), `composition` or `aggregation` (`from` is the whole), `association` (`navigable: true` adds an arrow) or `dependency` (`from` uses `to`). Associations, compositions and aggregations take multiplicities and roles at each end. The renderer draws three-compartment boxes with superclasses above their subclasses and each relationship's UML head; it owns every coordinate.

Native org charts: use `buildOrgChart` (Code Mode) or `generate --type org-chart` (CLI). Supply people with unique ids (no colons), a `name`, an optional `title` and `department`, and `manager`: the id of the person they report to (only the top person has none; `forest: true` allows several). `assistant: true` places an assistant beside their manager, `avatar: true` draws an initials placeholder, `dottedLines: [{ report, manager, label? }]` adds dotted-line or matrix reports (a stated scope or reason, such as "for clinical systems", goes in `label`), and `groupByDepartment: true` draws each department below the top as a labeled region. The renderer draws a card per person top-down, a bus line from each manager to their reports, long lists of reports stacked in one or two columns, and dotted lines dashed with a head at the second manager and their label beside them; it owns every coordinate.

Native Venn diagrams: use `buildVennDiagram` (Code Mode) or `generate --type venn` (CLI). Supply two or three `sets: [{ id, label, color? }]` and `regions: [{ sets, items }]`, one region per overlap: `["a"]` holds what is only in a, `["a", "b"]` what is in a and b but not c. A region holds `items` (short phrases) or a `count`, and an optional `label` naming the overlap; omit empty regions. A set may instead list its whole membership as `items`, and each item then lands in the region of exactly the sets that list it. The renderer draws equal, overlapping translucent circles in the standard symmetric arrangement, each set's label outside its circle, and each region's text centred inside exactly that region; it sizes the circles until every region fits and owns every coordinate.
2 changes: 1 addition & 1 deletion apps/cli/src/__fixtures__/help/agent-docs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Semantic color patch example:
sketchi patch release-flow --json '{"operations":[{"op":"setStyle","selector":{"nodeIds":["review","approve"]},"style":{"fillColor":"#dbeafe","strokeColor":"#2563eb","textColor":"#1e3a8a"}}]}'

Explicit network commands (one credential-free HTTPS request each):
sketchi generate [--prompt TEXT] [--type flowchart|mindmap|sequence|timeline|chart|state-machine|gantt|er|class|org-chart] [--model MODEL]
sketchi generate [--prompt TEXT] [--type flowchart|mindmap|sequence|timeline|chart|state-machine|gantt|er|class|org-chart|venn] [--model MODEL]
sketchi canvas --file PATH|- [--format png|excalidraw|scene] [--dest PATH|-]
sketchi share DIAGRAM_ID [--open]
sketchi pull DIAGRAM_ID --link URL|-
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/__fixtures__/help/generate.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Network and options:
Endpoint: https://playground.sketchi.app/api/v1/generate
The model call, schema validation, and quality gate run server-side; the finished diagram
and Excalidraw artifact are returned over plain HTTPS. Sketchi sends no credentials.
Without --type, the model selects flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, or org-chart from the request;
Without --type, the model selects flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart, or venn from the request;
a request for any other kind of diagram fails with a typed unsupported error.
--model defaults to gemini-3.1-flash-lite.
Override the endpoint for preview or local testing with SKETCHI_GENERATE_ENDPOINT
Expand Down Expand Up @@ -41,7 +41,7 @@ USAGE
FLAGS
--output text|json Result presentation format. (choices: text, json)
--prompt TEXT Diagram request text sent directly without interactive prompts.
--type TYPE Authoritative diagram type; omit for model selection. (choices: flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart)
--type TYPE Authoritative diagram type; omit for model selection. (choices: flowchart, mindmap, sequence, timeline, chart, state-machine, gantt, er, class, org-chart, venn)
--model MODEL Server-routed generation model id; default gemini-3.1-flash-lite.
--endpoint URL Unauthenticated generate API URL; defaults to the production Sketchi endpoint.
--format png|excalidraw|scene Artifact exported after generation; default png. (choices: png, excalidraw, scene)
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/code-mode-artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function codeModeFailure(
"invalid_er_diagram",
"invalid_class_diagram",
"invalid_org_chart",
"invalid_venn_diagram",
"quality_failed",
];
if (validationStatuses.includes(result.status))
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/generation-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const GENERATION_TYPE_CHOICES = {
er: { label: "ER diagram", hint: "best for database tables, keys and relationships" },
class: { label: "Class diagram", hint: "best for UML classes, members and relationships" },
"org-chart": { label: "Org chart", hint: "best for people, titles and who reports to whom" },
venn: { label: "Venn diagram", hint: "best for what two or three overlapping sets share" },
} as const satisfies Record<GenerationType, { readonly hint: string; readonly label: string }>;

/** Wizard options in registry order, one per canonical family. */
Expand Down
32 changes: 32 additions & 0 deletions apps/cli/src/png-renderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,38 @@ layer(Layer.mergeAll(builderLayer, CliPngRendererLive))("headless PNG renderer",
}),
);

it.effect("exports a Venn diagram of translucent circles with its items inside", () =>
Effect.gen(function* () {
const builder = yield* DiagramBuilder;
const renderer = yield* CliPngRenderer;
const built = yield* builder.build(
canonicalDocument({
type: "venn",
spec: {
title: "iOS vs Android",
sets: [
{ id: "ios", label: "iOS" },
{ id: "android", label: "Android" },
],
regions: [
{ sets: ["ios"], items: ["Apple Pay"] },
{ sets: ["android"], items: ["Google Pay"] },
{ sets: ["ios", "android"], items: ["Push notifications", "Dark mode"] },
],
},
}),
);
const ellipses = built.excalidraw.elements.filter((element) => element["type"] === "ellipse");
assert.sameMembers(
ellipses.map((ellipse) => String(ellipse["opacity"])),
["22", "22", "100", "100"],
);
const png = yield* renderer.renderPng({ scene: built.scene, excalidraw: built.excalidraw });
assert.isAbove(png.byteLength, 1_000);
assertInkHasPadding(png);
}),
);

it.effect("exports a class diagram whose lines bind elbowed with their UML heads", () =>
Effect.gen(function* () {
const builder = yield* DiagramBuilder;
Expand Down
53 changes: 53 additions & 0 deletions apps/cli/src/venn-cli.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { spawnSync } from "node:child_process";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";

const binary = resolve(process.cwd(), "apps/cli/dist/sketchi.js");

describe("offline Venn diagram input failures", () => {
it.each([
{
name: "a region naming an unknown set",
issue: "invalid_venn_region",
regions: [{ sets: ["ios", "web"], items: ["Push notifications"] }],
},
{
name: "a region with items and a count",
issue: "invalid_venn_region",
regions: [{ sets: ["ios"], items: ["Apple Pay"], count: 3 }],
},
])("reports $name as invalid_document with exit 3", ({ issue, regions }) => {
const environment = { ...process.env };
delete environment["FORCE_COLOR"];
delete environment["NO_COLOR"];
const result = spawnSync(
process.execPath,
[
binary,
"create",
"--output",
"json",
"--json",
JSON.stringify({
type: "venn",
spec: {
title: "Platforms",
sets: [
{ id: "ios", label: "iOS" },
{ id: "android", label: "Android" },
],
regions,
},
}),
],
{ encoding: "utf8", timeout: 20_000, env: environment },
);
expect(result.error).toBeUndefined();
expect(result.status).toBe(3);
const output: { ok: boolean; error: { code: string; details: string[] } } = JSON.parse(
result.stderr,
);
expect(output).toMatchObject({ ok: false, error: { code: "invalid_document" } });
expect(output.error.details.some((detail) => detail.startsWith(issue))).toBe(true);
});
});
Loading
Loading