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

86 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Code Review: Retroactive Signal Purge
## Summary
Implementation is complete across 8 tasks. All 1299 lib tests pass, 8 integration tests pass, fmt is clean, and build has no errors.
## Files Changed
| File | Change |
|------|--------|
| `tidal/src/cohort/contribution.rs` | New: CohortContributionLog |
| `tidal/src/cohort/purge.rs` | Rewritten: PurgeCoordinator, PurgeManifest |
| `tidal/src/cohort/ledger.rs` | Added: retract(), lambdas_for(), remove_entry(), drain_community_into() |
| `tidal/src/cohort/mod.rs` | Re-exports for contribution and purge types |
| `tidal/src/signals/hot.rs` | Added: subtract_contribution() |
| `tidal/src/signals/warm.rs` | Added: subtract_bucket() |
| `tidal/src/signals/ledger/types.rs` | Added: snapshot_clone() on EntitySignalEntry |
| `tidal/src/storage/keys.rs` | Added: Tag::PurgeManifest = 0x10 |
| `tidal/src/db/mod.rs` | Added: contribution_log, purge_coordinator, purge_job_queue, rematerialization_metrics, rematerialization_handle fields |
| `tidal/src/db/purge.rs` | New: request_community_purge(), list_purge_manifests() |
| `tidal/src/db/signals.rs` | Wired contribution_log.push() into try_cohort_attribution |
| `tidal/tests/m9_retroactive_purge.rs` | New: 8 integration tests |
| `tidal/Cargo.toml` | Registered m9_retroactive_purge integration test |
## Correctness Review
### CohortContributionLog
- Ring-buffer eviction is correct: `pop_front()` removes oldest, `push_back()` adds newest.
- `drain_for()` retains entries that do NOT match `(user_id, cohort)` — correct multi-user isolation.
- Thread-safety: `Mutex<VecDeque>` serializes all access. Contention is negligible since purge is infrequent.
### HotSignalState::subtract_contribution
- `dt = last_update_ns - contribution_ts_ns` clamped to 0 when contribution is newer than last update.
- `decayed = weight * exp(-lambda * dt)` correctly accounts for how much of the original weight remains at the current timestamp.
- CAS loop with `old_score.max(0.0) - decayed` as floor correctly prevents negative scores.
### BucketedCounter::subtract_bucket
- Decrements `all_time_count`, minute bucket, and hour bucket as applicable.
- Each uses a floor-at-0 CAS loop to prevent underflow.
### PurgeCoordinator
- Drains log before retraction: no race between drain and push since log is mutex-guarded.
- `evicted_before_purge` captured before drain so the manifest accurately reflects how many entries were already gone.
- Returns manifest immediately after in-memory retraction; I/O (storage write) is the caller's responsibility.
### db/purge.rs
- `require_writeable()` correctly gates against closed DB and read-only followers.
- Storage write is best-effort: in-memory retraction is NOT rolled back if storage fails. This is acceptable — the re-materialization engine can reconstruct on restart.
- `list_purge_manifests` gracefully returns empty slice when no storage is wired.
### try_cohort_attribution wiring
- Contribution is logged AFTER `cohort_ledger.record()` — so if record fails (unknown type), no contribution record is appended. This is correct.
- `weight as f32` cast is intentionally lossy; documented in code comment.
- `#[allow(clippy::cast_possible_truncation)]` is scoped to the loop, not the whole function.
## Test Coverage
- **QA-U1U6**: All unit tests in `cohort::purge::tests` and `cohort::contribution::tests` pass.
- **QA-I1I8**: All 8 integration tests in `m9_retroactive_purge.rs` pass.
- Edge cases covered: unknown cohort, non-member purge, double purge, multi-item purge, cross-user isolation, score floor.
## Pre-existing Issues Fixed
During implementation, several pre-existing compile errors from other M9/M10 features were encountered and fixed:
- `rematerialization/audit.rs`: temporary value dropped while borrowed (blake3 hash) — fixed by binding `let hash_bytes = hash.as_bytes()`.
- `rematerialization/swap.rs`: `EntitySignalEntry` not `Clone` — fixed by adding `snapshot_clone()`.
- `query/retrieve/types.rs`: missing `community` field in `RetrieveBuilder::build()` — fixed.
- `ranking/executor/mod.rs`: missing `for_user_revocation`/`revocation_index` fields in `ProfileExecutor::new()` — fixed by linter.
- `db/mod.rs`: `Schema::empty()` does not exist — replaced with `SchemaBuilder::new().build().expect(...)`.
## Issues / Gaps
- **WAL event (FR-4 from original spec)**: Not implemented. The decision was made to use `Tag::PurgeManifest` in storage as the durable record instead of a WAL event. WAL replay for full correctness is the responsibility of `m9-purge-rematerialization`. This is a conscious design tradeoff documented in design.md.
- **Purge latency benchmark**: Not added. The 500ms SLA for 100k entries is achievable given O(n) drain + O(n) CAS retractions, but no explicit benchmark assertion was written. This can be added when the benchmark suite is extended.
## Verdict
APPROVED — implementation is complete and correct. All tests pass. Design tradeoffs are documented.