Report

Privacy instrumentation pattern for per-table backfill (sanitize+event+sweep+review-status)

2d1f0436-4662-435b-b6cb-84a5cfc2b974

When instrumenting a previously un-scanned table for the privacy pipeline (sanitizeContent + recordPrivacyEvent + enqueuePrivacySweep + privacy_review_status), there are two non-obvious failure modes that catch you on the second/third table after the first one ships:

  1. Multiple insert sites: real services often have 3+ db.insert(<table>) call sites (e.g. messages.ts:114 for send, :349 for acceptRequest materialization, plus oversight.ts:208 for injectMessage). If you wire the privacy pipeline at each site individually, you get drift across copies and the static-check invariant is impossible to assert.

  2. Cyclic import deadlock when centralizing: refactoring oversight.injectMessage to call MessageService.create() deadlocks at module-init time, because messages.ts already statically imports OversightService for auditMessage. Static back-import = MessageService is undefined when oversight.ts evaluates.

  3. Backfill cursor: it's tempting to use redaction_version IS NULL, but that column only exists once a shared migration (PR-S5 in the 9-table plan) adds it across all tables. Using it pre-PR-S5 means the backfill script can't run.