Skip to content

feat: uncovered devices can be cleaned up, not just counted - #373

Merged
captainpacket merged 2 commits into
maint/2.9.xfrom
feat/uncovered-device-cleanup
Sep 9, 2026
Merged

feat: uncovered devices can be cleaned up, not just counted#373
captainpacket merged 2 commits into
maint/2.9.xfrom
feat/uncovered-device-cleanup

Conversation

@captainpacket

Copy link
Copy Markdown
Collaborator

What

Devices this sync created that Forward no longer reports can now be deleted,
not only counted. Exposed as --prune-uncovered on
forward_device_scope_reconciliation_audit, dry run unless --apply.

Folded into 2.9.3, which is cut but not tagged. The three compatibility tables
and the changelog say so.

Why the prune did not already do this

prune_orphan_devices acts on
out_of_scope = (previously_managed & netbox_names) - tagged_names - devices
claimed by the run that produced the current result. A device created by an
earlier run and dropped from scope since is never in that set.

A customer's shape is already a test case in
test_uncovered_absence_and_trend.py: orphans zero, owned-uncovered present.
For them the prune button is a no-op while the uncovered count climbs. The
bucket was diagnosable and not actionable, and the gap is structural rather
than a setting anyone missed.

Three changes

Absence streaks cover the uncovered set. They were kept for orphans only,
and partition_quarantined_orphans fails closed, so an uncovered device had no
absence row and could never leave quarantine however long it had been gone.
Without this the cleanup is a permanent no-op that looks like it works.

One delete path. The orphan prune's loop - ownership release, PROTECT
handling, child-before-parent ordering, cyclic-claim fail-closed - moves into
_delete_prunable_devices and both callers use it. A second copy is how one
path acquires a guard the other lacks, and these deletes are permanent.

Gated on cause. Only devices the census marks absent are eligible. One
Forward still reports without an include tag is a scoping decision;
vendor_excluded is the vendor guard working. A census that did not run raises
rather than reading as "nothing is absent" - those are identical in the counts
and only one makes deleting safe.

The shrink guard is new rather than reused: the orphan version measures orphans
against what the run previously claimed and returns early when orphans are
zero, which is exactly the shape this exists for. This one measures the set
being deleted against every device the sync created, with the same absolute
floor before the ratio.

Safety

Disabling a device in Forward removes it from the API exactly as
decommissioning does - confirmed against a live snapshot, from both the NQE
result and the REST inventory. Nothing available to this plugin tells them
apart, so the quarantine is what makes this safe rather than merely gated: a
maintenance window does not outlast it.

Not a UI button. The orphan prune's button caused the harm this repo has on
record; reading dry-run JSON is a different act from clicking red.

Tests

test_uncovered_device_cleanup.py, 15 tests, mostly negative space: a device
Forward still reports is never deleted; a vendor-excluded one is never deleted;
a device this sync did not create is never deleted; a recent absence is held;
no absence row at all is held; a failed census refuses; an empty Forward result
refuses; deleting most of what the sync created refuses; both overrides work.

Full invoke ci green on this tree: 412 harness, 70 scenario, 2643 Django
(9 skipped), bulk-merge scale, org-query audit, playwright, docs, package, and
the artifact upgrade gate.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75

captainpacket and others added 2 commits September 8, 2026 12:51
A customer reported 552 uncovered devices and a count that keeps
growing. The previous change answered why each one is uncovered and
whether the number is rising; nothing made any of them removable.

The prune looks like it should have. It does not. It acts on devices
claimed by the run that produced the current result, and a device
created by an earlier run and dropped from scope since is never in that
set. This customer's shape is already a test case - orphans zero,
owned-uncovered present - which is the prune being a no-op for them
while the count climbs. The bucket was diagnosable and not actionable,
and that gap is structural rather than a setting they missed.

Three changes.

Absence streaks now cover the uncovered set. They were kept for orphans
alone, and the quarantine partition fails closed, so an uncovered device
had no absence row and could never leave quarantine however long it had
been gone. Without this a cleanup is a permanent no-op that looks like
it works.

The orphan prune's delete loop - ownership release, PROTECT handling,
child-before-parent ordering, cyclic-claim fail-closed - moves into one
helper both callers use. A second copy of it is how one path ends up
without a guard the other has, and these deletes are permanent.

The cleanup itself is gated on cause. Only devices the census marks
absent are eligible: one Forward still reports without an include tag is
a scoping decision, and one the vendor guard excluded is that guard
working. A census that did not run raises rather than reading as
"nothing is absent", because those two are identical in the counts and
only one of them makes deleting safe.

The shrink guard is new rather than reused. The existing one measures
orphans against what the run previously claimed and returns early when
orphans are zero, which is exactly the shape this exists for - it would
have guarded nothing. This one measures the set being deleted against
every device the sync has created, with the same absolute floor before
the ratio.

Disabling a device in Forward removes it from the API exactly as
decommissioning does, confirmed against this customer's live snapshot
from both the NQE result and the REST inventory. Nothing available to
this plugin tells them apart, so the quarantine is what makes this safe
rather than merely gated: a maintenance window does not outlast it.

Exposed as `--prune-uncovered` on the audit command, dry run unless
`--apply` is passed. Not a button: the orphan prune's button is what
caused the harm this repo has on record, and reading dry-run JSON is a
different act from clicking red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75
The release was cut but never authorised or tagged, so what it contains
is still open. The customer who reported the growing uncovered count
reported it against this lane, and holding the fix for 2.9.4 would make
them wait a release for something 2.9.3 can carry.

The three compatibility tables and the changelog say so now. The
evidence recorded on 2026-09-05 describes a tree without this and is
superseded; the gate has to run again before the tag, which it would
have anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KT2R17AeokBdgaC3FmVR75
@captainpacket
captainpacket merged commit 90e3a43 into maint/2.9.x Sep 9, 2026
@captainpacket
captainpacket deleted the feat/uncovered-device-cleanup branch September 9, 2026 00:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant