Durable node identity
Status: Open — problem framing, no committed design. Originating request: gridaco/grida#775.
This RFD owns the reference half of #775. The serialization half
(serialize_node) shipped; see the package README. The positional
node_path / node_at_path proposal in #775 is superseded by this
document — see Why the obvious answer is insufficient.
The gap
The editor identifies a node with a NodeId. That id is a random
per-parse string, minted when the document is parsed and discarded on the
next parse. It has two properties that matter here:
- It never appears in serialized output. It is an in-memory handle, not a document fact.
- It regenerates on every parse.
load()the same bytes twice and the same logical element carries two different ids.
So there is no answer today to: "the user selected this element; give me
a reference I can store, persist, or hand to another process, and later
resolve back to the same element." The NodeId cannot do it — it is gone
the moment the document is reloaded.
The need is real. The motivating scenario in #775: embed the editor as a
function over SVG, let the user select an element, and hand a downstream
process — concretely an AI coding agent — that element for further work.
A NodeId is useless across that boundary: the agent sees SVG text, not
the editor's in-memory handles, and any reload re-mints the ids.
What "durable" has to mean — two distinct contracts
"Survives a reload" hides two very different requirements. They must not be conflated, because a mechanism that satisfies one can fail the other completely.
- C1 — survives a deterministic re-parse. The same (or
whitespace/comment-only-different) bytes are parsed again. The reference
must resolve to the same logical element. Use case: app restart, reload
of an unchanged file on disk, undo/redo across a
load(). - C2 — survives a structural rewrite. A different agent — a human in another tool, or an AI pass — edits the file: inserts, removes, reorders, re-types elements, then the editor reloads the result. The reference must still resolve to "the same element the user meant."
C2 is the one #775's motivating scenario actually needs (the whole point is to let the agent edit), and it is strictly harder. A reference scheme can ace C1 and be worthless for C2.
Candidate contracts, with honest scope
Positional child-index path
Address a node by the chain of child indices from the root: "first element child of root, then its third element child." Resolution walks the chain.
- C1: yes. Parsing is deterministic, so the index chain is stable across re-parse of the same bytes. Element-only indices (ignoring text/comment nodes) additionally tolerate whitespace/comment churn.
- C2: no. Any insert, remove, or reorder before the target shifts its index. The reference silently resolves to a different element, or to none. Exactly the edits an editing agent performs are the edits that break it — so under C2 it is not merely weak, it is misleading: it resolves successfully to the wrong node.
Why the obvious answer is insufficient
This is the node_path / node_at_path shape proposed in #775. It is
cheap and it is derivable today from the editor's public tree() snapshot
(walk parents for the path; descend children to resolve) in a few lines —
which is itself a reason not to mint it as a public contract prematurely
(no second consumer has shaped it). But the deeper objection is the C1/C2
gap above: shipping it under a "survives a reload" headline would advertise
durability it does not have for the scenario that asked for it. If a
positional path is ever exposed, its contract must say C1 only, in
those words.
The id attribute
SVG already has a serialized, author-facing identity mechanism: id="".
The editor already reads it (display labels, tree() names).
- C1: yes. It is a document fact; it round-trips.
- C2: mostly. An agent that preserves ids keeps the reference valid
across structural edits —
idis position-independent. It breaks only if the agent renames or drops the id, which a cooperative agent need not do. - The catch — sovereignty (P1). Not every node has an
id, so this cannot be a universal reference without the editor minting ids. And minting ids is exactly the proprietary-noise the package exists to refuse — cf. the README's complaint about Illustrator stampingSVGID_1_. Any design that leans onidmust answer: who writes the id, when, with the user's knowledge, and is an editor-authored id distinguishable from an authored one. "Silently inject ids so we have a handle" is a P1 violation, not a design.
Semantic anchor / selector
Address a node by a matchable description — a CSS/XPath-like selector, or a structural fingerprint (tag + key attributes + neighborhood).
- C1: yes. C2: best of the three — a selector keyed on stable authored attributes tolerates reordering and unrelated inserts.
- Cost. Needs a matching engine, a disambiguation rule when more than one node matches, and a defined failure mode when zero match. This is the most capable and the most expensive option, and it has the largest design surface to get wrong.
Non-goals / boundaries
- Not a private id baked into the file. Writing an editor-namespaced
attribute (
grida:nodeid) onto every element to carry identity is the proprietary-noise anti-goal in another costume. Rejected up front; if identity must be carried in the file, it rides the author's ownid, on the author's terms. - Not a sync/CRDT identity. This is single-document reference resolution, not multiplayer node identity. "Not a Figma-style multiplayer canvas" still holds.
Open questions
- Is C1 alone worth a public API, or does exposing a C1-only reference just invite the C2 misuse it can't support?
- For
id-based identity: what is the honest UX for "this element needs a stable handle but has no id" — refuse, prompt, or an explicit user-invoked "assign id" command (never silent)? - Does the downstream consumer (the agent embedding) actually need editor-resolvable identity, or does it need a document-level anchor it resolves itself (in which case the editor's job is only to report a good anchor for a selection, not to resolve one back)?
Promotion gate
No public reference API lands until ≥2 internal consumers have shaped
the contract (the package's P6 / promotion rule). The agent-embedding in
#775 is the first. A second — a persistence layer that stores selections,
or a layers panel that pins a node across reloads — must exist and exert
pressure on the shape before it escapes the package. Until then the
capability stays a documented recipe over tree() (for the C1 case) and
this RFD (for the rest).