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¶
- 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 bycrates/outcrop-server/tests/canvas_record.rs). - 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. - 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
blockand 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. - 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'sinputis the record path (defaultprojects/project-aurora.md). Persists in the canvas layout exactly like the other kinds — flatx/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, frontmattertype: noteplus acreatedtimestamp, body = the markdown. A note card is a record card in body-first clothing: sameinput= 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.yamlgenerates 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_flaggedcomputed column already exists inoutcrop-query) — merged withValidatediagnostics 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 whosestale_afteris on or before today, or whosestatusis 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/restoreexist; 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.