Sketchi v2 should make diagram generation boring in the best way: inputs are validated, rendering is deterministic, and UI states are exercised outside the app shell.
| Area | Source of truth | Proof |
|---|---|---|
| Diagram contract | packages/diagram/core |
core tests and fixtures |
| Deterministic scene | packages/diagram/renderer |
renderer tests and stories |
| Excalidraw output | packages/diagram/excalidraw |
real-scene validation |
| Agentic generation | packages/diagram/generation, then diagram-agent |
fixture/eval tests |
| Product surfaces | apps/* |
Nx app builds and Worker deploys |
flowchart LR
Prompt["Prompt or revision"] --> Generation["diagram-generation<br/>model request + candidate"]
Generation --> Core["diagram-core<br/>typed IR validation"]
Core --> Renderer["diagram-renderer<br/>scene model"]
Renderer --> Excalidraw["diagram-excalidraw<br/>persistable scene"]
Excalidraw --> UI["apps + diagram-ui"]
diagram-coreowns the intermediate representation, semantic validation, and reusable fixtures.diagram-rendererconverts validated diagrams into a deterministic scene model.diagram-excalidrawconverts deterministic scenes into persisted Excalidraw elements and validates real-scene invariants.diagram-scenariosowns maintained prompts, assertions, fixture evaluation, and local command-provider runs.diagram-generationowns provider request/response mapping and candidate parsing.diagram-agentowns the canonicalbuildFlowchartrequest/result vertical, structured issues, quality assessment, render/export orchestration, and accepted artifact persistence used by Studio, HTTP, and MCP.diagram-uirenders the scene model and owns user-facing component states.studio-projectsowns Studio project/diagram contracts, browser API helpers, anonymous and authenticated ownership, and object-bucket persistence. It has no dependency on app routes, Cloudflare bindings, or the Code Mode runtime.apps/eval-harnesscomposes the packages in a TanStack Start internal eval harness for scenarios, candidate inspection, and regression review.apps/playgroundowns the public Playground worker boundary: ephemeral chat generation over the canonical build runtime and the HTTP/browser routes for the persisted Studio foundation. A thin server adapter translates the R2 binding and validated Code Mode source artifacts intostudio-projects; the app never performs a second accepted-artifact write.apps/webowns the public home/docs surface and should stay free of diagram runtime dependencies unless docs become interactive.apps/excalidrawowns the internal Excalidraw rendering workspace and composes the diagram packages into a real canvas for isolated validation.apps/iconsownsicons.sketchi.appand serves the copied pre-cleanedsketchi-icons/outputtree from its app-local public assets.
See Agentic Generation for the intended Convex, Cloudflare Worker, MCP, AI SDK, Effect, and Nx boundaries.
The repository has one truthful project inventory:
- pnpm discovers package-backed projects through
apps/*,packages/*,packages/*/*, andtools/*; - Nx discovers the same package roots plus the generated-only
native-conversion-storybookcomposition project; - the root TypeScript solution references every composite app, buildable package, and generator library. It deliberately omits the Storybook-only composition root because that surface has no composite TypeScript project.
Every Nx project has a scope:* tag and an explicit type:* role. Packages
cannot import apps, normal apps cannot import apps, runtime packages cannot
import eval or app code, and persistence cannot import app, eval, or UI code.
native-conversion-storybook is the sole scope:composition surface and the
only project allowed to declare app dependencies. Buildable libraries may only
depend on other buildable libraries.
The flat ESLint configuration intentionally enforces architecture rather than
introducing a repository-wide style rewrite. Nx infers a lint target for all
projects and applies the dependency rules to project source, configuration,
test, and Storybook files. tools/project-graph.test.ts compares all three
inventories to the explicit intended roots, so deleting a manifest, composite
setting, or root reference cannot shrink both sides of the check. It also fails
if tags, lint targets, boundary coverage, or app dependency ownership drift.
Required CI builds all projects and then dry-runs Wrangler for every project in
the canonical Worker map, using the generated per-app deployment configs.
- The remote thread planner chooses a generated tool such as
generateDiagram,restructureDiagram, ortweakDiagram. - Generation produces or edits a typed intermediate diagram. The first hard target is the
flowchartcontract. diagram-corevalidates shape, references, and diagram-type invariants.diagram-rendererconverts the diagram into deterministic scene primitives.diagram-excalidrawconverts the scene into Excalidraw elements and validates real output constraints such as arrow bindings and bound text fit.diagram-scenariosruns maintained assertions against fixture or one-shot model output.- UI packages display those primitives in a real Excalidraw canvas and expose stateful review workflows.
This makes defects local: invalid references fail in core tests, scene drift fails in renderer tests, Excalidraw syntax issues fail before persistence, visual UI states fail in Storybook/component tests, and app routing/deploy failures stay in the app boundary.
The first high-reliability path is decision-heavy flowchart generation. The LLM should produce typed flowchart IR: start/process/decision/end nodes, labeled decision branches, and explicit edges. Code owns layout, real Excalidraw conversion, arrow bindings, and text wrapping. The canonical evaluation fixture is the pharma batch disposition flow.
The durable behavior is the generation runtime, not one chat surface. Keep the runtime importable by Convex actions, Cloudflare Workers, MCP routes, app routes, and local evals.
flowchart TB
Convex["Convex<br/>managed threads"] --> Runtime["shared generation runtime"]
Worker["Worker<br/>HTTP + MCP + AI Gateway"] --> Runtime
CLI["CLI/evals"] --> Runtime
Runtime --> IR["validated IR"]
Runtime --> Artifact["rendered artifact"]
AI SDK remains a thin adapter for model invocation, streaming, and tool-call plumbing. Effect can live inside the shared generation packages for schemas, typed errors, and pipeline composition. Nx still owns the project graph, builds, and affected checks.
The apps/eval-harness surface is not the public product Playground and
does not include the Convex remote agent loop. It is an independently
deployable internal testing ground for maintained scenarios, pasted model
output, and deterministic IR-to-Excalidraw conversion.
The public Sketchi Playground direction is anonymous prompt-to-diagram
generation with artifact handoff. That experience is implemented by
apps/playground on the durable sketchi-studio Worker while authenticated
Studio remains a separate future cutover.
The portable CLI path is:
pnpm nx scenario diagram-scenarios -- --scenario pharma-batch-disposition --fixture --out .memory/pharma-batch.excalidrawFor one-shot model runs through Nx, pass a local command in
SKETCHI_GENERATOR_COMMAND. The command reads the scenario prompt from stdin and
writes candidate IR JSON to stdout:
SKETCHI_GENERATOR_COMMAND="your-llm-command" pnpm nx scenario diagram-scenarios -- --scenario pharma-batch-dispositionThe CLI also supports direct --generator-command usage when it is invoked
outside Nx; put that flag last because the scenario CLI treats the rest of the
argv as the provider command.
This keeps local/OpenCode-style integrations separate from Convex auth, thread orchestration, and persistence.
The v2 workspace has five TanStack Start app surfaces:
eval-harness: internal scenario evaluation and prompt-output inspection.playground: public Playground worker boundary with chat generation, artifact handoff, and the Cloudflare/HTTP adapter for the persisted Studio foundation.web: public home/docs for the Sketchi product direction.excalidraw: internal Excalidraw rendering workspace; Excalidraw is an implementation/editor capability, not a standalone public product route.icons: a standalone browser for the curated Sketchi icon outputs.
flowchart LR
Workspace["Nx workspace"] --> EvalHarness["eval-harness"]
Workspace --> Playground["playground"]
Workspace --> Web["web"]
Workspace --> Excalidraw["excalidraw"]
Workspace --> Icons["icons"]
EvalHarness --> Deploy["preview/prod Worker matrix"]
Playground --> Deploy
Web --> Deploy
Excalidraw --> Deploy
Icons --> Deploy
Keep app-specific UI in the app that owns it. Generate those components with
@sketchi/generators:ui-component --projectRoot=apps/<app> so stories and tests
stay close to the route shell without adding shared package dependencies.
New UI components should be created with @sketchi/generators:ui-component.
The generator expands EJS templates into a component folder with implementation,
Vitest coverage, Storybook story, barrel export, and package export wiring.
New diagram types should be created with @sketchi/generators:diagram-type.
The generator updates the core diagram type registry and expands EJS templates
for a typed fixture, core contract test, renderer contract test, and Storybook
story. This keeps each diagram type previewable and testable before it is
connected to generation.
The registry is guarded by tests: every registered diagram type must have core, renderer, and Storybook coverage, and every reusable Studio component must have an implementation, test, story, local export, and package export.
Each app is scaffolded for Cloudflare Workers through Vite and Wrangler. The adopted production route map is:
sketchi.appandwww.sketchi.app->webplayground.sketchi.app->playground, which owns the public Playground boundaryicons.sketchi.app->icons- the eval harness and standalone Excalidraw workspace remain internal
- authenticated Studio remains deferred until it is deliberately launched
The manual domain attach target for playground.sketchi.app is the
playground project. Its durable Cloudflare service remains sketchi-studio;
authenticated Studio remains unexposed until deliberately launched.
Custom-domain attachment is a post-merge operator action described in
Production domain cutover. Until that cutover,
use stable workers.dev URLs for deployment and runtime proof.
Preview deploys strip production routes and deploy app-specific Workers using the durable preview prefix declared for each app.
Today scripts/lib/worker-apps.mjs maps each explicit deploy project ID to its
durable Worker name, input Wrangler config, and isolated dist/apps/<app>
output for eval-harness, playground, web, excalidraw, and icons.
eval-harness intentionally maps to the existing sketchi-playground Worker;
workflows never derive Worker identities from Nx names. Custom-domain
attachment remains an explicit manual workflow dispatch.
The live playground uses the Cloudflare AI Gateway Worker binding with
collectLog: true and scenario metadata on each request. Cloudflare stores
Gateway logs and can retain request/response payloads for prompt tuning when
the gateway is configured to keep payloads.
Inspect and summarize logs through the Cloudflare API MCP instead of keeping repo-local Cloudflare API scripts. Configure Codex or another MCP client with Cloudflare's Code Mode server:
{
"mcpServers": {
"cloudflare-api": {
"url": "https://mcp.cloudflare.com/mcp"
}
}
}Use the repo-local $sketchi-log-analysis skill in
.agents/skills/sketchi-log-analysis/SKILL.md
for reusable analysis. The skill keeps the operational behavior read-only, asks
for payload inspection only when retained by the Gateway, and turns model
failures into scenario or prompt-tuning follow-up.
Studio records a server-side usage trail for the harness-facing Code Mode
surface. The /api/v1/flowcharts/build,
/api/v1/artifacts/:artifactId/patch, and MCP execute paths create a
sketchi.codemode.usage.v1 event. Capture is scheduled with Cloudflare Workers
waitUntil so the response is not blocked on telemetry storage. Capture is
best-effort and must not fail artifact generation when telemetry storage is
unavailable.
The raw usage event stores operation, surface, run/attempt/event IDs, client headers such as harness/model/reasoning level when supplied, status, duration, artifact refs, and bounded request and response JSON snapshots when they are serializable and below the current size cap.
Events are date-partitioned under the existing artifact bucket:
codemode/usage/YYYY/MM/DD/<run_id>/<attempt_id>/<event_id>/event.json
Studio also sends normalized rows to Cloudflare Pipeline streams. The
CODEMODE_USAGE_EVENTS binding writes one flat row per Code Mode request and
the CODEMODE_USAGE_ISSUES binding writes one row per structured issue:
| Surface | Binding | Stream ID |
|---|---|---|
| Preview | CODEMODE_USAGE_EVENTS |
e9fc3bcd35314fa39fc6a89018207acc |
| Preview | CODEMODE_USAGE_ISSUES |
d95a1767edf246af8c637c5b9bf5a5c5 |
| Production | CODEMODE_USAGE_EVENTS |
d9044253316f4273a60298098f444a62 |
| Production | CODEMODE_USAGE_ISSUES |
f687dab6e7d742c1a76834089e709462 |
The committed stream schemas live in scripts/pipelines/. Keep Pipeline rows
flat and aggregate-friendly; keep the richer request/response snapshots in the
raw R2 event objects.
R2 Data Catalog and R2 SQL are the downstream aggregate-query path. The durable
catalog topology is recorded in scripts/pipelines/codemode-r2-catalog.mjs:
| Surface | Stream kind | Catalog bucket | Table |
|---|---|---|---|
| Preview | Events | sketchi-codemode-usage-analytics-preview-v4 |
sketchi_codemode.usage_events |
| Preview | Issues | sketchi-codemode-usage-analytics-preview-v4 |
sketchi_codemode.usage_issues |
| Production | Events | sketchi-codemode-usage-analytics-production-v4 |
sketchi_codemode.usage_events |
| Production | Issues | sketchi-codemode-usage-analytics-production-v4 |
sketchi_codemode.usage_issues |
Keep the credential boundary explicit: a Wrangler OAuth token can list catalog
metadata, but Data Catalog sinks and R2 SQL data scans require an R2 API token
with Admin Read & Write permissions. The 2026-06-29 smoke run passed by
querying the exact Pipeline-ingested row after the table metadata appeared, and
the real production/preview Code Mode streams were then verified by querying
both usage_events and usage_issues rows for live API run IDs through the v4
catalog buckets. Use pnpm r2sql:codemode:resources to print the durable
resource map and pnpm r2sql:codemode:verify-run -- --production-run-id <id> --preview-run-id <id> to query R2 SQL for a real end-to-end run pair. The
verifier polls R2 SQL because catalog ingestion is asynchronous. Event rows are
required; issue rows are queried and reported, but they are only required when
the probe is expected to emit issues and the command includes --require-issues
or explicit --production-issue-run-id / --preview-issue-run-id values.