feat(drivers): tag-set drift detection, gated on SupportsOnlineDiscovery
The second thing #516/§8.2 deliberately did not build. The original objection stands — Modbus, S7, MQTT, AbLegacy and Sql echo authored config from DiscoverAsync, so diffing discovered against authored is a tautology for them — but it is now ENCODED rather than used as a reason not to build. The discriminator already existed: ITagDiscovery.SupportsOnlineDiscovery is documented as "enumerates the tag set from the live backend rather than replaying pre-declared/authored tags", and it separates the fleet cleanly — FOCAS, TwinCAT, MTConnect and AbCip true; the five config-echo drivers explicitly false. The check runs only for a driver that is SupportsOnlineDiscovery AND reports the new AuthoredDiscoveryRefs. That is nullable rather than empty-by-default on purpose: an empty collection legitimately means "nothing authored, everything discovered is new", so conflating the two would report total drift for a driver that never opted in. Where the value is: AbCip and FOCAS browse a live backend but implement no IRediscoverable, so before this they had no change signal at all. A periodic compare is the only way to notice a PLC re-download. A finding that changed the design: FOCAS, TwinCAT and AbCip all re-emit their authored tags into the same DiscoverAsync stream as device-derived ones, so the browse picker can show an operator their existing tags. Discovered is therefore always a superset of authored, and a VANISHED tag can never appear missing — one whole direction is structurally undetectable for three of the four. Shipping a Vanished list that is permanently empty for them would have been the half-inert seam this whole exercise removes. So the driver declares DiscoveryStreamIncludesAuthoredTags, the detector does not compute the undetectable half, and the operator message says "missing-tag detection unavailable for this driver" rather than letting the absence of a missing-tag clause read as reassurance. MTConnect is the only driver where both directions work — its stream is purely probe-model-derived. Behaviour: a failed browse is not drift (an unreachable device would otherwise report every authored tag as vanished); a truncated capture is skipped for the same reason; an unchanged drift is reported once rather than re-stamping its timestamp; drift that resolves clears the prompt. Interval defaults to 5 minutes — it browses a real device, and a tag set changing is an engineering event, not a runtime one. It reuses the Stage-2 surfacing, raising the same /hosts re-browse prompt, and never rebuilds the served address space. Comparison logic is a pure, separately-tested type rather than actor-inline. 14 new tests. The gate was verified load-bearing by deleting the SupportsOnlineDiscovery half: the tautology-driver test goes red, and it asserts discovery was never invoked rather than merely "no prompt raised", which would have passed for the wrong reason if the timer never fired. Full suite green except the same three pre-existing fixture-gated integration suites, identical counts.
This commit is contained in:
@@ -530,6 +530,47 @@ half-adopting.
|
||||
All 12 drivers now honour a changed config in place; `DriverSpawnPlanner`'s stop + respawn remains the outer
|
||||
guarantee. Sql.Tests 226 / FOCAS.Tests 275 pass.
|
||||
|
||||
### 2026-07-28 — the two things §8.3 deliberately did NOT build (2 of 2)
|
||||
|
||||
**The discovery-drift detector is built, and gated.** The original objection stands and is now *encoded*
|
||||
rather than used as a reason not to build: Modbus, S7, MQTT, AbLegacy and Sql echo authored config from
|
||||
`DiscoverAsync`, so a diff for them is a tautology. The missing piece was a discriminator — and the
|
||||
codebase already had one. `ITagDiscovery.SupportsOnlineDiscovery` is documented as "enumerates the tag set
|
||||
from the **live backend** rather than replaying pre-declared/authored tags", and it separates the fleet
|
||||
cleanly: **FOCAS, TwinCAT, MTConnect, AbCip** true; the five config-echo drivers explicitly false.
|
||||
|
||||
The check runs only for a driver that is `SupportsOnlineDiscovery` **and** reports the new
|
||||
`AuthoredDiscoveryRefs` (**nullable**, default null = opt out — an *empty* collection legitimately means
|
||||
"nothing authored, everything discovered is new", and conflating the two would report total drift for a
|
||||
driver that simply never opted in).
|
||||
|
||||
**Where the value is:** AbCip and FOCAS browse a live backend but implement **no `IRediscoverable`**, so
|
||||
before this they had *no* change signal at all — a periodic compare is the only way to notice a PLC
|
||||
re-download. TwinCAT and MTConnect already have native signals (symbol-version, agent instanceId); drift
|
||||
detection is complementary there.
|
||||
|
||||
**A finding that changed the design.** FOCAS, TwinCAT and AbCip all **re-emit their authored tags into the
|
||||
same `DiscoverAsync` stream** as the device-derived ones, so the browse picker can show an operator their
|
||||
existing tags. That means discovered ⊇ authored *always*, and a **vanished** tag can never appear missing
|
||||
— one whole direction is structurally undetectable for three of the four. Shipping a `Vanished` list that
|
||||
is permanently empty for them would have been exactly the half-inert seam this register exists to remove.
|
||||
So the driver declares `DiscoveryStreamIncludesAuthoredTags`, the detector does not compute the
|
||||
undetectable half, and the operator message **says** "missing-tag detection unavailable for this driver"
|
||||
rather than letting the absence of a missing-tag clause read as reassurance. **MTConnect is the only
|
||||
driver where both directions work** — its stream is purely probe-model-derived.
|
||||
|
||||
Behavioural details: a **failed browse is not drift** (an unreachable device would otherwise report every
|
||||
authored tag as vanished — the health surface already covers unreachability); a **truncated** capture is
|
||||
skipped for the same reason; an **unchanged** drift is reported once rather than re-stamping its timestamp
|
||||
every interval; drift that **resolves** clears the prompt so the next one is not deduped against a stale
|
||||
signature. Interval defaults to 5 minutes — it browses a real device, and a tag set changing is an
|
||||
engineering event, not a runtime one.
|
||||
|
||||
The comparison is a pure, separately-tested type (`TagSetDriftDetector`) rather than actor-inline logic.
|
||||
14 new tests. The gate was verified load-bearing by deleting the `SupportsOnlineDiscovery` half — the
|
||||
tautology-driver test goes red, and it asserts discovery was **never invoked** rather than merely "no
|
||||
prompt raised", which would have passed for the wrong reason if the timer simply never fired.
|
||||
|
||||
### 2026-07-27 — §8.4 dispatch-map parity (G-1 … G-6)
|
||||
|
||||
**The maps had to become data before they could be guarded.** A Razor `@switch` compiles into
|
||||
|
||||
Reference in New Issue
Block a user