tidaldb/.sdlc/features/m9-retroactive-purge/tasks.md

3.6 KiB

Tasks: Retroactive Signal Purge

T1: CohortContributionLog

File: tidal/src/cohort/contribution.rs (new)

Implement CohortContributionLog with bounded Mutex<VecDeque<ContributionRecord>>.

  • ContributionRecord { user_id: u64, cohort: String, entity_id: EntityId, signal_type_id: SignalTypeId, weight: f32, timestamp_ns: u64 }
  • DEFAULT_CONTRIBUTION_LOG_CAP: usize = 5_000_000
  • CohortContributionLog::new() and ::with_cap(cap)
  • push(record) — evicts oldest if at cap; increments eviction_count
  • drain_for(user_id, cohort) -> Vec<ContributionRecord>
  • len(), is_empty(), eviction_count(), cap()
  • Unit tests: push/drain, eviction, multi-user isolation

Status: DONE

T2: Ledger Retraction Methods

Files: tidal/src/signals/hot.rs, tidal/src/signals/warm.rs, tidal/src/cohort/ledger.rs

  • HotSignalState::subtract_contribution(weight, ts_ns, lambdas) — CAS loop, floor at 0.0
  • BucketedCounter::subtract_bucket(ts_ns) — decrement all_time + current buckets, floor 0
  • CohortSignalLedger::retract(cohort, entity_id, type_id, weight, ts_ns) — silent no-op if entry absent
  • CohortSignalLedger::lambdas_for(type_id) -> &[f64]
  • CohortSignalLedger::remove_entry(cohort, entity_id, type_id)

Status: DONE

T3: Tag::PurgeManifest Storage Key

File: tidal/src/storage/keys.rs

  • Add PurgeManifest = 0x10 variant to Tag enum
  • Add 0x10 => Some(Self::PurgeManifest) to from_byte()
  • Update all tag test arrays to include new variant

Status: DONE

T4: PurgeCoordinator and PurgeManifest

File: tidal/src/cohort/purge.rs (rewritten from stub)

  • PurgeId = u128
  • ManifestEntry (serde surrogate)
  • PurgeManifest { purge_id, user_id, cohort, requested_at_ns, entries, evicted_before_purge } with to_json()/from_json()
  • PurgeCoordinator::request_purge(user_id, cohort, now_ns, evicted) -> (PurgeId, PurgeManifest)
  • Unit tests: drain/retract/manifest, idempotency, JSON roundtrip

Status: DONE

T5: TidalDb Struct Fields

File: tidal/src/db/mod.rs

  • Add contribution_log: Arc<CohortContributionLog> and purge_coordinator: Arc<PurgeCoordinator> to TidalDb
  • Initialize in both from_config (no-schema stubs) and schema-open path
  • Add purge_job_queue, rematerialization_metrics, rematerialization_handle for M9 re-mat engine

Status: DONE

T6: Wire Contribution Log into Signal Hot Path

File: tidal/src/db/signals.rs

In try_cohort_attribution, after each cohort_ledger.record(...):

self.contribution_log.push(ContributionRecord {
    user_id, cohort: cohort_name.clone(), entity_id,
    signal_type_id: type_id, weight: weight as f32, timestamp_ns: ts_ns,
});

Status: DONE

T7: Public API db/purge.rs

File: tidal/src/db/purge.rs (new)

  • TidalDb::request_community_purge(user_id, cohort_name) -> Result<(PurgeId, PurgeManifest)>
    • require_writeable()
    • purge_coordinator.request_purge(...) → in-memory retraction
    • storage.items_engine().put(encode_key(...), manifest.to_json()) for durability
  • TidalDb::list_purge_manifests(user_id) -> Result<Vec<PurgeManifest>>
    • Prefix scan on Tag::PurgeManifest

Status: DONE

T8: Integration Tests

File: tidal/tests/m9_retroactive_purge.rs (new)

8 tests covering:

  1. Basic purge retracts cohort score
  2. Second purge is idempotent (empty manifest, no double-retract)
  3. Purging user A doesn't affect user B's contributions
  4. Manifest persisted and listable in ephemeral mode
  5. Purging unknown cohort returns empty manifest (no error)
  6. Score floors at 0.0, never goes negative
  7. Multiple items purged in one request
  8. Non-cohort-member purge is a no-op

Status: DONE