On a Cron→Calc→Counter swarm, editing a node’s settings (formula, timer interval, wiring) while the nodes were firing drew an interaction arc from the node’s sources to itself — as if an upstream node had just invoked it. No invocation happened; it was a pure user edit.
Interaction arcs are lit by UniversalAppNodeProcessor.process() whenever the
per-node observer (DefaultNodeObserver) re-dispatches a node whose state
satisfies recordsInteractionArc (PROCESSING/EXECUTED). state is a sticky
field that rides along on every re-emission, not a per-invocation event. A
settings edit round-trips as STATE_CHANGE(USER_EDIT); EventClient then calls
ClientNodeManager.refreshNodeMeta() with the node it captured before the
edit. If that node was mid-pulse, updateMetaData() did node.copy(meta = …)
preserving the stale firing state, so the observer re-fired and drew an arc.
A metadata update is not an invocation, so it must never present a firing state.
ClientNodeManager.updateMetaData now neutralises an arc-recording state to
NONE before writing (val safeState = if (recordsInteractionArc(node.state))
NodeState.NONE else node.state). Health states (WARN/ERROR/…) are deliberately
preserved; a genuine invocation re-asserts PROCESSING/EXECUTED via its own
STATE_CHANGE. Regression: ClientNodeManagerMetaEditArcTest (red→green) —
updateMetaData/refreshNodeMeta on a pulsing node leave it in a non-arc state,
and a WARN survives.
A visual that means “a node was invoked” must be driven by an invocation
event/transition, never by the presence of a mutable lifecycle field on a
re-emitted node. Any mutator that copies a node forward for a reason other than
an invocation (meta/wiring edits, optimistic writes) must not carry a firing
(recordsInteractionArc) state with it. Rule mirrors the
pulse-reset-clobbers-snapshot lesson: a copy-and-write across a non-invocation
path must scrub fields whose semantics are “this just fired.”