Fix #63: repo delete cleans userspace references #66

Merged
crueber merged 1 commit from fix/issue-63 into main 2026-09-04 22:59:55 +00:00
Owner

Follow-up to the #59 backend audit: Registry.Delete sweeps only repos/<o>/<r>/, leaving users/*/starred|watching|notifications|invitations referencing the dead repo, and delete+recreate desynced star counters (stale record + fresh social.json: Star early-returned 0).

Design (a)+(c) from the issue: no reverse index (enumerating people stays a non-feature per 01), no tombstones (per-repo state dies with the prefix), no recreate scan.

  • Reads tolerate — Starred/tray/inbox skip dead-repo rows (one manifest HEAD per entry, fail-open on probe errors so a store blip never mass-hides); Counts/GET social 404 on ghosts; viewer/watch flags hide stale records.
  • Writes fail closed — star / watch / repo-invite-create on ghosts 404; accept on ghosts 409 with no binding written (the synthesized default never gains ghost bindings); unstar/unwatch still delete the record but skip the counter CAS (no social.json resurrection).
  • Recreate resyncs — the already-starred path repairs an absent/zeroed counter with one +1 (412 losers still just recount; corrupt counters keep record-is-truth tolerance). Known limit, documented: only the zero state repairs, so the first stale starrer reconverges; a second one uses unstar/restar (no reverse index to verify inclusion).
  • Retention drops dead notifications — window + bounded overflow sweep (1k scan / shared 200-delete cap), unread_count reconciled; UnreadCount stays O(1). Inbox rows additionally verify issuer truth, so swept rows stay hidden after a recreate (expiry semantics unchanged).
  • Emission deliberately NOT gated (handlers reach fan-out only after a live commit; millisecond race droppings self-clean via tray filter + retention).

Verification: gofmt/go vet clean; go test -race green on internal/social, internal/notify, internal/identity; cover 99.3% / 97.1% / 97.4% (gate ≥95%); new table-driven httptest for ghost repos in all three packages; E8 budgets updated with the +1 HEAD lines. Docs: Decisions entries in 07 (new §4.1), 06, 01. P6/auth and deletion admin-gating untouched.

Follow-up to the #59 backend audit: `Registry.Delete` sweeps only `repos/<o>/<r>/`, leaving `users/*/starred|watching|notifications|invitations` referencing the dead repo, and delete+recreate desynced star counters (stale record + fresh `social.json`: `Star` early-returned 0). Design (a)+(c) from the issue: **no reverse index** (enumerating people stays a non-feature per 01), **no tombstones** (per-repo state dies with the prefix), **no recreate scan**. - **Reads tolerate** — `Starred`/tray/inbox skip dead-repo rows (one manifest `HEAD` per entry, fail-open on probe errors so a store blip never mass-hides); `Counts`/`GET social` 404 on ghosts; viewer/watch flags hide stale records. - **Writes fail closed** — star / watch / repo-invite-create on ghosts 404; accept on ghosts 409 with no binding written (the synthesized default never gains ghost bindings); unstar/unwatch still delete the record but skip the counter CAS (no `social.json` resurrection). - **Recreate resyncs** — the already-starred path repairs an absent/zeroed counter with one +1 (412 losers still just recount; corrupt counters keep record-is-truth tolerance). Known limit, documented: only the zero state repairs, so the first stale starrer reconverges; a second one uses unstar/restar (no reverse index to verify inclusion). - **Retention drops dead notifications** — window + bounded overflow sweep (1k scan / shared 200-delete cap), `unread_count` reconciled; `UnreadCount` stays O(1). Inbox rows additionally verify issuer truth, so swept rows stay hidden after a recreate (expiry semantics unchanged). - Emission deliberately NOT gated (handlers reach fan-out only after a live commit; millisecond race droppings self-clean via tray filter + retention). Verification: `gofmt`/`go vet` clean; `go test -race` green on `internal/social`, `internal/notify`, `internal/identity`; cover 99.3% / 97.1% / 97.4% (gate ≥95%); new table-driven httptest for ghost repos in all three packages; E8 budgets updated with the +1 HEAD lines. Docs: Decisions entries in 07 (new §4.1), 06, 01. P6/auth and deletion admin-gating untouched.
Repo deletion sweeps only repos/<o>/<r>/, leaving userspace records
(stars, watches, notifications, invites) that reference the dead repo,
and delete+recreate desynced star counters (stale record + fresh
social.json: Star early-returned 0).

Design (a)+(c) per the issue: no reverse index (enumerating people
stays a non-feature), no tombstones, no recreate scan.

- Reads tolerate: Starred/tray/inbox skip dead-repo rows (one manifest
  HEAD per entry, fail-open on probe errors); Counts/social 404;
  viewer/watch flags hide stale records.
- Writes fail closed: star/watch/invite-create on ghosts 404; accept
  on ghosts 409 with no binding write; unstar/unwatch still delete
  the record but skip the counter CAS (no social.json resurrection).
- Recreate resyncs: the already-starred path repairs an absent or
  zeroed counter with one +1 (412 losers still just recount; corrupt
  counters keep record-is-truth tolerance).
- Retention drops dead-repo notifications (window + bounded overflow
  sweep) and reconciles unread_count; inbox rows verify issuer truth
  so swept rows stay hidden after a recreate.

Docs: Decisions entries in 07 (new 4.1), 06, 01; E8 budgets carry
the +1 HEAD lines. P6/auth and deletion admin-gating untouched.
Sign in to join this conversation.
No description provided.