# TODO.md — Sifter Source: PLAN-sifter.md (archived). Do not re-open decisions in §10 of the plan; they are inputs, not suggestions. Status: M1+M2 SHIPPED at https://sifter.psingletary.com (2026-08-14). M3–M5 coded (backup.js, cleanup.js, rateLimit.js, CleanupLog, delete gating). Remaining: author scan validation (M2 acceptance), author live test (M4), final README polish pass. --- ## Pre-work (human, before any build) — DONE - [x] Copy `src/contexts/AuthContext.js`, the login screen component, and shared layout/styles from Verifier into `vendor/` - [x] Paste Verifier's README section skeleton into `vendor/README-skeleton.md` - [x] Create Tangled repo (git@tangled.org:did:plc:322q2la7tha3kstkxcxjt45d) and push --- ## M1 — Scaffold (CRA + auth + deploy) — DONE - [x] CRA app (JavaScript, not TS). Ignore CRA deprecation warning; do NOT migrate to Vite. Jest via `react-scripts test` only. - [x] OAuth via `@atproto/oauth-client-browser` in `src/contexts/AuthContext.js` (lifted from vendor/). Scope: `atproto transition:generic` - [x] `clientMetadata` in AuthContext.js AND `public/client-metadata.json`, matching deployed domain sifter.psingletary.com - [x] Login screen + layout/styles lifted from vendor/ - [x] Deployed to wisp.place + custom domain sifter.psingletary.com (verified: root 200, SPA fallback 200, client-metadata.json 200) - [x] Tangled repo pushed (git@tangled.org:did:plc:322q2la7tha3kstkxcxjt45d) - [ ] LOGIN verified end-to-end (author: sign in at the live URL) ## M2 — Scan (shipped publicly on its own — decided, §10#2) — DONE - [x] `src/lib/scan.js` — pure: page `listBlobs` (limit 1000), `describeRepo`, page `listRecords` (limit 100) per collection, diff held − referenced - [x] `src/lib/extract.js` — generic recursive walk (never hardcode paths); fixtures F1–F5 in `src/__tests__/__fixtures__/` - [x] `missing` cross-check against `com.atproto.repo.listMissingBlobs`; disagreement → warning surfaced - [x] Per-collection blob-ref accounting (sanity check table) - [x] Fail-closed `trustworthy` flag → report read-only + banner + retry; deletion disabled - [x] Report UI: headline numbers, per-collection table, orphan grid (thumbnail via getBlob, truncated CID click-to-copy, MIME, size, checkboxes, select-all, per-item download), `missing` listed separately - [x] Tests: `extract.test.js` (F1–F5), `scan.test.js` (mocked paging incl. failure → trustworthy=false; clean mocked account → 0 orphans) — 17/17 - [ ] AUTHOR VALIDATION: scan real account (did:plc:stznz7qsokto2345qtdzogjb, PDS lionsmane.us-east.host.bsky.network), cross-check blob count against https://atproto.at/uri/at://did:plc:stznz7qsokto2345qtdzogjb#blobs - [x] Zero-orphan case renders as success state, not empty state - [x] Large-repo handling: cursor loops with live progress counts, memory = Sets of CIDs > **HARD STOP — author sign-off on M2 live scan before cleanup is used.** ## M3 — Backup (before any deletion exists) — CODED - [x] `src/lib/backup.js` — getBlob fetch each selected blob + `manifest.json` (CIDs, MIME, sizes, direct URLs) + zip via `jszip` - [x] Backup is the path of least resistance; delete gated behind completed backup OR explicit acknowledgement (two-click confirm) - [x] Per-item download from grid ## M4 — Cleanup, single blob (mechanism proof) — CODED, LIVE TEST PENDING - [x] `src/lib/cleanup.js` — serial reference-and-delete per orphan: createRecord in `com.psingletary.sifter.tempRef` (TID rkey, minimal value { $type, blob, createdAt }) → deleteRecord → PDS GC - [x] Blob metadata recovery via getBlob headers (Content-Type, Content-Length; fallback application/octet-stream / 0) - [x] Create rejected → retry once with `validate: false`; still failing → mark `undeletable`, move on. Never a different collection. - [x] Mandatory verification: re-run listBlobs, per-CID outcome `deleted` / `still-present` (abort run) / `undeletable`. Never trust deleteRecord 200. - [x] Rate limiting: ~1s pacing default; `ratelimit-*` header parsing in rateLimit.js (verify constants against Bluesky docs at build — done: headers authoritative, constants fallbacks) - [x] localStorage run state keyed by DID; startup sweep of own temp collection; idempotent per CID - [ ] **LIVE TEST — AUTHOR performs: delete ONE blob on the real account, verified via re-scan. Agent does not run the live test.** > **HARD STOP — wait for author's verified live test before batch cleanup use.** ## M5 — Batch cleanup — CODED - [x] Batch UI: per-CID live log (CleanupLog), pause/resume/stop, final verified summary - [x] Resume from localStorage after tab close / pause - [x] `applyWrites` single-transaction path deferred (two-call path shipped; only add if live test proves GC fires) ## M6 — Polish + README — MOSTLY DONE - [x] README matching Verifier's section order (vendor/README-skeleton.md): title, Features, What this does, Usage, Development, Build, Deployment, OAuth Configuration, License (MIT) - [x] Honest documentation of the mechanism incl. throwaway records + firehose visibility; dedup note - [x] Edge cases from §6 handled (already-absent, mechanism-doesn't-work abort, third-party lexicons, legacy blob format, non-image icons, zero orphans) - [ ] Add self-hosted PDS note (SQL cross-check against actor store's blob / record_blob tables) to README - [x] No API keys, no backend, no analytics --- ## Acceptance criteria (from plan §8) - [ ] (Manual, author) Scan on account with known orphans finds them - [x] (Auto) Clean mocked account → 0 orphans, stated plainly - [x] (Auto) Forced listRecords failure → trustworthy=false, deletion disabled - [x] (Auto) Extractor fixtures F1–F5 pass - [x] No orphan deleted without completed backup or explicit acknowledgement - [x] Every reported deletion confirmed by follow-up listBlobs - [ ] No temp records remain in `com.psingletary.sifter.tempRef` after any run, including interrupted mid-flight (unit logic; final proof on author's live test) - [x] Works from static hosting; no backend ## Out of scope (§9) Missing-blob fixing (report only) · server/admin cleanup · deleting user records · multi-account/delegated cleanup · CAR-file fast path (deferred idea only, behind validation)