Core entities
PartialCore 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.
The fleet: four structural entities
Section titled “The fleet: four structural entities”- A component is a deployed device, app, or service (a display, a DSP, a cloud UCC service). It
owns samples and points at the
productit 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, acomponent_typegenus. - 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, mirroringcomponent.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 atGET /location-typesto feed the location form’s type picker. Each type carries anicon(a glyph key likebuilding; unknown keys fall back tomap-pin) rendered in the tree, andallowed_parent_types(a set oflocation_typeids and/or the reservedrootsentinel): 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.
| 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:
| 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)
| 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)
| 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)
| 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 akindofmanufacturer,integrator, ordeveloper(defaultmanufacturer); the generalization of the former manufacturer-onlycomponent_make. - A
driver(Generic SNMP, Kestrel Device API, …) names the implementation that gets, emits, or sets a product’s signals, with an optionalversion. - 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.
Catalog reference data: component_type
Section titled “Catalog reference data: component_type”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:
| Type | Stem | Abbrev | Icon |
|---|---|---|---|
display-device (Display Device)
| display | disp | monitor |
display (Display)
| display | fp | monitor |
interactive-display (Interactive Display)
| inherits | inherits | inherits |
projector (Projector)
| projector | proj | projector |
screen (Screen)
| screen | scr | tv |
video-wall (Video Wall)
| videowall | vw | grid |
interactive-whiteboard (Interactive Whiteboard)
| whiteboard | iwb | pencil |
audio-device (Audio Device)
| audio | aud | speaker |
mic (Microphone)
| mic | mic | mic |
wireless-mic (Wireless Microphone)
| inherits | inherits | inherits |
ceiling-mic (Ceiling Microphone)
| inherits | inherits | inherits |
boundary-mic (Boundary Microphone)
| inherits | inherits | inherits |
gooseneck-mic (Gooseneck Microphone)
| inherits | inherits | inherits |
speaker (Speaker)
| speaker | spk | speaker |
audio-mixer (Audio Mixer)
| mixer | mix | sliders |
dsp (DSP)
| dsp | dsp | cpu |
dante-card (Dante Card)
| dante | dc | network |
amplifier (Amplifier)
| amp | amp | speaker |
video-device (Video Device)
| video | vid | video |
camera (Camera)
| camera | cam | camera |
ptz-camera (PTZ Camera)
| inherits | inherits | inherits |
capture-encoder (Capture Encoder)
| encoder | enc | video |
codec (Codec)
| codec | cdc | video |
control-device (Control Device)
| control | ctl | touchpad |
touch-panel (Touch Panel)
| panel | tp | touchpad |
button-panel (Button Panel)
| keypad | bp | grip-vertical |
control-processor (Control Processor)
| processor | cp | cpu |
scheduling-panel (Scheduling Panel)
| scheduler | sp | clock |
occupancy-sensor (Occupancy Sensor)
| sensor | occ | eye |
signal-device (Signal Device)
| signal | sig | shuffle |
presentation-switcher (Presentation Switcher)
| switcher | sw | shuffle |
matrix-switcher (Matrix Switcher)
| matrix | mx | shuffle |
av-extender (AV Extender)
| extender | ext | network |
distribution-amplifier (Distribution Amplifier)
| da | da | shuffle |
collaboration-device (Collaboration Device)
| collab | col | video |
video-bar (Video Bar)
| videobar | vb | video |
wireless-presenter (Wireless Presenter)
| presenter | wp | video |
playback-device (Playback Device)
| media | med | tv |
media-player (Media Player)
| player | mp | tv |
recorder (Recorder)
| recorder | rec | server |
infrastructure-device (Infrastructure Device)
| infra | inf | server |
power-device (Power Device)
| power | pwr | box |
network-device (Network Device)
| network | net | network |
generic-device (Generic Device)
| device | dev | box |
generic-app (Generic App)
| app | app | app-window |
generic-service (Generic Service)
| service | svc | server |
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.
Catalog reference data: product
Section titled “Catalog reference data: product”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.
Catalog reference data: system_type
Section titled “Catalog reference data: system_type”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:
| Type | Stem | Abbrev | Icon |
|---|---|---|---|
av (AV)
| av | av | layers |
room (Room)
| room | rm | door-open |
board (Boardroom)
| boardroom | br | inherits |
class (Classroom)
| classroom | cls | inherits |
meeting (Meeting Room)
| meeting | mtg | inherits |
training (Training Room)
| training | trn | inherits |
conference (Conference Room)
| conference | conf | inherits |
huddle (Huddle Room)
| huddle | hud | inherits |
auditorium (Auditorium)
| auditorium | aud | theater |
sign (Signage)
| signage | sgn | tv |
video-wall (Video Wall)
| video-wall | vw | inherits |
interactive-sign (Interactive Sign)
| interactive-sign | isgn | touchpad |
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.
Catalog reference data: standard
Section titled “Catalog reference data: standard”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.
| 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)
| 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
| Column | Type | Constraints |
|---|---|---|
role_id | uuid | PK, FK → system_role.id |
component_type_id | uuid | PK, FK → component_type.id |
| Column | Type | Constraints |
|---|---|---|
role_id | uuid | PK, FK → system_role.id |
product_id | uuid | PK, FK → product.id |
| 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)
Membership: what a role attaches to
Section titled “Membership: what a role attaches to”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-barA 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_typeandproperty_typecatalogs, theevent_typeandcommand_typeregistries) is the shared namespace a driver maps onto, identical install to install: seededofficial: truethrough an authoritative upsert (ON CONFLICT DO UPDATE), never written by an operator. The classification catalogs (vendor,driver,component_type,product) ride that path today, andlocation_typejoined 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
officialflag 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.
The variable-depth trees
Section titled “The variable-depth trees”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.
Sub-components and sub-systems
Section titled “Sub-components and sub-systems”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.
Ownership: the exclusive-arc
Section titled “Ownership: the exclusive-arc”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_kindenum, plus - the matching typed FK (
component_id/system_id/location_id/node_id), plus - a CHECK that exactly the column matching
owner_kindis 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.
How an entity is identified
Section titled “How an entity is identified”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
rm215ainboi.17c.rm215a). Renameable is the point:kestrel-vroomis 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:
borealneeds 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 projectscoalesce(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 sayorder by label nulls last, nameand mean it: Postgres sorts NULL last already, and an ordering that had to saynullif(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.
| table | why 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.
| table | why 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.
Which tables carry a label
Section titled “Which tables carry a label”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:
| table | why 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 |
An address: a uuid or a dotted path
Section titled “An address: a uuid or a dotted path”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):
| Function | What it does | Example |
|---|---|---|
title | Upper-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 |
upper | Upper-cases every letter. | dsp renders DSP |
lower | Lower-cases every letter. | DSP renders dsp |
slug | Reduces 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 |
words | Turns 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:
| Key | What 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:
| Key | What 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:
| Key | What 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.