In industrial maintenance and shutdown work, a status change rarely stays inside one work package. Work on a pipe system makes the related rooms unavailable. A calibration that needs that room cannot start. A further task that itself depends on the calibration is blocked as well. Spreadsheets and general project tools record tasks. They do not compute the transitive lock.
The model answers one question: given the authored graph and a single status event, which other items must change? It does not choose the next job, and it is not a safety case for the plant. It computes the cascade.
The network is directed and typed. Nodes are either ACTION or RESOURCE. Edges are one of three kinds:
| Type | Reads as | Role in the cascade |
|---|---|---|
| BLOCK | action a locks resource b while a is active | Direct lock: b gains a in blocked_by |
| REQUIRE | item a needs item b available | Back-propagation: a gains b in required_from when b is locked or required |
| LOCATED | item a sits spatially in b | Layout / twin only — no cascade |
Actions drive. Resources are passive: they do not fire cascades on their own. When an action enters an active status, its BLOCK edges fire; REQUIRE edges then carry the “unavailable” mark upstream, recursively, along the require chain.
LOCATED stays in the model because plants are spatial. It must not pretend to be a constraint edge. If it cascaded, every room assignment would lock half the site.
Each item keeps two statuses:
Evaluation is three rules, in order:
if blocked_by ≠ ∅ → DepLock
else if required_from ≠ ∅ → DepReq
else → own
When the lists clear, evaluated falls back to own on its own. That separation is the whole point: intent stays visible under a temporary lock.
Three lists sit on the item marking:
blocked_by — actions currently locking this item via BLOCKrequired_from — items whose unavailability makes this item requiredconflicts_with — predictive overlap: other actions whose BLOCK targets intersect resources this item transitively requiresThey behave as reference counts. An item stays DepLock or DepReq while the respective list is non-empty. Multi-blocker is ordinary: two actions locking the same resource both appear in blocked_by; releasing one does not free the resource until the other is gone.
conflicts_with is a warning mark, not an eval status. It does not invent edges and it does not schedule. It flags interference before a second action starts, if you choose to look.
Operationally the cascade is:
(snapshot, status_event) → set of marking / eval changes
Pure: same snapshot and event yield the same changes. Side effects (persist, websocket) sit outside. Depth-bounded: REQUIRE back-propagation aborts past a configurable max_depth (with visited sets on cyclic conflict walks), so cycles in the authored graph cannot hang the operator.
What fires is configurable per layer (and optionally per edge):
blocked_by / required_from during propagation (classic case: Done)Immunity is a design choice, not magic. Re-opening an immune item without rescan can leave a wrong plain Open while the world is still locked — reopen, if you enable it, rehydrates from the live network.
blocked_by → R1 evaluates DepLock.required_from → A2 evaluates DepReq. If something requires A2, the require walk continues — until depth or the chain ends.blocked_by. If the list is empty, R1 returns to own; unrequire clears downstream required_from the same way.Nothing in that walk invents a dependency. The graph was authored. The cascade only reads it.
Honesty about the boundary matters as much as the algorithm:
conflicts_with is graph geometry, not a forecast model.Large ops tools grow side channels: override flags, admin marks, special-case statuses that short-circuit the rules. Every side channel teaches operators that the graph is optional. The cascade I trust is the one where own stays own, eval is a projection, BLOCK and REQUIRE do the work, LOCATED stays spatial, and depth bounds keep the walk finite.
That is enough to keep a picture of the maintenance or shutdown consistent. Scheduling, the safety case, and optimisation remain separate tasks.