tidaldb/.sdlc/features/m10-signal-revocation-controls/design.md

144 lines
4.2 KiB
Markdown

# Design: Signal Revocation Controls
## Overview
Concrete module layout, data structures, storage encoding, read-path
integration, and preference vector rebuild strategy.
---
## Tag: `Tag::Revocation = 0x0F`
Added to `storage/keys.rs` (0x0E = CommunityMembership, 0x0F = Revocation,
0x10 = PurgeManifest, 0x11 = CommunityLeave).
Key format (stored in `users_engine`):
```
[user_id: 8 bytes BE][0x00][0x0F][revocation_id: 16 bytes UUID raw]
Value: serde_json bytes of SignalRevocation
```
---
## New Files
```
tidal/src/
entities/
revocation.rs -- RevocationId, RevocationScope, SignalRevocation, RevocationIndex
db/
revocation.rs -- TidalDb methods: revoke_signal, cancel_revocation, list_revocations
```
---
## Data Structures
### `RevocationId`
Newtype over `uuid::Uuid` with `new()`, `as_bytes()`, `from_bytes()`.
Derives `Serialize`/`Deserialize` via the uuid serde feature.
### `RevocationScope`
```rust
pub enum RevocationScope {
SignalType { signal_type: String, since_ns: Option<u64>, until_ns: Option<u64> },
TimeRange { since_ns: u64, until_ns: u64 },
AgentSession { session_id_raw: u64 }, // u64 because SessionId has no serde
}
```
Note: `AgentSession` stores `session_id_raw: u64` (from `SessionId::as_u64()`)
because `SessionId` does not derive `Serialize`/`Deserialize`.
### `RevocationIndex`
```rust
pub struct RevocationIndex {
inner: DashMap<u64, Vec<SignalRevocation>>,
}
```
Fast path: `is_suppressed` returns `false` immediately if user has no entry
in the DashMap (no lock acquisition, just a shard lookup that misses).
---
## `TidalDb` Field
```rust
revocation_index: Arc<crate::entities::RevocationIndex>,
```
Initialized in both `from_config` and `from_parts` as
`Arc::new(RevocationIndex::new())`.
---
## `db/revocation.rs` — Public API
Three methods on `TidalDb`:
- `revoke_signal(user_id, scope) -> Result<RevocationId>` — validate, persist, upsert index
- `cancel_revocation(user_id, id) -> Result<bool>` — cancel in index, re-persist
- `list_revocations(user_id) -> Result<Vec<SignalRevocation>>` — read from index
- `revocation_index() -> &Arc<RevocationIndex>` — accessor for executor
Internal helper `persist_revocation` writes to `users_engine` using
`serde_json::to_vec`.
Public function `rebuild_revocations(index, storage)` — startup restore
(called from `db/mod.rs` open path).
---
## Read-Path Integration
In `ranking/executor/mod.rs`, during the signal scoring loop:
```
score = ledger.read_decay_score(entity_id, signal_type, idx)
last_ts = ledger.entry_last_update_ns(entity_id, signal_type_id)
if for_user.is_some() and revocation_index.is_suppressed(uid, signal_type, last_ts, session_id):
score = 0.0
```
`entry_last_update_ns` is already implemented on `SignalLedger` (T3).
---
## Startup Restore
`rebuild_revocations(index, storage)` in `db/revocation.rs`:
- Full-keyspace scan of `users_engine`
- Filters for `Tag::Revocation` via `parse_key`
- Groups by `user_id`, calls `load_for_user` per group
Called from `db/mod.rs` open path alongside `rebuild_collections` and
`rebuild_suggestion_index`.
---
## uuid Dependency
`uuid` is available as a transitive dependency (confirmed: build passes without
explicit declaration). Adding it explicitly to `Cargo.toml` for clarity.
---
## Files Changed / Created
| File | Change |
|------|--------|
| `tidal/Cargo.toml` | Add `uuid = { version = "1", features = ["v4", "serde"] }` |
| `tidal/src/storage/keys.rs` | Add `Tag::Revocation = 0x0F`; update `from_byte`, test arrays |
| `tidal/src/entities/revocation.rs` | New — core types and RevocationIndex |
| `tidal/src/entities/mod.rs` | Add `pub mod revocation;` and re-exports |
| `tidal/src/signals/ledger/core.rs` | Add `entry_last_update_ns()` helper |
| `tidal/src/db/mod.rs` | Add `revocation_index` field; add `mod revocation;` |
| `tidal/src/db/revocation.rs` | New — public API + rebuild function |
| `tidal/src/db/state_rebuild.rs` | Call `rebuild_revocations` in open path |
| `tidal/src/ranking/executor/mod.rs` | Apply suppression check during scoring |
| `tidal/src/lib.rs` | Export `RevocationId`, `RevocationScope`, `SignalRevocation` |
| `tidal/tests/m10_revocation.rs` | New — 10 integration tests |