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:
Multiple insert sites: real services often have 3+
db.insert(<table>)call sites (e.g.messages.ts:114forsend,:349foracceptRequestmaterialization, plusoversight.ts:208forinjectMessage). If you wire the privacy pipeline at each site individually, you get drift across copies and the static-check invariant is impossible to assert.Cyclic import deadlock when centralizing: refactoring
oversight.injectMessageto callMessageService.create()deadlocks at module-init time, becausemessages.tsalready statically importsOversightServiceforauditMessage. Static back-import =MessageServiceis undefined whenoversight.tsevaluates.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.