Symptom

Two adjacent files in shared/.../node/ handled the same situation — “this node’s metadata is not the subtype I expected” — with opposite philosophies. Node.snapshot() blind-cast (meta as DataPointMetaData) and threw ClassCastException; the wiring functions next door safe-cast and returned the input unchanged. Neither reported the common cause, and the crash landed inside executor coroutines rather than at the boundary that accepted the bad node.

Separately, and found by running the test suite on main: SwarmWorkMetaData and SwarmBatchMetaData had been added as SourceMetaData subtypes without being enumerated in the three wiring whens, so Swarm Work and Swarm Batch nodes had a silently read-only Sources tab. The drift guard added for #901 (NodeMetaDataSourceWiringExhaustiveTest) was red on main, catching exactly the failure mode it was written for.

Root cause

Node’s KDoc calls the type↔meta pairing an invariant — “the concrete subtype matches type” — but nothing enforces it. POST /node/{id} deserializes the two independently: the polymorphic discriminator lives inside the meta object and is never checked against type. A body pairing "type": "DataPoint" with a SpacerMetaData payload therefore parses, persists, and replays out of H2 without complaint. snapshot() dispatched on type and then cast meta to whatever that discriminator implied, so the first executor to read such a node as an input — SMTP, MQTT, outbound webhook, or serial — died on the cast, in a coroutine, with no mention of the node id.

The else -> Snapshot() arm looked like a guard but was not: it only covers node types that legitimately have no snapshot semantics. Every enumerated arm was its own unguarded crash site.

Fix

snapshot() now routes each arm through a private reified helper, Node.snapshotFrom<M> { … }, which safe-casts meta to M and, on mismatch, logs the node id, its type, the expected subtype and the actual one before returning an empty Snapshot. That is the contract NodeMetaDataSourceWiring.unenumeratedSourceMetaData already documented for its own defect arm (#901) — safe cast, loud log, safe fallback — so the two files now agree. Behaviour for correctly-typed nodes is byte-identical, including the two arms that derive rather than read their snapshot (Server.Pin from state, Trigger.CronTimer from timestamp). SwarmWorkMetaData and SwarmBatchMetaData were added to all three wiring whens. SnapshotTypeSafetyTest in shared/commonTest covers both directions.

Prevention