Skip to content

fix(platform): re-check legal holds per erasure pass and fix the receipt - #3178

Open
Israeltheminer wants to merge 1 commit into
mainfrom
fix/erasure-receipt-hold-recheck
Open

fix(platform): re-check legal holds per erasure pass and fix the receipt#3178
Israeltheminer wants to merge 1 commit into
mainfrom
fix/erasure-receipt-hold-recheck

Conversation

@Israeltheminer

Copy link
Copy Markdown
Collaborator

Two ways an erasure receipt could claim more than happened, and the panel meant to show the detail rendered blank.

Replaces #3144, which was stacked on #3122's pre-merge branch. Refs #3142.

Why

The legal hold was read once and never again. processErasure gates on it before the cascade, then runs the passes as separate statements — only the finalize is in a transaction. Two of them fan out per-thread lineage purges and per-document blob and corpus deletes, so the window is real, and a hold placed during it was ignored for every remaining pass.

0.4 re-read holds inside all 19 of its arms, and the helper's docstring names why: "a hold placed AFTER scheduling and BEFORE the per-table mutation runs must still win… a custodian-hold race could let GDPR erasure delete data the hold was meant to preserve — FRCP 37(e) spoliation."

A hold that stopped the cascade halfway still reported done. Only a thrown pass set partial. Under Art 19 that receipt is the subject's confirmation, so done has to mean every category was reached.

The Full breakdown panel rendered blank for every recent request. A pass records a plain count; the renderer did if (typeof value !== 'object' || value === null) continue;, so every entry was dropped before the empty-category branch. The result was an empty list with not even the "no data in N other categories" line. The headline counts still rendered, so it looked populated.

What changed

The hold is re-read before each pass. An unreadable hold table skips the pass rather than running it.

A held-off pass is tracked separately from a failed one. Both make the receipt partial, and the error line names which, so an operator can tell a hold from a fault.

The breakdown fold reads both shapes — receipts written before the backend change still carry the object form, so dropping it would break the panel in the other direction.

Both rules are extracted so they are testable at all: the status decision out of the cascade, the entry fold out of the drawer. The bug was in pure logic buried in a component.

What is not covered

The per-pass re-check itself has no automated test. Proving it needs a hold to appear between two passes, and every way I could arrange that was timing-dependent — a flaky test on a spoliation guard is worse than none. What is pinned instead is the rule the re-check feeds: a held-off pass makes the receipt partial and says so. If someone reverts the re-check, that test stays green. Naming the gap rather than implying coverage.

Tests

12 assertions across the two extracted units.

Mutation Went red
status ignores held-off passes the hold case
the hold reason dropped from the error line the combined-reason case
the fold's plain-count branch removed the count-shape case
the fold treats a held-off category as empty two cases

The fold mutation is the original bug, so seeing that assertion red is seeing the defect reproduced.

Re-verified after #3122 and #3143 merged: both mutations still turn red on the merged base.

Gate: typecheck, oxlint --type-aware, oxfmt --check, lint:sast green; branched from origin/main at 899fcc08a.

Two ways an erasure receipt could claim more than happened.

The legal hold was read once, before the cascade, and never again. The
passes are not one transaction, and two of them fan out per-thread lineage
purges and per-document blob and corpus deletes, so a hold placed
mid-cascade was ignored for everything after it. 0.4 re-read holds inside
all 19 of its arms and its helper names the reason: FRCP 37(e)
spoliation. The re-check now runs before every pass, and an unreadable
hold table skips the pass rather than running it — a table you cannot
read is not evidence that nothing is held.

A held-off pass now lands the receipt 'partial' and says which, tracked
separately from a failed one so an operator can tell a hold from a fault.
Before, only a thrown pass did, so a hold that stopped the cascade halfway
still reported 'done' — and that receipt is the subject's Art 19
confirmation.

The Full breakdown panel also rendered blank for every recent request: a
pass records a plain count, and the renderer skipped anything that was not
an object, so every entry was dropped before the empty-category branch. It
reads both shapes now, because receipts written earlier still carry the
older one.

Both rules are extracted so they are testable at all — the status decision
out of the cascade, the entry fold out of the drawer.

Refs #3142.
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