ADR 0033: The asset tree is a site-scoped Infrastructure branch beside the ISA-88 process tree, and a thing enters it only when the product operates it¶
Status: Accepted Date: 2026-08-04 Issue: #1224
Context¶
#1168 gave the Kubernetes nodes that run the control software a place in the product, and ADR 0032 gave their faults a way to reach an operator. What shipped is an inventory. The next wave of work (per-site server lists, node admin verbs, and a model for third-party control devices) needs a settled answer for where non-ISA-88 things live before any of it is built, because each of those features has to place something in the navigation tree and each one would otherwise settle the question by accident.
Two hierarchies are being carried in one tree¶
The process tree is ISA-88 and describes what the plant makes: Enterprise, Site, Area, ProcessCell, Unit, ControlModule. It is a standards-defined hierarchy, it is what recipes bind to, and it is the tree the HMI navigates.
The asset tree describes what the plant runs on: a site, the racks and panels in it, and the boxes in those. It answers maintenance questions. Which chassis is this workload pinned to, what breaks if this box goes down, which PLC is on the far end of that cable.
Most things belong to exactly one of the two. A Controller is the rare thing
that legitimately belongs to both, because it is compute in the asset tree that
hosts control in the process tree. That dual citizenship is why Controllers
feel natural where the tree already puts them, and why the Servers branch felt
bolted on when it arrived at a different scope beside them.
Part of the answer shipped before this ADR was written¶
Three phase-1 fixes landed while this decision was open, and each of them committed to a piece of it:
- #1211 renamed the cluster-level section to Servers, because the shipped name collided with the Platform Administration docs category where the same word means the software an administrator operates.
- #1228
put the Servers branch at the scope of the site it hangs under, reading a
dcs.io/sitenode label and leaving unlabeled nodes in an unassigned bucket that links to the cluster inventory (internal/gateway/servers.go,internal/gateway/static/js/tree.js). - #1226 separated a Controller's lifecycle phase from its health.
This ADR therefore ratifies as much as it decides. Recording it matters anyway, because the shipped behaviour is the consequence of a structural position that nothing has yet written down, and the three issues that follow it (#1229, #1233, #1239) each need that position stated before they can be built consistently.
The no-CRD ruling rested on drift, and the word has been read too broadly¶
#1168 and ADR 0032 both refused to mirror a Kubernetes Node in a CR. The
argument was specific: kubelet reconciles the Node object continuously, so
any CR carrying a copy of it is stale by construction and hands an operator a
second answer that disagrees with the first. Read as a general prohibition on
new infrastructure kinds, it would also forbid modeling a Modbus PLC, and
Kubernetes has never heard of a Modbus PLC. The rule needs restating in the
terms that actually carry it.
Four words are competing for the same territory¶
The surface currently carries Servers for the chassis tier,
Infrastructure for the per-site branch holding Controllers and Servers, and
Platform Administration as a docs nav category where "platform" means
software. The epic's proposal adds Devices as a fourth. That word is
already spent three times over in this codebase: dcs.io/role=device-node
marks a controller chassis under
ADR 0031, deviceClass is a
declared per-module presentation field whose values include controller
(ADR 0016), and "device discovery" names the
OPC UA browse path in
ADR 0020. A tree showing
"Controllers" and "Devices" as peers, where the Controllers are the things
running on device nodes, teaches the wrong distinction on sight.
A gap the current shape already has¶
ADR 0021 made
IOModule.spec.controllerRef optional for the network protocols, so a Wago
coupler reached over Modbus TCP is a first-class object with no Controller.
The sidebar tree builds its IOModule rows by indexing on ControllerRef
(internal/gateway/overview.go, ioByCtrl), so a controller-less IOModule
lands under a key no Controller carries and is emitted nowhere. It is reachable
through the site detail's Infrastructure tab, and it has no position in the
tree at all. Any answer to "where do non-ISA-88 things live" has to have a
place for it.
Constraints in force¶
- The ISA-88 boundary from ADR 0004 and ADR 0032 holds. A server is site infrastructure and stays outside the physical model.
- No CR mirrors a Kubernetes
Node. - ADR 0015 canonical addressability applies to anything that gains a tree position.
- ADR 0031's division of labor holds: the deployment layer labels and taints, and the product reads.
Decision¶
Each site carries two branches. The ISA-88 process tree is one, and a site-scoped Infrastructure branch is the other. A thing enters the Infrastructure branch only when the product operates it, and it gets a record of its own only when no Kubernetes object is already authoritative for it.
1. One tree, two branches¶
Site
|- Areas -> ProcessCell -> Unit -> ControlModule (process tree, ISA-88)
\- Infrastructure (asset tree)
|- Controllers -> IOModule
|- Servers
\- Field Devices
Cluster inventory (a view, not a tree position)
\- etcd quorum, node totals, unassigned nodes, version skew
The plant engineer has one plant, so the product gives them one navigator. The alternative of two trees behind a switch turns the Controller's dual citizenship into a duplication problem, when it is really a modeled fact.
A resource may occupy more than one tree position. Identity is the canonical address (ADR 0015), and the path taken to reach it is presentation. This is what makes dual citizenship safe rather than ambiguous.
2. What earns a place: three questions, in order¶
- Does the product run its software on the thing, or exchange process data with it? A no ends the enquiry. The thing does not enter the asset tree.
- Is a Kubernetes object already authoritative for it? A yes means the product classifies at read time and stores nothing. Servers take this path.
- Otherwise the product holds the record, because it is the only thing
that has one. Field devices take this path, as
IOModuleandControlModulealready do.
Applied to what exists today and what is coming:
| Thing | Question 1 | Question 2 | Representation |
|---|---|---|---|
| Server chassis running control software | Runs our software | Node is authoritative |
Read-time classification, no CR (#1168) |
| Controller | Runs our unit runtimes | Node is authoritative for the machine |
Controller CR adopted from a labelled node (ADR 0004) |
| I/O module or coupler | Carries process data | Nothing in Kubernetes knows it | IOModule CR (ADR 0021) |
| Third-party PLC, package unit, smart instrument | Carries process data | Nothing in Kubernetes knows it | A product-held record, shaped by #1239 |
| Managed switch, router, firewall, UPS, NTP server, HMI thin client | The product does nothing with it | — | Out of scope, see below |
3. Support infrastructure is deferred, and the reasoning is recorded¶
Switches, routers, firewalls, UPS units, NTP servers, BMCs and HMI thin clients stay out of the model. The plant depends on all of them and the product operates none of them. Modeling them is a configuration-management database, which is a different product with a different buyer, and most plants that want one already have one. The line to hold is whether a plant engineer's control decision depends on the thing being in this tree. For a switch it does not.
This deferral is a decision rather than an oversight, and reversing it means building the CMDB deliberately.
One carve-out, because it is load-bearing for #1237. A BMC is an attribute of the server it powers. Where the power provider interface reaches a chassis out of band, the endpoint is a field on that server's read-time representation, supplied by the deployment layer alongside its credentials. The BMC gets no tree position, no record, and no identity separate from the machine it belongs to. A management controller that could be addressed on its own would be a second name for one box, which is how the wrong box gets powered off.
4. The no-mirror rule, in the terms that carry it¶
No CR may mirror an object the cluster already reconciles. That is the
durable form of the #1168 ruling, and it forbids a Server CRD for exactly the
reason #1168 gave. It says nothing about a Modbus PLC, because there is no
object to mirror and therefore no drift to fear. The product's own record is
the source of truth for a field device in the same way it already is for an
IOModule.
One physical thing gets one record. Where a thing already has a citizen in
the process tree, the asset branch renders that citizen in its asset position
and mints nothing new, as the Controllers sub-tree already does. This is the
constraint #1239 must resolve the vendor-skid case within: ADR 0001 settled
that a smart skid is a Unit with a serviceBinding, so a skid needs an asset
position rather than a second object.
5. The lexicon, settled¶
| Word | Means | Where it appears |
|---|---|---|
| Infrastructure | The asset branch under a site | Tree section header, per site |
| Controllers | Control-zone compute the product adopted and runs unit runtimes on (ADR 0004) | Infrastructure sub-tree |
| Servers | Chassis that run the control software and host no unit runtime (#1211) | Infrastructure sub-tree, and the cluster inventory view |
| Field Devices | Networked things the product exchanges process data with and does not run | Infrastructure sub-tree, built by #1239 |
| Platform | The software an administrator operates | Documentation navigation only |
"Platform" never returns as a product-UI noun. #1211 removed it from the surface and this keeps it removed, because the docs category owns the word.
The third branch is "Field Devices" (founder ruling, 2026-08-04). The bare
word "Devices" is rejected on the collision described in the context above. #1239
chooses between a new kind and a discriminator on IOModule, and either
way the name it lands on may not be the bare word Device. Where a kind is
minted it is FieldDevice.
6. Site scope comes from the deployment layer, and the cluster tier is a view¶
Servers take their site from a dcs.io/site node label applied by the
deployment layer, which is the same division ADR 0031 already draws and the
same label ADR 0004 already requires for controller node discovery. A
controller-bound node takes its site from the adopting Controller CR's
namespace instead, because that is the product's own record of the adoption and
the label can lag behind it. A node carrying no label belongs to no site and
appears as an unassigned count that links to the cluster inventory, so a
deployment that has not labelled anything sees why the branch is empty.
Labelling every node is a documented deployment expectation, including on a single-site cluster where the answer looks obvious. Inferring the site from "there is only one" would be the product guessing at plant topology, and the guess becomes wrong the day a second site is added to the cluster.
The cluster tier is a view and not a tree position. Quorum, node totals, version skew and the unassigned bucket are facts about a deployment artifact. A Kubernetes cluster is not a place in the plant, so it earns no root beside the Enterprise and Site hierarchy that ISA-88 defines. It keeps its own home at the cluster inventory, reachable from the sidebar and from the unassigned row.
7. Multi-site clusters and edge deployments¶
Several sites on one cluster. Each site's Infrastructure branch shows the chassis labelled for that site. Hardware that genuinely serves every site is a cluster fact and appears in the cluster inventory, which is where quorum already lives.
A site that is its own cluster. The two scopes collapse and the tree keeps its shape. The site branch lists that site's chassis and the cluster inventory lists the same machines alongside the quorum derivation. Rendering one set of nodes under two headings is the cost of a single-site install and a multi-site install being the same product, and it is cheaper than a shape that changes underneath the operator when a second site arrives.
8. Addressability follows uniqueness, and never the path¶
Address arity is decided by the scope over which the underlying object's name
is unique, and a tree position never changes it. A Kubernetes node name is
unique across the cluster, so Server stays cluster-scoped in the ADR 0015
route table (#/r/Server/{name}) even though it renders under a site. A
namespaced CR is site-scoped (#/r/{Kind}/{site}/{name}). Anything the asset
tree gains a position for is addressable on this rule.
9. The ISA-88 boundary, restated¶
Nothing in the asset branch enters the physical model. The asset branch carries
no ISA-88 semantics at all: no state machine, no phases, and no recipe binds to
anything in it. A field device's tags enter the process tree as
ControlModules, exactly as ADR 0020 already has discovery emit them. The box
stays in the asset tree. The Controller remains the single dual citizen, and
it is already modeled as compute rather than as a source of tags (ADR 0004).
Alternatives Considered¶
Two separate trees behind an app or tab switch. A process navigator and an asset navigator, each complete. Rejected because there is one plant and the switch is a tax on every maintenance question that starts from a process symptom. It also turns the Controller's dual citizenship into duplicated identity, which the canonical-address rule solves for free in the two-branch form.
Leave Servers cluster-scoped where phase 1 shipped them. A single branch listing every node, at the tree's root. Rejected in #1228 already: a section header sitting at a different scope than its parent reads as a bug on sight, and an engineer responsible for one site was shown every site's hardware with no way to tell which chassis was theirs.
Model the asset tree as CRs: a Rack, a Panel, a Chassis hierarchy. It would
give the tree real depth and let maintenance data hang off each level. Rejected
on three counts. It mirrors Node for the part Kubernetes already owns, it is
a CMDB for the part Kubernetes does not, and it creates a second containment
hierarchy competing with the one ISA-88 defines.
Extend the ISA-88 physical model downward to hold infrastructure, making a server a Unit or a ControlModule so it inherits the existing tree, alarms and addressing. Cheapest by a wide margin. Rejected because ISA-88 Part 1's physical model describes process equipment, a server makes no product, and both ADR 0004 and ADR 0032 hold this line already. Inheriting the alarm path this way was refused in ADR 0032 for the same reason.
Name the third branch "Devices", as the epic proposed. Rejected on the
collision with device-node (a controller chassis), deviceClass (a declared
presentation field whose values include controller), and device discovery.
Infer a single-site cluster's servers into that site. Rejected because it is the product guessing at plant topology, and the unassigned bucket already reports the missing label honestly.
Consequences¶
- #1228's shipped behaviour is now the implementation of a recorded
decision rather than a bug fix that happened to pick a structure. The
dcs.io/siteread, the unassigned bucket, and the controller-namespace override are the ADR's position in code. - #1239 is unblocked with a shape. It chooses between a new kind and a
discriminator on
IOModule, under three constraints from here: the branch is labelled Field Devices, the name is not the bare wordDevice, and one physical thing gets one record. - #1237 gains a constraint it did not have. A BMC is a field on a server and never an addressable object, which settles what the power provider interface is configured against.
- A gap becomes visible and needs its own fix. A controller-less network
IOModule(ADR 0021) has no sidebar tree position today, becauseinternal/gateway/overview.goindexes IOModule rows byControllerRef. The Field Devices branch is where it belongs, and until that branch exists the object is reachable only through the site detail's Infrastructure tab. Filed as #1241. - The deployment layer takes on a documented obligation to label every node
with
dcs.io/site, including on single-site clusters. This extends the ADR 0004 labelling convention from controller nodes to all nodes and needs a line indocs/production-deployment.mdand in the chart's values documentation. - Support infrastructure stays invisible in the product. A plant that wants its switches and UPS units on one pane will not get that here, and the reasoning is recorded so the request is answered rather than rediscovered.
- The compliance posture is unchanged. This ADR restates the ISA-88 physical-model boundary that ADR 0004 and ADR 0032 already hold, and moves nothing across it.
- Reversibility is asymmetric. The tree structure and the lexicon are presentation and cost a rename to undo. The no-mirror rule and the one-record rule are cheap to keep and expensive to reverse, because reversing either means migrating objects that customers authored.
Related Documentation¶
- ADR index
- ADR 0001: Dissolve Asset CRD into ControlModule + Unit pattern
- ADR 0004: Node join is delegated to the deployment layer
- ADR 0015: One canonical resource address across the gateway apps
- ADR 0021: IOModule.controllerRef is optional for network protocols
- ADR 0031: Device nodes are tainted unit-runtime-only
- ADR 0032: Server degradation annunciates per site
- Web UI
- Architecture