Skip to content

Core entities

Partial

Core entities are the things an operator manages, component, system, location, and node, each with its own identity so that every sample, event, alarm, and config names exactly one owner. The shapes they pin are templates; the data they own is samples; the physical tables are storage.

  • A component is a deployed device, app, or service (a display, a DSP, a cloud UCC service). It owns samples and points at the product it is, the source of its shape; the pointer is required (a generic product covers anything not yet modeled more specifically), so a component always resolves a contract and, through its product, a component_type genus.
  • A system is a set of components doing one job: a meeting room, a classroom, a video wall. It is located at a location and conforms to a standard, the blueprint it is built against; the pointer is optional, mirroring component.product_id.
  • A location ties systems and components to a physical place, classified by location_type, its only shape-definer (no template). A starter type set ships seeded and platform-owned, an operator’s change to one held as a fork over it (the seed model), read at GET /location-types to feed the location form’s type picker. Each type carries an icon (a glyph key like building; unknown keys fall back to map-pin) rendered in the tree, and allowed_parent_types (a set of location_type ids and/or the reserved root sentinel): empty is unconstrained, a non-empty set is enforced on create and move. The shipped set and its parents are rendered from the seed on the location types guide rather than listed in two places.
  • A node is the edge process (omniglass node run) that pulls work, reaches components over interfaces, and ships results (nodes). It is structural because it is a first-class owner of its own self-health telemetry and node-owned alarms.
locationsystemcomponentcomponentcomponent

| Entity | What it is | |---|---| | component | a deployed instance (dsp-boardroom-3); points at its product, the source of its shape, required | | system | a composition of components / subsystems (the service tree); conforms to its standard | | location | a place tree; no template (the location_type is the only shape-definer) | | node | the edge process; addressed by its name, identified by the principal_id it is bound to (identity and access) |

The column mechanics render from the generated schema facts:

component (fleet): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null The unique operator-facing address (ADR-0062)
label text
parent_id uuid FK → component.id The self-referencing tree
location_id uuid FK → location.id
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
product_id uuid FK → product.id, not null The shape source, required; on delete restrict
name_generated boolean not null, default false
ordinal integer
label_generated boolean not null, default false
CHECK constraints and unique indexes on component
  • component_ordinal_check: CHECK (((ordinal IS NULL) OR (ordinal >= 1)))
  • component_location_name_key: CREATE UNIQUE INDEX component_location_name_key ON public.component USING btree (location_id, name) WHERE ((parent_id IS NULL) AND (location_id IS NOT NULL))
  • component_orphan_name_key: CREATE UNIQUE INDEX component_orphan_name_key ON public.component USING btree (name) WHERE ((parent_id IS NULL) AND (location_id IS NULL))
  • component_parent_name_key: CREATE UNIQUE INDEX component_parent_name_key ON public.component USING btree (parent_id, name) WHERE (parent_id IS NOT NULL)
system (fleet): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
label text
parent_id uuid FK → system.id The self-referencing service tree
location_id uuid FK → location.id Where the service lives
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
standard_id uuid FK → standard.id Optional pointer at the blueprint the system conforms to
system_type_id uuid FK → system_type.id
label_generated boolean not null, default false
name_generated boolean not null, default false
ordinal integer
CHECK constraints and unique indexes on system
  • system_ordinal_check: CHECK (((ordinal IS NULL) OR (ordinal >= 1)))
  • system_location_name_key: CREATE UNIQUE INDEX system_location_name_key ON public.system USING btree (location_id, name) WHERE ((parent_id IS NULL) AND (location_id IS NOT NULL))
  • system_orphan_name_key: CREATE UNIQUE INDEX system_orphan_name_key ON public.system USING btree (name) WHERE ((parent_id IS NULL) AND (location_id IS NULL))
  • system_parent_name_key: CREATE UNIQUE INDEX system_parent_name_key ON public.system USING btree (parent_id, name) WHERE (parent_id IS NOT NULL)
location (fleet): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
name text not null
label text
parent_id uuid FK → location.id The self-referencing place tree; the forest has N unparented tops
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
location_type uuid FK → location_type.id, not null
label_generated boolean not null, default false
name_generated boolean not null, default false
ordinal integer
CHECK constraints and unique indexes on location
  • location_ordinal_check: CHECK (((ordinal IS NULL) OR (ordinal >= 1)))
  • location_parent_name_key: CREATE UNIQUE INDEX location_parent_name_key ON public.location USING btree (parent_id, name) WHERE (parent_id IS NOT NULL)
  • location_root_name_key: CREATE UNIQUE INDEX location_root_name_key ON public.location USING btree (name) WHERE (parent_id IS NULL)
node (collection): generated from the migrated schema by make gen
Column Type Constraints Notes
principal_id uuid PK, FK → principal.id
name text not null The address the node enrolls and heartbeats under; fixed at create, unlike a component name
description text not null, default ''::text
last_heartbeat_at timestamp with time zone Liveness, stamped by the heartbeat lane
enrolled_at timestamp with time zone
labels jsonb not null, default '{}'::jsonb
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
label text
location_id uuid FK → location.id
CHECK constraints and unique indexes on node
  • node_name_subject_safe_check: CHECK ((name ~ '^[^.*> \t\n\r]+$'::text))
  • node_name_key: CREATE UNIQUE INDEX node_name_key ON public.node USING btree (name)

Catalog reference data: vendor, driver, capability

Section titled “Catalog reference data: vendor, driver, capability”

The component-classification catalogs are flat, seed-and-custom registries on the same official/custom pattern as the *_type registries (Location Types guide); the product catalog (below) sits above them as the concrete SKU a component points at:

  • A vendor (Boreal, Polaris, …) names an organization, with a kind of manufacturer, integrator, or developer (default manufacturer); the generalization of the former manufacturer-only component_make.
  • A driver (Generic SNMP, Kestrel Device API, …) names the implementation that gets, emits, or sets a product’s signals, with an optional version.
  • The capability registry retired (#626): a product’s device-class genus is now component_type (below), and what a system role requires is a typed-slot guard over it, not a capability set.

See the Vendors guide for the operator surface.

A component_type (display, projector, video-bar, dsp, mic, …) is the device-class genus: a seeded-plus-custom taxonomy that nests by parent_id and mirrors the OpenAVCloud AV Device Taxonomy v1.1 (ADR-0131): category roots, subcategory types, and concrete leaves, microphones subtyped by form factor (mic over wireless-mic, ceiling-mic, boundary-mic), on the same official/custom pattern as the other classification catalogs, operator-graftable at any node (ADR-0085). It classifies the product (product.component_type_id, required), not the component directly, so a component inherits its type through the product it is. The row carries the identity facts that genuinely span products, inheriting down the tree with override at any node: the naming stem (the auto-generated component name’s prefix, e.g. display-1), label, icon (the console glyph), abbrev (the two-to-three character hostname stem: fp, cam, dsp, cp), and default_tags. A blank fact on a child means “inherit the nearest ancestor’s”, and a node that states one of its own can be put back to inheriting: the patch reads an explicit "" on stem, abbrev, icon or label_rule as “this node declares nothing”, clearing the column to NULL (#716). A root’s stem is the one exception, since there is no ancestor behind it. It is not a shape-definer: contracts, declared properties, and drivers stay on the product. The seed discipline: a subtype exists only where a standard’s slot would name it (wireless-mic versus ceiling-mic, real subtypes; panel technology like lcd versus led stays a product fact, never a type).

The registry listing serves the resolved icon beside the raw one (resolved_icon next to icon, on both this registry and system_type), because a console that climbed the chain itself would be a second implementation of this walk in another language, and the failure that produces is a wrong glyph on a type the two disagree about (#695). Both fields ship because they answer different questions: an edit blade posts the raw one back, where blank still means inherit, and a list cell draws the resolved one. It costs no extra read: the listing is the whole registry in one query, so every ancestor is already in hand.

The listing also serves what a row would inherit, which is a third question and not the second one (#716). resolved_icon says what a row SHOWS, so on every row that states its own it is that row’s own value; inherited_stem, inherited_icon and inherited_abbrev say what the row would show if it stated nothing, each with the name of the ancestor it comes from (inherited_stem_source and its two siblings). That is the question an edit blade’s placeholder asks, and answering it with the shown value would print the string an operator had just deleted back at them as the thing they were about to inherit. The source is per fact rather than per row, because one type can take its stem from a grandparent and its abbrev from its parent, and it is measured rather than assumed to be free: TestListComponentTypesCostIsFlatInRegistryDepth grows the registry twenty levels deeper and pins the read at the same one statement.

The shipped taxonomy, rendered from the seed, with a blank cell meaning the row inherits that fact from its nearest ancestor:

TypeStemAbbrevIcon
display-device (Display Device) displaydispmonitor
display (Display) displayfpmonitor
interactive-display (Interactive Display) inheritsinheritsinherits
projector (Projector) projectorprojprojector
screen (Screen) screenscrtv
video-wall (Video Wall) videowallvwgrid
interactive-whiteboard (Interactive Whiteboard) whiteboardiwbpencil
audio-device (Audio Device) audioaudspeaker
mic (Microphone) micmicmic
wireless-mic (Wireless Microphone) inheritsinheritsinherits
ceiling-mic (Ceiling Microphone) inheritsinheritsinherits
boundary-mic (Boundary Microphone) inheritsinheritsinherits
gooseneck-mic (Gooseneck Microphone) inheritsinheritsinherits
speaker (Speaker) speakerspkspeaker
audio-mixer (Audio Mixer) mixermixsliders
dsp (DSP) dspdspcpu
dante-card (Dante Card) dantedcnetwork
amplifier (Amplifier) ampampspeaker
video-device (Video Device) videovidvideo
camera (Camera) cameracamcamera
ptz-camera (PTZ Camera) inheritsinheritsinherits
capture-encoder (Capture Encoder) encoderencvideo
codec (Codec) codeccdcvideo
control-device (Control Device) controlctltouchpad
touch-panel (Touch Panel) paneltptouchpad
button-panel (Button Panel) keypadbpgrip-vertical
control-processor (Control Processor) processorcpcpu
scheduling-panel (Scheduling Panel) schedulerspclock
occupancy-sensor (Occupancy Sensor) sensorocceye
signal-device (Signal Device) signalsigshuffle
presentation-switcher (Presentation Switcher) switcherswshuffle
matrix-switcher (Matrix Switcher) matrixmxshuffle
av-extender (AV Extender) extenderextnetwork
distribution-amplifier (Distribution Amplifier) dadashuffle
collaboration-device (Collaboration Device) collabcolvideo
video-bar (Video Bar) videobarvbvideo
wireless-presenter (Wireless Presenter) presenterwpvideo
playback-device (Playback Device) mediamedtv
media-player (Media Player) playermptv
recorder (Recorder) recorderrecserver
infrastructure-device (Infrastructure Device) infrainfserver
power-device (Power Device) powerpwrbox
network-device (Network Device) networknetnetwork
generic-device (Generic Device) devicedevbox
generic-app (Generic App) appappapp-window
generic-service (Generic Service) servicesvcserver

This partially reverses ADR-0047, which retired the component-level component_type in the fields fold: the reintroduced shape is deliberately different, above the product rather than beside the component.

A product (Kestrel VRoom, Boreal Edge 55, …) is the concrete SKU: a stable id and label, a kind (device / app / service, no default, required at create so a mislabeled cloud service can never read as correct through a silent fallback; ADR-0086 retired vm, folded into app, since nothing forked on it that did not fork the same way on app), a required component_type_id (the genus above it), an optional icon override (unset inherits the component_type’s icon; a per-SKU glyph is the exception, not the rule), optional vendor_id and driver_id, an optional parent_product_id (a variant points at its base), and the official boolean. The vendor, driver, and parent FKs are on delete set null; the component_type FK is on delete restrict (a type still classifying a product cannot be deleted). Three generic products (generic-device, generic-app, generic-service) ship official, each pointed at the matching generic component_type, so an unmodeled component always has somewhere to point.

A component points at the product it is through component.product_id, required (on delete restrict): the referential guard the leaf catalogs deferred, so a product still referenced by a component cannot be deleted (409), and a component can no longer exist unclassified. A component’s component_type is never stored on the component itself; it is read through the product (component.product_id -> product.component_type_id). See the Products guide.

A system_type (board, class, meeting, video-wall, …) is the coarse space genus: what kind of space a system is. It is the system-side counterpart of component_type and it nests the same way, by parent_id, on the same official/custom pattern, operator-graftable at any node (ADR-0096). The shipped tree is universal, and it is rendered from the seed rather than restated here, so a blank cell is a fact the row genuinely inherits rather than one somebody forgot to copy:

TypeStemAbbrevIcon
av (AV) avavlayers
room (Room) roomrmdoor-open
board (Boardroom) boardroombrinherits
class (Classroom) classroomclsinherits
meeting (Meeting Room) meetingmtginherits
training (Training Room) trainingtrninherits
conference (Conference Room) conferenceconfinherits
huddle (Huddle Room) huddlehudinherits
auditorium (Auditorium) auditoriumaudtheater
sign (Signage) signagesgntv
video-wall (Video Wall) video-wallvwinherits
interactive-sign (Interactive Sign) interactive-signisgntouchpad

The row carries the identity facts that span every space of that kind, inheriting down the tree with override at any node: the naming stem (the prefix a generated system name is built from, e.g. boardroom-2), label, icon (the console glyph), and abbrev (the compact label form: br, cls, vw). A blank fact on a child means “inherit the nearest ancestor’s”, so board states its own stem and abbrev while taking its icon from room, which overrides av’s. A node that states a fact of its own goes back to inheriting by patching that field to "", the same three-state sentinel component_type uses (#716). A root must carry a stem of its own: it has no ancestor to inherit one from, so a blank there is not “inherit”, it is “nothing”, and clearing a root’s stem is refused (422) exactly as creating one without a stem is.

A system is classified through system.system_type_id, nullable for now (a floor waits until the shipped tree has proven out). It is on delete restrict, as parent_id is: a type that still classifies a system, or still parents another type, cannot be deleted.

Unlike component_type, it carries no default_tags. A product’s instances start from its type’s tag set, while a system’s effective tags come from the platform, its location, and its own system tree, so the column would have no reader.

The type is not the standard, and both stay. The type says what a system is; the standard below says which blueprint it is built to. One fleet holds ten signage standards and six classroom standards under a single coarse type, which is exactly why standard’s own parent_standard_id inheritance (which expresses design forks, two ways to build the same kind of room) is the wrong axis for this question.

A standard (Huddle Room, Classroom, …) is the blueprint a system conforms to: a stable id and label, an optional parent_standard_id (a variant points at its base), and the official boolean; the promotion of the former system_type label registry (ADR-0048).

A system conforms through the optional system.standard_id; a one-off system is first-class and carries no contract. The standard is a separate axis from system_type above: a system can carry either, both, or neither. Conformance is not a copy: the standard’s contract defaults resolve live for every conforming system until it overrides one. See the Standards guide.

Declared properties: the classifier contracts and the declared rows

Section titled “Declared properties: the classifier contracts and the declared rows”

A classifier also declares what its instances expose, one row per declared name, unique per (classifier, name): product_property, standard_property, and location_type_property carry a property_type_id FK into the property_type catalog, and their metric siblings (product_metric, standard_metric, location_type_metric) carry a metric_type_id into metric_type, each with an optional default_value in jsonb and a required flag; the API body carries the type name, resolved to the FK at the edge. Type and validation live on the catalog row, never repeated here.

The value lives in the property series itself: an operator-set value is a declared series row (provenance='declared', jsonb value, the same owner exclusive-arc, type FK, and instance discriminator every sample carries), beside the rows a device observed, a rule calculated, and a command intended (ADR-0079). An edit appends, an unset appends a tombstone, and the history is the series (samples).

The read is EffectiveProperties(ownerKind, ownerID), one query with two arms: the contract arm (every property the classifier declares, resolved to coalesce(the instance's current declared row, the contract default), marked from_contract) and the off-contract arm (values set directly on the instance); an instance with no classifier set (a productless component, a one-off system) resolves to its off-contract arm alone, and a node, with no classifier, always does.

One resolver serves all four owner kinds. The pairs differ only in five identifiers (instance table, classifier column, contract table, contract key, arc column), held as data in an ownerContract table, so the SQL is written once, the primitive-first move (ADR-0048):

| Owner kind | Classifier column | Contract table | |---|---|---| | component | component.product_id | product_property | | system | system.standard_id | standard_property | | location | location.location_type | location_type_property | | node | (none) | (none) |

Every arc is scope-checked on the write: setting a value outside your scope is a non-disclosing 404.

This pair replaces the retired field_definition / field_value feature: a “field” was always a property with declared provenance (ADR-0047). See the Properties guide.

System roles: the slots a system needs filled

Section titled “System roles: the slots a system needs filled”

A contract says what a system carries; a system role says what it needs filled: a table microphone, a main display. A role is a slot, not a component, so an unfilled slot is visible rather than absent.

Five tables carry it. system_member is the membership binding a role attaches to, many-valued and cascading from both ends; system_role is the slot, on the same arc property uses (one-set CHECK, unique nulls not distinct over the arc plus name); system_role_type and system_role_product are the typed-slot guard, the component_types a filling component’s product must be classified within (self or a descendant) and, optionally, the specific products it pins; and system_role_assignment is who fills the role here.

system_member (fleet): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
is_primary boolean not null, default false The one membership that carries the system band in the cascade
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
component_id uuid FK → component.id, not null
system_id uuid FK → system.id, not null
CHECK constraints and unique indexes on system_member
  • system_member_one_primary_idx: CREATE UNIQUE INDEX system_member_one_primary_idx ON public.system_member USING btree (component_id) WHERE is_primary
  • system_member_system_id_component_id_key: CREATE UNIQUE INDEX system_member_system_id_component_id_key ON public.system_member USING btree (system_id, component_id)
system_role (identity): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
owner_kind text FK → choice_alternate.owner_kind, not null
name text not null
label text
quorum integer not null, default 1 How many fillers the slot wants before health degrades
created_at timestamp with time zone not null, default now()
updated_at timestamp with time zone not null, default now()
impact text not null, default 'degraded'::text What a broken filler does to the rollup
system_id uuid FK → system.id
standard_id uuid FK → standard.id
capacity integer
position_labels ARRAY not null, default '{}'::text[]
alternate_id uuid FK → choice_alternate.id
owner_ref uuid FK → choice_alternate.owner_ref
CHECK constraints and unique indexes on system_role
  • system_role_capacity_check: CHECK (((capacity IS NULL) OR (capacity >= quorum)))
  • system_role_impact_check: CHECK ((impact = ANY (ARRAY['outage'::text, 'degraded'::text, 'none'::text])))
  • system_role_owner_arc_check: CHECK ((((owner_kind = 'standard'::text) AND (standard_id IS NOT NULL) AND (system_id IS NULL)) OR ((owner_kind = 'system'::text) AND (system_id IS NOT NULL) AND (standard_id IS NULL))))
  • system_role_owner_kind_check: CHECK ((owner_kind = ANY (ARRAY['standard'::text, 'system'::text])))
  • system_role_quorum_check: CHECK ((quorum >= 1))
  • system_role_name_key: CREATE UNIQUE INDEX system_role_name_key ON public.system_role USING btree (owner_kind, standard_id, system_id, name) NULLS NOT DISTINCT
system_role_type (identity): generated from the migrated schema by make gen
Column Type Constraints
role_id uuid PK, FK → system_role.id
component_type_id uuid PK, FK → component_type.id
system_role_product (identity): generated from the migrated schema by make gen
Column Type Constraints
role_id uuid PK, FK → system_role.id
product_id uuid PK, FK → product.id
system_role_assignment (identity): generated from the migrated schema by make gen
Column Type Constraints Notes
id uuid PK, default uuidv7()
role_id uuid FK → system_role.id, not null
created_at timestamp with time zone not null, default now()
component_id uuid FK → component.id, not null The filler; on delete restrict so a staffed component cannot vanish
system_id uuid FK → system.id, not null
position integer not null
CHECK constraints and unique indexes on system_role_assignment
  • system_role_assignment_position_check: CHECK (("position" >= 1))
  • system_role_assignment_component_key: CREATE UNIQUE INDEX system_role_assignment_component_key ON public.system_role_assignment USING btree (system_id, component_id)
  • system_role_assignment_position_key: CREATE UNIQUE INDEX system_role_assignment_position_key ON public.system_role_assignment USING btree (system_id, role_id, "position")
  • system_role_assignment_system_id_role_id_component_id_key: CREATE UNIQUE INDEX system_role_assignment_system_id_role_id_component_id_key ON public.system_role_assignment USING btree (system_id, role_id, component_id)

A component doing a job in a system is a member of it, and the role is what that membership does: one attachment seen at two levels. Membership is many-valued (a rack DSP feeding three rooms is a member of all three). Staffing a role creates the membership, so nobody says it twice; the reverse is not symmetric: giving up a role does not end membership, and a member carrying no role is ordinary (the spare on the shelf).

is_primary marks which membership seeds the cascade for a caller without a system in hand: a default, never a rule overriding a caller who named a system. The first membership takes it automatically; a partial unique index forbids a second primary.

A component create naming a system writes exactly this row, which is why it costs what the membership route costs: system:update, the same permission and the same scope as PUT /systems/{name}/members/{component}, and not the component:create that admits the rest of the form (ADR-0107). An operator holds no system permission, so it creates components and a deploy tech binds them into systems.

Membership cascades from both ends, and does not restrict the component the way system_role_assignment does: that restrict is load-bearing (deleting a filler would silently break health); a role-less membership is not worth blocking a delete over.

A role is declared on a standard or on a system, the same exclusive-arc pattern the sample tables use: standard-declared roles are inherited live by every conforming system, like a contract default; system-declared ones are ad-hoc, how a one-off system gets roles and a conforming system adds beyond its standard. A role carries a quorum (two ceiling mics is one role with quorum 2), at least one.

The typed-slot guard is what AssignRole checks (#626). A component fills a slot only when its product’s component_type falls within a type the role’s system_role_type set names (self or a descendant; empty accepts any type), and, if system_role_product pins specific products, only when its product is one of them.

The read side is EffectiveRoles(system): the standard’s roles (marked from_standard) UNION those declared directly, each with its typed-slot requirement, quorum, and fillers here, serving assigned and understaffed (quorum minus assignments, floored at zero) so every surface reads the same.

Assignment is refused when the component cannot fill the role, naming both parties:

component "panel-1" is a display; role "table-mic" wants a video-bar

A refusal on modeled grounds, and it runs once, at assignment; afterward an occupant keeps its slot unless its own health verdict goes to outage (a lesser alarm degrades it but does not cost it the slot) (ADR-0049).

A role also declares its impact: outage, degraded, or none, what an impaired role (fewer satisfying components than quorum) means for its system. It lives on the role because the same broken box matters differently per slot: a dead confidence monitor is not a dead main display. Impact is the one input health takes from the declaration side; quorum is the redundancy knob beneath it. Staffing stays readable without health (a role wanting two with one assigned is under-staffed today); health adds whether the assigned components can currently do the job (ADR-0050).

The seed model: forked templates versus canonical catalogs

Section titled “The seed model: forked templates versus canonical catalogs”

Shipped content is two kinds, never conflated:

  • Example content (a standard) is created by forking an in-code template (#317; shipped boot seeds stand in today). The fork is one-time, no inheritance: nothing points back at the template, so it can improve per release without touching a tenant. What lands is an operator-owned row (official: false), installed only if absent (ON CONFLICT DO NOTHING) so re-seeding never reverts an edit; a shipped standard’s roles ride the same lane.
  • Canonical vocabulary (the metric_type and property_type catalogs, the event_type and command_type registries) is the shared namespace a driver maps onto, identical install to install: seeded official: true through an authoritative upsert (ON CONFLICT DO UPDATE), never written by an operator. The classification catalogs (vendor, driver, component_type, product) ride that path today, and location_type joined them in ADR-0106: a fleet still shapes its own place vocabulary, through the registry fork rather than by owning the row, which is what lets a release WITHDRAW a shipped value.

Forking applies to template -> row, never to classifier -> instance: a system conforms, never forks.

An operator forks a shipped row instead of the platform writing it

Section titled “An operator forks a shipped row instead of the platform writing it”

A shipped official: true row was flatly read-only, which is correct as far as it goes and left an operator with nowhere to go: they could neither correct a label nor change an icon on a row the release owns. They now fork it (ADR-0095, #655):

  • An edit stores the operator’s whole version of the row’s mutable columns in registry_shadow, keyed on the shipped row’s own uuid, and every read resolves the shadow over the official row.
  • The shipped row is never written, so the next release can still improve it, and restore (POST /component-types/{id}:restore) discards the shadow to take those improvements back.
  • The address never changes: one uuid and one name per logical row, forked or not, so foreign keys, the tree walks, the audit trail and the URL all keep naming what they already named. The namespace is not an addressing concept and never reaches a caller.
  • A fork restates facts, never structure: the uuid, the name, the official flag and the parent link stay official. On a nested registry that makes inheritance resolvable per node, so forking an ancestor reaches every descendant that does not override the field, and forking a leaf does not cut it off from what it inherits.
  • The console reads it as three origins: official (shipped, untouched), custom (yours), and overridden (a shipped row carrying your version).

component_type is the first adopter. The other registries carrying official adopt as their slices land; the primitive is registry-agnostic and needs no schema of its own per adopter.

component, system, and location are each a variable-depth tree: a parent_id self-reference nesting to arbitrary depth (campus -> building -> floor -> room). The trees are the structural backbone of the cascade, which resolves over an entity’s containment path; precedence is stated once, there.

componentproductlocationsystemstandard is a (N:1)parent (tree)conforms to (N:1)located at (N:1)parent (tree)parent (tree)

The parent_id self-reference is same-kind nesting: a chassis with line cards is a parent component over children, and likewise a parent system over room subsystems. The nesting feeds the cascade (deeper wins) and the health rollup (sub-system into parent system, sub-component into parent component, health). The practical starting depth is 3 levels, not a hard cap.

Everything observed, asserted, or set attaches to exactly one structural entity through the exclusive-arc. Every sample table, plus event, property, alarm, and variable, carries:

  • an owner_kind enum, plus
  • the matching typed FK (component_id / system_id / location_id / node_id), plus
  • a CHECK that exactly the column matching owner_kind is set.

Every built CHECK admits exactly component | system | location | node.

This makes system-, location-, and node-level samples first-class (health is a property owned by a system; a node’s self-health is owned by the node); fleet-wide samples owned by global are the designed extension. The owner gives a recorded health verdict its level: component, system, and location each own a transition series under the same health property name. The same arc owns the event rows a sample produces, and is the design for alarm (component-local today), so a system-owned sample will yield a system-owned alarm. The full pattern and DDL are on storage.

The event sink: the first arc-owned occurrence

Section titled “The event sink: the first arc-owned occurrence”

The arc’s first built sink beyond the sample tables is event, the occurrence sink: a past occurrence with no “what is it now?”, against a metric or property’s sampled present value (the has-a-value-now razor).

An event row carries the identical owner exclusive-arc as a sample (same one-set CHECK) and the same provenance vocabulary (observed / calculated / intended / declared, default observed), so the same owner-confinement and reject-not-project gates apply at ingest. The occurrence type (event_type_id, resolved from the event_type name), the lineage columns, and the separate raw log_line lane (ADR-0066) are the events page’s model. Closing the loop, an intended-provenance sample names the command that opened it (command_id, a real foreign key on both sample tables); the command’s caused event stays stamped on the row but is optional (samples, ADR-0079).

Structural multi-membership (a component in N systems)

Section titled “Structural multi-membership (a component in N systems)”

system_member is many-valued; what stays single-valued is which system chain seeds the cascade. Why, how is_primary picks it, and the per-system effective views are stated once, on cascade.

The design below layered the role onto the system_member row and pinned it to a frozen template BOM; what shipped keeps the row to the binding alone and lets system_role_assignment carry the role.

Three things, and no fourth:

  • id, a uuid. Immutable, the primary key, and what every foreign key stores, so a rename has nothing to rewrite.
  • name, the renameable machine identifier an operator types and a URL, a CLI argument, and an address carry (the rm215a in boi.17c.rm215a). Renameable is the point: kestrel-vroom is a typo or a rebrand away from being wrong forever if it cannot change.
  • label, an optional friendly string a human reads (“HQ Boardroom DSP”). Absent is normal: boreal needs no label. Absent has exactly one spelling, SQL NULL: the column is nullable, and the rule that keeps the empty string out of it lives in Go at the gateway write path, where every other rule of its kind lives (ADR-0118). A read projects coalesce(label, ''), so nothing above the Storage Gateway has to know. A surface with no label renders the name verbatim, never re-cased or prettified. The one spelling is what lets a registry list say order by label nulls last, name and mean it: Postgres sorts NULL last already, and an ordering that had to say nullif(label, '') would be a read undoing the way the value was stored.

Because the id is what everything stores, audit_log.resource_id included, a rename moves exactly one column and the history of the row survives it intact. Changing a name is therefore its own act rather than a field write: a :rename custom method on the collection, gated <resource>:rename, with no name in the PATCH body at all (ADR-0076).

Every exception is declared in internal/storage/identity_shape.go and rendered below, so this page cannot drift from the code. Most tables bear a name on the ordinary entity rule and the shape explains itself. Two groups need a reason: the keyspace tables, the catalog registries whose names are fleet-wide vocabulary rather than rows inside a parent, and the tables carrying a human-readable identifier that is not a name at all.

A keyspace table carries a name on the same one kebab rule as every other name; the shape survives as a declaration of what the name is for, not a second rule. A keyspace name (icmp-rtt-avg) is fleet-wide vocabulary a telemetry record is typed by, where an entity name addresses one row under a parent. The dotted path rule and its 128-character ceiling retired with the one-name-rule collapse (#586): storage.ValidateName applies the one rule to every declared name-bearing table, so a call site cannot skip validation or invent a second rule. The console labels the identifier Name and the friendly string Label under both shapes.

tablewhy it is not on the entity name rule
command_type set-input, a command name
event_type call-started, an occurrence name
metric_type icmp-rtt-avg, a numeric-series name referenced from drivers and templates
property_type serial-number, a signal name referenced from drivers and templates

A human identifier that is not a name is the set worth stating out loud, because each looks name-shaped from a distance and must never acquire the name rule.

tablewhy it is not on the entity name rule
blob content-addressed by sha256, for the same reason
file a filename with an extension (codec-firmware-2.1.4.txt): not unique, already the label, and addressed by uuid
human a username: its own rule, its own uniqueness, and not an address, since a principal is addressed by uuid
service the username analogue for kind=service: unique like one (#563), on its own rule rather than the entity name rule, and not an address, since a principal is addressed by uuid
task content-addressed, the id IS hash(interface, kind, schedule, params), so a name would be a second identity for the same row

Everything else is id-only: a join row and a telemetry row are addressed by uuid because nobody names them. An operator names the component, not the metric.

Every table an operator names carries one, and the declaration says so rather than leaving it to be noticed. That is not how it started: the shape said what the identifier was and nothing about the friendly string beside it, so tag, variable, secret and interface went without a label, no guard failed, and an operator who could no longer write cost_center (the tightening ADR-0076 made) had nowhere to write “Cost Center” instead. All four carry one now (ADR-0119), and a name-bearing table that carries neither the column nor a reason fails the guard.

A table nobody names is outside the rule and carries no label at all: there is no name for an unset one to fall back to, so the column could only ever render a blank. The exceptions, which are tables an operator names that still carry no label, are these, with the reason each one gives:

tablewhy it carries no label
blob no operator surface of its own: a blob is reached through the file that references it, and nothing renders a blob row for a label to appear on
file its name is already the label, so a second string would be a second label rather than the identifier a label sits beside (#613 out of scope, D1 deferred to #755)
interface_type retires with the interface.type FK (ADR-0073); a label added here would be dropped with the table
service the identifier IS what every surface shows: principalIdent resolves a service principal to service.name, and it cannot resolve to a label, which is optional and not unique

Every component, system, and location resolves two ways, with different guarantees (ADR-0089). The uuid is the address: immutable, the only reference the platform itself ever generates or persists, surviving every rename and move. A dotted path is a positional lookup: location segments from a root, an accessor that switches plane ($comp into the component tree, $sys into the system tree, $role reserved for a future system-role read but resolved by nothing today), then plane-local segments: boi.17c.415a.$comp.dsp-1.dante-card-1 is a Dante card inside a DSP chassis in room 415a. Where the resource is already known from context (a CLI noun, a REST collection), a bare name with no accessor works the same way, resolving within that resource’s own placement.

The containment rule is what keeps a path from going stale on its own: the platform generates uuids only, so a dotted path exists only where a human typed one, into a CLI, a runbook, or a hand-built request. A path built entirely from platform-owned name segments cannot drift either, because a platform-owned name recomputes at every :move (below); only a segment an operator renamed can go stale, and only because the operator chose to.

Every reference form the API accepts resolves through one {ref} path parameter (GET /components/{ref}, and every custom method beside it): a uuid, a bare name, or a dotted address, tried in that order. A bare name matching more than one row in the caller’s scope is a 409 naming the candidates rather than silently resolving to whichever sorted first, since #627 scopes a name’s uniqueness to its placement, not the whole fleet: a component and a system both scope to their parent when they have one, else their location, else the fleet, a three-bucket split each carries its own partial unique index for; a location, which has no location_id column of its own, scopes to its parent when it has one, else the fleet, two buckets rather than three. The accessor is spelled with a $ precisely because the name rule excludes it, so $comp can never collide with a location legitimately named comp; the cost is that an unquoted $comp is exactly what a shell strips to nothing before the request ever leaves the terminal (url.PathEscape does not help; it leaves $ unescaped), so every CLI example carrying an accessor is single-quoted, 'boi.17c.415a.$comp.display-1', never bare.

The name rule is an allowlist, not a denylist, which is the fact the address grammar depends on: ^[a-z0-9][a-z0-9-]*$ never admits ., $, *, >, /, #, %, or :, so a name can never collide with a path separator, an accessor, a NATS or MQTT wildcard, a URL escape, or the router’s own :verb suffix, in any protocol the platform speaks today or adds later. A GET/LIST response on a component, system, or location carries the resolved path and path_segments alongside two display-only renders, a dash-joined form (boi-17c-415a-display-1) and a compact form that substitutes the last segment’s component type abbrev for its stem (boi17c415afp1); neither round-trips through the resolver, because stripping the accessor or compacting the stem both lose information a resolve needs back. The abbrev substitution is a single-row cost a LIST skips (no console surface reads it there): a listed row’s compact render falls back to the segment concatenated as-is (boi17c415adisplay-1), and only a single GET pays the extra lookup and substitutes the abbrev in full.

A component’s name can generate itself (ADR-0090). Left blank on create, the platform mints <component_type stem>-<n> from the resolved type and the lowest free ordinal in the component’s placement scope; while name_generated stays true the name recomputes on a :move that changes the bucket or a reclassify that changes the stem, riding that write’s own audit event (see what re-mints a name below, which is one rule across all three tiers). :rename freezes it; :resetName hands the pen back and regenerates from the component’s current facts, gated by the same component:rename permission :rename uses.

The console can ask for a generated name, and shows the one the row will get (ADR-0104). A create form asks for the classification and the placement first, since those are what the rules read, and then shows the name a blank field will be filled with, drafted by the server through POST /components:renderLabel (and its system and location twins): the resolved stem and the lowest ordinal free in that placement bucket right now, display-3 rather than display-n. Reading that number is not minting one, so the draft still takes no advisory lock, opens no write transaction and allocates nothing; a form may ask on every picker change without joining the queue every create in the fleet is waiting in. There is deliberately still no draft verb that MINTS and rolls back, for the reason it never had one.

The number is therefore true rather than reserved, and the create is where that is settled. The form posts the drafted NAME back as expected_name, the create allocates under its own lock exactly as it always did, and one that would stamp a different name is refused with a 409 naming what it would have stamped. The form re-reads the draft, shows the new name, and the operator submits again. A refusal is honest where a silent difference is not, and it is the only outcome the locked field’s promise admits.

The name and not the ordinal, because the name is the whole claim: it carries the stem, the suppression rule and the number, so a type edited under an open form invalidates it by construction where a number alone would have passed and landed monitor-1 under a field showing display-1. And it is still a precondition rather than the name field: the create leaves name empty, so the row is name_generated and the platform still owns the number. Posting it beside a name an operator typed is a 422, since nothing is allocated on that path for the expectation to be about.

A draft answers only for a bucket the caller could create in. Omitting the parent names the parentless bucket, which every create refuses without an all-scoped grant, so the draft refuses it with the same 403: the previewed ordinal is read from the bucket’s sibling names, so answering would report which of that bucket’s names are taken, for whatever stem the caller put in a type’s name rule.

The placement bucket is shown beside the field as a path, never as a prefix inside it: a name is unique within its placement rather than across the fleet, so it no longer carries its ancestry.

The ordinal is stored, and it says who owns the name (ADR-0097). component.ordinal records the number a generated name was minted from, and it is NULL on every row the platform did not name: an operator-typed name, or a row :rename handed over (which clears the column in the same statement it clears name_generated). That is what the bare render above reads, so a component an operator called rack-3 has no number to compact and keeps its own words. Allocation itself still reads sibling names, not this column, and returns the lowest ordinal whose minted name is free: an operator can hold display-1 by hand while owning no ordinal, and the uniqueness that has to be satisfied is the name’s. Asking the mint for candidates rather than picking siblings apart is also what makes a name that is nothing but its ordinal expressible, which is what a positional type (a parking deck) is named with below.

A system names itself too, and the first of a kind carries no number (ADR-0101). The pen, :rename and :resetName are the same three things on a system and a location that they are on a component. A system left nameless on create mints its system_type chain’s stem plus the lowest free ordinal in its placement bucket, and suppresses the ordinal on the first of that stem there: a room’s only boardroom is boardroom and the second is boardroom-2. Allocation stays lowest-free, so deleting the bare one while the second survives frees the bare name for the next create, and boardroom-1 never exists at all. A hand-typed boardroom blocks the generator exactly as a hand-typed display-1 blocks a component’s, because what has to be unique is the name. A system with no system_type cannot be named this way (422 naming the reason), and neither can it be un-classified while the platform owns its name: :rename claims the name first.

What re-mints a name is an act that changes an INPUT to the mint, and the mint reads exactly two: the stem (a component’s product to component_type chain, a system’s system_type chain, a location’s location_type rule) and the placement bucket. So a reclassify that actually changes the stem re-mints, and a :move that actually changes the bucket re-mints; a patch that re-states the classification and a move that re-states the placement change neither input and leave the name exactly where it is. The distinction is not tidiness: a lower ordinal freed by an earlier :rename means a spurious re-mint does not recompute to the same answer, it MOVES the name, under :update or :move with no rename asked for and possibly no :rename grant held. One rule, spelled per tier because the tiers reach their two inputs differently. A component’s and a system’s bucket comparison is the three-way one (a parent wins over a location, so relocating a parented row changes no bucket); a location’s is the parent alone. And on both tiers that mint from a stem, the reclassify compares the resolved stem rather than the classification it was handed, because neither classification IS the stem: a system_type inherits one from its ancestors, so two sibling types under a stem-carrying parent mint the same name, and a component names a product, which points at a component_type chain, one hop further still, so two products under one type do too.

A location type opts in, with a rule that is a declaration (ADR-0102). location_type.name_rule is nullable and its presence IS the opt-in: NULL means an operator names every location of that type, and there is no boolean beside it for the two to disagree about. No shipped place type carries one, so a shipped fleet names every location by hand: a campus, a building, a floor and a room each have a real-world name an operator holds, and 17c is ground truth rather than a default.

floor was the one exception for two slices, shipped positional, and ADR-0103 reversed it: a floor’s designation is not an integer. Buildings sign B2, LG, G, M, 1, 12A, P3, so an allocated ordinal is the wrong kind of value rather than an imprecise one, and the basement case people reach for first (-1 is unspellable under the name rule) turns out not to exist, since that floor is signed B1, already a legal name. The consequence is stated rather than hidden: location name generation ships dormant, correct and tested but demonstrated by nothing in a shipped fleet, and reached when an operator declares a positional type of their own.

The rule is a small declaration, a stem plus whether the first of that stem in a parent carries an ordinal, and it decodes straight into the same mint a component and a system are named from. An empty stem is a positional type, whose ordinal genuinely is its name: the first parking deck of a building is 1, the next is 2, allocated among that building’s own decks, so every building has its own deck 1. A positional type is right where the number is an arbitrary disambiguator (a deck, a rack row, a berth) and wrong where it is a fact somebody reads off a wall. That is deliberately not the label_rule template beside it on the same registry: a label that fails to render falls through to the next rung of the read ladder, and a name has no next rung, so a rule that would mint an illegal name is refused when the rule is edited by minting from it, rather than discovered by a create it breaks.

Editing a rule renames nothing. The name, the ordinal and the pen of every existing location stand; the new rule decides how the next nameless create, :resetName, move or reclassify names a row. There is no name-side counterpart of the label’s preview-then-apply recompute, and the asymmetry is the point: a bulk relabel is display and recoverable, a bulk rename breaks the bookmarks, runbooks and integration config stored outside this system. :resetName is the deliberate, one-row-at-a-time way to bring a location onto a new rule, and it is gated by location:rename because it is one.

A location whose type carries no rule still refuses :resetName (422, naming the missing fact), which is what every location did before this rule existed, and the pen shipped a slice ahead of the generator on purpose: a location an operator named then is already frozen now.

A label generates itself too, from a rule an operator can change (ADR-0098). label gains the same stored pair the name has: label_generated is the pen, true meaning the platform owns the field. Reading a label is three rungs, the one an operator typed, else the one the rule produced, else the entity’s name; the first two are the same column, told apart by the pen. Typing a label clears the pen, clearing the field returns it and the rule renders again, and a rule with nothing to say leaves the field unset rather than blank.

Rules resolve most-specific-first, and NULL at a tier means “no opinion, ask the next one out”: product then the component_type chain then the global rule for components, standard then the system_type chain then the global rule for systems, and location_type then the global rule for locations. Locations have two tiers rather than three because there is no location-side counterpart of a product, not because one is missing. On a nested registry the rule inherits by the same first-non-null walk stem and abbrev follow, per node across the fork boundary (ADR-0095), so setting a rule on a shipped type forks that row and leaves the official one byte-identical.

The rule language is Go text/template over a closed map of strings and a closed grammar. The map is the sandbox for what a rule can READ, and there is no mechanism by which a name outside it resolves to anything, so a secret is unreachable because it was never put in rather than because a filter refused it. The keys differ per entity kind, are rendered below from the one declaration the engine builds them from (#729), and every one of them is always present: an absent fact is the empty string, which is falsy, so {{if .Ordinal}} is how a rule adapts. The grammar is the sandbox for what a rule can DO: an allowlist over the parsed tree admits a closed set of node types and a closed set of function names, and refuses everything else text/template offers, printf and variable declarations and {{range}} among them. That second half is not tidiness: a value built inside a pipeline is never written, so a cap on rendered length cannot see it, and a few hundred bytes of rule can otherwise allocate gigabytes on every write to every entity of its type. A rule that does not parse, or that names something outside the grammar, is refused at rule-edit time (422) and never stored; a rule that fails while rendering one entity degrades to no label rather than failing the write it was riding.

The functions are the engine’s own, rendered here from its one declaration rather than restated, so a function this table names is a function the FuncMap defines and the grammar admits (#701):

FunctionWhat it doesExample
titleUpper-cases the first letter of each word and leaves the rest of the word alone, so casing a vendor chose survives. A word in the acronym list is replaced by that list's own form.ceiling mic renders Ceiling Mic
upperUpper-cases every letter.dsp renders DSP
lowerLower-cases every letter.DSP renders dsp
slugReduces a string to one kebab token of lowercase letters, digits and single hyphens, which is exactly the shape a name has to have.HQ Boardroom (North) renders hq-boardroom-north
wordsTurns the separators a name is built from into spaces, which is what lets a rule read a kebab name as something a person would say. It is slug's opposite number.north-wing renders north wing

Beside those, a rule may name the logic and comparison builtins ( and, or, not, eq, ne, lt, le, gt, ge ), and nothing else text/template offers. A rendered label is capped at 200 characters.

The map itself, per entity kind:

A component rule reads 9 keys:

KeyWhat it holds
{{.Name}}The component's own name, whether an operator typed it or the platform minted it.
{{.Ordinal}}The number the component's NAME carries, and empty when it carries none: an operator named the row, or the mint suppressed the first of its stem in the bucket (ADR-0101). Empty is falsy, so {{if .Ordinal}} is the whole of the suppression rule a rule author writes.
{{.TypeName}}The label of the component_type the product classifies it as, resolved over an operator's fork of a shipped row.
{{.TypeAbbrev}}The abbreviation inherited down the component_type chain, first non-null wins.
{{.Stem}}The naming stem inherited down the component_type chain, the token generated names are built from.
{{.ProductName}}The product's label.
{{.VendorName}}The vendor's label, empty when the product names no vendor.
{{.LocationLabel}}How the location this component sits AT reads: the label an operator typed or the platform rendered, else that location's own name. It is the row's own location, never an ancestor's and never its plane root's, so a nested component with no location of its own reads it as absent.
{{.SystemTypeLabel}}The label of the type of the system this component is a PRIMARY member of, empty when it is a member of none.

A system rule reads 7 keys:

KeyWhat it holds
{{.Name}}The system's own name, whether an operator typed it or the platform minted it.
{{.Ordinal}}The number the system's NAME carries, and empty when it carries none, so the two halves of a divisible boardroom read as the name shows them (ADR-0101).
{{.TypeName}}The label of the system_type it is classified as.
{{.TypeAbbrev}}The abbreviation inherited down the system_type chain, first non-null wins.
{{.Stem}}The naming stem inherited down the system_type chain.
{{.StandardName}}The label of the standard it conforms to, empty when it conforms to none.
{{.LocationLabel}}How the location this system sits at reads: the label an operator typed or the platform rendered, else that location's own name.

A location rule reads 2 keys:

KeyWhat it holds
{{.Name}}The location's own name, which an operator types unless its type carries a name rule.
{{.TypeName}}The label of the location_type it is classified as, resolved over an operator's fork.

The keys are flat, never dotted: the map’s values are strings, so there is nothing to traverse through, and {{.Location.Label}} would be a handle on another row rather than a fact copied out of one. The differences between the three are the design rather than gaps. A location has no product and no vendor because it is not an instance of a catalog row, and no placement fact because a rule that read its parent would stale every descendant on a campus rename.

A rule reads placement, and that is what the recompute verb is for (ADR-0100). LocationLabel and SystemTypeLabel above are the keys behind {{.SystemTypeLabel}} {{.LocationLabel}} {{.TypeName}} rendering Boardroom 204B Display, and they are the reason this epic has a recompute verb at all: they are facts on rows the labelled entity does not own.

Because a label is stored, every fact a rule reads is a fact some write has to re-render on, and those facts now live on other rows. The line is blast radius rather than ownership. Bounded by a placement, the fleet is restamped inside the act’s own transaction, so a reader who sees the rename sees the labels that follow from it: a location’s rename or relabel restamps what is placed at it, a system’s reclassify restamps its member components, and any act that moves a component’s primary membership restamps that component. Bounded only by the fleet, nothing is restamped at all and the operator applies it: editing a rule at any tier, or a component_type’s or product’s label, or the acronym list, leaves the fleet exactly as it was until :previewLabels shows the blast radius and :recomputeLabels applies it. A preview lists exactly the rows the apply then changes, including the components and systems a location recompute stales, and both are gated by the entity’s own :update.

Renaming a location does not restamp what is beneath it in the tree, only what is placed at it. A component reads the label of the room it is in, never that room’s building, so a campus rename is free. That is a property of the map rather than of the cascade, and if a later slice gives a location an ancestry fact the cascade owes it a subtree arm.

words is how a NAME becomes words, and title alone is not. title upper-cases each word and leaves the separator standing, so {{title .Name}} renders north-wing as “North-Wing”; {{title (words .Name)}} renders it “North Wing”, because words turns a run of - or _ into a single space (an edge run is dropped, and everything else, including whitespace the fact already carried, is untouched). It is slug’s opposite number, and it is what puts the acronym dictionary below within reach of a name: hq-west reads “HQ West” once HQ is in the list (ADR-0105).

That composition is the shipped global rule for locations, {{title (words .Name)}}. A location kind carries only two facts, its type and its own name, so a rule there is either a constant or a restatement, and the constant is refused: labelling every room “Room” is worse than the name it would replace. The restatement earns its place because it re-cases the name and runs the operator’s dictionary over it, which is a string the read ladder’s last rung cannot produce. That last rung is unchanged and still verbatim: a row with no stored label renders its name exactly, never a prettified version. The difference is that a rule renders a label and STORES it.

Because a rule change’s blast radius is the whole fleet, shipping this rule restamps nothing on its own. A new fleet gets a rendered label when each location is created; a fleet that already exists keeps its raw kebab locations until an operator runs :previewLabels and :recomputeLabels on locations. The upgrade is an operator’s act, not a release’s.

Ordinal is the number the row’s NAME carries, not the number the platform stored. They are the same on nearly every row and differ on exactly one: the first of its stem in a placement bucket, whose name is bare while its ordinal is 1 (ADR-0101). There the key is empty, the same empty a row an operator named carries, so {{if .Ordinal}} is the whole of the suppression a rule has to write and a label can never disagree with its own name about how many of a thing there are. The cost is that a rule cannot render “Boardroom 1” for a system named boardroom; the decision log records why that is a feature.

That key is what the shipped global rule for systems reads, {{.TypeName}}{{if .Ordinal}} {{.Ordinal}}{{end}}, which is the component rule’s shape on the system tier. Reading the type alone was not enough for the case AV fleets are full of: a divisible boardroom is two board systems in one room, and both rendered “Boardroom”, so the platform could tell them apart (boardroom and boardroom-2) and an operator reading the console could not. They now read “Boardroom” and “Boardroom 2”. The same restamps-nothing rule applies as for locations: a fleet that already exists keeps both halves reading “Boardroom” until an operator runs :previewLabels and :recomputeLabels on systems.

title reads an acronym dictionary an operator owns. Title-casing a word by upper-casing its first letter turns dsp into Dsp, so the platform ships a list of words that have a fixed form, resolved through the settings cascade as label.acronyms and consulted by title whole-word and case-insensitively (av, Av and AV all render AV; the entry is the form emitted, so PoE is spelled that way in the list). It is a platform-domain, client-visible namespace: one install-wide dictionary an admin writes, readable by any authenticated caller, because a label is stored once and read by everybody and the console renders one from the same list the server did. An operator’s list replaces the shipped one rather than extending it, which is the settings engine’s rule for every list-valued setting and the only way a shipped entry can be removed; provenance is what says which of the two is in force (ADR-0099). The engine follows the list without a restart: the dictionary is resolved when a label is rendered and the compiled engine is cached against the list itself, so an edit reaches the next write.

The dictionary matters less than it looks, and the honest limit is worth stating rather than discovering. A type’s label already holds correctly-cased English out of the catalog (“DSP”, “Video Bar”), and the shipped rules render that, so the list only bites where a rule title-cases a name the operator typed: mostly systems and locations. Product model numbers (vroom wanting VRoom) are unbounded and vendor-supplied, so a dictionary can never be complete, and a missing one degrades quietly to Vroom rather than failing. That is acceptable because the result is one click from being overridden on the row, and because the alternative, a rule that guesses at casing, is wrong more often than it is right.

What a rule deliberately cannot read is the entity’s ancestry. A component reads the label of the location it sits at and a location reads nothing about where it sits, so a location’s own map is Name and TypeName and no more. That is why moving a location restamps nothing: putting its parent’s name in the map would make one campus rename stale every label under it, and the ancestry is not lost anyway, since it is the path and the two renders above, beside the label rather than inside it.