Reconciled with the shipped build

LifeLab Malleability — model vs. what shipped

Companion to malleability-today.html. The unification shipped as a four-axis model — runtime × trigger × anchor × presentation — projected into a block_extensions registry, with a BehaviorEditor, runtime:template notes, an on_load trigger, and dead-end gating all live. This pass reconciles the design with the metal: where the build is richer than the "two settings" framing, where it's narrower, and the gaps that still hurt the note-taking experience. Surfaces below carry a shipped planned or gap badge.

00

Reasoning & assumptions

how I approached this
The problem, restated
  • Same data (a block's trigger/anchor) is shown in 4+ places, none authoritative.
  • Creation is split: hook modal vs pin modal vs renderer config vs tag-binding row.
  • "Where does this thing live and when does it fire?" has no single answer screen.
  • One-off code blocks (your calculator) have no home — they're neither hook nor drawer nor pin.
What shipped — and what bent
  • The bet held — one registry, one editor, one write path. Hook/drawer/pin are presets now.
  • Presentation became a verb, not chrome. show / spawn / insert / nothing — not drawer/callout/chip.
  • Our "insert vs show" instinct landed — and grew to four landings.
  • The "instant template" idea shipped as runtime:template notes.
  • Approval gate came back for scarce sidebar/topbar slots — you'd said remove it. Flagged below to settle.
The reframe. We pitched it as "two settings: WHEN × WHERE." The build says WHERE is really two things — where it lands (the anchor) and how its output materializes (the verb: show spawn insert nothing). The block's runtime decides which verbs are even coherent — that's the "verb from type" idea, now a hard rule the editor enforces (dead-end gating). So conceptually still WHAT/HOW/WHERE; the honest version just makes the materialization verb visible inside WHERE.
Three pillars, four axes ⟶ type WHAT · runtime + content WHEN · trigger WHERE · anchor + materialize verb pythonRunson a :project tag change, shows under each block
01

The model — WHAT × WHEN × WHERE

runtime × trigger × anchor × presentation · block_extensions registry shipped
You set WHEN it runs and WHERE it goes; the block's type does the rest. The sentence still reads naturally — its verb comes from the runtime — but WHERE now carries the part the build made real: how the output materializes. A code block runs, a jsx block renders, a template note inserts / spawns text. Four landings:
show — live view at the anchor spawn — a fresh block insert — text into a block nothing — side-effect only
luaRunson/summarize,andshows the result here.
templateInsertsa standup outline when ajournal/ page is created.
pythonRunsoningestandchanges data quietly.

The hard rule — runtime decides which landings are coherent (shipped as coherentPresentations — the editor never offers an impossible combo)

the block is a…showspawninsertnothing
code lua / python
jsx renderer
template note w/ {vars}
note plain (none)dead-end

Only a runtime executes. A jsx block is a component, so it can only show a live view. A plain note is passive — it can copy itself into a new block (spawn) but can't reach into another block to insert; mark it template (the editor's plain↔template toggle) and it can. This is the "verb from type" idea, now enforced top-to-bottom: the editor greys impossible verbs, CI (check-no-dead-ends.sh) fails if the gating regresses, and an ops scan finds raw-API rows that slipped a dead-end past the UI.

Concepts are still presets — now over four axes

when · pythona :project tag changes where · showunder each tagged block
when · lua/summarize where · showinline, here
when · templatepage_create journal/* where · insertinto the new page
when · pyon ingest where · nothingjust changes data

Named concepts → their coordinate

drawer tag / block+show
hook event / slash+nothing
template page_create+insert new
pin sidebar / topbar+show
scratch I run it+show — your calculator

The four-axis registry made the empty cells reachable: (event, lua, sidebar) — the classifier-labeler, once a hook-in-disguise — is now just a coordinate. template + insert is the instant-template you asked for.

02

The mount map — /extensions

spatial overview of the block_extensions registry not built — registry exists, no UI

/drawers and /automation already read the same block_extensions table — so they become one spatial overview. This page is for seeing what's mounted where, toggling, and troubleshooting — not authoring. There's no search, no "+ new block", no drag-to-reorder; a new extension is born as a code block in the notebook and simply appears here. Click any item to open the editor (§03). To relocate spatially, drag it in the page view (§05).

/extensions · mount map — every extension shown in the slot it lives in · click to edit
topbar Sentiment sparkline · chip
page body · title slots & tag drawers Project Summary · tag:project · drawer Food tracker · tag:food · drawer /summarize · self · inline Run · title chip
invisible — anchor: null

sidebar (context)

Classifier labeler · ghost Daily brief · pin

slot usage

sidebar · 2 mounted topbar · 1 mounted drag a block here in page view to add
Why keep it spatial if it's overview-only? Because the question no current screen answers is "what lives where" — and a slot layout answers it at a glance. The hatched invisible tray makes anchor: null a legitimate place to be, not a confusing absence. Editing happens by click → editor; moving happens by drag in the page (§05). The map itself stays calm.
03

The editor — when × where × how it materializes

BehaviorEditor.tsx — WHEN/WHERE chips + live sentence + dead-end gating shipped
One editor, two presentations. A Description, a live sentence, and two sections — when it runs and where it shows. It edits one block's metadata, rendered inline on the block (the canonical store) or summoned as a popover when the block isn't on-screen. The block's runtime isn't here — that's the block's own type. Every choice rewrites the sentence and shows a plain-English note.

The editor — description, live sentence, and a note on every choice

description
Summarize the current block into a TL;DR
Runs on/summarize, showing the resultinline, right here.
lua block the verb "Runs" comes from the block's type — edit the block to change it.
WHEN it runson /summarize
I run itan eventon ingesta /commanda taga schedule
command/summarize
Runs whenever you type /summarize in any block. Some triggers (ingest, on-update) just change data and show nothing — those grey out the "where" section below.
WHERE it goesshow, right here
right hereon a taga blockthe sidebarthe topbara page pattern
how it lands◑ show+ spawn◈ insert∅ nothing
Show puts a live, re-running view right below this block. Spawn writes a fresh block; Insert appends into an existing one; Nothing = silent side-effect. Only the verbs this lua block can do are offered.
gap  visual chrome (drawer / callout / chip framing) is not a verb — it was cut. A "show" is currently unstyled. See §06.
saved to the block · nothing else to sync
Spawn vs Insert — the only two ways output becomes content. Spawn creates a new block; Insert writes content into an existing block. That's the whole vocabulary — there is no third "paste." (Show is different: a live view that re-runs and isn't editable; Nothing = silent side-effect.) So a note (or a lua block that returns text), set to Insert on page_create, is an instant template — it fires and inserts itself into the new page. Two rules come from the block's type: a jsx block always renders a live view (only show — it's a component, not text), and code that returns nothing shows nothing — it just did its work.

A note set to Insert = a template

note◈ insert
## Daily standup - Yesterday: - Today: - Blockers:
noteInsertsthis text into every newjournal/ page

The four landings, side by side

+ spawn→ creates a new block · note code→text
◈ insert→ writes into an existing block · template code
◑ show→ live re-running view · code jsx
∅ nothing→ side-effect only · code mutation

The editor only offers the landings a block can actually do: a plain note offers spawn, a template adds insert, a jsx block offers show, a value-less script offers nothing. You never pick an impossible combo.

Are the sections independent? Mostly — with one friendly rule

Some triggers produce output (a /command, a tag, a manual run); some just change data quietly (ingest, on-update). When you pick a quiet one, the WHERE section dims — there's nothing to place. It does this non-destructively: your "where" choice is remembered, the section just greys. Switch the trigger back and it returns exactly as it was. So "does setting ingest wipe my anchor?" — no, it's only suspended.

Runson/summarize, showinginline
WHERE it showsinline, here
nowhereright heresidebartopbar
Runsoningest, showingnothing
WHERE it showspausedingest shows nothing
right heresidebartopbar
your choice inline is remembered — pick a showing trigger to bring it back.

Feasibility: trivial — keep all three values always, derive whereActive = triggerRenders(...), grey the section and skip the render when false. No destructive writes.

How the inline form is summoned

Any block whose metadata has a trigger or anchor shows a one-line behavior sentence under its title — that's the affordance. Click the line (or its ▸) to expand the editor in place; the on the map and right-click → Edit open the same thing. Plain blocks show nothing until you give them behavior — so ordinary notes stay clean.

Collapsed — the behavior line (click to open)

#+begin_src lua▶ run
-- summarize the current block return summarize(ctx.block)
#+end_src
Runson/summarize, showsinline

On a plain note block, the same spot reads a faint "+ give this block behavior" on hover — nothing permanent until you do.

Expanded — the drawers, in place

#+begin_src lua⚡ /summarize▶ run
-- summarize the current block return summarize(ctx.block)
#+end_src
WHEN it runson /summarize
I run itan eventa /commanda tag
WHERE it showsinline, here

Identical to the popover above — same drawers, same writes. "Show Metadata" just toggles this open instead of dumping JSON.

04

Right-click — one-click pins + spawn the drawers

BlockContextMenu.tsx partial — editor opens, no Pin-to submenu yet

A "show it here" pin is a single choice — one click, perfect for a menu, so Pin to ▸ is a submenu of places. But choosing when something runs needs sub-fields (which event, which filter, the command name) — too much for a submenu. So Edit behavior… spawns the drawers right under the block. No "Fire on…" submenu — picking a trigger lives in the editor, where there's room to explain it.

Right-click a block · "Pin to ▸" opens a submenu

+ Add block below
Turn into…
Pin to
Edit behavior…opens editor
Version history
Delete block
show it in…
The sidebar
The topbar
This tag · :food:
A header here
on every…
· Journal page journal/*
· Custom pattern…

"Edit behavior…" → the editor, inline

#+begin_src python▶ run
# estimate macros from a photo macros = analyze(ctx.image)
#+end_src
pythonRunsoningest, showsnothing
WHEN it runson ingest · photos
I run itan eventon ingesta /command
WHERE it showspausedingest shows nothing

So your instinct is the rule: right-click → editor for the part that needs explaining, with one-click "Pin to" places kept as a submenu. Quick when it can be quick, guided when it can't.

05

Page-view drag — set WHERE by hand

spatial gesture · targets: title · header · context · any tag not built

Hard to build, worth it. Grab a block by its * gutter handle and every valid WHERE lights up at once — the topbar/header, the context sidebar, the block's own title slot, and any tag chip in the body. Drop, a small confirm sets the anchor (presentation inferred from the target), done. Drag sets WHERE only — it's the one spatial axis; HOW comes after via the or the drawers.

dayweekmonth·pages
▾ drop → topbar / header Mod+. context
journal/2026-05-29 ▾ drop → title slot
:food: ← drop → tag drawer :journal:
#+begin_src python
# daily brief brief = summarize(today())
dragging "Daily brief" — every valid WHERE is glowing
Pin to sidebar? sets anchor: sidebar · presentation: pin

context · sidebar

Classifier labeler
drop here →
anchor: sidebar

Four target kinds, one gesture, all reversible (drag back out, or toggle off in the map). Dropping on the :food: chip sets anchor: tag:food and the block becomes that tag's drawer — anywhere that tag appears. The "Drawers…" button on the confirm is the escape hatch into §03 when you want to set HOW in the same motion.

06

Reality check — what shipped, what's planned, and the gaps that hurt UX

reconciled against the implementation spec

The scoreboard

PieceStateNote
Four-axis model + block_extensions registryshippedregistry table + partial indexes, not generated columns
BehaviorEditor — chips + live sentenceshippedour §03, essentially as drawn
show / spawn / insert / nothingshippedour "insert vs show", expanded to four
runtime: template notes ({vars})shippedthe instant-template; plain↔template toggle
on_load triggershippedpage-granular, fired on block load
Dead-end gating + CI/ops guardsshippedcoherentPresentations — the matrix in §01
Visual chrome (drawer / callout / chip)cutpresentation became a verb, not a style
Slot-aware positioningstubspawn always appends to page tail
Lazy / visibility triggerplannedtag renderers run for every host till then
Schedule executionplannedstored, not run
JSX = all 39 ui.* primitivesplannedonly 5/39 today — forces 2-block splits
The mount map (§02) + page-drag (§05)not builtregistry has no spatial UI yet

The five gaps that actually bite the note-taking experience

1No visual chrome — a "show" is naked
Presentation collapsed to a verb, so there's no way to say "frame this as a collapsible drawer / a callout / a title chip." This is the biggest delight loss. The difference between a result dumped inline and a tidy framed drawer is most of what makes a notebook feel crafted.
Fix: a thin styling layer on top of showstyle: drawer|callout|chip|plain — orthogonal to the verb. Doesn't touch the axis model.
2Spawn ignores the slot — lands at page tail
A journal template fired on page_create appends to the bottom of the page, not the top. Templates that don't land where you'd write feel broken. The anchor's slot (title/top/body/after) is parsed but not yet mapped to a real position (TODO(slot-positioning)).
Fix: honor slot for spawn/insert — the highest-leverage non-glamorous fix here.
3Tag renderers run for every host on the page
A tag:project Python renderer with an expensive query executes for every tagged block on screen at once — no lazy/visibility trigger yet. Tag-drawers on real data will stutter, which trains people to avoid the best feature.
Fix: the planned IntersectionObserver visibility trigger — run on scroll-into-view.
4No way to see what you've extended
The registry exists but has no spatial overview (§02) and no drag-to-place (§05). With dozens of pinned/hooked blocks, "what's mounted where, and is it on?" has no answer screen — the original complaint that started all this. list_hooks is also type-agnostic, so the list is noisy.
Fix: build the mount map as a read view over block_extensions; it needs no new model.
5Insert has no target picker; Run button still hardcoded
insert can only hit self / the active block — no "into that block" picker. And the Run button is still baked chrome, not the title-slot chip we proposed — so "code blocks declare what actions they expose" didn't land (it depended on the cut chrome layer).
Fix: rides on gap #1 (chip styling) + a small block-target picker.
TODOUnify template and note — one kind
runtime:template and runtime:none are the same block — a note, just with {vars} sprinkled in; a plain note is a template with no variables. Collapse them into one note runtime that always interpolates (verbatim when there are no vars). Then every note can insert, not just spawn — which removes the note(none)+insert dead-end and a whole row from the coherence matrix. The plain↔template toggle disappears: you just write, and blanks fill if present.
Spec'd in docs/TODO-unify-note-template.md — one code check first: confirm render_template is a byte-exact no-op on variable-free content.
?Open: the approval gate you'd cut is back
You said "no gate — users can do what they want." The build kept anchor.approved for scarce sidebar/topbar slots (to stop the AI/MCP self-promoting into global real estate). Worth settling: drop it entirely (your call), or keep it only for AI-authored claims and auto-approve anything you place by hand?
My lean: auto-approve human placement, gate only MCP — keeps your notebook frictionless without letting an agent grab the topbar.
Purity vs. delight — my read. The architecture came out clean: four orthogonal axes, one registry, dead-end gating that makes the editor honest. That purity earned real things — the instant template, the (event, lua, sidebar) cell, one write path. But every cut was a delight cut, and they cluster on the same axis: WHERE. Chrome (#1), positioning (#2), the overview (#4), the Run chip (#5) are all "how it looks and where it sits" — exactly the felt texture of note-taking. The model is sound; the gap is that presentation got flattened to a verb and the styling/positioning half never got rebuilt.

If I were sequencing for joy-per-effort: (2) slot positioning and (4) the mount map first — both are plumbing over shipped data, no model change, and they fix "feels broken" and "can't see my stuff." Then (1) the chrome layer as the one genuinely new piece — small, orthogonal to the axes, and the thing that makes the whole system feel designed rather than merely correct.