Reference 11 — Vertex roles and aggregation

Topic: the kinds of thing a path can name, and the rules for fronting several physical sources or sinks behind one logical address. This generalizes the rule that a vertex is unspecified beyond its API contract (02-graph-model.md §the graph imposes no shape) into a taxonomy of implementation patterns plus the addressing patterns that compose them. Audience: anyone designing a non-trivial graph topology; anyone deciding whether a shared canvas is streamed bytes, a state mirror, or both at once; anyone implementing a forwarder that joins several transports behind one path. Prerequisites: 02-graph-model.md, 03-addressing.md, 04-communication-flows.md, 07-host-embedding.md.


The vertex-facade principle

A path names a contract, not an implementation. Two vertices that satisfy the same read/write/subscribe contract are interchangeable from the API side, regardless of what backs them.

The protocol’s read/write/subscribe primitives (04-communication-flows.md) operate against three observable behaviors:

Operation

What the caller observes

tracer_read(path) → TLV

Some bytes the vertex produces now

tracer_write(path, TLV)

Bytes the vertex accepts now; effect is vertex-defined

tracer_await(path) / SUBSCRIBER

A stream of TLVs the vertex emits over time

What lives behind the path is deliberately unconstrained. It can be a stored byte buffer, a stream of arrivals, a state-machine fed by writes, an MMIO snapshot, the output of a transform, or an aggregate of several remote vertices. Subscribers do not know which, and almost never need to.

This is the facade principle. The :schema field (02-graph-model.md §schema and field discipline) is the only mechanism the protocol gives a subscriber to interrogate the contract — and it never reveals the implementation.

        flowchart LR
    H["path handle<br/>(canonical PATH TLV bytes)"]
    API[/"read · write · await"/]
    R{{"vertex role<br/>(stored / stream / sink+model /<br/>computed / proxy / aggregate / live MMIO)"}}
    BACK1[("RAM segment")]
    BACK2[("MMIO register")]
    BACK3[("function-on-read")]
    BACK4[("remote vertex<br/>via source route")]
    H --> API
    API --> R
    R --> BACK1
    R --> BACK2
    R --> BACK3
    R --> BACK4
    style H fill:#dcfce7,stroke:#166534
    style API fill:#dbeafe,stroke:#1e40af
    style R fill:#fef3c7,stroke:#92400e
    

A consumer holds a path handle and calls read / write / await. What sits behind the role boundary is invisible. Two vertices with the same schema are observationally equivalent regardless of role.

The rest of this document is the catalog of implementation kinds and the addressing patterns that compose them.


Vertex roles (kinds of things a path resolves to)

These are not protocol-level type codes; they are implementation patterns at L4. A given vertex has exactly one role at any moment, decided when the vertex is registered. Application code declares the role via the vertex-creation API of its libtracer implementation; the protocol does not surface the role to peers.

1. Stored value (last-write-wins)

The simplest vertex. A single TLV is held; writes replace it; reads return it; subscribers receive every replacement.

  • Behavior: read → most recent write. subscribe → every future write.

  • State size: one TLV (typically a few bytes to a few KiB).

  • Used for: configuration values, sensor readings, the shared-variable pattern of 06-user-data-packing.md §shared-variable pattern.

2. Stream (append-only, no replacement)

Writes do not replace prior values; they append to a stream. Reads return the most recent value (or a window); subscribers receive the sequence.

  • Behavior: read → most recent OR a windowed list (read("/x[..]")). write → append, no displacement. subscribe → every append.

  • State size: bounded in bytes by this vertex’s own injected mem::block_source_t — never a magic number and never a pool shared with another vertex or another plane (ADR-0079 Amendment). The retention intent on top of that bound is set owner-side by the host call graph_t::set_history_depth(v, keep) and has no wire surface — it is not a :settings knob (02-graph-model.md §storage is declared owner-side, and nothing is inherited); the protocol fixes no depth. A shortfall — an intent the source cannot fund — surfaces as a shed-with-gap or as backpressure, never as a silent shrink.

  • Who makes a vertex a STREAM: the party that wants a queue, on its own receiving vertex (RFC-0025 §4.6.1, Amendment 2). A producer never queues — its writes are always the lock-free path, on every delivery class and at every depth. A subscriber wanting history subscribes as always and makes its target vertex a STREAM; nothing is requested from the producer and no depth crosses the wire. The producer’s contribution is knowing the consumer’s sampling rate and shaping its traffic to it — for which batch folding is the preferred carrier (32.0x per-sample amortization; with a uniform stream descriptor, per-sample time costs 0 bytes).

  • Used for: log streams, sensor streams where each sample matters, address-shift slicing (each [N] slot is itself a stored value but the parent path acts as a stream).

3. Sink with internal model (mirror / state-machine)

This is Mode B of the canvas example below. Writes are commands that mutate an internal model held by the vertex. Reads return the materialized current state of the model.

  • Behavior: write({command: ...}) mutates internal state. read → serialized current state. subscribe → either the stream of mutations OR the materialized state on each change, configurable.

  • State size: arbitrary; bounded by the model.

  • Used for: collaborative documents, mutable objects, drawing canvases, anything CRDT-shaped.

The crucial property: a Mode-B canvas presents the same path as a Mode-A canvas. A consumer that reads the canvas image gets bytes either way.

4. Computed / derived (function-on-read)

Reads invoke a function; the returned TLV is computed from upstream state, not stored.

  • Behavior: read → call the registered function, return its result. write → optional; either rejected or treated as input to the function. subscribe → re-fire the function on every upstream change (typically registered via internal subscriptions to the upstream paths).

  • State size: zero (or small cache).

  • Used for: averages, filters, projections, joins of two sensor streams, FizzBuzz-style transforms.

5. Proxy (alias to another vertex)

The path is a redirect; reads/writes pass through to a target path that can be local or remote.

  • Behavior: every operation is forwarded to target_path. Schema is the target’s schema. Subscriptions register against the target.

  • State size: zero (a forwarding rule).

  • Used for: route proxies (the canonical form, see 07-host-embedding.md); per-host aliases; renaming for ergonomics.

6. Aggregate (fan-in or fan-out front)

The path fronts multiple physical sources or sinks. A read aggregates; a write fans out; a subscription merges streams.

  • Behavior: read → produces a TLV that combines the constituents (concatenation, last-of-each, sum, …). write → fanned to all sinks. subscribe → merged stream.

  • State size: configuration only; backed by the constituents.

  • Used for: a “global log” that merges per-peer logs, a “broadcast actuator” that writes to several physical motors at once, a multi-camera canvas where layers come from different hosts.

7. Live MMIO / register reflector

A view onto live memory whose value changes asynchronously to libtracer. Reads snapshot the current bytes; writes (if accepted) poke the memory.

  • Behavior: read → snapshot now (per the resolved memory-binding contract — 08-views-and-ownership.md §MMIO register-as-view and ADR-0012: snapshot is the recommended-safe default, a live binding is supported). write → poke. subscribe → polled at the interval the owning module was configured with host-side; there is no :settings knob for it — the core vertex SETTINGS namespace is empty (05-protocol-tlvs.md §0x0B).

  • State size: zero (the bytes live in MMIO).

  • Used for: GPIO registers, peripheral status, anything memory-mapped.

Role table summary

Role

read

write

subscribe

State

Stored value

most recent

replaces

future replacements

1 TLV

Stream

recent / window

appends

every append

N TLVs

Sink with model

materialized state

mutates model

materialized OR mutation stream

model

Computed

call function

input to function

re-fires on upstream

cache

Proxy

forwards

forwards

forwards

rule

Aggregate

combines

fans out

merges

config

Live MMIO

snapshot

pokes

polls

none

The protocol surfaces none of these to the wire. The schema may describe the contract (for example, “writes are JSON-encoded {op, args} mutation commands”) but cannot promise an implementation.


The canvas, worked through both ways

A canvas vertex at /canvas holds a 1024×1024×4-byte image as its observable state.

Mode A — Transferred (canvas as stored value or stream)

Publisher (one host owns the canvas, paints into it):
    write("/canvas:image", VALUE{bytes = 4 MiB raster})

Subscribers:
    read("/canvas:image")        → returns the 4 MiB raster
    subscribe("/canvas:image")   → receives every full-canvas write

Implementation: a stored-value vertex at /canvas:image. Each repaint is a 4 MiB write. Bandwidth-heavy; the simplest possible model.

For a 4 MiB canvas at 30 Hz, this is 120 MB/s — reasonable on a LAN, painful on a constrained transport. The raster-as-single-write does not have to hit the wire as 4 MiB on every change; address-shift slicing (03-addressing.md §address-shift slicing) lets the publisher write /canvas:image[0..N] and let subscribers reassemble. Still bandwidth-bound by the changed bytes, plus retransmission if any.

Mode B — Mirror (canvas as sink with model)

Publishers issue mutation commands:
    write("/canvas:ops", VALUE{op = "draw_line", from = ..., to = ...})
    write("/canvas:ops", VALUE{op = "fill_rect", rect = ..., color = ...})
    write("/canvas:ops", VALUE{op = "set_pixel", xy = ..., color = ...})

Subscribers can do either:
    A. read("/canvas:image")     → vertex serializes its model and returns the raster
    B. subscribe("/canvas:ops")  → subscriber maintains its own model and applies ops

Implementation: a sink-with-model vertex at /canvas. Internally, it owns the raster buffer and a renderer. Each write to :ops calls the renderer to update the buffer. Each read of :image serializes the buffer and returns it.

For a 4 MiB canvas updated at fine grain, this is a few hundred bytes per :ops write — the bandwidth scales with operations, not with canvas size. A one-pixel change costs ~30 bytes on the wire instead of 4 MiB.

Interchangeability of the two modes

read("/canvas:image")  → 4 MiB raster bytes

This call works identically in Mode A and Mode B. From the consumer’s API view, /canvas:image is just a path that returns a 4 MiB raster. The consumer:

  • Does not know whether the bytes are stored as the most recent write to :image or computed by serializing an internal model.

  • Cannot distinguish from inspection of the schema, unless the vertex chooses to expose its :ops field as a separate writable entry.

  • Can switch between Mode A and Mode B without code changes — the path API is identical.

The vertex implementor chose the trade-off:

  • Mode A: simpler implementation, lossless, large bandwidth.

  • Mode B: smaller bandwidth (per-op), requires renderer logic in the vertex, model can drift if any subscriber missed an op — repaired by a history window at least as long as the reconnect deadline, or by snapshot-and-resume on resubscribe.

The protocol’s contribution: the addressing scheme makes the choice invisible.

Hybrid — Mode B with periodic snapshot

A common production pattern: Mode B for live updates, plus a periodic Mode-A-style full-canvas write under :image for resync. Late joiners read :image once, then subscribe to :ops for the live tail.

Vertex setup:
    /canvas:ops      sink-with-model, accepts mutation commands
    /canvas:image    derived from the model; snapshot every N seconds
    /canvas:cursor   the last :ops index applied to the latest snapshot
                     (so subscribers can synthesize a join point on resume)

Late-joining subscriber:
    snap   = read("/canvas:image")
    cursor = read("/canvas:cursor")
    apply(snap)
    subscribe("/canvas:ops")
    ← ops at or below `cursor` are folded into `snap`, so the subscriber
      discards them. The join point is application logic, not a subscribe
      parameter — the protocol has no replay-from-index surface.

This is the same pattern Kafka, distributed log systems, and CRDT replicas use. The protocol does not impose it; the application composes it from the read/write/subscribe primitives plus the schema’s choice of fields.


Address grouping (one path, many things behind it)

The aggregate role above is one form of grouping. There are several structural patterns; this section names them.

Multi-source fan-in via subtree subscription

A fan-in vertex /log/global subscribes at the peers' common ancestor:

write("/peer:subscribers[]",
      SUBSCRIBER{ target = "/log/global" })

Every subscription is a subtree subscription (RFC-0005), so the edge at /peer observes every write to any /peer/{id}/log — and anything else under /peer. The consumer filters if the subtree carries more than logs; selecting only sibling-pattern paths like /peer/*/log would need the unratified per-segment wildcard grammar (03-addressing.md §per-segment wildcards (unratified)). /log/global becomes the place where every host’s log lands as if they wrote there directly.

This is not a special primitive — it falls out of subtree subscription plus the SUBSCRIBER target field. The aggregate-role vertex is just any vertex a subtree subscription pulls into.

Multi-sink fan-out

A vertex with multiple subscribers fans every write to each subscriber. This is the default behavior of 04-communication-flows.md §write (publish + fanout). Address grouping uses this directly:

Configure /actuator/all to fan out to specific physical actuators:

write("/actuator/all:subscribers[]", SUBSCRIBER{target = "/actuator/motor[0]"})
write("/actuator/all:subscribers[]", SUBSCRIBER{target = "/actuator/motor[1]"})
write("/actuator/all:subscribers[]", SUBSCRIBER{target = "/actuator/motor[2]"})

A single write("/actuator/all", ...) then drives all three motors. From the writer’s API view, /actuator/all is one path; behind it, the fan-out is invisible.

Compound vertex composed of children

A vertex’s children are themselves vertices. The parent path is a grouping of children. A compound canvas can be expressed as:

/canvas/layer[0]    ← background, source = host A
/canvas/layer[1]    ← drawing layer, source = host B
/canvas/layer[2]    ← overlay, source = host C
/canvas:image       ← derived: composite of all layers (computed, role 4)

A subscriber that wants the live composite reads /canvas:image. A subscriber that wants only one layer subscribes to that child. The router behind /canvas:image is a computed-role vertex whose function joins its children.


Transports and endpoints behind one path

The canvas vertex is the running example for this section.

Step 1 — Configuration

One node owns the canvas. Its configuration selects a vertex role and the connections that should carry traffic to and from it (illustrative, TOML-shaped):

[[vertex]]
path = "/canvas"
role = "sink_with_model"
model = "raster_2d_renderer"
:image_size = [1024, 1024, 4]

[[vertex.subscribers]]
target = "/local/snapshot_recorder"      # in-process subscriber for backups

[[connection]]
# Each connection is a vertex mounted and routed at /net/<module>/<name>
# (ADR-0027, ADR-0061). The module segment is NOT derived from (kind, role) —
# the application declares it with register_module (ADR-0073 4), and an
# undeclared pair answers SCHEMA_NOT_FOUND. `tcp-client` below is the name the
# built-in tcp transport SUGGESTS (kTcpClientSuggestedModule) and this node
# registered, so the dialling TCP link mounts at /net/tcp-client/tcp0.
name = "tcp0"
transport = "transport_tcp"

[[connection]]
name = "udp0"
transport = "transport_udp_multicast"

The vertex is one path. The node’s forwarder dispatches each inbound FWD frame to it; each remote subscriber’s deliveries leave over the link its stored return route names.

Step 2 — A write arrives

A peer writes /canvas:ops over TCP. The node:

  1. Receives the frame via the tcp0 link’s receiver.

  2. Validates the trailer (CRC, timestamp), strips it. (L2 → L3.)

  3. The frame is an FWD{WRITE} (RFC-0004) whose dst names a local vertex — a terminus request: the forwarder decodes it and applies the op.

  4. The bare payload TLV is dispatched to /canvas:ops (L3 → L4).

  5. The vertex’s sink-with-model handler runs the renderer, mutating :image.

  6. Peers that subscribed over UDP hold remote-subscriber slots on the vertex; the fan-out emits each one an FWD{WRITE} delivery along its stored return route, out over udp0.

  7. The local subscriber /local/snapshot_recorder receives it via fan-out.

Every transport boundary is a forwarder-internal concern. The application only sees /canvas:ops change.

Step 3 — A read arrives

A peer reads /canvas:image. The node:

  1. Receives the FWD{READ} request via the tcp0 link.

  2. Resolves the path against the local graph; finds the sink-with-model vertex.

  3. Calls the vertex’s :image accessor, which serializes the model into a 4 MiB TLV.

  4. Sends an FWD{REPLY} back over the link the request arrived on, routed by the request’s accumulated src (per 04-communication-flows.md §read).

The reader does not know the bytes were synthesized on the fly from a model rather than copied out of a stored buffer. They look identical. That is the facade principle in action.

Step 4 — A screen addresses the canvas through a route

A second host (a screen) displays the canvas without participating in the model. It names its TCP link to the canvas host and addresses the canvas through it — the path is the route:

[[connection]]
name = "canvas-host"           # mounts at /net/<module>/canvas-host — `tcp-client`
                               # here because this node registered that module name
transport = "transport_tcp"

The screen subscribes to /net/tcp-client/canvas-host/canvas:image and gets snapshots; or to /net/tcp-client/canvas-host/canvas:ops and runs its own renderer to apply ops locally. Same single canvas — just two views of it.

If the screen goes offline, the canvas host does not notice beyond the TCP socket closing. On reconnect the screen reads :image once for resync, then resumes :ops. The canvas host’s vertex is unchanged across all of this.

This works because:

  • The path is the contract; the transport is invisible.

  • The vertex role determines what reads/writes/subscriptions do; the protocol does not second-guess.

  • Forwarders are routing; they do not synthesize role.

  • Aggregation rules (fan-in / fan-out / compound) are configuration, not protocol surface.


Structural vertices (the net plane’s own bookkeeping)

Some vertices exist only to hold a position in the tree. The net plane mints two kinds: the net root (/net by default — the :children[] creation target), and each module segment (/net/<module>) a connection mounts under, created lazily on first use. Neither holds an application datum; both exist so that /net/<module>/<name> has somewhere to hang. This document calls them structural vertices (CONTEXT.md §structural vertex) — not “grouping vertices”, because §address grouping above already uses “grouping” for the unrelated fan-in / fan-out patterns.

They are invisible as such. A structural vertex is registered with the stored-value role and carries no field descriptor table, so a peer reading its :schema gets the same shape a real leaf gives — the NAME differs and nothing else. An embedder enumerating the graph therefore cannot tell a structural vertex from a value vertex whose owner forgot to describe it. The enumeration surface is graph_t::for_each_vertex(fn), which visits every registered vertex in ascending canonical-key byte order and hands fn the vertex’s key and handle. Unregistered intermediates — bare addressing scaffolding created on the way to a deeper registration — are not visited at all, so a never-registered placeholder is already distinguishable; a structural vertex is registered, and is not.

The graph does not answer “is this structural”

The library answers this question only for the vertices it minted itself, on the object that minted them — tr::net::transport_vertex_t::is_structural(key), whose signature is exactly the for_each_vertex callback’s key so no handle is unwrapped. There is deliberately no graph_t-level predicate, and the reason is worth stating so it is not re-proposed:

  • An application’s own structural vertex — a /zone that holds nothing but children — is indistinguishable from a connection vertex on every graph-visible surface: same visit, same :schema bytes, same behaviour under the RFC-0016 composed branch read. A graph_t::is_structural() would have to answer false for /zone and true for /net/tcp-client on no graph-visible basis. That is not a predicate; it is a guess.

  • “Has registered children” does not rescue it. A /gpio that holds its own value and has a /gpio/IDR child reads the same way a module segment does.

  • What the library minted, the library may report — reporting a fact it authored is not defining application semantics (ADR-0010: libtracer is a transport for application data, not a definer of application semantics); withholding it would just force every embedder to re-type the library’s own naming rule. What the application minted, only the application can say. That asymmetry is why the answer lives at transport_vertex_t scope and nowhere wider.

There is also no role_t::GROUP. A role names a read / write / subscribe behaviour (the seven above); a “group” role names none, would behave as a stored value everywhere in the runtime, and would put a new value into a public byte-wide enum that downstream switches already cover. The role stays invisible to peers either way — see §outside the scope of this document.

is_structural answers by name match, not provenance: connection creation deduplicates against the graph itself and keeps no record of what it minted, so a vertex an application registered at /net or /net/<module> first still answers true. The predicate states a structural position of that net plane. The RFC-0014 per-module creator endpoint /net/<module>/conn answers false: it is an addressable control surface — a Handler vertex that executes the SPEC/NAME writes reaching it — not a position-holder.


The contract between role and schema

The role is invisible to remote peers; the schema is visible. A well-designed vertex documents in its schema the things a peer must know:

  • What writes are accepted and what they mean.

  • What reads return.

  • Whether subscriptions are by-replacement or by-event.

  • Per-subscriber state shape (typically standard from 05-protocol-tlvs.md §0x04 — SUBSCRIBER, but with module-namespaced extensions per 02-graph-model.md §schema and field discipline).

Two vertices with the same schema are observationally equivalent. They may use different roles internally and still be drop-in replaceable. This is the discipline the protocol asks vertex authors to keep — the schema is the contract; the role is the implementation choice.

A schema’s relationship to vertex roles:

Schema declares

Vertex role hints (typical)

read returns the last write

stored value (role 1)

read returns a window of recent writes

stream (role 2)

:ops is writable, :image is read-only

sink with model (role 3)

read returns a function of upstream

computed (role 4)

Forwarded to another path

proxy (role 5)

read aggregates of children

aggregate (role 6)

read returns a snapshot of MMIO

live MMIO (role 7)

The schema cannot enumerate the role exhaustively; an implementer is free to invent new roles as long as the read/write/subscribe contract is satisfied.

Which role, though? The table above says what each role’s read returns; it deliberately states no decision procedure for picking one. That procedure is retention-shaped — the per-vertex retention switch is the role system — and it lives in 22-backpressure-and-sizing.md §4 “Do you need the last-known-value?”: the five planes that consume the last-known-value, when a vertex that needs none of them should be a HANDLER instead, and the two costs that choice does and does not remove.


In-band creation and the type catalog

Vertex registration is not only an out-of-band, local act. Creation is also an in-band, ACL-gated write: an orchestrator writes a controller-spec SPEC{type, path, config} to a device’s creator endpoint, and the device instantiates one of its own known types (ADR-0017, superseded by ADR-0059, which replaces the earlier :children[] / :controllers[] creation-field spelling). Creation and binding are separate steps: creation exposes the new vertex’s port vertices, and binding wires them with SUBSCRIBER edges (reference/13).

What this makes visible is the device’s type catalog — the types it can instantiate. Roles stay invisible: no peer can interrogate how an existing vertex is implemented.

The net plane carries the same shape for connections:

  • The surface an implementation meets. A connection is created by a SPEC write to the /net:children[] catalog carrying a kind field that selects the transport; the two catalog child types are the dial shape and the listen shape. The created connection vertex is mounted — and routed — at /net/<module>/<name>, where <module> is the module name the application declared for that (kind, role) pair via register_module — never a library-derived name, and an undeclared pair is refused with SCHEMA_NOT_FOUND (ADR-0061, ADR-0073 §4).

  • The accepted direction. RFC-0014 replaces the single global catalog with a per-module creator endpoint at /net/<module>/conn: SPEC{name, config} creates /net/<module>/<name> (gated CREATE), NAME{<name>} retires it (gated WRITE), and a read of conn:schema is that module’s catalog. The role becomes positional — it is the module — rather than a config field. The endpoint is implemented (S2b): declaring a module mints it, and SPEC/NAME writes to it create and remove connections with the role fixed by the module. It is also hidden from /net/<module>:children[] (S4) — that listing returns the module’s member connections, and the endpoint is the control that creates them, not one of them; it stays addressable, which is what §6’s read <module>/conn:schema creatability probe depends on. Still open: the conn:schema catalog read (S3) and the CREATE/WRITE gating split (S2c) — today the endpoint is gated by the ordinary vertex WRITE right. The global /net:children[] catalog with a kind selector still works in parallel, and the mount-and-route rule at /net/<module>/<name> is realised.


Pitfalls

  • Role is not discoverable, so do not infer one. An implementation that reads a schema, concludes “this is a stored value”, and caches the last write as authoritative will serve stale bytes for a computed or live-MMIO vertex, whose read is defined to produce bytes now. The schema constrains the contract, never the backing.

  • A Mode-B mirror without a resync point diverges silently. A subscriber that only ever subscribes to :ops and never reads :image stays permanently wrong after missing one mutation, with no error to observe. A model-mirroring vertex needs either a history window at least as long as the reconnect deadline, or a snapshot-plus-cursor join point.

  • A subtree subscription captures the whole subtree. Subscribing at /peer to collect /peer/{id}/log also delivers every other write under /peer; the consumer filters. v1 has no per-segment wildcard, so there is no way to ask for the sibling pattern instead.

  • Redundant routes are not deduplicated. A consumer subscribed over two routes to the same remote vertex receives two deliveries per write. An implementation that suppresses the second as a duplicate destroys the failover signal that is the second route’s entire purpose.

  • A routed address is not /net/<name>. The connection segment is two segments — module then name. An implementation that builds /net/<name>/<remote path> addresses a module vertex that does not exist and the route fails to resolve.

  • A structural vertex is not discoverable from the graph. /net and /net/<module> enumerate exactly like value vertices; ask the object that minted them (transport_vertex_t::is_structural) instead of inferring from path shape. “Two segments under the root” is wrong — it claims every application /zone/<child> too.

  • Fan-out configuration appends. write("/x:subscribers[]", …) appends an edge; write("/x:subscribers[N]", …) addresses slot N. Building a fan-out front by repeatedly writing [0] yields one subscriber, not three.


Outside the scope of this document

  • A role-discovery wire format. The seven roles above are intentionally invisible — a peer cannot interrogate how a vertex is implemented. This is distinct from a device’s type catalog, which is visible (ADR-0017): the catalog names types the device can instantiate, not the internal role of any existing vertex. Schemas describe what a vertex does; they do not name how it does it.

  • A CRDT or consistency protocol for sink-with-model vertices. If a model needs convergence guarantees across replicas, they belong in the application layer (per 04-communication-flows.md §network partition and recovery).

  • Fan-in and fan-out scheduling beyond first-arrived-wins-by-timestamp. Implementations may add policies (prefer_low_latency, quorum_n_of_m); this document observes the structural patterns only.


Where these patterns are specified elsewhere

Concept

Specified in

Role here

Vertex backing is unspecified (RAM, MMIO, file, function-on-read)

02-graph-model.md §the graph imposes no shape

The taxonomy that develops it

Shared-variable pattern via a subscribe requesting durability (durability_request, RFC-0022 §3.A)

06-user-data-packing.md §shared-variable pattern

Role 1 (stored value)

Route proxy as a vertex that forwards

07-host-embedding.md §per-host view

Role 5 (proxy)

Subtree subscription mechanics

03-addressing.md §subtree subscriptions

Builds aggregate-role vertices

Address-shift slicing for large payloads

03-addressing.md §address-shift slicing

Composes with role 2 and the Mode-A canvas

Schema as the visible contract

02-graph-model.md §schema and field discipline

The only role-adjacent discovery surface

Redundant links as distinct explicit routes

07-host-embedding.md §global topology

§redundant links are distinct explicit routes

Structural vertices of the net plane (/net, /net/<module>)

ADR-0027, ADR-0061

§structural vertices

MMIO snapshot semantics

10-module-catalog.md §hard integrations (mem_mmio reads: TOCTOU on a live register)

Role 7 (live MMIO)

The toctree in README.md is the order of record for this suite.