Skip to content

Canvas — design and roadmap

The canvas is Outcrop's dashboard builder and graph explorer. Tonight's thin slice (query card + graph card + scenario picker, clients/apple) is v0. This document ideates the next layers. Everything here rides the existing architecture — DataFusion SQL, the link graph, scenarios, MCP — and none of it requires new storage concepts: a card is a view over a query; the canvas is a document.

Design invariants

  1. Cards are queries. Every card type — table, chart, image, web, agent — reduces to "run something against a scenario, render the result." The scenario picker therefore re-renders every card, always, for free.
  2. The canvas is itself a record. A canvas persists as one .md file with frontmatter (type: canvas) and a fenced JSON block for layout. Canvases are then queryable, diffable, scenario-branchable, and merge like any other record. No second persistence system, ever.
  3. Wires carry tables. Card-to-card links pass an Arrow-shaped result set (columns + rows), not bespoke objects. Any output port can feed any input port; a mismatch is a visible diagnostic on the wire, not a crash.
  4. User vocabulary. Cards, wires, scenarios, snapshots. Never SQL jargon in chrome (the SQL box itself is opt-in "advanced" surface), never git vocabulary anywhere.

Card taxonomy (roadmap order)

1. Aggregate cards

The query card grows a summarize mode: pick a folder, group-by field(s), and an aggregate (count / sum / avg / min / max) through a form UI that compiles to SQL (SELECT team, count(*) FROM people GROUP BY team). Render options per card:

  • Stat tile — single number + label + optional delta vs. another scenario ("12 active projects, +3 in reorg-draft"). The scenario-delta stat is the killer demo: one tile, two data states.
  • Bar / line / donut — small, opinionated chart set; no chart-library safari. Bars for group-by, lines for date-bucketed group-by (created by month), donut only for ≤6 slices.
  • Pivot table — two group-by fields, counts in cells; click a cell to spawn a filtered table card (drill-down = new card wired from the pivot).

2. Linkable data flows (wires)

Cards get ports: every card exposes its result table as an output port; cards accept input ports as named table bindings.

  • A wire from card A into query card B binds A's result as a table named input (or input_1..n): SELECT * FROM input WHERE status = 'active'. Implementation: DataFusion MemTable registered per bound port — trivial server-side, already supported by the engine.
  • Selection is a port too. Clicking rows in a table card or nodes in a graph card emits a selection table (_path column). Wire a graph card's selection into a query card and clicking a node re-runs the query filtered to that record. This gives master–detail dashboards with zero new concepts.
  • Flows are DAGs; re-evaluation is topological, debounced, and incremental (only downstream of the changed card). Cycles are refused at wire-drop time with a visible reason.
  • The layout JSON gains wires: [{from: cardId.port, to: cardId.binding}] — still one document, still diffable.

3. Agent / skill embedding

An agent card is a chat surface bound to the store's MCP server — the same server outcrop-server already exposes. The card's context is scoped:

  • Scope = wired inputs. Whatever tables/selections are wired into the agent card are its working set; its MCP tool calls (query, upsert_record, validate, scenario tools) are constrained to the canvas's active scenario. The agent proposes writes; the card shows the resulting diff ("Propose changes") and the user applies or discards — transactions make agent edits safe by construction.
  • Skills as saved prompts with ports. A skill = named prompt template + declared input ports + output port ("Summarize these records", "Draft a status note per project", "Flag records that look stale"). Skills appear in the card palette like any other card type; running one materializes its output table so downstream wires update. Skills persist as records (type: skill) — shareable, versionable, mergeable.
  • Agent output that is tabular flows out the output port; prose renders in the card. One card type, both modes.

4. Media & external cards

  • Image card — renders an image referenced by a record field or embedded in a record body (![…](…) in markdown). Because attachments live in the bundle, images are scenario-aware too: flip scenario, see that scenario's asset.
  • Web card — sandboxed webview for a URL from a record field (e.g. a project's resource: or url: frontmatter). Read-only, no script bridge to other cards except an output port emitting {url, title}. This is also the natural seam for the future resource data-virtualization hook: a web card pointed at a warehouse dashboard today, a federated table card tomorrow.
  • Record card — a single record rendered as markdown, editable field-by-field; edits go through the normal transaction path (staged, then committed with a History entry). The conflict UI (Keep mine / Keep theirs / Keep both) surfaces here, field-scoped, per BRIEF.md #8.
  • Note card — free markdown on the canvas that is stored as a normal record in a canvas-notes/ folder. Annotation without a second content model.

5. Themes & styles

  • Token-based theming: one theme = a small set of tokens (background, card surface, accent, text, mono font, radius, shadow). Light/dark variants derived, not hand-authored. Themes persist as records (type: theme) so a bundle carries its look; the canvas references a theme by record path.
  • Per-card accents only. Cards may pick an accent tag (e.g. status color mapping for stat tiles); no per-card font/layout overrides — dashboards stay coherent by construction.
  • Print/export style: a theme includes a compact density used by read-only/export mode.

Sequencing (post-MVP)

Phase Ships Why first
C1 Canvas-as-record persistence (shipped — see "C1 as-built" below; record card v1 shipped too, see AUTHORING.md) Kills the ad-hoc layout file; unlocks diff/merge of dashboards
C2 Aggregate cards (stat tile, bar/line, pivot) Highest demo value per line of code; pure SQL
C3 Wires + selection ports Turns cards into dashboards; needs MemTable binding in outcrop-query
C4 Agent card + skills Rides the existing MCP server; needs C3's ports for scoping
C5 Image/web/note cards; themes Polish layer; independent of C1–C4 internals

C1 as-built (shipped)

The canvas persists as a NORMAL Outcrop record — no engine changes were needed; the server is generic over folders. The frozen convention:

  • Path: canvases/<slug>.md; the default canvas is canvases/default.md.
  • Frontmatter: type: canvas, name: <display name>, layout_version: 1.
  • Body: a # <name> H1 heading, then a ## Layout H2 section containing exactly one fenced ```json code block holding the layout object:

json {"scenario": "main", "cards": [{"id": "…", "kind": "query|graph", "x": 200.0, "y": 200.0, "input": "…"}]}

x/y are flat numbers (client-neutral — no CGPoint nesting).

Because a canvas is a record, it is queryable (SELECT _path, name FROM canvases; the layout surfaces as the section_layout column), diffable, and scenario-branchable — validated by crates/outcrop-server/tests/canvas_record.rs, including the lossless round-trip (an upsert touching only name leaves every other byte identical). The macOS client (clients/apple) loads/saves the record via GetRecord/UpsertRecord on the main scenario; the record is the only store. (A local layout.json shadow copy predating canvas-as-record was removed once the in-client engine closed the "server down" launch case — there is no launch where the record store is unreachable.) The record card from the C1 row ships as Record card v1 — see docs/AUTHORING.md.

Cross-platform clients (MVP prep)

Everything a canvas client needs is already platform-neutral by design: the frozen proto, and the C1 record convention above (flat x/y, no CGPoint nesting — that line was written for exactly this moment). What a new platform must implement is small and well-defined:

  • The client surface is five methods (see the Swift OutcropClientProtocol, the reference implementation): query, neighbors, listScenarios, getRecord, upsertRecord. That is the complete surface the macOS canvas is built on. Parity today: Apple 5/5, Windows 2/5 (Query, ListScenarios), Android 1/5 (Query, and its row parsing is naive string-splitting that breaks on embedded commas).
  • Port the mock, not just the client. MockOutcropClient + MockRecordStore (seeded, in-memory, scenario-aware) is what lets the canvas open with no server, previews render, and UI automation run hermetically in CI. Each platform needs its equivalent before it needs pixels.
  • Unknown-card-kind passthrough is the forward-compat seam: a client must preserve card kinds it doesn't render (the Swift layout codec's JSONValue passthrough), so per-platform card rollout never corrupts a shared canvas.
  • Apple → iOS is the cheapest expansion: OutcropCanvasKit is already portable; OutcropCanvasUI has exactly two macOS-isms (Color(nsColor:), .searchable(placement: .sidebar)) plus the platforms: declaration and app-shell split. The hosted-window test target stays macOS-only.

Every client hosts its own engine (decided)

There is no remote client-server model — see the decision record bundle/decisions/clients-host-their-own-engine.md. Each platform runs against an engine on the same device, and the frozen proto stays the only client surface in both hosting modes:

Platform Engine hosting Transport to it
macOS / Windows desktop shared on-demand instance (docs/ENGINE.md) UDS (TCP on Windows)
iOS / iPadOS in-process library — iOS cannot spawn processes in-process channel
Android in-process library (spawn possible but pointless) in-process channel

Cross-device consistency is git sync against a remote, not a live wire: every device holds a full replica (git is the WAL), the remote's TLS + auth secure transit, and divergence resolves through the existing user-vocabulary merge flow — which therefore must ship on every platform, not just desktop. Verified enablers: the transaction layer is libgit2 (git2 crate, no subprocess), and DataFusion + tantivy are pure Rust; all cross-compile. Accepted costs, recorded in the decision: no live cross-device coherence (sync cadence, not push), desktop MCP agents see a device's edits only after it syncs, and mobile pays the embedded binary tax (unmeasured until the first device build). The engine workstream owes a library facade (same proto surface as the socket) for the in-process rows above.

Two client-side gaps remain real under this model:

  1. Sync surface — clients need "Sync now" (and eventually scheduled sync) against the bundle's git remote, in user vocabulary, with merge resolution reachable on touch platforms.
  2. Folder discovery — the explorer sidebar degrades to search-only because SHOW TABLES needs information_schema the engine doesn't enable. list_folders/list_record_paths already exist in the server service; exposing them as additive RPCs (with the pending A3/A4 batch) fixes the sidebar on every platform at once.

Server-side prerequisites (small, in the monorepo)

  • outcrop-query: register wired inputs as in-memory tables per query call (extend QueryRequest with repeated BoundTable {name, columns, rows_json} — additive proto change, one field).
  • outcrop-server: scenario-scoped MCP session tokens so an agent card cannot escape its canvas's scenario.
  • Nothing else — every card type above is client-side rendering over the existing Query RPC.