flat blocks: what to do
Position is layout; names are data. Where a block sits confers nothing a query can see. Structure a query can see must be written on the block itself.
1 · Schema — ship this, nothing more
blocks: id · page_id · order · stars · text · tags[] · metadata
- No
parent_id. No tree table. Ever. Indent is content:starsis just the count of leading*, same rank as a heading level. - Every
**/***line is a full block: taggable, addressable, transcludable, metadata-bearing. - The only structural derivation is the span — two linear scans, zero recursion:
span(i): scan forward while stars > stars[i] // "subtree"
context(i): scan backward, first block per smaller stars // "ancestors"
Both run in the app layer on page order. The database never answers a hierarchy question.
2 · Operations — the collapsed-unit rule
Collapsed = acts as a unit. Expanded = acts as a single line. What you see is what you act on.
| op | expanded block | collapsed block |
|---|---|---|
| collapse space | hides its span | shows it again |
| indent tab / ⇧tab | ±1 star, this line only | ±1 star on every line in the span |
| move ⌥↑ ⌥↓ | this line hops over its neighbor | the whole span hops as one slice |
| delete | this line only | the whole span |
| drag | don't build it — swap-with-neighbor covers reordering; collapse-then-move covers subtree moves | |
Orphan spans (a ** with no * above after an edit) render as-is, indented. Never auto-normalize stars — indent is content, and the user typed it.
3 · Tags — no inheritance in storage, span context at read time
- Store tags per block only. Never copy tags down to the span (that's reparenting with extra steps).
- Tag queries return exact matches. The tag view offers an “include sub-blocks” toggle that expands each hit to
span(hit)at render — inheritance as a view option, not a schema fact. (Org-mode precedent: computed at search, never stored.) - Nested tags are the tree you keep:
:project/lifelab/mcp:is a materialized path — ancestors spelled out at write time, queries stay flat prefix scans. :project:must not match:project/lifelab:implicitly — that broadens silently (invariant 5). Prefix match is explicit::project/*:, completed as a visible chip (“includes 12 nested tags”).
the pitch, twice: don't organize by dragging into trees —
organize by naming.
indent for reading; tag for finding.
4 · Lens Spec Draft 3 — three sentences to edit
The lens grammar already contains no hierarchy predicate — no parent:, no under:; depth: is page nesting. Only the ownership wording assumes a tree:
- Invariant 8: “text and children belong to the author” → “text and the span beneath belong to the author.” Children are a derived view, not a field.
- §10.2 delete: “deletes your text and children” → adopt the collapsed-unit rule: expanded deletes the line, collapsed deletes the span.
- Add a note:
![[page#headline]]andrender:transcluderesolve as order-range scans (span(i)on the home page) in the app layer — never a recursive DB query.
Result rows are render output — they never occupy positions in the flat array, so “yours above (│), borrowed below (┊)” costs nothing.
5 · One honest gotcha to design for
A user indents a block under * Project Lifelab and expects :project/lifelab: to find it. It won't — position confers nothing. Mitigations, in order:
- ✓ born-matching (draft 3 §7): typing into a tag-lens result applies the marks automatically.
- ✓ Mod+Shift+L makes tags ↔ lens conversion one keystroke.
- optional: indenting under a lens block offers — never auto-applies — “tag this into the window above?”
6 · The one positional tree you keep
Pages nest (depth:, subpages) and that stays — pages are containers users deliberately file into, the one place traversal is paid for. The coherent story: pages nest · tags namespace · blocks just sit in order.
7 · Do / don't
| do | don't |
|---|---|
| flat array + stars int | parent_id, closure tables, recursive CTEs |
| span scans in the app layer | hierarchy queries in the DB |
| collapsed-unit semantics everywhere | drag & drop of subtrees |
| tags per block, span context at read | materialized tag inheritance |
explicit :project/*: prefix chips | implicit ancestor-tag matching |
| render orphan spans as typed | auto-normalizing stars |