105 lines
3.6 KiB
Markdown
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
|