Skip to content

Latest commit

 

History

History
314 lines (244 loc) · 12.7 KB

File metadata and controls

314 lines (244 loc) · 12.7 KB

Prototype V2 — Decks

Version: 0.2 (design) Date: August 3, 2026 Status: Approved for implementation Supersedes: parts of PRD.md §4, §5.2, §5.5, §5.7


1. Why

The MVP ties a card to exactly one board. That was the right first cut, and the PRD flagged the limit itself (Open Question 1: "should the schema anticipate one card appearing on multiple boards later? … the ULID makes migration feasible"). This is that migration.

The research method the app is built around does not actually work one-board-per- card. A reader collects excerpts from a book once, then draws on them repeatedly: some go into a chapter, some into an essay, some into a talk, and the good ones go into all three. Copying a card into each project breaks the guarantee the whole app rests on — one note, one file — and quietly creates four divergent versions of the same quotation.

V2 separates the material from the argument:

  • A Deck holds the cards drawn from one source. It owns the files.
  • A Board composes cards into an argument. It owns only an arrangement.

One copy on disk, referenced from anywhere.

2. Model

Card — an atomic note. Lives in exactly one deck, as exactly one file. Carries its content and its attribution, and nothing about where it sits on any canvas.

Deck — the cards drawn from one work: Meditations, or a set of interview transcripts. A deck defines the source (title, author) that its cards inherit. A deck is a folder; its cards are the files inside it.

Board — a project: a book, a chapter, an essay. A board holds placements, not cards. A board is a single Markdown file.

Placement — one card's appearance on one board: which card, where it sits, its z-order, and its category on that board.

Category being a property of the placement rather than the card is the load-bearing decision here. The same excerpt from Marcus Aurelius can be Perception in one book and Intro in another; the theme belongs to the argument being made, not to the passage. Colours and the legend stay per-board exactly as they are today.

Deck ──owns──> Card <──references── Placement ──belongs to──> Board
   (one file on disk)                  (a line in the board file)

3. On-disk layout

NotesRoot/
├── Decks/
│   ├── Meditations/
│   │   ├── _deck.md
│   │   ├── amor-fati.md
│   │   └── the-obstacle-is-the-way.md
│   └── Letters from a Stoic/
│       ├── _deck.md
│       └── on-the-shortness-of-life.md
└── Boards/
    ├── The Obstacle Book.md
    └── Stillness Project.md

A board is now one file rather than a folder, because it no longer contains anything — it is the composition. That file is meant to be readable on its own: opened in Obsidian it reads as an ordered outline of an argument.

Card file

Position, z-order and category are gone; they were never properties of the note.

---
id: 01J4QZ8K3M9P2R5T7V9X1Z3B5D
title: The obstacle is the way
source:
  title: Meditations
  author: Marcus Aurelius
  page: "5.20"
created: 2026-08-03T14:22:07Z
modified: 2026-08-03T15:01:44Z
---

> The impediment to action advances action.

_deck.md

---
id: 01J4R0A1B2C3D4E5F6G7H8J9K
name: Meditations
source:
  title: Meditations
  author: Marcus Aurelius
created: 2026-08-03T14:00:00Z
modified: 2026-08-03T14:00:00Z
---

Gregory Hays translation. Page numbers are book.section, not printed pages.

New cards created in a deck inherit source.title and source.author; page is per card.

Board file

---
id: 01J4R0C5D6E7F8G9H0J1K2L3M
name: The Obstacle Book
grid: 20
scroll: { x: 0, y: 0 }
categories:
  - name: Uncategorized
    color: "#8A8F98"
  - name: Perception
    color: "#C2553D"
cards:
  - id: 01J4QZ8K3M9P2R5T7V9X1Z3B5D
    position: { x: 480, y: 320 }
    z: 14
    category: Perception
  - id: 01J4QZ9M2P8R4T6V8X0Z2B4D6F
    position: { x: 780, y: 320 }
    z: 15
    category: Action
---

Chapter order is still unsettled — see the Will cluster bottom-right.

Cards are referenced by ULID, which is stable across renames and already exists on every card. Resolving a board means looking each id up in an index built by scanning the decks.

Dangling references. A board referencing an id no longer on disk skips that placement and reports it once, rather than failing to open. This makes the format forgiving of a card deleted outside the app.

4. Semantics

Deletion — the distinction that matters

Action Where Effect
Remove from board Board canvas: Delete, or the card's menu Deletes the placement only. The file is untouched and the card stays in its deck and on every other board. No confirmation — it is trivially undone by dragging the card back.
Delete card Deck Explorer, or the editor Moves the file to the OS trash and removes every placement of it from every board. Confirmed, and the confirmation names how many boards it will disappear from.

This is the asymmetry requested: a board is a view, so removing from it costs nothing; a deck owns the file, so deleting from it is the real thing.

Creating cards

Cards are authored in the Deck Explorer only. A board composes existing material; it does not manufacture it. Ctrl/Cmd+N creates a card in the selected deck and opens the editor, so capture stays one keystroke — this replaces double-click-on- canvas, which now opens the Deck Explorer instead.

Placing cards

Dragging a card from the Deck Explorer onto the canvas creates a placement where it is dropped, snapped to the grid, with category Uncategorized and the top of the z-order. A card already placed on that board cannot be placed on it twice; the drag is refused and the existing placement pulses instead.

Editing

Editing a card edits the one file, so the change appears on every board showing it — which is the point. The editor's Category field edits the placement, so it appears only when the editor was opened from a board.

Search

The index covers every card in every deck. Board search restricts results to cards placed on that board; Deck Explorer search restricts them to the selected deck, or searches all decks when none is selected.

5. Views

A topbar spanning the window switches between two ways of looking at the same cards. Switching loads nothing: the open board and its selection, search and category filter all survive a trip to the deck view and back.

Board view

The free-form canvas, plus the Deck Explorer on the right — a panel toggled from the toolbar, which exists to get cards onto the canvas.

  • A deck list at the top: name, source, card count.
  • The selected deck's cards below, as a single-column list — compact rows with title, a snippet and the page locator. Compact because the panel's job is to find a card and drag it, and compact rows fit more of the deck on screen while doing so.
  • A search field scoped to the panel, which filters the rows.
  • + New card on the selected deck.
  • Drag a row onto the canvas to place it. Rows for cards already on the current board are marked and cannot be dragged again.

Dragging uses pointer events and a ghost element following the cursor, matching the canvas drag already built rather than introducing HTML5 drag-and-drop, which behaves poorly in the webview.

Deck view

One deck as a page: every card at reading size, in the same .card box the board draws, wrapped into as many columns as the window affords. The sidebar lists decks instead of boards, and the explorer is hidden — beside the grid it would be a second list of the same cards.

A deck has an arrangement, and the deck file records it. This reverses the earlier position that a deck's order carries no meaning. Rearranging is drag-to-insert: pick a card up, drop it in a gap, and everything after it shifts along. The result is written to _deck.md as an order: sequence of card ULIDs — one drag, one write, the deck-side counterpart of a board's cards:.

The sequence need not name every card. One just written, or a file dropped into the folder by hand, is absent until the next rearrange and sorts to the front, where a fresh capture belongs. An id left behind by a deleted card ranks nothing and is dropped the next time the deck is rearranged, so deleting a card never rewrites _deck.md — which also means a card restored from the Recycle Bin comes back where it was.

Search here dims non-matches rather than filtering them out, as the board does: the arrangement is the thing the user built, and pulling cards out of it to show a result would destroy the shape they were looking at.

Deleting from the deck view destroys files, so unlike the board's Remove it is never bound to a bare keypress — only to the toolbar button and the context menu, both of which confirm.

6. Migration

Runs once, automatically, when a V1 notes root is opened — detected by finding card files inside board folders at the top level, with no Decks/ present.

Each V1 board folder becomes both a deck and a board:

  1. Read the old _board.md and every card in the folder.
  2. Write Boards/<name>.md with the board's name, grid, scroll and categories, plus one placement per card carrying that card's current position, z and category. The new location now holds everything before anything moves.
  3. Move the folder to Decks/<name>/.
  4. Write Decks/<name>/_deck.md, taking its source from the cards' common source when they agree, and trash the old _board.md.
  5. Strip position, z and category from each card file, since the board now owns them.

The order means an interruption is recoverable: the board file is written before any file moves, and each later step is safe to repeat. Every card keeps its ULID, so nothing needs re-identifying.

Backups. Migration moves files. It runs against real notes, so it asks for confirmation first and states plainly what it will do.

7. What changes in the code

Area Change
internal/card Drop Position, Z, Category from the model and the writer. Cards keep unknown keys as ever.
internal/deck New. _deck.md parse/write, deck scan, card listing.
internal/board Board becomes a file, not a folder. Add Placement and the cards sequence, with the same round-trip discipline.
internal/vault Ownership inverts: a card index keyed by ULID across all decks; boards resolve placements against it.
internal/vault/index.go One global index; callers pass the set of ids in scope.
internal/migrate New. The V1 → V2 conversion above, with tests on fixture roots.
internal/watcher Watch Decks/, each deck folder, and Boards/.
Frontend DeckExplorer, placement-shaped canvas props, drag-to-place, split delete semantics.

8. Risks

  • Migration touches real notes. Mitigated by ordering, by asking first, and by tests over fixture roots — but a bug here costs a user's research. This is the part to be slowest and most careful about.
  • A board file and a card file can now disagree — a board referencing a card that no longer exists. Handled by skipping and reporting, never by failing.
  • Capture gets one step longer. Authoring lives in the Deck Explorer, so a thought that arrives while arranging a board takes a detour. Ctrl+N is the mitigation; if it still bites, an "inbox deck" that boards can write into is the obvious follow-up.
  • Two files change per placement move. Dragging a card writes only the board file now, which is strictly better than V1 — but a board with 500 placements rewrites one larger file per drag rather than one small one. Worth measuring against NFR-1 once boards get big.

9. Delivery

Phase Contents
V2.1 Data model, deck/board/placement formats, global card index, migration, tests. No UI change beyond what compiles.
V2.2 Board renders placements; Deck Explorer panel; drag-to-place.
V2.3 Split delete semantics, card creation in decks, editor changes, search scoping.
V2.4 Polish, empty states, performance pass on a multi-deck root.

Checkpoint at the end of each, as with V1.

10. Open questions

  1. Should a deck be allowed no source — a loose "Inbox" or "Ideas" deck for material that is not from a work? Assumed yes, with attribution simply blank.
  2. Should placements be reorderable as a sequence in the board file, so the board doubles as an outline for export? Deferred; the cards list is currently in z-order, which is close but not the same thing.
  3. Does a card need to know which boards use it? Not stored — it is derived by scanning boards, which is cheap at this scale and avoids a second source of truth that could disagree with the first.