LifeLab · model · schema cost of the one-outline reading
Two of the three kinds of nesting are already in your schema and already free: blocks nest under blocks (parent + order), and pages nest under pages (the / in the name). The one-outline framing doesn't migrate anything — it's a reading of what you already store. What lenses add is the third kind: nesting content that lives somewhere else. And the one thing that looked like it would need a column — putting a child page at a chosen position inside its parent's outline — turns out to be a lens too.
blocks.parent_id · blocks.order
Real nesting. tab writes a parent. This is your outliner today and the one-outline reading changes none of it.
pages.name = "project/LifeLab/docs"
Derived nesting. Ancestry is name.split('/') — no parent_page_id, ever. Hierarchy is computed from a string you already have, which is why rename already works.
blocks.content = "![[…]]{todo}"
Mounted nesting. Lives in a field you already write. Nothing points at the lens; the lens points out. Zero tables, zero columns, zero migration.
So the correction is small but load-bearing: lenses aren't how you nest — they're how you nest other people's content. Ordinary indentation still does ordinary nesting, and it stays real, in the database, where it belongs.
In the previous doc I put docs at an authored position among its parent's blocks — between a lens and a folded sibling. That would need an order key for a page inside its parent's child list, i.e. a column. It doesn't: writing ![[project/LifeLab/docs]] in the parent is the placement. The child page appears where you wrote its lens, in the parent's existing block order, using the block row you already created by typing. Your current UI is already built on this — it says 2 without a Lens and offers + Lens per row.
placed — a lens exists, so it's in the outline
The order key for docs is the order key of the block you typed the lens into. Drag it up and you're dragging a block — the machinery already exists.
so SUBPAGES was never a taxonomy — it's a tray
Which resolves the thing that bothered you. SUBPAGES isn't a section describing where rows came from — it's a list of children you haven't placed yet. That's not a taxonomy, it's a loose-ends tray, and it deserves the treatment loose ends get: small, at the bottom, actionable, and gone when empty.
Same move fixes LINKED: it's the root node's default lens — ![[:this-page:]] — present until you write your own, at which point yours wins. A default, not a surface.
And the dedupe rule falls out with no state: a child page is in the tray iff no lens in this page resolves to it. Pure function of the text. Nothing to keep in sync.
| thing | schema | what it takes |
|---|---|---|
| page as root node | none | Render the title as node 0 and let it hold content (its own filter expression). The header surface collapses into the outliner you already have. |
| page ancestry | none | Derived from name. Already true — you're just showing it as indentation instead of a section. |
| lens mounts results | none | Render-tree only. A lens node's children are query output, discarded on unmount. |
| placing a child page | none | A block whose content is ![[child]]. Position = that block's existing order key. |
| unplaced tray | none | Children minus lens-resolved names. A query, not a stored set. |
| zoom = re-root | none | View state. The way back is the ancestor chain you can always recompute from the name and the parent ids. |
| view state per occurrence | real work | The one genuine tax. Fold / cursor / selection must key off the render path, not the node id — otherwise folding a block in one lens folds it everywhere. No schema, but it does mean touching every place that remembers UI state. |
| drag semantics | real work | Drop must resolve to re-home, re-parent, or make it match depending on the target's contract. Again no schema — but it's new logic, and it's where the model becomes tangible to a user. |
Net: the whole restatement is a UI and language change over your existing tables. That's the strongest argument for it — it's reversible. If the one-outline reading turns out to feel wrong in your hands, you delete a renderer, not a migration.