serde_mask 0.1.0

Mask sensitive data during serde serialization for LLM ingestion
Documentation
# serde_mask

Mask sensitive data during serde serialization for LLM ingestion, with secrets attached to the object, not a separate struct like expunge does.

The crate intercepts serde serialization and substitutes sensitive values with placeholders. A `deanonymize` method on the struct lets you replace placeholders in LLM responses with the original values.

## Usage

```rust
use serde_mask::{anonymize, Anonymize, AnonymizeTrait};

#[anonymize]
#[derive(Debug, Anonymize)]
struct Query {
    #[anon]
    username: String,
    public: String,
}

let q = Query {
    username: "my_secret_username".to_string(),
    public: "visible".to_string(),
    __state: std::sync::OnceLock::new(),
};

// Serializes with secret masked
let json = serde_json::to_string(&q).unwrap();
// {"secret":"ANON_123456","public":"visible"}

// LLM responds with something like "Contact user ANON_123456 immediately."
// then we deanonymize it back to "Contact user my_secret_username immediately."
let response = "Contact user ANON_123456 immediately.";
let restored = q.deanonymize(response.to_string());
// "Contact user my_secret_username immediately."
```

## Custom types

Implement `AnonymizeTrait` for your own types:

```rust
use serde_mask::AnonymizeTrait;

struct Email(String);

impl AnonymizeTrait for Email {
    type State = String;

    fn anonymize(&self) -> Self::State {
        format!("EMAIL_{}", fastrand::usize(0..1_000_000))
    }

    fn deanonymize(&self, state: Self::State, serialized: &str) -> String {
        serialized.replace(&state, &self.0)
    }
}
```

## Design notes

The state is stored in a `OnceLock` field on the struct itself. This means the anonymization mapping is computed once on first serialize and reused, so `deanonymize` always uses the same placeholders that were serialized.

Alternative approaches considered:
- Separate derived struct (like snafu/expunge): more complex, requires conversion
- Thread locals: doesn't work well with async

Crate is breaking invariant of types, for example:

```rust
struct Age {
    age: usize
}
```
`age` type would get converted to String type at serialization. For JSON serde would wrap the value with double quotes, but I bet LLM wouldn't notice.