Skip to content

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/site node 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

  1. 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.
  2. Is a Kubernetes object already authoritative for it? A yes means the product classifies at read time and stores nothing. Servers take this path.
  3. Otherwise the product holds the record, because it is the only thing that has one. Field devices take this path, as IOModule and ControlModule already 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/site read, 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 word Device, 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, because internal/gateway/overview.go indexes IOModule rows by ControllerRef. 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 in docs/production-deployment.md and 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.