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

105 lines
3.6 KiB
Markdown

# 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(...)`:
```rust
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