4.2 KiB
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
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
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
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 indexcancel_revocation(user_id, id) -> Result<bool>— cancel in index, re-persistlist_revocations(user_id) -> Result<Vec<SignalRevocation>>— read from indexrevocation_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::Revocationviaparse_key - Groups by
user_id, callsload_for_userper 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 |