Names Are Projections, Not Stored Copies
Why a block's or field's displayed name is computed live from label, handle, or content, why the display is the editor, and why the Control Panel label row was removed
Decision
A displayed name — block or field — is a live projection, never a stored
copy. The stored label is a deliberate override that wins when present;
editing happens where the name displays.
data block label ?? humanize(handle)
content block label ?? first heading ?? first line (truncated) ?? humanize(handle)
field label ?? humanize(handle)Every block and field therefore always has a resolved name; "derived" and "without an explicit override" describe the same state, and this record avoids calling it "unnamed".
The projection is computed wherever a name renders — the data-block header,
the @ reference picker, the glyph menu's About footer. Derived names render
in muted scalar text, explicit labels in normal scalar text; that contrast is
the only place the distinction surfaces in chrome, because the user never
meets the word "label" — they rename a block, and the system stores a label.
Editing lives on the surface that displays the name: Document, Module, Content Block and Data Block headers all expose their inline label editor when structural editing is permitted. The live page binds Module and Block label changes to the existing Folio rename operation, addressed by stable folio keys; rejections reach the existing inline editor. There is no Control Panel label row on any facet.
GB-218 settlement (2026-09-09): required fresh-context review accepted the connected Module and Block rename wiring with no blocking findings. Connected-page regressions and browser editor stories passed; manual live-canvas verification was unavailable because browser control returned stale accessibility state.
All editable Folio header names use one domain-neutral inline-name control. The displayed name itself is the affordance; selecting it swaps the same box for the same input at every tier without moving or resizing it. Enter or blur commits and displays the draft optimistically while persistence runs; Escape abandons, clearing an explicit name restores its live projection, and a refused write rolls the display back and reopens with the attempted text intact. The control owns that interaction and its visual states. Each Document, Module, or Block adapter owns only its resolved name, capability gate, persistence callback, and refusal channel. Version filenames may reuse the control, but remain version-owned export names rather than Folio labels.
Settled 2026-08-08 by a grilling session recorded in the
2026-08-08 names are projections amendment of
plans/archived/2026-07-28-control-panel-and-notifications.md, which carries the full
save, uniqueness, gating, and proposal rules.
Context
The Identifier Model gives blocks label → handle → key, with the label
optional. That left two open questions: where does an unlabelled block's
display name come from, and where does the label-edit affordance live? An
earlier amendment (2026-08-07) answered the second with a Control Panel
Block label row shown on Content/Fields and hidden on About — which made
the panel jump on facet switches, and gave a rare operation a permanent row.
Typeform settled both questions the other way, and its answer transfers
because it shares the relevant separation: mutable display text over a
stable binding. A question's Recall-menu label is its current text (a
projection, stored nowhere), the system ref underneath is the binding, and
there is no dedicated label-edit affordance anywhere — you edit the name
where it displays. Airtable represents the genuine alternative: every table
name is explicit and always editable in its header. This codebase takes
Typeform's answer for content blocks (the content supplies the displayed
name; identity stays with the blk_ key) and Airtable's for data blocks (an
explicit-name culture, because the label feeds a form) — one projection rule
covering both.
Why not store the derived name
A stored derivation is a one-time copy that goes stale the moment the first
heading is rewritten — a lie that looks authored. This is the same failure
the field plan's R2 rejected for label-following handles: "it follows until
you edit it" is a worse mental model than no relationship at all. A live
projection cannot go stale because there is no second copy; renames are safe
because references bind blk_ ids
(stable IDs decision),
so any rename re-renders and never re-points.
The accepted cost: a derived name is unstable by design. Edit a content
block's first heading and its name changes in the @ picker between visits.
Two sibling content blocks may even display the same derived name — allowed,
because a derivation is not a write and a content edit must never bounce off
a naming rule, while stored labels keep their module-uniqueness refusal
(deliberately naming two siblings identically is almost certainly a
mistake). The muted rendering is what makes the instability honest: muted
means "borrowed from content, will follow it."
Consequences
- The set-label and remove-label mutations are distinct: setting refuses an
empty value; removal is a first-class operation restoring the projection,
because the constrained About face locks keys and a user cannot delete the
label:line by hand. - First naming derives the handle — while a block still wears its untouched minted placeholder handle, the first real name (typed or accepted from a Goldie proposal) also derives the handle, collision-refused. After that, name edits touch the label only. This closed the "block handle is minted rather than derived" backlog row.
- "Untouched placeholder" is durable state, never string inference. The
row carries an explicit provenance marker (a
placeholderHandle-style flag) set at mint and cleared by the first explicit naming or any hand edit of the handle — and never reset, including by remove-label, so a second naming can never re-derive. Matching the handle string against a placeholder pattern after reload is inference, not provenance, and is forbidden: a user may legitimately choose a name that collides with the pattern. - Any new surface that shows a block name must render the projection with
the muted/normal contrast, not read
labeldirectly.
AI naming rides the override channel
The pre-AI systems this model was tested against baked naming into either pure mechanism (Typeform's first-text derivation) or pure human effort (Airtable's explicit names). This product has a third thing: a resident block-scoped agent with a proposal channel. AI naming therefore lands as a feature on this architecture, never a revision of it:
- The projection stays mechanical. It runs at render time — reactive, free, deterministic, and correct-by-construction because it is read off the content. A model call must never sit inside the resolver: too slow for a path that runs on every picker open, and caching its output would store a derived name — the stale-copy failure this decision exists to prevent.
- An AI-suggested name is a write. Goldie reads the whole block — content, fields, guide, not just the first heading — and proposes a label through the existing label-proposal path, accepted through the same set-label mutation and uniqueness rule as a typed name. Proposal atomicity is split by lifecycle, matching the plan amendment: a creation-time proposal (the block still wears its placeholder handle) may carry the initial label alongside the edit, rendered in the diff grammar as an ordinary value change — visible chips, never a silent side effect. Renaming an established block is always a distinct label-only proposal; a name never rides along inside a content-changing edit.
- Her introduction advertises the capability. Goldie's first-turn chrome introduces what she can do for the block, naming included — the affordance is conversational, not another control in the chrome.
- Acceptance transfers authorship. An accepted AI name is the user's name: stored label, normal text, no third visual state tracking that a human did not type it. If a later content edit leaves the name stale, the remedy is behavioural — Goldie, already watching the block, may re-propose — not a new naming state.
Fields follow the same projection
Fields are part of this decision, not an afterthought to it; their chain is
simply shorter, because a field is a declaration with no content to derive
from — label ?? humanize(handle), as the Decision block states.
The projection title-cases the humanized handle — shoe_size displays as
"Shoe Size", muted. This applies to the humanize(handle) leg of every
chain, blocks included, so an unlabelled data block reads "Lead Capture
Data", not "lead capture data". A derived name and an explicit label may therefore be
string-identical; the muted/normal weight is the distinction, and it is a
real one: the muted name follows a handle rename, the normal one is pinned.
An authored declaration (shoe_size|Number:) stores the handle only — a
generated label would be a stale copy and would break the byte-identical
round trip. Explicit labels remain the override where title-casing reads
wrong (abn wants "Provider ABN", not "Abn").
Fields already shared everything else — optional label, first naming derives
the handle, label and handle independent thereafter, fld_ identity under
@ references. The one divergence was the field plan's R2a, which persisted
a placeholder label (Label, Label 2) on create. That is revised: a
new field mints a type-named placeholder handle only (label_2,
number_2), stores no label, and displays the muted projection. A stored
placeholder label was indistinguishable in data from a name someone chose;
a muted "Number 2" declares itself derived on every render, which was R2a's
own goal done properly. Crash-safety is unchanged — the row still persists
on create. The set/remove label mutation split applies to fields too.
Existing stored placeholder labels are treated as explicit forever. The
indistinguishability that motivates this revision cuts both ways: no
migration or opportunistic pass can tell a leftover Label 2 from a name
someone deliberately chose, so nothing may delete one automatically.
Removal is always a deliberate act through the remove-label mutation, by a
user or an accepted Goldie proposal. Only rows created after this
revision carry the no-stored-placeholder behaviour.
Amendment — 2026-08-09: a typed name is stored, matching or not
A name a user types is stored as label, whether or not it equals
humanize(handle). The earlier rule dropped the redundant case — "Shoe
Size" over shoe_size stored nothing, because the projection already renders
those characters. That is now reversed at both write sites
(renameBlockFieldHandle and the add-field path in documents.ts).
The suppression was redundancy-avoidance applied to the wrong fact. derived
does not ask "would the projection produce this string?" — it asks "did
anybody choose this name?", and dropping the label destroyed the only record
of the answer. It also failed hardest where it mattered most: the Fields row
derives the handle from the typed name, so a hand-typed name almost always
humanizes back to itself. Typing "New Field Edited" gives new_field_edited,
which humanizes to "New Field Edited" — discarded, then rendered muted as
though it were an untouched fallback. Only a name whose handle could not match
it, like ABN over abn, survived to read as pinned. The common case was the
broken one, and it was reported from the canvas (owner, 2026-08-09).
This extends the paragraph above rather than contradicting it. That rule says
the system must never guess whether a stored label was chosen; this one says
it must not throw away the evidence when it knows. A label matching its
handle is not the stale copy this decision warns about: nothing recomputes
label, so the two can diverge only when somebody edits one — and at that
point the stored name is precisely the fact worth keeping.
Unchanged: absence still means derived, so an unlabelled corpus renders exactly as before and the byte-identical round trip holds; an authored declaration still stores no generated label; and a minted placeholder handle stores no label, because nobody typed it.
A Module Is One Row
The module card carries its title row alone; the Contains row lives on the document, and no count is a filter
The Folio Tiers Share Parts, Not a Recipe
Why there is no shared folio card composer — a shared behaviour is promoted into the Stack molecule as a slot default the tier mounts and wires, never a shell the tier configures with flags