RFC 0013 — Readable creatable-child-type catalog: the :children.schema read¶
Warning
Status: superseded. This page is a superseded change proposal, kept as history; it is not in force. Superseded by ADR-0059. RFCs are proposals and history, not the standard. The normative specification is Protocol v1 and the annexes its §3 incorporates; where an RFC and the specification differ, the specification wins. All RFCs, with their status, are listed in the ADR and RFC index.
Field |
Value |
|---|---|
RFC |
0013 |
Title |
Readable creatable-child-type catalog: the |
Status |
superseded by ADR-0059 (2026-07-17, maintainer-ratified grill) — see §Why this RFC is superseded |
Author(s) |
AvatarSD (maintainer) |
Created |
2026-07-17 |
Comment window closes |
— (withdrawn before it closed; never accepted, nothing normative here) |
Tracking issue |
|
Target spec version |
v1 (additive — occupies previously-erroring space only) |
Why this RFC is superseded¶
The problem is real and is being solved; the spelling proposed below is not the solution. Read this document for the problem statement (§Motivation is accurate and still worth reading) and as the record of a design that lost.
This RFC hangs the catalog off a field: read <vertex>:children.schema. The
2026-07-17 grill ruled that :schema is vertex-only — exactly one per
vertex, describing that vertex’s own structure, never its children’s, and no
schema hangs off a field. That ruling leaves :children.schema nowhere to
live: it is not the vertex’s schema, and there is no such thing as a field’s
schema.
The ruling was not aimed at this RFC. :children.schema was inherited, not
invented here — reference/05 §0x0E
literally calls the catalog “the :children field’s :schema”, and
ADR-0017’s §Decision
spelled it the same way. This RFC faithfully specified what the docs said. The
docs were wrong, and that error stayed invisible precisely because the read
half was never implemented — nothing ever tried to address it, so nothing ever
discovered it had nowhere to live. (The same reason ADR-0017’s error survived;
see ADR-0059
§Context.)
Where the catalog actually lives: on a creator endpoint vertex, per
ADR-0059. write /net/export SPEC{type,name,config} creates; read /net/export:schema is the catalog — a vertex’s own schema, so the
vertex-only ruling holds with no field-level schema anywhere. Everything this
RFC wanted survives: the catalog is readable, in-band, device-bounded, with no
new wire verb, type code, or error identity, and a device without a creation
surface still answers SCHEMA_NOT_FOUND. Only the address changed — from a
field on the parent to a vertex of its own.
What must still be done, and is not done here: ADR-0059 deliberately
specifies no bytes. The SPEC layout, the catalog reply shape, the
unexport payload, the gating right, and the write-only value semantics all
need pinning — that is the forthcoming creator-endpoint RFC’s job, and it should
lift §B and §C below (the descriptor shape and the config-key
described-vs-opaque split, which the grill did not disturb) rather than start
from a blank page.
Governance note. This RFC was never accepted and contributes nothing
normative; superseding it before its comment window closed forecloses no
implementer’s comment, because acceptance — not withdrawal — is what the window
gates, and docs/implementations.md registers no
third-party implementations to comment.
Summary¶
The device’s creatable-child-type catalog — the set of type selectors a
SPEC (0x0E) write into a vertex’s :children[] will accept
(ADR-0017)
— becomes readable, at read <vertex>:children.schema. The reply is one
POINT whose members are one descriptor per creatable type: the exact NAME
selector bytes a SPEC may use, plus an optional SETTINGS record describing
the type’s config keys (universal keys described; kind-private keys stay
opaque, per the conn_settings_t / ADR-0043 §5 precedent). The spelling is a
field-of-field read that already exists grammatically: reference/05
literally calls the catalog “the :children field’s :schema”, and
reference/03’s field grammar (field-chain = field *( "." field )) parses
children.schema today — no new grammar, no new top-level field name, no new
wire verbs, type codes, or error identities. A device with no dynamic-creation
surface keeps SCHEMA_NOT_FOUND on read exactly as it has it on write: the
catalog read and the :children[] write surface are one capability, two
faces, and MUST agree.
Motivation¶
The spec already names this surface; no implementation can serve it. Reference/05 §
0x0ESPEC — normative by incorporation (spec v1.md §3, ADR-0007) — says: “The device validatestypeagainst its catalog (the:childrenfield’s:schema); an unknown type returnsERROR{tr::schema::not_found}.” The validation half is implemented (core/src/graph.cppcreate_child: unknown selector ⇒SCHEMA_NOT_FOUND, theENOTTYof creation). The read half — “the:childrenfield’s:schema” — does not exist anywhere:graph_t::read_schemaserves only the per-vertex schema (the RFC-0010 §B.2 two-part POINT), no per-field catalog read exists, and the field-read dispatch ingraph_t::readreturnsSCHEMA_NOT_FOUNDfor thechildren.schemachain. The parenthetical is a dangling normative reference; this RFC makes it real.The doctrine promised visibility. ADR-0017: the orchestrator “selects a type from the device’s controller-type catalog, which is the creation field’s schema (a POINT enumerating accepted
types and each one’s config schema)” — pinned in 2026-05, never given a read surface. CONTEXT.md §In-band vertex creation repeats the promise (“what becomes visible is the device’s controller-type catalog”), and §Module set’s avoid-list asserts the catalog “is runtime-queryable, per-device” — asserted, currently false.The catalog already exists in-core; the read only exposes it. The reference holds exactly this structure today:
graph_t::child_types_(populated byregister_child_type— the built-instored_valuein thegraph_tctor,client/listenerincore/src/transport_vertex.cpp), and modules extend it at runtime (the ADR-0043 quic module extends the transport catalog throughregister_transport_type— the open/closed seam thetransport_vertex.cppcomments document). Nothing new is stored; bytes the device already consults on everySPECwrite become servable.A real consumer flow is blind without it. Network formation (reference/13) is an orchestrator creating controllers and transport connections on devices it did not build (
:children[]SPECwrites), then binding them. Today the orchestrator must know each device’s types out of band, or probe by writing — a side-effecting,CREATE-gated discovery on what should be the read plane. A generic constructor UI (select-a-type-from-a-dropdown, the wiring-diagram pattern of the originating production firmware — an ESP32-C6 smart-agriculture node) has no dropdown source.
Proposed change¶
A. The address: read <vertex>:children.schema¶
The catalog is read at the schema sub-field of the children field —
one .subfield step per reference/03 §Field-path resolution:
/dev/ctrl:children.schema
└───┬───┘ └──────┬──────┘
vertex field chain (depth 2 of the allowed 8)
Why this spelling and not the alternatives (§Alternatives has the full engagement):
It is grammatically legal today. Reference/03’s one grammar:
path = root … [ field-sep field-chain ],field-chain = field *( "." field )— one:per path, dots thereafter.:children.schemaparses in every conforming parser now (the reference’sfield_path_talready carries it to the dispatch, which returnsSCHEMA_NOT_FOUND— exactly the previously-erroring space an additive v1 change may occupy). The double-colon spelling:children:schemais not in the grammar and would force a grammar change through every parser and the parse-oncepath_t(ADR-0054) for zero expressive gain.It spends no new top-level field name. Top-level protocol bare names are the scarce resource (RFC-0010 §A.1’s economy argument); a
:catalog[]sibling would mint one to say somethingchildrenalready owns.It makes the existing prose literal. “The
:childrenfield’s:schema” (reference/05 §0x0E) is:children.schema— the RFC changes the docs least by changing the address not at all.
Resolution and gating:
Stage 1 (vertex address) unchanged: nonexistent vertex ⇒
ERROR{tr::path::not_found}.The read is a control-surface read gated by the ordinary READ right on the vertex, like
:schema—tr::access::deniedotherwise.Only the bare two-step chain is a surface: indexed, append, or any deeper form (
:children.schema[],:children.schema[0],:children.schema.x) MUST returnERROR{tr::schema::not_found}.The surface is read-only: a write to
:children.schemaMUST returnERROR{tr::schema::not_found}(no write surface — same identity as writing:schema; the catalog is device state, mutated only through the device’s own registration API, the RFC-0010 §A.2 owner-initiated doctrine).Remote reads ride the ordinary
FWD{READ}with the field chain per RFC-0004 — nothing new.
B. Reply shape — byte-precise¶
One POINT (0x07, opt.PL=1); each member is one POINT (PL=1)
describing one creatable type:
POINT (0x07, PL=1) { ; the catalog — zero or more members
POINT (PL=1) { ; one member per creatable type
NAME <type> ; MUST be first: the SPEC `type` selector,
; byte-verbatim what a SPEC write may carry
SETTINGS (PL=1) { ; MAY follow: the type's config descriptor
NAME <key> SETTINGS (PL=1) { ; one record per described config key
NAME "dtype" NAME <t> ; SHOULD — value shape (utf8/u16/u32/bool/…)
NAME "required" VALUE <bool u8> ; MAY
NAME "default" VALUE <bytes> ; MAY
NAME "enum" POINT (PL=1) { ; MAY — closed value set, one NAME per value
NAME <value> …
}
NAME "label" NAME <human> ; MAY
… ; owner-defined extras — opaque
}
…
}
}
…
}
The member wrap mirrors the
:children[]member listing (reference/05 §0x0Eread-members:POINT{ POINT{NAME} … }): a type with no config descriptor is byte-shaped exactly like a member entry, and the catalog read is visibly the schema dual of the members read. Members are self-delimiting, so the optionalSETTINGSnever creates pairing ambiguity.The per-key record reuses the RFC-0010 §B.1 descriptor form (
NAME <name> SETTINGS{…}) and, like it, only the structure is normative: the key vocabulary (dtype/required/default/enum/label) is SHOULD-level convention so generic consumers converge; extras are opaque.The empty catalog is the outer POINT with no members — 4 bytes:
07 40 00 00 ← POINT, opt=0x40 (PL=1), length=0
Worked example — a two-type catalog:
stored_valuebare,clientwith a one-key descriptor (kind, dtypeutf8); 71 bytes total:
07 40 43 00 ← catalog POINT, len=67
07 40 10 00 ← member POINT, len=16
02 00 0C 00 73 74 6F 72 65 64 5F 76 61 6C 75 65 ← NAME "stored_value"
07 40 2B 00 ← member POINT, len=43
02 00 06 00 63 6C 69 65 6E 74 ← NAME "client"
0B 40 1D 00 ← config descriptor SETTINGS, len=29
02 00 04 00 6B 69 6E 64 ← NAME "kind"
0B 40 11 00 ← key record SETTINGS, len=17
02 00 05 00 64 74 79 70 65 ← NAME "dtype"
02 00 04 00 75 74 66 38 ← NAME "utf8"
C. Semantics — normative clauses¶
One capability, two faces. A device MUST serve
read <vertex>:children.schemaon every vertex where it accepts:children[]SPECwrites, and MUST returnERROR{tr::schema::not_found}on the read wherever it returns it on the write (a device without dynamic creation staysENOTTYon both — the read and write surfaces of the creation field never disagree).Completeness at the instant of the read. Every
typeselector that aSPECwrite into this vertex’s:children[]would accept at the moment the read is served MUST appear as a member, carrying the selector bytes verbatim. (The reference serves one device-wide table at every creation-capable vertex — informative; the clause deliberately binds per-vertex so an implementation MAY scope catalogs per creation point.)Snapshot, extend-only. The catalog read is a snapshot. Types MAY be registered after node start (a module extending the catalog — ADR-0043’s registration seam) and MUST appear on subsequent reads; a node SHOULD NOT remove catalog entries within its lifetime (the catalog is extended, never modified). Consumers MUST treat the catalog as descriptive: the
SPECwrite gate remains authoritative, and a write racing a registration may still getSCHEMA_NOT_FOUND— reading first confers no admission.Empty ≠ absent. An empty-member POINT means “this vertex has a creation surface and currently zero creatable types” — distinct from
SCHEMA_NOT_FOUND(“no creation surface”), exactly the declared-but-empty vs undeclared split RFC-0010 §A.4 draws (NOT_FOUNDvsSCHEMA_NOT_FOUND) transposed to the catalog.Config descriptors are self-description, not a validation schema. The runtime MUST NOT validate a
SPEC’sconfigagainst the descriptor; the type’s factory is the semantic gate (the RFC-0010 lazy-validation split: the runtime validates addressing — is the type in the catalog — and the owner of the vocabulary validates shape). A type SHOULD describe its universal config keys; it MAY omit or partially describe kind-private keys (theconn_settings_tprecedent:addr/kind/port/role/keepalive/max_frameare universal and parsed centrally, while quic’scert/keynever land inconn_settings_t— the kind’s factory parses them from the raw config TLV, ADR-0043 §5 leanness). An undescribed key is unstated, not forbidden.Runtime-extended selectors below the type level. A transport kind registered through
register_transport_type(udp/tcp/ws built-ins; quic from its module) is not a catalog type: theSPECgrammar has one selector (type), and kinds are config vocabulary of theclient/listenertypes. A device MAY advertise the currently-registered kind set as thekindkey’senumin those types’ descriptors, refreshed per read (snapshot semantics, clause 3), and SHOULD omit theenumwhen its kind set is open. This keeps the catalog N types, not N×M type-kind products, and keeps the module seam open/closed — a new kind module changes a descriptor’s bytes, never the catalog’s shape.Interaction with write-creates (RFC-0005): unchanged. A plain data write still creates stored-value vertices
mkdir -p-style without consulting any catalog; the catalog governs only the typed:children[]SPECpath. (The reference’s built-instored_valuecatalog entry is the explicit spelling of the same capability — it appears in the catalog read like any other type.)
D. Reference-implementation seam (informative)¶
register_child_type gains an optional per-type descriptor — static bytes
or a provider callback (the ADR-0044
on_children synthesized-listing precedent), so transport_vertex_t can
serve a live kind enum. The field-read dispatch in graph_t::read gains one
branch (children + schema plain steps) that emits the members from the
child_types_ snapshot — the same emit-and-wrap pattern as read_schema /
read_children. No cross-vertex orchestration, no storage change.
Files this RFC edits (on acceptance)¶
docs/reference/05-protocol-tlvs.md— §0x0ESPEC gains a “The catalog read (:children.schema)” subsection carrying the §B layout; the existing “(the:childrenfield’s:schema)” parenthetical becomes a cross-reference to it. (Normative via v1.md §3 incorporation — nodocs/spec/v1.mdedit needed.)docs/reference/02-graph-model.md— §Core writable fields table gains the:children.schemarow (structured TLV, read-only, creation-catalog self-description); §Schema and field discipline cross-references it beside the:schematwo-part read.docs/reference/03-addressing.md— §Examples gains/dev/ctrl:children.schema; §Field-path resolution notes.schemaas an ordinary sub-field step (explicitly no grammar change).CONTEXT.md— §In-band vertex creation / creation field: the catalog gains its read spelling, and the term widens from “controller-type catalog” to creatable-child-type catalog (it was never controllers-only — transport connections (ADR-0027) andstored_valuecreate through it); §Module set’s avoid-note (“runtime-queryable, per-device”) points at the read.core/reference implementation +core/CHANGELOG.md(§D seam) — follow this RFC, not normative.
Conformance vectors (what proves it)¶
New vectors under tests/conformance/vectors/v1/ (additive; no existing
vector changes):
field/field-children-schema— the canonical encoding of a path carrying thechildren.schemafield chain (the addressing/dispatch vector, sibling offield/field-nested).tlv-types/point-catalog— the §B worked example: the 71-byte two-member catalog (stored_valuebare;clientwith thekind/utf8key record), decoded meaning asserted member by member.tlv-types/point-catalog-empty— the 4-byte empty catalog (07 40 00 00): creation surface present, zero types.
Host-side tests (local surface, like RFC-0010’s):
read⇔write agreement — a graph with creation: catalog read lists exactly the registered types and a
SPECnaming each succeeds; an unlisted type’sSPECand the read on a no-creation build both returnSCHEMA_NOT_FOUND.runtime extension —
register_child_typeafter first read ⇒ the new type appears on re-read; aregister_transport_typekind appears in theclientdescriptor’senumon re-read (when advertised).gating and shape — read without READ ⇒
tr::access::denied; write to:children.schema, and any indexed/deeper form, ⇒SCHEMA_NOT_FOUND; nonexistent vertex ⇒tr::path::not_found.
Compatibility¶
No protocol-v1 break. The read occupies space that is
SCHEMA_NOT_FOUNDin every existing implementation; a device that never serves it is exactly a device without dynamic creation, which is already a conforming posture (reference/05 §0x0E: no:children[]capability ⇒SCHEMA_NOT_FOUND). No new wire verbs, type codes, or error identities; no grammar change (the chain parses today).New conformance vectors as listed above; no existing vector changes.
Migration: devices implement the read when they implement dynamic creation (clause C.1 couples them); orchestrators SHOULD switch from out-of-band type knowledge to reading the catalog, keeping the write gate as the authority (clause C.3 — no TOCTOU assumption to unlearn, because none was ever offered).
Alternatives considered¶
:children:schema(a second:, literal field-of-field). Rejected — outside the reference/03 grammar (field-sepappears once;*in the chain is"." field), so it is a grammar change rippling through every parser,path_t(ADR-0054), and the FIELD frame (RFC-0004) for a spelling the dot form expresses identically.A new top-level field,
:catalog[]/:types[]. Rejected — mints a scarce top-level protocol bare name (the RFC-0010 §A.1 economy), splits the creation story across two field names, and the array forms are meaningless (types are named, not slotted — the same reason:children[N]falls through today).Fold the catalog into the vertex’s
:schemaPOINT (a third part beside the synthesized protocol part and the RFC-0010 owner part). Rejected — it bloats the every-vertex self-description read with a potentially large, rarely-needed listing, churns the two-part:schemashape RFC-0010 §B.2 just fixed (and itspoint-schema-appvector), and loses the per-creation- point scoping clause C.2 permits. The catalog stays demand-paged behind its own address.Discovery by probing (issue a
SPECand interpretSCHEMA_NOT_FOUND). Rejected — side-effecting,CREATE-gated discovery on the read plane’s job; a successful probe creates a vertex. This is the status quo being fixed, not a design.Flatten kinds into first-class catalog types (
client/udp,client/quic, …). Rejected — the SPEC grammar has one selector; kinds are one type’s config vocabulary behind an open/closed module seam (ADR-0043 — the quic module extends the kind map, never the creation grammar), and flattening makes the catalog a type×kind product that grows with every module.A wire operation that edits the catalog remotely. Rejected — ADR-0017’s bound: the device instantiates only its own known types (that is what keeps in-band creation from being code injection); the catalog is device state, mutated only by the device’s local registration API — the same owner-initiated doctrine as RFC-0010 field declaration.
Discussion¶
Genuinely open points, flagged for the comment window (per GOVERNANCE.md, the tracking issue #413 stays open through 2026-07-31; sustained objections and their resolution to be recorded here):
Does
.schemageneralize? This RFC specs exactly one sub-field schema read (children.schema) because exactly one consumer need exists. A general rule (“every field MAY serve.schema” —:subscribers.schema,:acl.schema) is deliberately not proposed; if a second instance appears, it should generalize by its own RFC rather than this one speccing surfaces nothing serves (the reference/02:description/:liveness.*lesson, RFC-0010 §Discussion 5).READ vs CREATE gating. The read is READ-gated like every control-surface read. Gating it on CREATE (hide the catalog from peers who cannot create) is coherent but makes the catalog a secret, which it is not — it describes the device class, not the deployment; and split gating would diverge from
:schema. Objections welcome.How much key vocabulary to freeze. Clause C.5 makes only the
NAME <key> SETTINGS{…}structure normative, vocabulary SHOULD-level — mirroring RFC-0010 §B.1 and its open Discussion 4. The constructor-UI and reconciler consumers should say whetherrequired/enumneed MUST strength before v1 freezes.Advertising open kind sets. Clause C.6 lets a device omit the
kindenumwhen its set is open, which means a consumer cannot distinguish “kinds unknown” from “no kinds”. If that ambiguity bites, aMAY-levelopenmarker inside the key record is the escape hatch — deferred until a consumer asks.Is the empty catalog reachable? The reference always registers
stored_value, so its catalog is never empty; clause C.4 keeps the empty POINT for implementations that scope catalogs per-vertex or register nothing. If the window concludes it is dead surface, collapsing empty intoSCHEMA_NOT_FOUNDwould couple “has types now” to “has a creation surface”, which clause C.3’s snapshot semantics argue against — the RFC keeps them distinct.