Skip to content

IEEE register generated

Catalog schema (CDDL)#

Normative. This schema wins over the prose.

This is Appendix C in full: the normative encoding of the channel catalog. §8.1 is its prose companion, and the CDDL wins on any disagreement with it.

The page is generated from spec/schema/catalog.cddl. It cannot drift from that file, because --check fails the build the moment it does.

Reading it#

The schema is written in CDDL (RFC 8610), the concise data definition language for CBOR. Three conventions carry most of the meaning:

Notation Meaning
1 => uint map key 1, holding an unsigned integer. Keys are the wire numbers from the registry.
? 8 => ... the key is optional. Absent is not the same as null: the deterministic profile never encodes null (§5.3).
* entry / + field zero-or-more, and one-or-more.

Comments carry normative constraints that the grammar cannot express — the depth budget, which keys are mutually exclusive, and why. Read them.

The schema#

; Valence catalog schema (valence/1) — NORMATIVE (SPEC.md §8.1, Appendix C)
; The catalog_etag (SPEC.md §8.3) is the first 8 bytes of SHA-256 over the
; catalog encoded per the deterministic CBOR profile (SPEC.md §5.3), entries
; sorted ascending by id.
;
; DEPTH BUDGET (§5.3 caps nesting at 4). Counting from a channel-entry map:
;   entry map -> layout array -> field map -> options array   = 4  (AT THE CAP)
;   entry map -> layout array -> field map -> bits map        = 4  (AT THE CAP)
;   entry map -> schema map    -> field map -> options array  = 4  (AT THE CAP)
;   entry map -> store map                                    = 2
; The leaves of those depth-4 containers are SCALARS (tstr / uint / access) by
; construction — nothing nests further. Any future annotation that wants a map
; or array INSIDE a field map is therefore blocked; it must ride the entry
; level instead. This is a real design constraint, not a formality.
;
; RAM NOTE for implementers (feasibility pass 2026-07-25): an entry carries a
; layout OR a schema OR a store descriptor, never more than one. Building a
; catalog struct with all three arrays present in every entry is what turned
; a uniform Catalog<48,50> into 320 KiB. Field capacity is PER-ENTRY.

catalog = [ * channel-entry ]

channel-entry = {
  1  => uint,              ; id: u16 channel id
  2  => tstr .size (1..32),; name
  3  => class,             ; channel class
  4  => direction,         ; h2c / c2h
  5  => access,            ; minimum role to subscribe (or send, for INTENT) — the FLOOR; per-op `access` on a schema-field may raise it
  6  => float32,           ; max_rate_hz (0.0 = on-change only)
  7  => priority,          ; default priority class
  ? 8  => [ + layout-field ],  ; packed classes (STATE, STREAM): wire-order fields
  ? 9  => { + int => schema-field }, ; CBOR classes (INTENT, EVENT): key -> field
  ? 12 => store-descriptor,    ; STORE class only (RFC-021)
  ; ---- RFC-009 settings metamodel (entry level) ----
  ? 10 => uint,            ; category: registry ui_categories (RFC-047/048, Phase C2). 1..15 registered (5 and 9 retired, RFC-094)
                           ;   (+ vendor 0x40..0x7E device-defined); 0 and 16..0x3F reserved. Was
                           ;   setting_categories (0..4) pre-Phase-C2 — same wire key, new vocabulary,
                           ;   a pre-tag restructuring (registry header permits it before v1.0).
  ? 11 => tstr .size (1..24),  ; category_label: REQUIRED iff category is in the vendor range (0x40..0x7E)
  ? 13 => uint,            ; replay_depth: entries the hub MAY replay on grant (RFC-017). Presence is THE exception to §9.4's no-replay rule.
  ? 14 => uint,            ; setting_channel: the u16 INTENT channel that writes this entry's setting-annotated fields. REQUIRED iff any field carries setting_key.
  ? 15 => uint,            ; stream_kind: registry stream_kinds (STREAM class only, RFC-014/023). Absent = 0 (samples) — replaces the M5 unit-string heuristic for segment-class classification (§9.2, §10.4).
  ; ---- RFC-048 rendering metamodel (entry level, Phase C2) ----
  ? 16 => uint,            ; rank: registry ui_ranks (RENDERING.md §4) — how much THIS CHANNEL
                           ;   matters, independent of any per-field key-19 rank (a separate axis,
                           ;   not inheritance). Absent = detail (2), per the unknown/absent-rank rule.
  ; ---- RFC-070 ----
  ? 17 => uint,            ; store_id: on a store's roster STATE (its {generation, count, capacity}
                           ;   dynamic half) and on the INTENT entry carrying its action.store op
                           ;   select: the store-descriptor key-1 store_id of the STORE entry it
                           ;   belongs to (SPEC §8.7). Optional; absent or unresolving = unlinked.
  ; ---- RFC-065 ----
  ? 18 => tstr .size (1..24),  ; role: registry channel_roles; the whole entry's purpose (SPEC §8.1, §9.4).
                               ;   Unregistered values legal; MAY repeat across entries.
  ? 19 => { + uint => tstr .size (1..24) },  ; event_kinds: EVENT class only; event_kind -> label.
                               ;   Append-only across firmware. MUST be absent on spec-core channels.
                               ;   Depth: entry -> map -> tstr = 2.
  ; ---- RFC-066 as amended ----
  ? 20 => [ uint, uint ],      ; mod_target: on a modulator's entry (mod.* roles), the field it rides:
                               ;   [channel id, field], field = layout index on a STATE/STREAM target,
                               ;   schema key on an INTENT target (the RFC-078 target addressing).
}
; Exactly one of key 8 / key 9 / key 12 MUST be present, matching the class:
;   STATE | STREAM -> 8  (layout)
;   INTENT | EVENT -> 9  (schema)
;   STORE          -> 12 (store descriptor)

class     = 0 ; STATE
          / 1 ; STREAM
          / 2 ; INTENT
          / 3 ; EVENT
          / 4 ; STORE  (RFC-021: a blob store — presets, saved positions, trust
              ;         ledger. An ordinary catalog entry on purpose: a parallel
              ;         top-level array would break the catalog root shape, the
              ;         id sort, the etag computation and the depth rules, all four.)

direction = 0 ; h2c
          / 1 ; c2h

; Access tiers renamed by RFC-027; WIRE VALUES UNCHANGED.
access    = 0 ; watch      (was: viewer)
          / 1 ; control    (was: controller) — includes STREAM publishing
          / 2 ; configure  (was: admin)

priority  = 0 ; background
          / 1 ; normal
          / 2 ; elevated
          / 3 ; critical

; ---------------------------------------------------------------------------
; A packed field, in wire order. Wire value = physical value * scale.
;
; Keys 1..7 are the wire description. Keys 8..15 are the RFC-009 ANNOTATION
; BLOCK — all optional, all ignorable. A client that reads none of them
; behaves exactly as a v1-draft client did. A client that reads them can build
; its entire settings surface from the hub, so a control added in firmware
; populates on every client's next connect, with the label, grouping and
; explanation coming from the hub.
;
; Valence describes what things ARE, never how they LOOK: no widget hints, no
; layout, no ordering, no styling, ever. Widget choice is a client-side
; function of type + constraints (bool-u8 -> toggle, u8+options -> select,
; bitfield8 -> checkbox group, numeric+min/max -> slider, str<N> -> text,
; no setting_key -> read-only display with unit).
; ---------------------------------------------------------------------------
layout-field = {
  1 => tstr .size (1..24), ; name
  2 => packed-type,        ; registry packed_field_types
  3 => tstr .size (0..8),  ; unit ("mm", "mm/s", "mA", "degC", "%", "count", "flag", "")
  4 => float32,            ; scale (wire = physical * scale); 1.0 for unscaled
  ? 5 => float32,          ; min (physical units)
  ? 6 => float32,          ; max (physical units)
  ? 7 => { + uint => tstr },  ; bits: bit-index -> meaning (bitfield8 only)
  ; ---- RFC-009 annotation block ----
  ? 8  => uint,            ; setting_key: the CBOR key in the entry's setting_channel that WRITES this field.
                           ;   PRESENT  = this field is a SETTING (stored config; adopt it into a control).
                           ;   ABSENT   = read-only (effective state or telemetry; display it, NEVER write it back
                           ;              into a setting's shadow).
                           ; RFC-003's stored/effective distinction falls out of this presence test with no
                           ; separate flag — it is why RFC-003 is superseded rather than landed.
  ? 9  => setting-default, ; default: the FACTORY value, same type as the field
  ? 10 => [ + tstr .size (1..24) ],  ; options: labels for a single-select; WIRE VALUE = ARRAY INDEX
  ? 11 => tstr .size (1..24),        ; group: free-form card heading within the category tab
  ? 12 => tstr .size (1..128),       ; desc: user-facing description/tooltip (limits.desc_max_bytes)
  ? 13 => tstr .size (1..24),        ; role: registry field_roles vocabulary; unknown roles render generically
  ? 14 => float32,         ; step: range granularity hint
  ? 15 => uint,            ; flags: bitmask of registry setting_flags (advanced / restart_required / secret / destructive)
  ; ---- RFC-037 forward decodability ----
  ? 18 => uint,            ; size: this field's packed width in BYTES, stated explicitly.
                           ;   Decoders prefer this over the type-derived width; an unknown TYPE
                           ;   with a declared SIZE is a SKIPPABLE HOLE instead of a decode wall.
                           ;   Without it, the first registry-added packed type strands every
                           ;   existing client at the first field that uses it — later offsets
                           ;   become unknowable and the entire layout tail goes dark (both
                           ;   shipped generic clients carry the identical defensive truncation).
                           ;   For known types conformance checks declared == type-derived width;
                           ;   a mismatch is an authoring error. (Key 18: 16/17 are taken by
                           ;   schema-field access/option_access under the shared numbering.)
  ; ---- RFC-048 rendering metamodel (Phase C2). Keys start at 19, not 16: 16/17
  ; are reserved by schema-field's access/option_access under the shared
  ; numbering (layout-field never uses them, but the numbering is shared so one
  ; reader handles both kinds — see the file banner), and 18 is RFC-037 size.
  ? 19 => uint,            ; rank: registry ui_ranks (RENDERING.md §4). Absent = detail (2).
  ? 20 => uint,            ; aspect: registry value_aspects (RENDERING.md §5.1). Absent = live (0).
  ? 21 => uint,            ; scope: registry value_scopes (RENDERING.md §5.2). Absent = session (0).
  ? 22 => uint,            ; provenance: registry value_provenance (RENDERING.md §5.3). Absent = actual (2).
  ? 23 => uint,            ; unit_id: registry unit_ids (RENDERING.md §6), a frozen numeric companion
                           ;   to the existing tstr `unit` (key 3). Absent/unrecognized -> the client
                           ;   renders `unit` (key 3) verbatim, per the unknown-unit rule.
  ; key 24 is schema-field destructive_options (RFC-063), unused here under the shared numbering.
  ? 25 => setting-default, ; safe (RFC-076): the value this field takes in an accessory's safe state
                           ;   (SPEC §8.10, §13.3.1); same type, within min/max. REQUIRED in an
                           ;   accessory declaration on every c2h STREAM layout field.
}

setting-default = int / float32 / bool / tstr

packed-type = 0  ; u8
            / 1  ; i8
            / 2  ; u16
            / 3  ; i16
            / 4  ; u32
            / 5  ; i32
            / 6  ; f32
            / 7  ; bitfield8
            / 8  ; str16   \
            / 9  ; str32    > RFC-026: fixed-width, zero-padded UTF-8. Fixed width
            / 10 ; str64   /  keeps every offset static and evolution append-only.
; STREAM sample layouts remain STRING-FREE — the motion hot path never pays for text.

; ---------------------------------------------------------------------------
; A CBOR-payload field.
;
;   INTENT — the fields of the intent's `value` map (CBOR key 20).
;   EVENT  — the fields of the event's `body` map (CBOR key 40). Kind-specific
;            EVENT fields ride `body` and draw their integer keys from THIS
;            schema, NOT from the global cbor_keys space. That is the v1.0
;            grammar fix: with kind-specific fields at the top level, every
;            device-authored EVENT channel would have needed a registry PR to
;            name its own fields — the exact coupling the self-describing
;            catalog exists to prevent. (`event_kind` 33 and `seq_of_state` 34
;            stay at the top level: they are protocol framing, not payload.)
;
; Annotation keys are NUMBERED IDENTICALLY to layout-field wherever the meaning
; matches, so one reader handles both. Key 8 (setting_key) is absent by
; construction: for a schema-field the MAP KEY *is* the write key.
; ---------------------------------------------------------------------------
schema-field = {
  1 => tstr .size (1..24), ; name
  2 => cbor-type,
  3 => tstr .size (0..8),  ; unit
  ? 5 => float32,          ; min
  ? 6 => float32,          ; max
  ? 9  => setting-default, ; default
  ? 10 => [ + tstr .size (1..24) ],  ; options (wire value = array index)
  ? 11 => tstr .size (1..24),        ; group
  ? 12 => tstr .size (1..128),       ; desc
  ? 13 => tstr .size (1..24),        ; role — incl. RFC-019 `action.<name>`, which marks this
                                     ;   field as a VERB rather than a value
  ? 14 => float32,         ; step
  ? 15 => uint,            ; flags (setting_flags)
  ? 16 => access,          ; PER-OP minimum role; overrides the entry's `access` floor upward
                           ;   or downward. Without this, the role-EXEMPT safety ops (estop,
                           ;   stop) would force channel 0x0005 down to `watch` access, and a
                           ;   generic renderer would then offer hold/pause/takeover to every
                           ;   viewer — discovering otherwise only by NACK, which violates
                           ;   gray-never-hide.
  ? 17 => [ + access ],    ; option_access: per-OPTION minimum role, index-aligned with key 10.
                           ;   Needed because an op-style INTENT can carry its verb as one
                           ;   ENUM-VALUED field (safety-intents does: `value` key 1 holds the
                           ;   op code), and per-FIELD access cannot vary across the values of
                           ;   one field. Omit it for the RFC-019 one-field-per-action shape,
                           ;   where key 16 alone is sufficient.
  ; ---- RFC-048 rendering metamodel (Phase C2) — numbered identically to
  ; layout-field's own 19..23 (see that block for per-key meaning); rare on a
  ; schema field (INTENT/EVENT payloads are mostly verbs, not display values)
  ; but not disallowed — e.g. an aspect-group `action.reset` targets a
  ; specific aspect (RENDERING.md §5.4).
  ? 19 => uint,            ; rank
  ? 20 => uint,            ; aspect
  ? 21 => uint,            ; scope
  ? 22 => uint,            ; provenance
  ? 23 => uint,            ; unit_id
  ; ---- RFC-063 ----
  ? 24 => uint,            ; destructive_options: bit i marks option i (key 10) destructive (SPEC §8.8).
                           ;   Options past bit 63 cannot be marked; split the op table across fields.
  ? 25 => setting-default, ; safe (RFC-076): as layout-field key 25. REQUIRED in an accessory
                           ;   declaration on every value-bearing INTENT field (action.* verbs exempt).
}

cbor-type = 0 ; uint
          / 1 ; int
          / 2 ; float32
          / 3 ; bool
          / 4 ; tstr
          / 5 ; bstr

; ---------------------------------------------------------------------------
; STORE class (RFC-021). The entry declares the store; ITEMS never appear in
; the catalog — they move over BLOB_REQ/BLOB_CHUNK in blob_namespaces.store,
; addressed by {store_id, slot}. A store's dynamic side (generation, count) is
; a separate tiny STATE channel, so the catalog stays invariant per firmware
; (§8.6) while the roster changes freely.
;
; `payload` is OPAQUE: the protocol layer NEVER decodes it, and RFC-028's
; depth and allocation budget explicitly does not extend inside it. A client
; that decodes a preset does so above the protocol boundary.
;
; THE ONE CARVE-OUT (M4b, RFC-027.4/029.2): the trust ledger — the store whose
; `kind` is "trust.ledger", declared by spec-core channel 0x000C — has a
; REGISTERED item grammar (`trust_ledger_keys`). Presets are DEVICE content and
; are genuinely opaque; the trust ledger is PROTOCOL content whose fields the
; spec names, which every `configure` client must render and act on, and where
; "revoke device 3" has to mean the same thing on every hub. Opacity is the
; default and stays the default: the store MACHINERY is reused verbatim
; (chunking, repair, generation, caps) and only this one store's payload
; grammar is agreed centrally. Its `access` is `configure` like the rest of the
; admin surface — the paired-device list is not open reading, and a hub MUST
; gate BLOB_REQ on the declaring entry's access exactly as it gates SUBSCRIBE.
; ---------------------------------------------------------------------------
store-descriptor = {
  1 => uint,               ; store_id: u8, unique per hub; selects this store in `blob`.store_id.
                           ;   Roster and writer entries name it by entry-level key 17 (RFC-070).
  2 => tstr .size (1..32), ; kind: namespaced payload kind, e.g. "pattern.frayd", "trust.ledger"
  3 => uint,               ; capacity: item slots (conformance floor limits.preset_capacity_min)
  4 => uint,               ; per_item_max: max encoded payload bytes (default limits.preset_item_max_bytes)
  5 => uint,               ; name_max: max item-name bytes
}

; ---------------------------------------------------------------------------
; A STORE ITEM (RFC-073, SPEC §8.7): the self-contained document a BLOB_CHUNK
; stream for blob ns = 1 reassembles into, and a full-item `save` import. Keys
; are the registry `blob_keys` (one key space for one concept). Not a catalog
; type: it never appears in the catalog. The "trust.ledger" kind keeps its own
; `trust_ledger_keys` grammar (§12.6).
; ---------------------------------------------------------------------------
store-item = {
  3  => uint,              ; slot
  5  => tstr,              ; name, <= the store's name_max
  6  => tstr,              ; kind: "<domain>.<variant>"; domains open, advisory
  7  => bstr,              ; payload: opaque, <= the store's per_item_max
  ? 11 => bstr .size 32,   ; digest: SHA-256 over payload alone
}