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¶
- 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.
- The canvas is itself a record. A canvas persists as one
.mdfile 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. - 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.
- 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 (
createdby 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(orinput_1..n):SELECT * FROM input WHERE status = 'active'. Implementation: DataFusionMemTableregistered 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 (
_pathcolumn). 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:orurl:frontmatter). Read-only, no script bridge to other cards except an output port emitting{url, title}. This is also the natural seam for the futureresourcedata-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
compactdensity 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 iscanvases/default.md. - Frontmatter:
type: canvas,name: <display name>,layout_version: 1. - Body: a
# <name>H1 heading, then a## LayoutH2 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:
OutcropCanvasKitis already portable;OutcropCanvasUIhas exactly two macOS-isms (Color(nsColor:),.searchable(placement: .sidebar)) plus theplatforms: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:
- 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.
- Folder discovery — the explorer sidebar degrades to search-only
because
SHOW TABLESneeds information_schema the engine doesn't enable.list_folders/list_record_pathsalready 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 (extendQueryRequestwith repeatedBoundTable {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
QueryRPC.