# B1 — purge byte scope

**Decided 2026-08-05.** Supersedes the default recorded in `docs/p1-0-a-group-decisions.md` §B1
("purge deletes bytes only when no other capture references the canonical record"), which was
recorded as a default requiring confirmation and was never a decision taken.

---

## What B1 actually was

The residual triage called B1 *"the most severe item on either list"* — purging one discarded
capture destroying the bytes of a `Posted` sibling. That is the symptom. **The cause is that the
storage model was never written down.**

`docs/p1-0-discard-purge-lifecycle.md` §2.4 says the byte store *"deletes every byte object
belonging to the capture"*. §6 makes byte-identical captures distinct `Document` rows sharing one
canonical `RetainedContentHash` row keyed `(CompanyId, Sha256, ByteLength)`. **Nowhere does either
section say whether a byte object belongs to a capture or to a hash.** An implementer who reads §6
and reaches for the natural implementation — content-addressed storage, one object per hash —
writes the destructive bug without contradicting a single sentence of the draft.

A purge policy cannot be decided on top of an unspecified storage model. So the storage model is
what is decided here.

## Decision

**Byte storage is per capture. A byte object belongs to exactly one capture event and is never
shared between captures, even byte-identical ones. `RetainedContentHash` is a duplicate-detection
index and is never a storage pointer.**

Consequences, all of them intended:

- **Purge deletes that capture's own objects, and nothing else can reach them.** The deletion unit
  and the eligibility unit are the same unit by construction, which is precisely what B1 found
  missing.
- **Reference counting is not implemented**, because the sharing that would require it does not
  exist. There is no reference count to get wrong, no unlink path, no partial-success state.
- **§1's `Purged` invariant — *"bytes are permanently absent"* — is true exactly as written**, at
  the capture unit the tombstone describes.
- **Duplicate captures duplicate bytes.** Accepted, and small: §5 suppresses byte-identical
  resubmissions before extraction and auto-discards them, so duplicate captures are junk by
  definition and are the very population purge exists to remove. The cost falls on the case that
  gets deleted anyway.

## Why not the recorded default (content-addressed + reference counting)

**It fixes the symptom and creates a state lie.** §1 says `Purged` means the bytes are permanently
absent. §2.5 says the system *"never reports `Purged` merely because a delete call was attempted"*.
A reference-counted unlink leaves the bytes on disk and reports `Purged` — contradicting two
clauses of its own draft. Making it honest requires a new outcome state (*unlinked, bytes retained,
N references remaining*), a rewritten §1 invariant, and a tombstone that says something different
from what every other tombstone says. **Removing the sharing costs less than making the sharing
honest.**

And a guard has to be right every time it runs. An invariant that says the bytes are never shared
is checked once, in code review, and cannot decay under a later refactor that "optimises storage".

## Why not purge at the canonical hash

It makes an unrelated sibling capture block the removal of junk, and it turns a one-document
operation into an N-document one whose authorization, reasons and tombstones must all cover N.
Content-level erasure is a genuinely different capability with a different authority model — it is
named below and it is not this.

## What this decision does not do

**It does not erase content, and it is never to be described as if it did.** Purging one capture
leaves a byte-identical sibling's own copy intact — by design, because that sibling is a separate
capture event with its own retention basis.

**Purge is storage hygiene. It is not RGPD erasure.** Nothing in P1-1a may be named, documented,
logged or presented to a user as erasure, deletion of personal data, or a right-to-erasure remedy.

**Content-level erasure — deleting every copy of a hash across every capture in a company — is a
named future capability. It is out of P1-1a scope and is not implemented.** When it is designed it
gets its own authority model, and §3 already governs its central conflict: anything that ever
reached `Posted` or `ReferenceOnly` is purge-denied, so fiscal retention wins over an erasure
request. That is the correct reading of the conflict and this decision does not change it.

## §3 eligibility is unchanged, and is now the second barrier rather than the first

§3 already refuses purge when the capture, any revision, **or any duplicate relationship** has ever
reached `Posted` or `ReferenceOnly`. Under strict enforcement, B1's catastrophic case was therefore
already denied on paper — B1's real finding is that nothing tied that predicate to the deletion
step, and that the deletion step was stated at a unit the predicate did not cover.

§3 stays exactly as written, as a second and independent denial. **Safety now rests on the storage
invariant, not on an eligibility predicate remembering to cover a case.** Two barriers, neither
load-bearing alone.

---

## What P1-1a implements

- Byte objects keyed by capture identity. No object is addressable by hash alone, and no two
  `Document` rows may resolve to the same storage asset ID.
- `RetainedContentHash` and the `DocumentCaptureHash` ledger as specified in §6 — as the
  duplicate-detection index they are, with **no** storage-pointer column and no path derived from
  the hash.
- The tombstone path, unchanged from §4, with the storage asset IDs and deletion receipts of that
  capture's own objects.
- §3's eligibility predicate, unchanged, rechecked under row lock immediately before deletion.

## What P1-0d folds into `docs/p1-0-discard-purge-lifecycle.md`

- **§2.4** — restate deletion as *this capture's own byte objects*, and name the storage invariant
  in the same sentence so the two can never drift apart again.
- **§6** — state explicitly that `RetainedContentHash` is a duplicate-detection index and never a
  storage pointer, and that storage asset identity is per capture.
- **§1** — keep the `Purged` invariant as written; it is now true. Add the one sentence that says
  purge is capture-scoped storage hygiene and not content erasure.
- **§7** — add the acceptance case below.

## Acceptance case to add to §7

> Two byte-identical captures exist in one company, one `Discarded` and one `Posted`. Purge of the
> discarded capture is denied by §3. **And if the eligibility check is bypassed in a test harness,
> the posted capture's bytes are still intact** — because they were never the same bytes. The
> storage model, not the check, is what makes the second half of that sentence true, and the test
> asserts it with the check disabled.
