# Rust Coding Guidelines

## Core Stack

| Component | Required | Forbidden |
|-----------|----------|-----------|
| Error handling | `thiserror` (lib), `anyhow` (bin) | `.unwrap()`, `.expect()`, `panic!` |
| Serialization | `rkyv` (zero-copy) | `serde_json` for hot paths |
| Hashing | `blake3` | `sha256`, `md5` |
| KV Store | `sled` (MVP), trait-abstracted | Direct `rocksdb` coupling |
| Async | `tokio` (if needed) | `async-std` |
| Logging | `tracing` | `log`, `println!` |
| Testing | `proptest`, `rstest` | Mocking core types |

## Error Handling

```rust
// Library errors: use thiserror
#[derive(Debug, thiserror::Error)]
pub enum StemeError {
    #[error("assertion not found: {0}")]
    NotFound(Hash),

    #[error("invalid signature for agent {agent}")]
    InvalidSignature { agent: AgentId },

    #[error("storage error: {0}")]
    Storage(#[from] sled::Error),
}

// Always add context
fn load_assertion(hash: &Hash) -> Result<Assertion, StemeError> {
    let bytes = self.store
        .get(hash)
        .context("failed to read from store")?
        .ok_or(StemeError::NotFound(*hash))?;
    // ...
}
```

## Type Safety

Use newtypes for domain IDs:

```rust
// Good: Strong types
pub struct EntityId(pub String);
pub struct RelationId(pub String);
pub struct Hash(pub [u8; 32]);
pub struct AgentId(pub [u8; 32]);

// Bad: Stringly-typed
fn query(subject: String, predicate: String) -> ...
```

## Formatting

- Use underscores in numbers: `1_000_000` not `1000000`
- Use inline interpolation: `println!("{hash:?}")` not `println!("{:?}", hash)`
- Max line length: 100 characters
- Run `cargo fmt` before commit

## Testing

```rust
#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    // Property test for critical invariants
    proptest! {
        #[test]
        fn assertion_round_trips(bytes in prop::collection::vec(any::<u8>(), 0..1000)) {
            let assertion = /* create from bytes */;
            let serialized = rkyv::to_bytes(&assertion)?;
            let deserialized = rkyv::from_bytes(&serialized)?;
            assert_eq!(assertion, deserialized);
        }
    }

    // Descriptive test names
    #[test]
    fn test_lens_consensus_returns_highest_vote_count() {
        // Arrange
        let assertions = vec![/* ... */];

        // Act
        let result = ConsensusLens.resolve(&assertions, &ctx);

        // Assert
        assert_eq!(result.value, expected);
    }
}
```

## Module Structure

```
crates/stemedb-core/
  src/
    lib.rs           # Public API, re-exports
    assertion.rs     # Assertion struct
    types.rs         # EntityId, RelationId, Hash, etc.
    store.rs         # Storage trait
    error.rs         # StemeError
  tests/
    integration.rs   # Cross-module tests
```

## Documentation

```rust
/// Resolves conflicting assertions using weighted voting.
///
/// # Arguments
/// * `candidates` - All assertions matching the query
/// * `context` - Query context including agent reputations
///
/// # Returns
/// The assertion with highest weighted support, or None if empty.
///
/// # Example
/// ```
/// let lens = ConsensusLens::default();
/// let result = lens.resolve(&assertions, &ctx);
/// ```
pub fn resolve(&self, candidates: &[Assertion], context: &QueryContext) -> Option<Assertion> {
    // ...
}
```

## Clippy

Must pass with zero warnings:

```bash
cargo clippy --workspace -- -D warnings
```

Common fixes:
- `needless_return`: Remove explicit `return` when implicit works
- `redundant_clone`: Check if clone is actually needed
- `missing_docs`: Add doc comments to public items
