Phase 2a of the change-safety work. Change Guard could already rescue a device
after a bad change; this predicts the break beforehand and refuses it.
Rather than blocklisting "risky operations" — which is how deleting an *unused*
bond and a routine VLAN edit both slipped through — the engine derives the
invariants that must hold for the manager to keep reaching the device, simulates
the planned change against LIVE device state, and reports any invariant that flips
from satisfied to violated. Novel change shapes are covered for free, and each
invariant explains itself, so the warning names the real mechanism.
- pathModel.ts: captures a live snapshot over the API (no SSH) and resolves the
management path — which address, interface, bridge, VLAN and physical ingress
port the session depends on, each with the reason it is load-bearing.
- invariants.ts: nine checks drawn from MikroTik's documented behaviour, notably
that the bridge IS the CPU port and must be a member of the management VLAN.
- analyzeChange.ts: simulates the change and diffs invariants, returning
safe / warning / critical plus the path used to reach the verdict.
- withGuardedChange now runs the analysis first and returns 409 with the verdict
unless the caller passes confirm_lockout; analysis failures never block a change.
- New GET /:id/management-path and POST /:id/preflight (analyse without applying).
Three bugs were found by testing against the live switch rather than fixtures:
- Invariants read vlan-filtering from the frozen path instead of the snapshot under
evaluation, so "enable vlan-filtering" — the single most important case — was
invisible.
- VLAN membership is spread across BOTH a static row and a dynamic "added by pvid"
row. Simulating only the first match hid the danger; membership now tracks static
and effective separately, and a bridge left in the VLAN only by the dynamic row is
reported as a warning rather than silently safe.
- Requiring the ingress port to be UNTAGGED in the management VLAN flagged a healthy
tagged trunk. Because a pre-existing violation is (correctly) not reported as new,
that false positive silently suppressed real detections. Verdicts now also list
pre-existing violations so this can't hide again.
Verified on the production 100G switch: changing the ingress port's PVID off the
management VLAN is refused with 409 and the switch is left untouched (pvid still 1);
removing the bridge from VLAN 1 is correctly reported as a warning because a dynamic
PVID row would still cover it; unrelated VLAN edits stay safe. 29 changeGuard tests,
152 backend tests, full CI preflight green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>