# Task 05: FollowerDb

## Delivers

Wire `TidalDb` to support `NodeRole::Follower` startup in `tidal/src/db/open.rs`. Guard all write methods (`signal`, `write_item`, `write_creator`, etc.) to return `TidalError::ReadOnly` when role is `Follower`. Start `SegmentReceiver` on open for follower nodes.

## Complexity: M

## Dependencies

- Task 04 (SegmentReceiver)
- Phase 8.1 (NodeConfig, NodeRole)

## Technical Design

### Write guards in TidalDb

```rust
// tidal/src/db/mod.rs

impl TidalDb {
    /// Guard that returns ReadOnly if this node is a follower.
    fn require_writeable(&self) -> crate::Result<()> {
        if !self.config.cluster.accepts_writes() {
            return Err(TidalError::ReadOnly);
        }
        Ok(())
    }

    pub fn signal(
        &self,
        signal_type: &str,
        entity_id: EntityId,
        weight: f64,
        timestamp: Timestamp,
    ) -> crate::Result<()> {
        self.require_writeable()?;
        // ... existing implementation ...
    }

    pub fn write_item(
        &self,
        entity_id: EntityId,
        metadata: &HashMap<String, String>,
    ) -> crate::Result<()> {
        self.require_writeable()?;
        // ... existing implementation ...
    }

    // All other write methods follow the same pattern.
}
```

### Follower startup in open.rs

```rust
// tidal/src/db/open.rs

pub fn open_db(config: Config) -> crate::Result<TidalDb> {
    // ... existing open logic ...

    let db = TidalDb { /* ... */ };

    if config.cluster.role == NodeRole::Follower {
        // Start segment receiver background task.
        // The transport is set by the caller via db.start_replication(transport).
        tracing::info!("TidalDb: starting as follower for shard {:?}", config.cluster.shard_id);
    }

    Ok(db)
}
```

### TidalDb::start_replication

```rust
impl TidalDb {
    /// Wire up replication transport for follower nodes.
    ///
    /// Must be called after open() for NodeRole::Follower nodes.
    /// No-op for NodeRole::Single or NodeRole::Leader.
    pub fn start_replication(
        &self,
        transport: Arc<dyn Transport>,
        leader_shard: ShardId,
        shutdown_rx: tokio::sync::watch::Receiver<bool>,
    ) {
        if self.config.cluster.role != NodeRole::Follower {
            return;
        }
        let receiver = Arc::new(SegmentReceiver::new(
            transport,
            Arc::clone(&self.signal_ledger),
            Arc::clone(&self.replication_state),
            leader_shard,
        ));
        receiver.start(shutdown_rx);
    }
}
```

### TidalError::ReadOnly

```rust
// tidal/src/error.rs (or wherever TidalError is defined)

#[derive(Debug, thiserror::Error)]
pub enum TidalError {
    // ... existing variants ...

    /// This node is a read-only follower; write operations are not permitted.
    #[error("this node is read-only (follower)")]
    ReadOnly,
}
```

## Acceptance Criteria

- [ ] `TidalError::ReadOnly` variant added to the error enum
- [ ] All write methods (`signal`, `write_item`, `write_creator`, `write_item_embedding`, `write_creator_embedding`, `close_session`, etc.) return `Err(TidalError::ReadOnly)` when `role == Follower`
- [ ] Read methods (`retrieve`, `search`, `read_decay_score`, etc.) work normally on followers
- [ ] `TidalDb::start_replication(transport, leader_shard, shutdown_rx)` wires `SegmentReceiver` for follower nodes; is a no-op for `Single`/`Leader`
- [ ] Integration test: open as Follower, verify all writes fail with ReadOnly; open as Leader, verify writes succeed
- [ ] All existing tests pass (they use Single node, unaffected)
- [ ] `cargo clippy -D warnings` and `cargo fmt` pass
