libtracer — context glossary

The canonical vocabulary of the libtracer protocol. It tracks the reference suite and the normative protocol specification, and it is the vocabulary of record: where a term here and a term elsewhere in the doc set disagree, the other page is brought into line with this one.

Each entry names the canonical term, states the rule and the reason for it, and lists the near-misses that must not be used for it. An Avoid line is not stylistic — every phrase on it denotes something the protocol does not have, or denotes the wrong one of two things the protocol keeps deliberately separate.

Language

Versioning

Protocol version: The integer version of the libtracer wire format and of the specification that defines it — v1. Frozen on release and immutable thereafter; a wire-incompatible change is protocol v2. It is not encoded per frame — peers learn it at the discovery layer (mDNS _libtracer._tcp = v1, _libtracer-v2._tcp = v2). Avoid: “wire format v0.1”, “format version 0.1”, per-frame version, VR / version bit.

Release version: The semantic version of an implementation — a git tag or package version (VERSION, library.json, Cargo.toml, package.json). It is decoupled from the protocol version and carries no protocol meaning: a whole series of releases speaks protocol v1, so the two numbers move independently and a release number never signals a wire change. The breaking-change boundary is the protocol version, never this one. Avoid: calling this “the protocol version” or “the wire format version”; reading a wire-compatibility boundary out of a release number.

Discovery-layer versioning: The mechanism that keeps incompatible protocol versions apart — a distinct service name / port / CAN-ID prefix per protocol version — used instead of a per-frame version field.

Version bit (VR): Does not exist. The wire format carries no per-frame version field; opt bit 7 is a forever-reserved MUST-be-zero bit (a VR version-bump bit is a rejected design, 01 §rejected designs). Avoid: “VR bit”, “version bit in opt”, “opt.VR”.

Capability negotiation: Does not exist. Receivers MUST accept every LL/CW/TF variant, so senders just default to compact and there is nothing to negotiate; protocol v1 has no other negotiable wire features (ADR-0013). Avoid: “per-peer capability discovery”, “feature negotiation handshake”.

Wire format

Canonical per reference 01 and 05.

opt byte: The 1-byte options bitfield at offset 1 of every TLV; bits 7→0 are R│PL│TS│CR│LL│CW│TF│R. PL=payload-is-structured, TS=trailer-timestamp, CR=trailer-CRC, LL=length width, CW=CRC width, TF=timestamp form; bits 7 and 0 are reserved-must-be-zero (non-zero ⇒ reject as INVALID). The bitfield is opt_t (core/include/libtracer/tlv.hpp:118). Avoid: the legacy VR│PL│TS│FP│CR│reserved layout; any VR (version) or FP (finite-pool) bit — neither exists.

TLV header: A 4-byte header (type u8, opt u8, length u16 LE), or 6 bytes when opt.LL=1 (length u32 LE). Integrity and wire-time live in the optional trailer, never the header. Avoid: “8-byte header”, “crc in the header”, “length: varint”.

Length field: Fixed-width little-endian — u16 (default) or u32 (opt.LL=1). No u64; oversize payloads address-shift across ep[0..N]. Avoid: “LEB128”, “finite-pool length encoding” (both rejected, 01 §rejected designs).

Trailer: Optional bytes appended at egress and stripped at ingress, leaving the payload byte-identical across hops. Carries an optional wire-time timestamp (opt.TS) and/or CRC (opt.CR).

CRC: A trailer-resident frame check, gated by opt.CRCRC-32C by default, CRC-16-CCITT when opt.CW=1 (core/include/libtracer/crc.hpp), computed over payload + trailer_ts (header excluded). A bit-flip detector, not adversarial integrity. Avoid: “XOR-16” (a checksum from the pre-spec code), “CRC in the header”, “CRC always present”.

Structured TLV: Any TLV with opt.PL=1, whose payload is purely concatenated child TLVs; its type code declares what the children mean (SUBSCRIBER, POINT, FWD, SETTINGS, …). There is no generic container type. No address form is one: PATH (0x06) carries packed §Segment records and PATH_REF / PATH_REF_REVERSE carry fixed-stride elements, all three with opt.PL=0. Avoid: “LIST”, “type 0x05”, “graph-node-as-TLV LIST” — 0x05/LIST is retired and the code assigns no type to it (core/include/libtracer/tlv.hpp:37-94).

Validation timing (lazy, per-level): Structural validity is checked where a level is consumed, never by an eager whole-tree walk at ingress: ingress verifies the trailer CRC (a linear scan — CRC never needs the tree) and the top-level header; a malformed child TLV surfaces its error (INVALID, tr::tlv::*) at the consumer that opens that level. The only whole-tree walks are termini that materialize or apply transactionally (arena decode, §branch-write admission), whose per-open-level work-stack node is drawn from the receiver’s injected decode resources (RFC-0006) — no depth constant exists anywhere. The spec’s MUST-reject rules bind on observation of a violation, wherever consumption occurs, and so do not imply an eager scan. Avoid: “ingress rejects malformed frames” (ingress checks CRC + top header only); “depth cap / kMaxDepth” (no such constant — RFC-0006); “validation is a separate pass” (it rides consumption).

Graph, addressing & API

read / write / await: The entire data API — three calls, plus refcount management. There is no connect / disconnect / subscribe primitive. Avoid: “connect”, “disconnect”, “subscribe()” as API verbs.

Field-write (the : control plane — the vertex’s ioctl): The control surface: subscriptions, QoS, ACLs, liveness are all writable fields addressed via the : separator on a vertex (e.g. /sensor/temp:subscribers[3]). Subscribing is writing a SUBSCRIBER into a :subscribers[] slot. The mental model is Linux on one fd: read/write of the vertex = the data plane; :field writes = the control plane (the vertex’s ioctl); await / :subscribers[] = the readiness/notify plane (epoll) — all on one identity. The : separator (vs /) is what keeps these as facets of one vertex rather than dissolving it into sub-vertices, exactly as ioctl does not spawn a new fd. :fields are optional (an unsupported one returns SCHEMA_NOT_FOUND — the ENOTTY of an unsupported ioctl), so a minimal endpoint (a 9-byte input) is a char-device with read/write only and no fields. They are also of two kinds, like ioctls: protocol-defined standard fields (:subscribers, :acl, :settings, :children — uniform, so a generic orchestrator works cross-device) and device-private fields (a device’s own control ops, on the same vertex, device-bounded). The protocol owns the addressing; the device owns its catalog of what those ops accept. Avoid: “a vertex’s control facets are sub-vertices” (a sensor’s :acl/:subscribers are : facets of one identity, not / children) — but a genuinely distinct subsystem with its own lifecycle/stats/ACL (a transport, a live connection) IS its own / vertex, created via the same in-band creating write (ADR-0027); the rule is control facet ⇒ :, distinct identity ⇒ /; “every vertex must implement the control fields” (they are optional/ENOTTY-able); “device-specific control means leaving the protocol” (it is a device-private :field, like a driver-private ioctl).

Application field / field descriptor table (RFC-0010): An application field is an owner-defined property under the one reserved subkey of the settings container — :settings.app.<name> — the device-private half of §Field-write’s ioctl model, given substrate. The field descriptor table is how the owner declares them: a per-vertex table {name, access {ro,rw,wo}, descriptor bytes, initial value?} installed through the local host API only (the RFC-0009 owner-initiated doctrine — no wire operation declares a field; remote peers write declared fields, never invent them). Declaration, remote-writability gating, and :schema self-description are ONE structure, so they cannot drift. Undeclared names answer SCHEMA_NOT_FOUND — the ENOTTY default survives, opened only where the owner named a hole. Values are bare TLVs riding the vertex (no subscriber list, no per-field vertex; cost = bytes + one table slot), stored and served verbatim — the runtime never validates dtype/range against the descriptor (consumer self-description; the owner’s apply seam is the semantic gate). Avoid: “remote field declaration” (owner/local only); “bare app field names on the property plane” / “app knobs flat in settings.*” (both rejected — only settings.app. is collision-proof); “the runtime validates app-field writes against the schema” (addressing only — one table lookup); “app fields wake await” (see §Announce write).

Schema (:schema) — exactly one per vertex, describing that vertex: A vertex’s self-description, synthesized on read from state the runtime already holds (no stored bytes): the vertex’s NAME, its protocol knobs, and — iff an owner installed a descriptor table — the app part (RFC-0010 §B). It is a vertex-level facet: one per vertex, describing that vertex’s own structure — never its children’s. A child’s schema is read from the child (<parent>/<child>:schema), because a child is its own identity carrying its own facets (§Field-write: control facet ⇒ :, distinct identity ⇒ /). Correspondingly there is no schema hanging off a field:schema is not a sub-level of :children or of anything else; it addresses the whole vertex or nothing. Avoid: “the :children field’s :schema” / “:children.schema” (a category error — schema is a vertex facet, not a field facet, and a parent never publishes its children’s schemas); “a schema per field”; “:schema lists the children” (:children[] enumerates members; :schema describes the vertex); “read the subtree’s schema from the root” (each vertex answers for itself).

Node identity (:identity) (RFC-0011): The node’s public key served as a vertex facet: SETTINGS(PL=1){ NAME "kind" VALUE u8, NAME "key" VALUE <key> } in that fixed order, kind 0x01 = ed25519 with a 32-byte key, making a 60-byte record (core/src/graph.cpp:3574-3607). A kind that contradicts the key length is TYPE_MISMATCH and never reaches the wire. The record is node-scoped and pre-serialized, so every vertex serves byte-identical bytes (core/src/graph.cpp:3614-3643), and it resolves above the READ gate (core/src/graph.cpp:4348-4351) so an unauthenticated peer can pin the key on first use. A node with no keypair answers SCHEMA_NOT_FOUND. Avoid: “per-vertex identity” (node-scoped); “identity is behind the ACL” (it resolves above the READ gate — pinning would otherwise be impossible).

Announce write (RFC-0010 §C): The one change-notification convention of the property plane: a field write — protocol or app, local or remote — does NOT wake await, does not advance the write sequence, and does not propagate; the : plane is silent by design. A property change consumers should notice is followed by an ordinary data-plane write at the vertex (assign + propagate), performed by the owner once the change is actually applied — an announce is a statement that the device applied the change, and only the owner can truthfully make it. Notification is at vertex granularity, by the owner’s act, on the owner’s cadence; subscribers get one ordinary delivery and re-read the small property tree if they care which knob moved. Avoid: “per-field notification / await on a :field” (promote to a vertex instead); “the graph announces a remote field write automatically” (the owner’s apply handler does, after applying); “consumers poll fields for changes” (forbidden — subscribe and re-read on announce).

Field promotion (notification by vertex-promotion): Notification granularity is chosen by where a datum sits in the path algebra, never by protocol machinery: a : field is bare attributed data — no subscriber list, no per-field notification, readable in one walk of its vertex — while anything a consumer must observe independently is promoted to a / child vertex (a distinct subscribable identity, created by an ordinary §Write-creates data write). Promotion is the schema author’s opt-in trade: one vertex’s cost buys one subscription point. The boundary with RFC-0010 app fields: an app field is the bare-datum case (owner-declared, bytes riding the vertex, changes signalled by §Announce write); the moment a datum needs its own subscribers, it stops being a field and is promoted — the RFC changes nothing about this rule. The producer decides the shape — a consumer cannot retrofit notification onto another device’s bare field. Leaf vertices typically hold zero own ACEs (an empty list costs nothing; the composite’s INHERIT ACEs cover them), but a leaf ACE stays legal — it is precisely how a single-input sink refuses fan-in device-locally (§SUBSCRIBER direction). Avoid: “per-field subscriptions / await on a :field” (promote instead); “every config knob is a vertex” (bare fields cost only their bytes); “leaf vertices must not carry ACLs” (convention, not constraint — the fan-in gate depends on leaf ACEs); “the consumer can promote a producer’s field” (the producer owns its catalog).

Structural vertex: A vertex that exists only to hold a position in the path tree — it carries no application datum, and something deeper is what an owner actually declared. The net plane mints two: the net root (/net by default, the :children[] creation target) and each module segment (/net/<module>) a connection mounts under. They are registered vertices with a stored-value role and no field descriptor table, so they enumerate through for_each_vertex and answer :schema exactly like a value vertex whose owner forgot to describe it — the distinction is invisible on every wire and graph surface, by design (reference/11 §structural vertices). Only the object that minted one can report it: tr::net::transport_vertex_t::is_structural(key) answers for the net plane’s own two, and the graph never answers in general — an application’s own position-holder (a /zone with nothing but children) is indistinguishable from a connection vertex on every graph-visible basis, so a graph_t-level predicate would be a guess. Distinct from a structural placeholder, which is an unregistered intermediate the addressing scaffolding created on the way to a deeper registration: a placeholder is never visited and find does not answer for it. Avoid: “grouping vertex” (§Address grouping already owns “grouping” for fan-in/fan-out, and reference/11 uses “address grouping” for that unrelated concept); “the graph knows which vertices are structural” (only the minter does); “two segments under /net means structural” (a path-shape rule claims every application /zone/<child> too); “a role_t::GROUP” (a role names a read/write/subscribe behaviour; a position-holder names none, and the role is never on the wire).

Path-as-route (a transport vertex mounts the peer’s graph): A remote endpoint is addressed by its full path from the caller’s own root, walking through transport-vertices — there is no separate global name or destination field; the path-suffix below a transport vertex is the address of a vertex on its peer. A transport/connection vertex (ADR-0027) therefore mounts the peer’s graph under itself: its :-facets (:settings, :acl, :children) are the link’s own control; its /-subtree is the peer’s tree, reached by forwarding the unresolved suffix. A per-transport :stats facet is not part of the model. For example, from a web UI: /net/ws-client/<name>/can[0]/ow/<temp_sensor> = walk the local ws connection vertex → forward /can[0]/ow/<temp_sensor> to the board → walk its can[0] vertex → forward /ow/<temp_sensor> over CAN → the 1-Wire sensor. Segments already carry the identifiers (can[0] = bus number, <deviceid> deduced from advertisement), so the path needs no extra naming layer. Routing is hop-by-hop source-routing: each transport vertex strips its whole mount run — every segment of net/<module>/<name>[/<peer>] that names it, not one segment (RFC-0014 S2a / ADR-0061’s strip-K descent) — and forwards the rest (+ payload for write); read/await replies retrace the same bidirectional link per-hop, so no reply-address or correlation-id is needed. This is the send-side dual of the receive-side transport-vertex mount: the suffix a caller routes through a transport vertex equals the prefix that vertex mounts inbound data under — they MUST stay consistent, because they are one operation — the mount write is the FWD terminus deliver_local at the leaf (ADR-0038). Avoid: “each hop strips one NAME segment” (it strips its whole net/<module>/<name> mount run — RFC-0014 S2a); “a remote write needs a separate target/destination field or a global device name” (the path-suffix is the address); “read/await need an RPC reply-address / correlation-id” (replies retrace the bidirectional link per-hop); “the path is location-independent” (it encodes the route — it is relative to the caller’s root, like a URL or a mount path); “a transport vertex’s /-subtree is local” (below a transport vertex is the peer’s graph).

Path element: The model-layer unit an address is made of, and the canonical word for it (tr::wire::path_element_t, core/include/libtracer/path_element.hpp). The two-layer sentence, in full: a PATH is a list of path elements; an element’s kind is NAME or LABEL; a NAME element is encoded as a §Segment record, a LABEL element as an escape record. That is one layering, not two vocabularies competing: “path element” says what an address is composed of, while “segment record” and “escape record” name the two differently-framed byte patterns that spell the kinds (§Segment / view for the record grammar, §Path label for what a LABEL element carries). Both encoding words stay — an escape is precisely not a segment, so the packed-body grammar sentence (“a body is a sequence of records: segment records or escape records”) cannot be said with one word (#1347’s 2026-08-16 ruling, decided on a measured zero perf delta and a failed cleanliness test). An element self-describes by its kind, never by its position (RFC-0027 §5.1, applying RFC-0024 amendment 2’s ruling unchanged), which is why a mixed body is read one element at a time and why skipping an element is not expressible. A host that does not own an element’s kind still steps over it by its declared length; the two refusals are different answers and must not be folded together — a foreign kind is relayed intact, a malformed LABEL refuses the address (RFC-0027 §12.5 erratum 1). Avoid: “path segment” as a synonym for the element (a LABEL element is not a segment — that is the whole point of the layer); “segment record” / “escape record” for the model-layer unit (they are encodings; the element is the thing encoded); “each PATH element is a NAME” (true before RFC-0027, false since); bare “element” where a §Bound path’s PATH_REF element (one per host, replacing the whole address) could be meant.

Bound path (RFC-0024, accepted; §4-§7 incorporated): The second normative path form, alongside the canonical PATH (0x06, packed §Segment records) — a PATH_REF TLV carrying one §Vertex ref per host on the route, each host’s own reference to its next-hop connection vertex, the last element the terminus vertex itself. It is still §Path-as-route source routing — an explicit, monotonically shrinking route, loop-free by construction — spelled in resolutions instead of names, so a forwarding hop dereferences one element instead of running the strip-K mount descent. The canonical form is untouched and stays the mint key: a bound path is minted from a canonical op (an in-band flag; each hop appends its ref to the reply as it returns), and a bound path that fails validation is dropped and re-minted from the canonical original, never repaired and never delivered — so no address is ever reachable in bound form alone. Authorization is unaffected: every bound-form op re-checks the target’s :acl per operation, exactly as the canonical form does. A hop that cannot contribute its ref strips the accumulating PATH_REF instead of relaying it: a list that skips a host is not a shorter route but a wrong one — the origin would consume its own element and the skipped host, finding one element left, would read another host’s ref against its own map. Avoid: “a bound path replaces the canonical path” (it is a second, optional form; canonical is mandatory, is the mint key and is the fallback); “a bound path is a route cache at each hop” (no hop holds anything — the state is the origin’s path object and the delivery-side subscription edge); “a bound path is a capability / grants access” (it is an address; the ACL check is per-operation at the dereferenced vertex); “a stale bound path is repaired in place” (drop, NACK with the failing hop index, re-mint).

Vertex ref (vref): The 8-byte element a §Bound path is built of: (u32 vertex-map index, u32 generation), little-endian. Node-scoped — it is meaningful only on the host that minted it and means nothing anywhere else, which is why a bound path is a stack of refs rather than one global identifier. The index is bounds-checkable against the pinned, pointer-stable, insert-only vertex map (an arbitrary address would not be, which is why the wire carries an index and never a pointer); the generation is the anti-mis-route guard, compared against the vertex’s retirement stamp on every use, and it saturates rather than wraps — a wrapped generation would let a stale ref validate falsely and deliver to a path’s new occupant, the misroute class the per-link §E.1 label allocator already saturates to avoid. Generations only move forward, so a stale ref never becomes valid by waiting. Avoid: “vref” for RFC-0004 §E.1’s label (a label is the per-link u16 delivery-compaction alias, swapped at each hop; a vref is a per-host vertex reference, carried end to end and swapped by nobody) or for a §Path label (the third member of the family — a per-host alias for one element of an address, not a whole-address element); “the ref is a pointer” (an index — a peer-supplied address is unforgeable-validation’s opposite); “a generation match authorizes the operation” (it says the vertex is the same one, never that the caller may still act on it).

Path label (RFC-0027, accepted 2026-08-15; byte layout closed by amendments 4–6): A 32-bit, per-host, per-ELEMENT alias for one local part of an address: (u16 slot index, u16 generation), little-endian. On the wire it is one 7-byte element inside the packed PATH body — RFC-0018 §8’s escape record, 00 <u8 kind = 0x16> <u8 len = 4> <u32 LE> — and not a TLV child (there is no PATH_LABEL type code; the 8-byte child spelling was ruled and then never built). One label covers a hop’s whole local part — its entire mount run, however many segments — never one segment per label. A PATH carrying one is never a §Path lookup key: canonical keys stay pure-string, which is what keeps one spelling per address and byte-prefix-implies-ancestor true. Always qualified “path label”, never bare “label” — an unqualified label stays RFC-0004 §E.1’s per-link u16 and nothing else (RFC-0024 §2.1’s rule, narrowed to the unqualified word by its amendment 3, not withdrawn). The index is host-assigned at mint time — a slot in the minting host’s own table, bounds-checkable — and is never a content hash: a hash collision would be a mis-delivery, the class the doc set closes by construction rather than by digest width. Distribution is passive: there are no label-distribution frames, no advertise, no request flag — each forwarding hop rewrites its own part of src/dst from string to label on a reply it was relaying anyway, so the first reply returns fully minted. Mixed paths are legal and expected: an element self-describes — a literal segment by its length byte, a path label by its kind — and is skippable by length, so a hop that does not mint (or refuses to) simply leaves its part a string and every other hop’s part still compacts. The generation saturates and its slot RETIRES PERMANENTLY — never reused, never wrapped (§4.3.1), which preserves the §Vertex ref rule verbatim across both (slot, generation) fields at zero wire cost. A stale or unknown path label answers a NOT_FOUND-class error; the sender falls back to the full-string path it still holds and re-mints from the next reply — no withdraw frame, no unbind, no lease, no TTL, no aging. Minting is post-auth only and the table is injected, per-peer ceilinged, and refuses new mints on exhaustion (never evicting a live one). A minting hop therefore does hold per-hop state — knowingly, and recorded: it buys per-element degradation and terminus-residual compaction, which a stateless hop cannot reach. Avoid: bare “label” for this (that is §E.1’s per-link u16); “vref” for it (a vref is one element per host, replacing the whole address; a path label is one element of a path, in place); “a path label is a route cache” (it is not keyed on canonical bytes, performs no lookup on the forwarding path, and adds no second invalidation mechanism — the generation bump is the invalidation); “a path label authorizes” (it is an address; every labelled operation re-checks the ACL at the dereferenced vertex, and a generation match authorizes nothing); “a stale path label is repaired in place” (drop, NOT_FOUND, string fallback, re-mint); “a failed mint is an error” (a refusal, a retirement, a non-minting hop and an exhausted table are all one case — the string path — and none is observable on the wire).

SUBSCRIBER direction (producer-holds): A SUBSCRIBER edge lives on the source vertex’s :subscribers[] and carries a target (the delivery destination). The source holds its own subscriber list and fans out, matching the Subscriber{target_key} shape of the reference implementation and the per-slot subscriber list of the originating production firmware (an ESP32-C6 smart-agriculture node). So “the controller input subscribes to the sensor” means write('/sensor/temp:subscribers[]', SUBSCRIBER{target='/dev/ctrl0/in/temp'}) — the consumer is the target, the edge is stored on the producer, and subscribe-authorization is gated by the source’s :acl (the producer authorizes its own subscribers). The subscribe-write is consumer-initiated — issued by the consumer acting as a client (REST-shaped: it holds no source endpoint, only its origin_peer_id), or by firmware / NVS config / an orchestrator issuing the identical write on its behalf (ADR-0026). There is no privileged “default binding” — firmware-baked, NVS-restored, and third-party subscriptions are the same client-write.

The target is singular per edge, and the target is subscription-unaware: delivery is an ordinary write to the target, indistinguishable from a direct write — the target does not know which subscription, or that any subscription, produced it (load-bearing claim 2: delivery is a write). The delivery also terminates at the target (RFC-0007): it applies the target-local write effects (store, write-ACL, await wake, local reaction) and never re-dispatches to the target’s own :subscribers[] — propagation past a target is exclusively the act of the logic behind it (a controller reading its input ports and writing its output ports on its own execution; a handler re-emitting). Pure relay is wired as a direct subscription to the source, not a chain of plain vertices. Many subscriptions MAY fan into one target; they resolve per the target’s role (overwrite for stored-value, append for stream), not by the target arbitrating. Fan-in is gated by the target’s own :acl (“who may write to me”) plus optional firmware arity, so a single-input sink rejects a second writer device-locally, even with no orchestrator present — the dual of the source’s subscribe-gate. The target is control-passive but data-rich: it grows no source plane, yet spans scalar → structured TLV → rope/stream (e.g. a GB RTSP frame group via advertise+id-match). Consequently, any provenance a consumer needs (which source/child changed) must travel in the delivered data/metadata (RFC-0003), never be inferred by the target from the subscription. Avoid: “the subscriber declares/stores its own sources” (producer-holds); “the consumer’s ACL gates subscribe” (the source’s does); “the source subscribes to the consumer” (the source fans out to its targets); “the target knows which subscription wrote it” (it is subscription-unaware — it just receives writes); “the sink cannot refuse fan-in” (the target’s write-:acl does, device-locally); “the target must be a dumb/simple endpoint” (it is control-passive, not data-simple — it spans up to a GB rope/stream); “subscribing needs a connect primitive” (the consumer’s connect(to=…) is SDK sugar over the field-write — no wire verb, ADR-0006); “a delivery relays onward to the target’s own subscribers” / “chained plain vertices auto-relay” (delivery terminates at the target — RFC-0007; re-emission is the target’s logic).

Graph (address) composition / composite subscription: The third composition axis — distinct from the memory-rope and TLV-tree of §Two compositions — is the tree of addressable vertices by path / :children[] membership. A composite is a parent vertex; subscribing to it covers its whole subtree with one edge (every subscription is a §Subtree subscription, RFC-0005), saving a per-leaf SUBSCRIBER. Delivery is the written TLV as-is (the producer’s frame at its produced granularity — a leaf value or a whole §Branch write); the aggregate stays available as the composed branch read — a plain read of the parent serves the folded POINT tree of its registered subtree (RFC-0016). The common pattern is one composed read on join, then subscribe (to the parent) for the tail. Avoid: “subscribing a subtree needs a SUBSCRIBER per leaf”; “composite delivery re-encodes a delta or tags it with a path” (delivery is the written TLV as-is; remote concrete-path tagging is the separate, draft RFC-0003); “the graph tree is the same as the TLV tree” (the aggregate projects the subtree onto the TLV axis, but identity/hierarchy is its own axis); “read('/x:[]')” (a bare :[] field is unparseable; the aggregate is a plain read of the parent, RFC-0016).

Subtree subscription / vertical bubbling (RFC-0005): Every subscription observes writes to its vertex and to any descendant — a leaf subscription is the trivial case. A write at a vertex therefore delivers to subscribers there and at each ancestor (“bubbling”), once per subscriber, carrying the written TLV as-is through the ordinary delivery machinery (local view clone; remote return-route FWD). Writes stay near-free when nobody listens: the ancestor fan-out happens only when a subscriber exists at-or-above the written vertex. await is NOT subtree-scoped — it observes stores at its own vertex only. Avoid: “a subscription sees only its own vertex’s writes”; “bubbling re-encodes or path-tags the delivery” (as-is; provenance travels in the data); “every write walks its ancestors” (only under a live ancestor subscriber); “await wakes on descendant writes” (subscription concern, not the readiness plane).

Branch write / decomposition (RFC-0005): A write whose payload is a POINT tree rooted at the target vertex. It decomposes: each value-carrying node lands at the corresponding descendant vertex as a refcount subview of the written frame (zero copy — address-shift-style slicing, never re-encoding), creating missing vertices on the way; each covered subscription point is notified once with its slice (leaf ⇒ its VALUE, interior ⇒ its POINT subtree, root and above ⇒ the frame as-is). Values are the truth at the vertices where they land; a branch is a view, and reads keep one store per vertex — the latest stored value, never behind a notification. Cross-leaf atomicity is not promised (each leaf is a consistent refcounted snapshot; coherence is the (origin, ts) sample group). Batching several subtrees is N self-contained frames in one send(iov); the producer owns cadence (rate caps / flush intervals / dirty tracking / timers are application concerns). Avoid: “a branch write is stored opaquely at the parent” (it decomposes; only value-carrying nodes store); “the branch is a transaction” (admission is all-or-nothing, application is per-leaf); “a wire batch container” (the retired-LIST lesson — N frames, one send(iov)); “libtracer throttles or schedules pushes” (the producer owns cadence).

Write-creates (RFC-0005): A LOCAL data write (the in-process host API) targeting a nonexistent vertex creates it, mkdir -p style with its missing intermediates, gated by the existing CREATE access bit on the nearest existing ancestor’s effective ACL (PermissionDenied when denied; no ancestor ⇒ open). A decomposed branch write likewise creates its landing vertices, beneath a target that already resolved. A REMOTE fieldless FWD{WRITE} to an unresolved dst does NOT create — it answers tr::path::not_found (RFC-0005 §D amendment 1): creation authority is local-or-governed-channel, and a peer creates through the creator endpoint. A child’s appearance is still just its first write bubbling to the parent’s subscribers — a creator-endpoint create IS a write. :field writes, read, and await on a nonexistent vertex answer tr::path::not_found. Avoid: “a remote write to an unknown path creates it” (withdrawn by amendment 1 — that arm is not_found); “the local and remote write paths create alike” (the asymmetry is the ruling: the in-process caller owns its graph, a peer does not); “creation needs :children[]” (typed/controller creation is the creator-endpoint path of ADR-0059); “field writes create” (no vertex, no control surface).

In-band vertex creation / creator endpoint: Creating a vertex is an in-band, ACL-gated write — an orchestrator writes a controller-spec to a device’s creator endpoint (ADR-0059, superseding the :children[] / :controllers[] creation-field spelling of ADR-0027 and ADR-0017), and the device instantiates one of its own known controller types. Creation is a first-class graph operation expressed through the existing read/write API — no new primitive, it is a write — which supersedes the “registration is out-of-band, roles invisible” stance of reference 11. Roles stay invisible; what becomes visible is the device’s controller-type catalog, read as the creator endpoint’s own :schema (§Transport vertex fixes the endpoint’s address), never the internal role. Avoid: “vertices can only be registered locally / out-of-band”; “the orchestrator injects an arbitrary role or code” (it selects a device-declared type); “creation is a new create primitive”; a single global creator path under /net shared by every transport (the endpoint is per-module — §Transport vertex).

Controller vertex / controller ports / binding: A controller is a device-known unit (the origin firmware’s logic-executor / wiring-diagram pattern; a reference 11 role-3/role-4 vertex). Creation and binding are separate steps:

  • Create — an orchestrator writes a controller-spec {type, path, config} (a type from the device’s catalog, no bindings). The instantiated controller exposes its own port vertices — input-port and output-port endpoints in the graph.

  • Bind — a distinct step: the orchestrator wires those ports to other vertices with SUBSCRIBER edges (a source’s :subscribers[] → a controller input port; a controller output port’s :subscribers[] → a sink).

The controller’s logic reads its input ports and writes its output ports; the graph carries data across the bindings. A controller therefore subscribes to nothing at creation — what it consumes/produces is decided entirely by the separate binding step (a patch-cable / dataflow model). Avoid: “creation wires the controller” (creation exposes ports; binding is separate); “the controller subscribes during instantiation”; “a controller is one monolithic vertex” (it exposes port endpoints); “the controller is a separate process” (it is vertices in the same graph); “a controller type the orchestrator defines” (the device defines its catalog).

Transport vertex / connection vertex: A transport and each live connection or listener inside it are first-class / vertices — addressed with /, configured by creation-time config carried in the creating SPEC (addr, port, keepalive, max_frame, backoff, connect_timeout) and parsed into the transport-private tr::net::conn_settings_t (core/include/libtracer/transport_vertex.hpp:133) — not a vertex :settings surface, since RFC-0022 §3.B emptied that core namespace outright — observed with await (link up/down), ACL-gated, and created by the same in-band creator-endpoint SPEC write as a controller (ADR-0059; the :children[] spelling of ADR-0017 / ADR-0027 is superseded). The creator endpoint is a per-(transport, role) flat module mounted under /netws-client, ws-server, tcp-client, tcp-server, can, … — each designating its own control (its config catalog, its :schema, its gating): a client and a server take different config (dial-target vs. bind-port), so they are separate modules with separate catalogs, and the endpoint that creates into a module is that module’s own conn/net/<module>/conn (RFC-0014). Both transport and role are positional (they are the module — ws-client = DIAL, ws-server = LISTEN, can = a multi-peer bus); role is not a :settings/config field, and the earlier client/listener controller-type split collapses into the module identity. Addressing is uniform net module name [peer]: a multi-peer module (ws-server, can) addresses an accepted peer by a peer segment resolved in that module’s own peer table, a point-to-point module (ws-client) has none (routing per ADR-0061). A node is therefore one path tree — data endpoints, controllers, and transports — addressed and orchestrated uniformly. This is not a violation of the :-not-/ rule: a connection is a distinct identity with its own lifecycle, not a control facet of a data vertex. The default link direction is consumer-dials / producer-pushes (the SSE shape that pairs with consumer-initiated subscription and lets a constrained leaf dial out through NAT); role makes it overridable (dial out to a router for a constrained producer or NAT-on-both-sides).

The reference implementation has two creation doors. The RFC-0014 one is the per-module creator endpoint (S2b): declaring a module through transport_vertex_t::register_module mints /net/<module>/conn as a HANDLER vertex whose write is executed, never assigned (core/src/transport_vertex.cpp:339), and the written TLV’s TYPE selects the operation — SPEC{name, config} creates /net/<module>/<name>, NAME{<name>} removes it, anything else is refused (core/src/transport_vertex.cpp:384). The module in the path fixes both the transport and the role, so that SPEC carries neither a type nor a role. The superseded door still accepts the creating SPEC write at /net:children[] with the kind named in its config (core/src/transport_vertex.cpp:63) and routes the catalog through the two child types client and listener (core/src/transport_vertex.cpp:230-238); RFC-0014 S7 retires it. Either door mounts the created connection at /net/<module>/<name>, where the module is the one the application declared for that (kind, role) pair through transport_vertex_t::register_module (core/src/transport_vertex.cpp:274) — declared-only (ADR-0073 §4): there is no derived <kind>-client / <kind>-server fallback, and an undeclared pair answers SCHEMA_NOT_FOUND so creation fails explicitly instead of mounting under a library-minted name (core/src/transport_vertex.cpp:294). A transport kind is added to the catalog through transport_vertex_t::register_transport_type (core/src/transport_vertex.cpp:263), never by editing the net plane.

The connection vertex and the link it names are two distinct lifecycles. The vertex is an explicit, named, persistent identity — created and removed only by an explicit creator-endpoint write, never implicitly, and the path carries the name, never the IP (addr/port are creation-time config on the transport-private conn_settings_t, reached through the transport’s own config door — ADR-0021 §Decision 3’s device-private facet). There is no reconfiguration door: the only accessor, transport_vertex_t::settings_of, hands out a const view (core/include/libtracer/transport_vertex.hpp:597), so moving a peer is retire (NAME) + re-create (SPEC), which un-routes the link and cascade-evicts the subscriptions routed through it. The link is the live socket underneath, managed automatically: self-healing (it re-dials on demand or after loss — the only “lazy” establishment; there is no lazy vertex creation) and refcount-gated (the socket closes when no binding is using a DIAL link, while the vertex stays — an idle declared connection costs a dormant vertex, not a socket/thread). A LISTEN link keeps its listen socket up while the vertex exists (reachability is its purpose). Once a peer attaches, a link is bidirectional; role is only who dials. Avoid: “transport config is a :settings field on some vertex” (a connection is its own / vertex, and the core :settings namespace is empty — a write to any flat knob name answers SCHEMA_NOT_FOUND, core/src/graph.cpp:3448); “the address lives in :settings” / “reconfigure a connection by writing its :settings” (creation-time and const — retire + re-create); “a connection’s params are /-path nodes” (the connection is the identity; its scalars are its creation-time config); “opening a link needs a transport-specific API” (it is a creator-endpoint SPEC write, like any creation — ADR-0059); “an idle connection is torn down” (the DIAL socket goes dormant; the vertex persists until an explicit removing write); “using an address opens the connection” (creation is always an explicit named write; the address never appears in the path); “a single global connection catalog” (the catalog is per-module).

Link state: The six values a connection vertex’s liveness takes — DORMANT, DIALING, RECONNECTING, UP, LISTENING, BIND_FAILED (link_state_t, core/include/libtracer/transport_vertex.hpp:105-112). A DIAL link uses the first four, a LISTEN link the last two. The value is await-able and subscribable like any other datum. The DIAL transitions are driven by the RFC-0014 §4 S5 liveness engine (tr::net::self_heal_link_t, core/include/libtracer/self_heal_link.hpp) for kinds registered with transport_kind_traits_t::self_heal_dial — dormant creation, demand dial bounded by connect_timeout, self-heal with backoff while a standing binding (acquire_link/release_link) holds, forever (no give-up bound). Everywhere else — provided links, LISTEN links, kinds not opted in (which today includes the built-ins) — the value is still set by the caller. Avoid: “the link state machine runs itself everywhere” (only engine-managed DIAL links; the built-ins are not yet opted in); naming a seventh state; using UP for a listener (a listener is LISTENING).

Peer / peer symmetry: A peer is anything that speaks the wire format: an MCU, a host process, a container, a browser tab. Peers are symmetric — the protocol defines no main node, hub, master, coordinator, or satellite, and nothing on the wire marks one peer as more central than another. The only asymmetries are per-operation and transient: who dialled (role, see §Transport vertex), who holds which ACL rights, and which peer happens to own the vertex being addressed. All three can differ per link and per request, so none of them promotes a peer to a tier. The transport does not change a peer’s standing either — the same graph forms over CAN, WebSocket, TCP or UDP, so “a board and a container” and “two containers” are the same topology to libtracer. A co-processor relationship (one MCU front-ending another on the same physical unit) is a property of a product’s board layout, not of the protocol: those two parts are still ordinary peers on their shared bus. Avoid: “satellite”, “main node”, “master/slave”, “the hub” (there is no tier — say peer, and name the property meant: the dialer, the ACL owner, the named listener); “this needs a second board” (a container is a peer — reach for the transport’s own loopback or a container peer before declaring a hardware block); “device A is the coordinator” (whoever holds WRITE_ACL may orchestrate, temporarily — see §Network formation).

Naming authority / minting boundary: A minting boundary is any point where a name enters the graph: a wire SPEC creation carrying a child name, a transport naming an accepted session, a module registration, the local string parser. The invariant (ADR-0073): a name that enters the graph must be expressible in the addressing grammar — enumerable implies addressable — enforced at every minting boundary through one shared predicate (the same segment-validity check the local parser applies), never per-site copies. Naming policy belongs to the application: libtracer never invents path text (no derived module names, no mandated /net — that is a documented recommendation), and the only library-minted name is the creatorless-session fallback p<slot> (a session identity, not a device identity — device-stable identity is a named link). A bus link’s own connection NAME is not a routable next-hop; only its peers’ names route. Avoid: “enumerable but not addressable” as an acceptable state (it is the defect class this rules out); “core derives the module name” (declared-only, by the app); “/net is the network root” as a protocol fact (convention only); validating names at one tier and trusting another (tier drift is the recurring defect).

Network formation / orchestrator (ephemeral admin peer): Forming a graph across nodes is done with ordinary vertex writes — there is no orchestration-specific protocol. An orchestrator (typically a web UI) is just a peer the owner granted WRITE_ACL, which joins temporarily, then on other devices creates controllers + transport connections (creator-endpoint SPEC writes, ADR-0059) and binds data flows (consumer-initiated writes into producers’ :subscribers[]), then departs. The intended end state is the patch-cable that outlives the hand — the wired devices still talking to each other — and the direction is ruled: a subscription’s target address is spelled in the producer’s frame, so the edge belongs to the delivery link rather than to the session that wrote it (RFC-0021). That is built: a wire SUBSCRIBER whose PATH target routes through a transport mount binds the edge to that mount’s link and the residual below it, so the orchestrator’s departure evicts nothing and B keeps delivering to A. A target that names no mount still binds to the arrival session, as before — RFC-0021 §4.B.2’s purely-local arm is unruled (§7 q3), and in-tree consumers still spell that PATH in their own frame. The end-to-end flow is reference 13. A declarative layer (a desired-state network manifest + a continuous reconciler) is tooling over this wire model — it diffs the manifest against live reads and converges by issuing exactly these create+bind writes; it adds no wire behavior. Avoid: “the orchestrator is a special role / needs its own protocol” (it is a peer with admin doing vertex writes); “a third party’s subscribe binds to the writer’s own session” (only when its target names no mount; a mount-routed target binds the delivery link — RFC-0021); “the orchestrator must proxy the data” (it must not — proxying is the browser-relay this model exists to retire); “the manifest/reconciler is part of the protocol” (it is tooling-domain).

Access control (ACL) / subject-token: Access is authorization — a device-held list of subject rights (read / write / subscribe / create / admin) on a vertex or field, enforced locally on each operation (PermissionDenied on failure). The subject is a token, and the token is pluggable — this separates authorization from identity-provenance. v1 uses the transport-authenticated origin_peer_id as the token (advisory on an unauthenticated bus, since the protocol does authorization, not authentication — identity authenticity is the transport / security module’s job). A security module may supply asymmetric credentials as a stronger token without changing the ACL model — the ratified form is raw-key ed25519 with trust-on-first-use pairing (the public key is the identity; certificate-authority X.509 PKI is rejected — ADR-0045), served as §Node identity. So ACL-lists and capabilities are not rival models — they are the same subject→rights authorization over a weaker vs. stronger token; the token exchange / key management is a separate concern. An owner peer is the provisioned root that bootstraps a device’s ACL and delegates admin to orchestrators (enabling third-party binding). Avoid: “capabilities vs ACL is an either/or”; “ACL authenticates” (it authorizes; the transport authenticates the token); “the subject is always a peer_id” (it is a pluggable token); “stronger identity means X.509 PKI” (rejected — raw-key ed25519 TOFU).

ACL entry (ACE, NFSv4-style) / inheritance: Each grant is an ACE (access control entry, NFSv4-style): {type: ALLOW|DENY, flags, subject-token, access_mask, expires_ns?}. The access_mask is a bitfield — READ/WRITE/SUBSCRIBE/CREATE (add child)/DELETE/READ_ACL/WRITE_ACL/WRITE_OWNER — so the admin right is precisely WRITE_ACL (may modify the ACL / delegate), distinct from acting and from WRITE_OWNER. ACEs ALLOW or DENY (ordered, first-match-per-bit), and the one special subject is EVERYONE@, which avoids enumeration. It is reserved in the otherwise opaque subject-token space, so a subject resolver may not return it and a caller that resolves to it is refused (#908). OWNER@ is not a special subject — earlier revisions of this glossary, reference-05 and ADR-0020 said it was, but no evaluator ever special-cased it, so an OWNER@ ACE matched nobody and locked the vertex it was meant to delegate (#1033). It is an ordinary opaque token; owner semantics would need a per-vertex owner identity the graph does not hold. Composite ACLs propagate by an INHERIT flag (NFSv4 inheritance, riding the graph/address composition): an ACE on a composite applies to its whole subtree — :acl is not written per-leaf, the way :subscribers[] is not. A vertex’s effective ACL = its own ACEs + inherited ancestor ACEs. The wire layout is the full model; the required-modules MCU profile enforces a subset (ALLOW-only, single INHERIT flag), with full DENY / ordered evaluation in the security_acl host module. Avoid: “admin is a vague catch-all” (it is WRITE_ACL); “ACL is per-vertex only” (composites inherit via INHERIT); “a grant is just a subject→permission bitfield” (it is an ACE with type/flags/mask); “MCU must implement DENY ordering” (the subset is ALLOW-only).

Per-subscriber delivery policy: A subscriber’s delivery QoS — carried in its SUBSCRIBER TLV’s SETTINGS child (so it is per-edge, not per-vertex like :settings) and enforced producer-side, before fan-out. Its concrete form is one packed u16 under the key delivery_policy (RFC-0022 §3.A): bits 0–1 reliability, 2–4 priority, 5 durability_request (deliver the producer’s latched last value on join), 6–15 reserved — written 0, ignored on read, never rejected. Absent ⇒ all-zero ⇒ the default behaviour. It is byte-agnostic only. No magnitude is packed into it — a bit-width on a magnitude is a synthetic limit (§No synthetic limits), so a deadline or a queue bound would arrive as a full-width field in the subscription’s cold half. Numeric or semantic filtering — deadband, tolerance, unit-aware gating — is not a libtracer concern; it is application logic, implemented as a schema-aware filter vertex (a reference 11 computed role) between producer and consumer. This keeps L4 dispatch from ever numerically interpreting opaque payload (load-bearing claim 5). Avoid: “deadband is a SUBSCRIBER or QoS field”; “L4 dispatch compares payload values” (it may compare bytes for on-change, never interpret them); “delivery policy is per-vertex”; “the vertex has a durability / reliability / priority knob” (all three are the subscription’s; a per-vertex value has no coherent meaning across a heterogeneous fan-out, which is why they were writable for a year and consumed by nothing); “a magnitude can be packed into the policy bits”.

Owner-side storage declaration: What RFC-0022 §3.B left of the vertex :settings core namespace: nothing. settings_t is deleted, so every flat knob name answers SCHEMA_NOT_FOUND on read and on write, caller-independently, and the bare :settings read is the container alone — SETTINGS{ [NAME "app" SETTINGS{…}] }, empty when no app fields are declared. The two knobs that still drive behaviour were never QoS: they are construction parameters, declared by the OWNER through the host API and carrying no wire surface at allset_history_depth (the STREAM ring trim depth, an application retention intent, re-read on every store) and set_pin_payload_ratio (the ADR-0042 §3 / RFC-0022 §3.D zero-copy store amplification ratio K — pin iff payload * K >= segment — a deployment copy/pin trade, read on every view-delivered write). Nothing is inherited (§3.F): a declaration reaches exactly the vertex it names — no ancestor walk, no subtree push, no cached ancestor reference, and no propagation question when a parent is reconfigured after its children exist. Both readers stay a single inline load off the vertex’s own extension block. Registration no longer carries a policy parameter, so it can no longer force that block: strictly more vertices stay extension-less than before. set_history_depth is declared on the vertex that holds the ring, which — since RFC-0025 §4.6.1 (Amendment 2) — is the receiving vertex of whichever party wants depth: a producer never queues, and the intent is bounded in bytes by that vertex’s own injected mem::block_source_t, never a shared pool. Avoid: “the vertex’s QoS block” / “the vertex’s storage policy” (there is no per-vertex policy object; there are two owner-declared magnitudes); “:settings resolves up the tree” and “a child inherits its parent’s storage policy” (nothing is inherited — §3.F); “history_keep_last / store_ref_min_bytes are :settings knobs” (both were withdrawn from the wire; they are host-API declarations); “deadline_ns / queue_max_bytes are knobs” (deleted as inert — a write answers SCHEMA_NOT_FOUND). Also avoid store_ref_min_bytes as the name of the owner-side declaration — that surface is set_pin_payload_ratio / pin_payload_ratio; the old name survives only as the withdrawn wire knob and as the refuted absolute-threshold predicate it once named.

Pin borrow (of the inbound RX segment): What a pin actually is. A pinned value borrows its inbound RX segment for its whole lifetime — not for the delivery window, but until the value is displaced by the next write or the vertex dies. On a pooled RX backend that borrow is a pool slot, i.e. receive capacity withheld from the transport; an exhausted RX pool has no reply channel, so it presents as a dropped datagram rather than as BACKPRESSURE. The library makes the deferred release safe (segment refcounts are atomic — a borrow outliving the receive frame is never a use-after-free); the application owns the budget, because only it knows its pool geometry and which of its vertices retain. Size against live pinned values × segment_bytes: K bounds the waste per value and never the number of values, so no K is a remedy for a retain-heavy workload (RFC-0022 §3.D/§6 + Amendment 2, measured at the ESP32-C6 RX geometry in bench/run_pin_net.sh). Class posture: NARROW sets the sentinel (never pin); MID/WIDE may borrow freely. The sentinel is the shipped default on both targets, so nothing borrows unless a deployment says so. Avoid: “pinning saves memory” / “zero-copy means free” (pinning always holds more than the copy, by exactly segment payload; it buys latency and pays in RAM — RFC-0022 §3.D); “the pin lasts for the receive callback” (it lasts for the stored value’s lifetime — the copy is the one bounded by the callback); “K bounds pool occupancy” (it bounds per-value waste only); “the library should cap the borrow” (bounding it is app-owned policy — the library’s obligation is safe deferred release); “pinning is a safety problem” (it is a capacity problem — reclamation schemes such as QSBR address a different axis, cf. the WIDE/NARROW capacity axis vs. the reclamation safety axis).

Lazy / on-demand source (subscriber-gated production): A vertex that produces only while it has subscribers — the RTSP / camera-stream pattern (subscribe to an “empty” vertex ⇒ frames begin flowing). Because subscribing is a field-write into :subscribers[] (load-bearing claim 2), a handler-role vertex observes its own subscriber-count edge: 0→1 activates the source (begin producing/publishing), 1→0 tears it down. The graph runtime’s only contribution is “a vertex may observe writes to its own control fields”; there is no separate subscribe/unsubscribe wire primitive. Avoid: “subscribing returns the data directly” (it registers an edge; the vertex reacting is what produces); “a dedicated on-subscribe wire hook” (it is a field-write the vertex observes); “the source always runs” (a lazy source is gated on subscriber count).

Array-whole read / atomic multi-field write (the LIST replacement): An array-whole read like read('/x:subscribers[]') returns a PL=1 reply whose children are the element TLVs (SUBSCRIBER 0x04 for subscribers). An atomic multi-field write is a SETTINGS (0x0B) TLV. Neither uses a generic container. Avoid: “returns a LIST”, “write a single LIST TLV”.

Element addressing ([] appends, [n] addresses) (RFC-0017): One array notation for the whole protocol: [n] selects the n-th child TLV of whatever it is applied to — the subscriber list, the child list, or a vertex’s own value — and the two operators have strictly separate jobs. [] appends exactly one element (drawn from the injected resource, so exhaustion is BACKPRESSURE); [n] addresses an element that already exists and never grows anything, which is why an out-of-range index is simply the absence of an element rather than a guard. An indexed write is payload-discriminating: an ordinary TLV replaces the element, the empty-STATUS sentinel clears it, [*] on a write is INVALID_PATH. Reaching a vertex’s value (rather than a :field) is spelled by a FIELD selector carrying only index/index_mode with no leading NAME — “no field name, therefore the value plane”. Indexing is structural, never temporal: it counts children of the stored value, never entries of a STREAM vertex’s history ring. And the index is a property of the event, not of the edge — a subscriber subscribes to the vertex as always, while a delivery mirrors the shape of the write that caused it, so an element write notifies with one element and a whole-value write notifies with the whole value. That is what makes per-element notification need no comparison: the writer names the element. Avoid: “[n] selects append-vs-overwrite” (storage semantics are the vertex’s role, set by the app at registration — an element write mutates content, never policy); “[n] reads the history ring” (the ring is drain-only; a consumer wanting a queue makes its own receiving vertex a STREAM — RFC-0025 §4.6.1: a producer never queues, and that ring is bounded in bytes by the receiving vertex’s own injected source); “subscribe to element n” (there is no per-edge index); “the node diffs the value to see what changed” (it never compares — see §SUBSCRIBER direction).

Addressed whole (a field with no member or slot surface): A :-field addressed as one unit: :acl, :subscribers and :children (all writable) and :schema (read-only — it has no write branch at all) admit no deeper selector, so :<field>.<anything> and (where the field is not an array) :<field>[N] name nothing and answer ERROR{tr::schema::not_found} — the depth gates are core/src/graph.cpp:3227 (:subscribers), :3347 (:acl), :3387 (:children) — one shared field_selector / whole_field classification since #869. The rule is enforced rather than conventional, and the failure it prevents is silent: without the gate a trailing step falls through to the branch’s action — :subscribers[0].liveness.last_seen_ns reaches the unconditional [N] clear and destroys the slot, :children[].bogus creates a child — and each answers success, byte-identical to a legitimate operation. Avoid: “an unknown trailing step is ignored” / “extra selector steps are harmless”; treating :subscribers[] (append) or :subscribers[N] (the unsubscribe, below) as counter-examples — those are the field’s own forms, one step deep, not member addressing.

:subscribers[N] is the unsubscribe: Writing to an indexed subscriber slot is payload-discriminating (RFC-0009 §D.1): an empty STATUS (09 00 00 00) clears the slot, a SUBSCRIBER replaces its edge through the same admission door as an append (so it passes the SUBSCRIBE gate), and anything else is TYPE_MISMATCH with the slot untouched. An index no slot answers to is INVALID_PATH — a wire-supplied index never grows the slot vector. [*] is a read/deferral selector only: on a write it is INVALID_PATH (core/src/graph.cpp:3281). Avoid: “write a SUBSCRIBER to :subscribers[N] to register/install/retarget record N” (it unsubscribes whoever is there and reports success); “the slot index is stable across a retarget”.

Index mode (SCALAR / ELEMENT / WILDCARD): The three forms a FIELD selector level can take, carried on the wire as an optional trailing 1-byte value (index_mode_t, core/src/op_resolve_walk.hpp:366): absent ⇒ SCALAR (:name, or :name[N] with an index), ELEMENT (:name[N] one slot, or :name[] append), WILDCARD (:name[*]). [] and [*] differ by one byte on the wire and greatly in effect, so tooling that reads only the index and not the mode renders three different operations identically.

WILDCARD is an encoding without semantics. The encoding is part of v1 and the resolver guards it: a [*] level on any field other than subscribers answers INVALID_PATH (core/src/op_resolve_walk.hpp:1081-1082), and a write bearing [*] answers INVALID_PATH before it can reach the [N] clear (core/src/graph.cpp:3281). Nothing downstream consumes the flag: the :children and :subscribers[N] read arms both require its absence (core/src/graph.cpp:4282, :4446) and a [*] read falls through to SCHEMA_NOT_FOUND. So [*] names a shape the wire can express and no operation performs. Avoid: naming the modes anything else; describing [*] as either fully working or purely a future encoding (the encoding is in v1, the behaviour is not); confusing it with a textual path wildcard, which does not exist — * may not appear in a NAME.

Fixed-stride array: An array-typed vertex field whose elements are all the same size, so :field[N] resolves by direct offset (base + N × stride, O(1)) on contiguous backing. Variable-size arrays — and arrays whose in-memory rope scatters elements across segments — resolve by walking children (O(n)). Array-ness is a schema (L4) property, never a wire bit; on the wire an array is just a PL=1 TLV with homogeneous children (ADR-0008). Avoid: “array type code”, “opt.ARRAY bit”, a wire-level array marker — none exist.

Address-shift slicing: The application-level replacement for wire-level fragmentation: a logically large payload is split across N child endpoints ep[0..N] sharing one timestamp; the receiver reassembles. A group is identified by (origin_peer_id, ts) — the same in-flight identity as the cycle-dedup recent-set — with each slice’s [index] giving its position. Totality is opt-in (expected_count or a :manifest): a dropped trailing slice is not guaranteed-detectable (ADR-0011), while a missing interior slice surfaces tr::flow::address_shift_gap (since RFC-0025 that code names any §Flow gap, not slicing alone). Avoid: grouping by ts alone (it collides across publishers); “fragmentation”, “FRAGMENT type code”.

origin_timestamp (per-producer monotonic) / coherent sampling: The ts half of the (origin_peer_id, ts) identity is a per-producer monotonic value (HLC-style), not literal wall-clock: strictly increasing per origin, never regressing or colliding (wall-clock-seeded where available, bumped logically on coarse/low-res clocks or NTP backward jumps). This is what makes (origin, ts) a collision-free identity for cycle-dedup and slice-grouping even when node clocks diverge. Wall-clock meaning is advisory (display / coarse correlation); cross-producer ordering is undefined by design (reference 04: no global clock / CRDT). A ts is optional on any TLV (opt.TS); its primary use is coherent sampling — endpoints stamped with the same (origin, ts) form one coherent sample-group/snapshot (the same group primitive as address-shift slices). Cross-producer coherence needs a coordinated trigger or external clock sync, never cross-origin ts comparison. Three clocks, three carriers (RFC-0025 §4.2.1, Amendment 1): WIRE/TX time is the trailer TS, stamped at interface transmit, on the outermost frame only, always TF=0; SAMPLE time is a payload TIME (0x0C) TLV inside the value; PLAYOUT time is on no wire at all — the receiver derives it from the RTT/offset it estimates off read/write carrier echoes. A batch carries one TIME{u64 base} child; a uniform stream derives per-sample time from its §4.3 descriptor’s dt_ns at 0 bytes/sample, a non-uniform one carries a packed i32 offset array. TF=1 is reserved grammar — decoded and recorded, relayed verbatim, declined as an echo root, not written. Avoid: “origin_timestamp is wall-clock”; “two nodes’ timestamps are comparable”; “per-producer ts can regress/collide”; “coherent sampling needs synced clocks” (within one producer it does not); “the trailer TS is the sample time” (it is wire/TX time — sample time is a payload TIME TLV); “the receiver reads playout time off the frame” (it derives it); “each sample frame carries its own trailer offset” (a uniform stream carries none at all).

Cycle termination: Both planes are loop-free by construction — no dedup state, hop counter, or depth cap exists anywhere. On the wire, the FWD plane (ADR-0040): a frame’s forward route (dst) strictly shrinks per hop, so a delivery travels exactly as far as its route names and no further — there is no visited-set and no revisit ERROR, and a dst that spells out a physical cycle simply routes around it as many times as the route names, then stops. In-process (RFC-0007 / ADR-0051): a write propagates exactly one hop plus upward bubbling because §SUBSCRIBER delivery terminates at the target — re-emission is the target’s logic — so a dispatch-level subscription cycle cannot form. Application-level feedback loops (controller ports wired in a ring) are the user’s design, surfaced by design-time analyzer/reconciler tooling, never policed by the runtime. Avoid: “the net plane needs a hop_count/dedup set” (explicit source routes cannot loop); “the in-process dispatch-depth cap (32)” (there is none, and nothing replaces it — cycles are impossible by construction); “the runtime detects/limits wiring loops” (analyzers do, at design time).

Wildcard delivery metadata: How a wildcard subscriber learns which concrete path produced each delivered TLV. Local delivery passes it out-of-band (implementation-defined); remote delivery carries the matched concrete PATH (0x06) on the wire (proposed under RFC-0003).

Framing modes: full-TLV (full caps) vs header-elided (non-interactive bindings): Two complementary on-wire framing modes, chosen per-transport (and mixable per-frame); the forwarder is uniform across both and never does an identity↔path lookup (“does not feel the difference” — load-bearing claim 4).

  • Full-TLV (“full caps”): self-describing frames carry the full PATH + control surface — enabling discovery, dynamic paths, in-band creation/ACL (the full feature set). Used on capable transports (IP/WS) where a 4-byte header is negligible, and for occasional control frames everywhere.

  • Header-elided (“non-interactive bindings” / transport-native addressing): the transport keys on its native frame identity (CAN ID, WS channel) via a dynamic identity↔path map held inside the transport (e.g. transport_can), self-establishing decentrally via in-band advertise frames (ADR-0030); the TLV header is synthesized on ingress / elided on egress, so it never hits the constrained bus (zero added overhead — existing CAN/WS frames unchanged). For high-rate data on constrained buses (e.g. 100 ksps over CAN).

They coexist (a) per-deployment — an elided CAN leaf joined to a full-TLV IP backbone, the transport adapter being the stateless translation point; and (b) per-transport — an occasional full-TLV control frame establishes the elided binding (“full caps” sets up “non-interactive bindings”), which is the discovery_static (pre-config) vs discovery_mdns (dynamic announce) split. Zero-copy for large elided payloads needs the rope-delivering transport seam (§Rope delivery); small samples cost a negligible ingress-synthesis copy. Avoid: “elided vs full-TLV is an either/or” (they coexist); “the forwarder maps CAN IDs” (the transport adapter does); “the TLV header rides the CAN bus” (synthesized host-side); “header elision makes the forwarder transport-aware” (the adapter uniforms first).

Advertise + id-match → dynamic rope groups: The advertise+id-match mechanism generalizes from a single-value binding (id path; lean frames are values) to a rope/group binding (group-id (path, slice structure)): a full-TLV advertise frame carries a runtime manifest (N slices, layout, total), and the lean id-matched slice frames that follow are chained into a rope by id+index at the reassembly layer. This is ADR-0011 address-shift slicing made dynamic — the advertise frame is the manifest the ADR otherwise carries as a static expected_count/:manifest. The same mechanism thus spans a 9-byte elided CAN sample → a GB advertised rope group. Zero-copy of the assembled rope requires the transport to deliver the group through the owning tier as a rope (§Rope delivery) — so advertise+id-match (graph protocol) and the rope seam (transport capability) compose; the flat-span seam alone forces a per-slice copy. Avoid: “advertise+id-match obviates the rope delivery seam” (it composes with it for zero-copy); “dynamic slicing is a different mechanism from elided binding” (same advertise+id-match, with a structure in the advertise).

Errors

tr:: error namespace: The identity space for protocol/stack errors — tr::<concept>::<error>, keyed by stable protocol concept (frame, tlv, path, schema, flow, access, transport, version), never by an implementation module. A concept is shared by every implementation in any language; a module name is not, and changes under refactor, so a frozen namespace cannot ride on one. Prefix-filterable like a path (tr::flow::*). Specified in RFC-0002 and ADR-0009. Avoid: a flat byte registry (0x01 NOT_FOUND, …); tr::<layer>::<module> (module-keyed); a 0x80–0xFF user-error range.

tr:: (two registers — error identities vs. C++ symbols): tr:: names two disjoint things that must never be conflated. (1) On the wire and in logs it is the error-identity namespace above, keyed by the eight protocol concepts. (2) In the C++ reference implementation it is the root namespace for code symbols, whose sub-namespaces mirror the layer modeltr::mem = L0 (core/include/libtracer/mem_heap.hpp:217), tr::view = L1 (core/include/libtracer/view.hpp:26), tr::wire = L2/L3 codec (core/include/libtracer/frame.hpp:24), tr::graph = L4 (core/include/libtracer/graph.hpp:53), tr::net = the transport plane (core/include/libtracer/transport.hpp:35) — never the concept words. The two never collide because error identities are concept-keyed string paths, never C++ symbols, and code sub-namespaces are layer-keyed, never concept-keyed. Seeing tr::frame::* ⇒ an error identity; seeing tr::mem::pool_t ⇒ a C++ symbol. A non-layer sub-namespace is allowed only for code that is not part of the layered stack at all: tr::detail (implementation internals) and tr::testing (the shared test harness in core/tests/test_support.hppcheck, check_quiet, summary, the collectors). Neither names an error concept, and nothing under core/src or core/include may name tr::testing. Avoid: a C++ sub-namespace named for an error concept (tr::frame, tr::flow as code) — that re-introduces exactly the concept-vs-module conflation the error model forbids; and a non-layer sub-namespace invented for library code, which is what the layer model exists to prevent.

Registered code / string identity: An error’s on-wire identity is either a compact registered code (a u16 the frozen registry assigns to a built-in tr::… path) or the literal string path (for unbounded third-party stack extensions). Optional structured detail may attach to either. The split is the built-in-vs-extensible split.

Severity / disposition: Per-error properties of the registry entry, never on the wire: severitywarn|error|critical; dispositiontransient (retry) | permanent (don’t retry this request) | fatal (tear down the peer). Derived at L4 on receipt.

Closed error boundary: Applications never emit a protocol error; there is no user error range. An application failure is ordinary data, self-described by the application’s schema — the same way the protocol defines no application data types (ADR-0010).

ERROR (0x08): The TLV that carries a tr:: error identity (registered code or string) plus optional detail. Always opt.PL=1; the first child is the identity — a VALUE for a registered code, a NAME for a string. Byte layout in RFC-0002 §C. The rejected alternative is “code as a leading child VALUE” (RFC-0001 §C.1, withdrawn).

Flow gap (tr::flow::address_shift_gap — a discontinuity in an ordered flow): The one gap concept and the one code (0x0042, err_t::FLOW_ADDRESS_SHIFT_GAP) for it: a receiver-visible signal that elements which should have arrived in order did not. Generalized by RFC-0025 §4.5 from its address-shift origin to two ruled contexts — a missing interior slice of an §Address-shift slicing group (the original ADR-0011 meaning, unchanged), and a ring-overflow shed under RFC-0025’s best-effort pressure contract (drop-oldest with the gap signal). One code means one receiver-side gap-handling path; the per-context classification (severity/disposition) is documented at the registry entry (core/include/libtracer/error.hpp), never re-derived per site. Every gap counts into delivery accounting — a shed with no signal and no accounting is non-conforming. Avoid: a new gap/loss code per producer of gaps (one concept, reused); “silent overwrite” / “silent drop-oldest” (loss must surface and be accounted); reading “address-shift” in the identity as scoping it to slicing (the name is historical; the concept is the ordered-flow discontinuity); confusing it with tr::flow::backpressure (the reliable-side answer at the producer — a gap is the best-effort-side answer at the receiver).

tr::version::mismatch: A discovery/link-level error — “peer advertised an incompatible protocol version”. Not a frame-parse outcome, because there is no per-frame version field to read. It replaces a byte code (VERSION_MISMATCH 0x06) in a flat registry. Avoid: “opt.VR set higher than receiver supports”; the 0x06 byte code as an identity.

Modules & memory substrate

Required modules: The modules every conforming node links (frame codec, path resolver, view/refcount machinery, FWD forwarder/dispatcher when ≥2 transports) — equivalently conformance profile P0. They are not architecturally privileged. Avoid: “Core” as a noun for a fixed privileged build (the core/ directory and “core type codes 0x01–0x1F” are unaffected).

io_dir_t: The L0 backend cache-coherency hook direction enum (enum class, SCREAMING_SNAKE scoped enumerators, core/include/libtracer/backend.hpp:40): io_dir_t::DEVICE_TO_CPU (DMA-in / RX ⇒ invalidate cache before the CPU reads HW-written bytes) and io_dir_t::CPU_TO_DEVICE (DMA-out / TX ⇒ clean cache so HW reads the CPU’s last writes). Consumed by the two mem_backend_t cache hooks before_io (prep the cache for the device, pre-transfer) and after_io (reconcile the cache for the next reader, post-transfer; core/include/libtracer/backend.hpp:150,158); the method carries timing, the enum carries direction, the backend maps the pair to clean/invalidate. No-ops on cacheless cores (Cortex-M0/M3/M4). Avoid: the IO_DIR_READ/IO_DIR_WRITE spelling, the unscoped IO_DIR_DEVICE_TO_CPU form, and any other integer-value set — one canonical spelling only.

Memory-binding spectrum / transparent byte router: The L0/L1 substrate is a modular binding layer: an endpoint’s bytes may be bound as a heap snapshot, a shadow vertex, or a live/raw view (MMIO register, program variable — no copy, no CRC, lock-free). In the live case libtracer is a transparent byte router — it imposes no snapshot/copy/CRC. Each backend module (mem_backend_t, core/include/libtracer/backend.hpp:106) owns and declares its per-architecture contract: allocation, cache hooks, ISR-safety, atomicity granularity, memory ordering, destroy thread-affinity. Safety (snapshot/shadow) is recommended, never mandated (ADR-0012). Avoid: “the protocol forbids live/raw memory binding”, “endpoints must snapshot/copy”.

Module ABI: The contracts between modules — implementation-defined by design, not a protocol property. In the reference implementation the mechanism is decided per seam by the ADR-0047 appropriateness rule: the L0 backend seam is a compile-time contract (a concept shape plus constexpr traits, dispatched through the target’s §Module set — superseding the runtime-vtable spelling of ADR-0016 §3), while the net plane’s transport_t keeps a runtime virtual surface (wiring-frequency calls; kinds arrive as data). Two conforming nodes interoperate over the wire, never via a shared ABI (ADR-0013). Avoid: “the protocol defines the module ABI”, “modules are binary-portable across implementations”, “the L0 seam is a C vtable” (superseded — concept + tag-dispatched module set), “all seams must be compile-time” (the appropriateness rule decides per seam).

Module set (build-time-closed): The per-target, build-time-closed set of module types at a pluggable seam, realized two ways per the ADR-0047 appropriateness rule: where identity is per-target-fixed and the path is hot or size-critical (the L0 backend set), the set is a compile-time type list with tag dispatch (single-member sets fold to direct calls; multi-member sets keep heterogeneous coexistence, e.g. a mixed heap+GPU rope); where dispatch runs at wiring frequency or is keyed by runtime data (the net plane’s transport set), the set closes at link time — the sources a target compiles and the factories it registers — with ordinary runtime dispatch inside. Adding a platform module means appending to the target’s set, never editing core. Types close at build time; instances stay runtime: a connection vertex is created by an in-band creator-endpoint SPEC write naming its kind as data (§Transport vertex). Avoid: “registry” for this concept (the wire type-code registry and the error registry are unrelated); “catalog” (the device’s controller-type catalog is runtime-queryable, per-device); “manifest” (the network manifest and the address-shift :manifest are unrelated); “closing the type set makes connections static” (instances are runtime); “everything compile-time” / “everything vtable” (the appropriateness rule decides per seam).

Resource bound (no synthetic limits): Every limit on runtime or protocol behavior is either a real injected resource (pool/arena/queue capacity, whose exhaustion surfaces as backpressure or a resource-reject error) or per-target / per-connection configuration — never a hardcoded magic constant. Ratified instances: TLV nesting depth is receiver-resource-bounded (RFC-0006 — there is no fixed 32; tr::tlv::nesting_too_deep means “exceeds this receiver’s decode resources”, core/include/libtracer/grammar.hpp:371-376); the in-process dispatch-depth cap does not exist and nothing replaces it (§Cycle termination, RFC-0007 / ADR-0051); transport max-frame is a per-connection :settings value. Protocol-defined TLV shapes nest ≤ 5 by construction, so conformance needs no numeric floor. Named ratified exception — the addressing bounds (NAME ≤ 64 B, PATH body ≤ 1024 B, segment count ≤ 255, field depth ≤ 8; core/include/libtracer/path.hpp:32-38): these are wire-grammar constants, identical on every peer, not resource limits. Making them per-target is foreclosed by name (docs/design/config/00-configuration-space.md §”What is deliberately not configurable” — “an interoperability failure dressed as a RAM saving”), and a bound both peers can size a buffer from is what interop needs (RFC-0023 §2.3, §9 — which also priced the 255 rather than inheriting it). Tuning knobs whose overflow changes cost, not behavior (the rope’s inline link count, the walk stack’s inline slots), are optimizations, not limits. Design hygiene (wiring loops, dead chains) is analyzer/reconciler tooling, never runtime enforcement. Avoid: “nesting depth cap 32”; “dispatch depth cap”; “a hardcoded max frame size” (per-connection :settings); “the runtime protects users from bad designs” (analyzers do, at design time); “conformance minimum depth” (protocol shapes are bounded by construction; user-data depth is a capability like frame size).

Block source / failable allocation: The second L0 allocation seam (tr::mem::block_source_t in the reference implementation), beside the byte-buffer backend. It vends raw, single-owner blocks — no refcount, no header — and reports exhaustion by value: the allocate call returns null, and the operation answers whatever reject fits it (the terminus decode answers tr::tlv::nesting_too_deep, core/include/libtracer/grammar.hpp:371-376; the branch-write decode answers tr::schema::type_mismatch, core/src/graph.cpp:2341, which does not distinguish exhaustion from a malformed value). It exists because std::pmr::memory_resource structurally cannot report failure by value, and on a -fno-exceptions target a failed allocate reaches abort(). Any allocation a peer can provoke therefore draws from a block source rather than a bare memory_resource (ADR-0065). Use the backend (mem_backend_t) for payload bytes many views share, which need the refcounted segment; use the block source for anything a peer can make the node allocate. A node may point both at the same store (“one slab, whole stack”) or split them. Avoid: calling this the “control-plane allocation seam” — control plane is already bound above to the : field-write addressing plane, and the two axes are orthogonal (a data-plane branch write draws from the block source). Say failable allocation, or “the allocations a peer can provoke”. Also avoid: “the memory resource” / “the allocator” as if there were one seam; “a block source hands out segments” (it hands out bytes — segments are the backend’s); “exhaustion throws” (the point of the seam is that it does not — see §Resource bound).

Store composition (folded / per-plane / per-thread) (ADR-0079, Amendment 2026-08-20): How many block sources a node wires, and which seams point at which one. Three named points: folded — every seam shares one source (“one slab, whole stack”), one cap, tightest RAM, contention-free only because the target has one thread; per-plane — one source for the graph plane and one for the net plane (segments always separate, §Block source), whose single claim is the blast radius (a peer-provoked flood is fenced in the net-plane store and the graph runs on); per-thread — one source per RX thread, the only point that survives a fan-out. No composition is the default: every injection seam defaults to the process heap, so an un-wired build is all-heap, and composition is knobs varied per target — per-thread on a multi-RX host, folded on a single-threaded MCU, per-plane when the fence is what is being bought. It is chosen by injection at build time, never by a core edit, and it is what makes §Resource bound’s “a bound is an injected resource” deliverable. Avoid: “NARROW / MID / WIDE composition” — retired by ADR-0079’s 2026-08-20 amendment, because it collided with the canonical target spectrum (where NARROW = a constrained MCU node and WIDE = a big many-core host) and collided with it inverted: the old composition-WIDE was the MCU recipe and composition-NARROW the many-core-host one. NARROW/WIDE now describe targets only. Also avoid: “per-plane is the default” (withdrawn — nothing is); “per-plane avoids contention” (measured to collapse identically to folded under fan-out — it buys isolation, not scaling); “composition bounds the node” (the store’s size does; composition decides what shares a cap with what); “fold the segment backend in” (need C stays separate — §Block source).

Reclamation domain (hazard domain): The replaced-block problem: a heap block a control-plane writer replaces while a lock-free reader may still hold a raw reference into it. The one generalized domain that was to answer it everywhere — readers announce before dereferencing and clear after, a writer retires, the domain frees once no announcement pins the block — is not built and is refuted: the announce/clear pair costs the read path double-digit percent, which is a REJECT, and the design carried blocking defects a reproducer catches (ADR-0072 §Supersession carries the rounds and the numbers). What the repo has instead is per-tenant, and deliberately so: the value seam gets the explicit collector (§Seam park); the mount_tlv rebind gets immutability by construction (the replaced bytes are a pure function of the slot key, so nothing is replaced); the LKV slot keeps its own private hazard policy (ADR-0069). The published edge array — the subscriber array a fan_out reader holds across dispatch — has no answer: it is the open question, and the collector is not it (see §Seam park). Avoid: “the reclamation domain” as a thing that exists (it does not — the ADR is superseded); “hazard pointers” as this codebase’s general answer (the one hazard domain is lkv_slot.hpp’s, private to the LKV slot); “epoch reclamation” for this problem (considered and not chosen — ADR-0069 §2, ADR-0072 §1).

Seam park / collect: What retirement does with a vertex’s value seam and how it ends. The population is keyed on handler presence, never role: adopt_identity allocates the seam iff on_read, on_write or on_children was installed at registration, so a STORED_VALUE vertex with an on_children parks one and a HANDLER vertex with an empty handlers_t parks none — the production /net/<module>/<name> identity vertex is exactly the first shape. Its teardown parks a seam only when the link exposes a bus facet (link->bus() != nullptr — CAN, or a tcp/ws server wired peer_named = true); a point-to-point connection parks nothing. retire() detaches the seam and parks it on the graph — it cannot free it, because the seam is read lock-free and a reader may still hold the raw pointer. graph_t::collect() is the other end: a public, explicit collector the embedder calls, which swaps the park into a local under the map lock and lets it destruct after the lock is released — so the free runs on the caller’s thread, outside every graph lock, and a seam callback’s destructor may re-enter the graph. graph_t::parked_seam_count() makes an uncollected park observable rather than silent. The contract — call it where no lock-free reader holds one — is what the embedder pays for the free being free of hot-path cost; it is satisfiable here because connection teardown is rare and embedder-controlled, and it is not satisfiable for the subscriber edge array a fan_out reader holds across dispatch. Avoid: “park it until teardown” (the park has an explicit end now; teardown is a growth backstop only — retired_seams_ is declared before map_mutex_ and root_, so it destructs LAST and a seam whose destructor re-enters the graph re-enters a half-destroyed object, which is why such an owner must NOT be left to it); “the park is the HANDLER-role vertices” (it is the handler-BEARING ones, of any role); “collect() is reclamation” (it neither waits for nor detects readers — it is a free at a moment the embedder names); “collect() is the pattern for the subscriber edge array” (it is not — that tenant’s readers hold a block across dispatch).

Segment / view: A view is a {owner, offset, length} window over a refcounted segment of backing memory (tr::view::segment_t, core/include/libtracer/segment.hpp:78). Distinct from a NAME segment — a single /-separated path component. Since RFC-0018 a NAME segment is encoded on the wire as one segment record[u8 len][len bytes of UTF-8], len in 1..64, with no per-segment type or option byte — not as a NAME TLV (0x02). The record IS the encoding; a PATH body is a self-delimiting run of them and is byte-identical to the vertex-map key. len == 0 is the escape record 00 <u8 kind> <u8 len> <bytes>: admissible in a frame path (a hop that does not implement kind steps over it by length rather than dropping a frame it is relaying) and rejected in canonical / key context, because a label is not canonical bytes. kind = 0x16 is reserved for RFC-0027’s label element; nothing mints one today. Both record words are encoding-layer: the model-layer unit above them is the §Path element, whose NAME kind a segment record spells and whose LABEL kind an escape record spells. Avoid: using bare “segment” for a path component; prefer “NAME segment” there and “view” for the L1 window. Also avoid: “a PATH’s NAME children” / “each PATH child is a NAME” (retired with RFC-0018 — a packed record has no type byte, which is exactly why an address now has one spelling); “the escape is rejected everywhere” (a frame path admits it); “NAME (0x02) was retired” (it survives for SETTINGS keys, :schema labels and :children[] members — RFC-0018 removed it from PATH bodies only).

Rope delivery / owning receiver: A transport’s owning delivery tier hands the graph a rope — a chain of refcounted views over the transport’s receive segments — with a contiguous frame being the trivial single-link case. This generalizes (does not sit beside) the view-delivery seam of ADR-0042: “delivers views” and “delivers ropes” are one capability, not two tiers. Borrowed span delivery remains the separate non-owning tier. A scattered frame (CAN reassembly group, fragmented WS message) crosses the seam as the rope it already is — trimming transport padding is shortening the last link, never flattening. Avoid: “rope delivery is a third receiver tier beside span and view” (it is the owning tier, generalized); “delivering a scattered frame requires flatten/copy at ingress” (the rope crosses as-is; decode is rope-aware per §Two compositions); “a single-link delivery allocates a chain” (the single-link case is the hot path).

Rope / assembly (reassembly): A rope is an ordered chain of views over (possibly different) segments — the L1 representation of a logically-contiguous payload that physically lives in scattered backing memory. Assembly and reassembly mean constructing a rope by chaining views — pointer-linking, zero-copy, never memcpy. The rope is also the transport-agnostic scatter-gather representation: each transport lowers it to its native DMA (iovec/sendmsg, CAN descriptor chains, RDMA verbs). A contiguous copy occurs at exactly one place — a substrate boundary a transport’s DMA cannot span (e.g. lwIP pbuf → CAN region), flattened by the transport at egress, never per-fanout and never as “reassembly”. Avoid: “reassemble = copy the slices into a contiguous buffer”; “the substrate chooses the DMA” (the transport does); calling a per-fanout or per-hop copy “reassembly”.

Two compositions (memory vs TLV): The same bytes belong to two orthogonal composite trees, and conflating them is a category error. (1) Memory composition (L1) composes storage — where bytes physically live: the leaf is a view (one window over one segment), the composite is a rope (a chain of views across segments). (2) TLV composition (L3) composes meaning — what bytes are: the leaf is an opaque TLV (opt.PL=0), the composite is a structured TLV (opt.PL=1) whose type code says what the children mean. Both are the Composite pattern, over different axes; they are decoupled, and that decoupling is the zero-copy story. Consequences: a rope is not a TLV list (storage vs meaning); a view boundary may fall anywhere, including mid-TLV-header (hence rope-aware / link-walking decode); and a FWD uses a rope (the route and payload bytes it carries onward) but is not one (it composes meaning around them). Avoid: “a rope is a list of TLVs”; “a memory split must align to a TLV boundary”; “the router is a rope” / “rope inherits TLV”.

Terms that are commonly confused

Each row names the canonical term and the near-miss most often used for it.

  • “version” — two axes, one word. Protocol version is the integer wire-format version (v1), conformance-bearing and carried by the discovery layer; release version is an implementation’s semantic version, arbitrary with respect to the wire. Say which axis. “v0.1 is the wire format” is a category error; “protocol v1 is the wire format” is the claim meant.

  • “segment” — the most overloaded word in the vocabulary, with three unrelated senses. A segment is the refcounted block of backing memory a view windows (L1). A NAME segment is one /-separated path component, encoded as one packed segment record [u8 len][utf8] (RFC-0018), not as a NAME TLV. A route segment is a segment record a transport vertex strips as it forwards; a hop strips the whole mount run of them, not one (§Path-as-route). Never write bare “segment” for either of the last two. All three senses are unaffected by the §Path element layer above them: an element is what an address is made of, a segment record is one way of spelling one.

  • “LIST” — there is no LIST type and no 0x05. Nesting is opt.PL=1 plus a purpose type byte; an array-whole read concatenates element TLVs; an atomic multi-field write is a SETTINGS.

  • “Core” — not a privileged unit and not a build. It means the required modules (conformance profile P0). The core/ directory and the “core type codes 0x01–0x1F” are different things that share the word.

  • “error identity” — the canonical form is the concept-keyed namespace tr::<concept>::<error>, carried on the wire as a registered u16 code or as the literal string path. The near-misses are a flat byte registry (0x01 NOT_FOUND, …), which cannot be prefix-filtered and cannot be extended by a third party, and a module-keyed tr::<layer>::<module>, which binds the namespace to one implementation’s file layout.

  • tr:: — two disjoint registers. Concept-keyed (tr::flow::backpressure) ⇒ an error identity on the wire. Layer-keyed (tr::mem, tr::view, tr::wire, tr::graph, tr::net) ⇒ a C++ namespace in the reference implementation, plus the two non-layer ones, tr::detail and the tests-only tr::testing. A C++ namespace named for an error concept collapses the two.

  • io_dir_tio_dir_t::DEVICE_TO_CPU / io_dir_t::CPU_TO_DEVICE, scoped, one spelling. Not IO_DIR_READ/IO_DIR_WRITE, and not the unscoped form.

  • “array indexing” — array-ness is an L4 schema property (fixed-stride ⇒ O(1) on contiguous backing), never a wire bit. There is no array type code and no opt array flag.

  • “registry” — three unrelated things carry the word: the wire type-code registry, the error registry (codes, severity, disposition), and — wrongly — the build-time module set, which is neither. Say module set.

  • “control plane” — bound to the : field-write addressing plane and nothing else. The failable-allocation seam is the block source, not “the control-plane allocator”; a data-plane branch write draws from it.

  • “creator endpoint” — the per-module /net/<module>/conn surface, one catalog per (transport, role) module. Not a single global creator path under /net, and not a :children[] creation field, which is the superseded spelling.