Which patches does this roadside controller actually need? Resolve the supersedence graph, prove no CVE gets dropped, and order the install set for a window that might get cut short.
Patching a fleet of roadside controllers is not patching a fleet of laptops. The devices are on poles, behind isolated networks, reachable only when a technician drives to the site. A site visit costs hours. You get one shot.
| Problem | Vendors publish updates in supersedence chains — C replaces B replaces A. Install all three and you waste a maintenance window; install A when C exists and you deploy a patch that's obsolete before the technician's back in the van. Operators resolve this by eyeballing icons in a console, under time pressure, at a roadside cabinet. The failure mode is silent. |
| Solution | Model supersedence as a DAG. Approve the leaves, decline everything upstream of them. Refuse to answer on a corrupt catalogue. Prove that declining a patch never drops a CVE. |
| Result | 17 tests. Branch and merge cases that version-number comparison gets wrong. Cycle detection. CVE continuity checking. Zero dependencies. |
| Stack | Python 3.12 · pytest · GitHub Actions |
Supersedence is not a total order:
flowchart LR
A["Patch A<br/>CVE-2026-1111"] --> B["Patch B<br/>supersedes A"]
A --> C["Patch C<br/>supersedes A"]
D["Patch D"] --> E["Patch E<br/>supersedes B, D"]
B --> E
style B fill:#e8f5e9,stroke:#2e7d32
style C fill:#e8f5e9,stroke:#2e7d32
style E fill:#e8f5e9,stroke:#2e7d32
style A fill:#ffebee,stroke:#c62828
style D fill:#ffebee,stroke:#c62828
One patch can supersede several. Several can supersede the same one. Chains branch when a vendor forks a fix across product versions, and merge when they consolidate. "Compare version numbers" breaks on the first branch — quietly, which is the worst way.
What you want is the set of nodes with no outgoing supersedence edge. That's a graph problem, so it's solved as one.
g = SupersedenceGraph([a, b, c]) # B and C both supersede A
[r.update_id for r in g.resolve() if r.decision is Decision.APPROVE]
# ['B', 'C'] ← both are current; neither supersedes the otherA cycle means the catalogue is corrupt — so it raises. A supersedes B supersedes A cannot be true. If it appears, something upstream is broken and every ruling derived from it is untrustworthy. Refusing to answer beats answering wrongly when the answer sends someone to a roadside cabinet.
A hole in the chain gets surfaced, not swallowed.
g.dropped_cves() # CVEs in declined patches that the approved set doesn't coverThis should always be empty — a superseding patch is supposed to carry the fix forward. When it doesn't, a device comes back from a site visit still vulnerable, and nobody's driving out there again for three months. Worth failing a build over.
References outside the sync window are tolerated. Real catalogues reach back further than your sync. A resolver that errors on an unknown reference is a resolver that never runs on real data.
g.approved() # criticals first; within a severity, no-reboot first
g.reboots_required()A technician who runs out of time should already have the criticals in. Within a severity, no-reboot patches go first — they cost nothing to abandon halfway.
pip install -e '.[dev]'
pytest # 17 passedNo deployment mechanism. No credentials. No device inventory. No network topology.
This is the decision layer: given a catalogue, what should be installed and in what order. How updates reach a device, which devices exist, and how to reach them are operational specifics that belong in an internal system, not a public repository. See docs/SOURCE-POLICY.md — the source gate in CI blocks hostnames, IP ranges and credentials on every push.
Original work. Supersedence semantics follow publicly documented vendor update practice. No operational procedure, runbook, or infrastructure detail is reproduced here.
MIT