Skip to content

Authoring & curation on the canvas

Outcrop's product is managing knowledge, and managing includes authorship. The canvas (docs/CANVAS.md) is where reading, querying, and now writing meet: the same surface that shows a record lets you change it, and every change rides the engine guarantees that already exist — lossless field edits, git-backed transactions, per-folder constraint policies. Nothing in this document requires new storage concepts, and almost nothing requires new server code (see the sequencing table at the end).

Principles

  1. Lossless. A field edit from the canvas sends only the changed frontmatter keys (UpsertRecordRequest.set_frontmatter_json — "absent keys are untouched") and the body only if edited (optional string body, unset = untouched). The server splices at CST level; every other byte of the file is preserved. The client never re-serializes a record — losslessness is a server guarantee the canvas simply doesn't get in the way of (validated by crates/outcrop-server/tests/canvas_record.rs).
  2. Transactional. A save is a History entry with a human-readable message. A multi-field editing session can go up staged (stage_only: true — pending changes, no History entry yet); Propose adopts them as one entry, Discard throws them away. In engine terms discard is a rollback of the working set — but per BRIEF.md #8 the user only ever sees "Discard changes". Crash mid-edit loses nothing already staged.
  3. Validated, not blocked. Diagnostics render inline, per field, in the card — a warning is a yellow note under the control, never a dialog. When a folder's policy is block and a write is refused, the card explains it in user vocabulary — "This folder requires status to be one of active, paused, done" — and keeps the user's text so they can fix it. A refused write changes nothing; a violating file that arrived externally is a flagged record, never corruption.
  4. Never modal-heavy. Editing happens in place on the card. No sheets, no wizards; the only confirmations are destructive ones (discard, delete). The edit affordance is a toggle, not a mode switch that hides the rest of the canvas.

Record card v1 (shipped with this doc)

The first slice, implemented in clients/apple/Sources/OutcropCanvas/RecordCard.swift:

  • New CardKind.record; the card's input is the record path (default projects/project-aurora.md). Persists in the canvas layout exactly like the other kinds — flat x/y, kind: "record".
  • View mode: frontmatter as a sorted key/value list (parsed from frontmatter_json), body as scrollable monospaced text. Re-loads on scenario flip via .task(id:), like every other card.
  • Edit toggle: scalar fields (strings, numbers) become text fields; non-scalar values (lists, nested maps, booleans) render read-only in v1. The body becomes a TextEditor.
  • Save builds the set-JSON from changed fields only and sends the body only if edited; unchanged keys are never transmitted. Cancel discards local edits. A status line shows saved / error / policy-blocked outcomes (blocked is reported in user vocabulary with the server's diagnostic messages, and the user's edits are kept).

v1 limitations, addressed below: no per-field inline diagnostics yet (only the post-save status line), edits are direct commits (no stage_only session), a scenario flip while editing reloads the card, and non-scalar fields are read-only.

Record card v2+

  • Body editing with live preview. Split the body area: markdown source (monospaced editor) beside a rendered preview (AttributedString markdown is enough — no web view). Wikilinks in the preview are tappable and spawn or refocus a record card.
  • Frontmatter form from _schema.yaml. The folder's schema drives the controls instead of bare text fields:
Schema type Control
string + enum menu (picker) over the allowed values
date date picker
integer / number number-formatted field with stepper
list token field (add/remove chips)
link (+ target_folder) record picker with search — backed by search() / SELECT _path, name FROM <folder>
required, missing control outlined, "required" hint

Unknown keys (OKF pass-through) keep the v1 plain text field — tolerated, never dropped. - Inline diagnostics per field. After a debounced edit, call Validate with target = the record path; render each Diagnostic under its field's control, warning vs. violation styled distinctly. Under a warn policy the save proceeds with the notes attached; under block the Save button explains what must change before it will land. - Staged-changes chip. Edits go up with stage_only: true as the user moves between fields. The card grows a chip — "3 pending changes — Propose / Discard". Propose adopts the pending changes as one History entry with a user-supplied title; Discard throws them away. The service layer already has pending / adopt / discard_pending (crates/outcrop-server/src/service.rs); they need thin additive RPCs (sequencing, A3). - Conflict resolution, field-level. The watcher reindexes external edits; if a re-fetch shows a field changed under an open edit session, the card surfaces it exactly like a merge conflict: per field, Keep mine / Keep theirs / Keep both (both = list concatenation, as in Resolve). Never a raw diff, never conflict markers.

Note card

Quick capture without a second content model:

  • Capture. Toolbar "New Note" (or double-click on empty canvas) drops a note card and immediately persists it as a NORMAL record — canvas-notes/<slug>.md, frontmatter type: note plus a created timestamp, body = the markdown. A note card is a record card in body-first clothing: same input = path, same save path, same History. Notes are therefore queryable, diffable, and scenario-branchable for free.
  • Promote to folder. Drag a note onto a folder (or "Move to folder…" in the card menu) to turn a captured thought into a typed record: the folder's _schema.yaml generates a pre-filled form (name from the first heading, body carried over), the user supplies required fields, and the promotion is one upsert to the new path + one delete of the note — staged together, proposed as a single History entry ("Promoted note to projects/…"). Links to the note are left as-is in v1 (the flagged-link diagnostics will surface any breakage); link rewriting is future work.

External editor as first-class co-author

The bundle IS an Obsidian vault / plain folder (BRIEF.md #5). The watcher already detects and incrementally reindexes external edits, so the canvas does not need to own authorship — it needs a good handshake:

  • Open in editor. Every record card gets "Open in " (NSWorkspace open on the file; a configurable editor preference later). Edit in Obsidian, save, and the watcher reindexes; the card re-fetches on window focus / a light poll, so the canvas refreshes live. If the user had unsaved canvas edits, the field-level conflict UI above takes over.
  • Curation loop. A "Needs attention" panel is just a saved query — SELECT _path, _flagged FROM <folder> WHERE _flagged IS NOT NULL (the _flagged computed column already exists in outcrop-query) — merged with Validate diagnostics for detail. Each row jumps to (spawns or refocuses) a record card on the offending record with its diagnostics inline. Externally introduced violations thus become a to-do list, never an error state.
  • Deep links (future work). A registered outcrop:// scheme — outcrop://record/<scenario>/<path> opens the canvas focused on that record's card; outcrop://query/... for saved queries. Usable from Obsidian notes, terminal output, and agent messages. Sketch only; not scheduled below.

Curation workflows

  • Saved validation queries as cards. They are ordinary query cards; the card palette gains presets ("Needs attention", "Missing owner", "Broken links") that pre-fill the SQL. Because the canvas is a record, a curation dashboard is shareable and mergeable like anything else.
  • Stale content. OKF lifecycle fields (status, stale_after — docs/okf/SPEC.md §5.5) make staleness a query: a preset card lists records whose stale_after is on or before today, or whose status is not a live value. Curation = flip the card's scenario, fix, propose.
  • History / Restore per record. The record card menu shows the record's History (entries that touched its path, newest first) and offers Restore — which adds a new entry, never rewrites (the service methods history / restore exist; they need RPCs — A4).

Sequencing

Phase Ships Size Server work
A1 Record card v1 (shipped — see above) S None — GetRecord / UpsertRecord as-is
A2 Note card + quick capture; "Open in editor" + focus-refresh; needs-attention panel (query presets + Validate) M None — UpsertRecord, Query (_flagged), Validate all exist
A3 Schema-driven frontmatter form + inline per-field diagnostics; staged-changes chip (Propose / Discard) M Thin, additive: expose existing pending / adopt / discard_pending service methods as RPCs; a small GetSchema RPC (or schema fields folded into Validate) so the client can read _schema.yaml without special-casing paths
A4 History/Restore on the card; field-level external-edit conflict resolution; promote-note-to-folder M–L Thin, additive: History / Restore RPCs over the existing service methods; conflict detection is client-side over GetRecord

The pattern holds from docs/CANVAS.md: the engine and service layer (crates/outcrop-server/src/service.rs) already implement everything the authoring surface needs — the only server work in the whole plan is additive proto plumbing for methods the CLI can already reach.