Add a new spam-detection rule (text normalization → score → action). Wire into the cascade after dedup + CAS, with corpus tests under server/tests/spam_corpus/.
Add a new spam-detection rule (text normalization → score → action). Wire into the cascade after dedup + CAS, with corpus tests under server/tests/spam_corpus/.
Wire into spam_service.rs cascade (registered with a default weight).
Corpus YAML at server/tests/spam_corpus/{rule}.yaml — ≥5 positive + ≥5 negative. No rule without corpus.
Register the weight key in chat_config.spam_weights JSONB schema.
Walk the corpus in server/tests/spam_pipeline.rs.
Record matched rules in moderation_actions.reason JSON for explainability.
Document in server/docs/spam-detection.md rule table.
Gotchas
Input is already normalized. Don't re-NFKC inside the rule — wastes CPU and the rule doesn't see what the rest of the cascade sees.
Score accumulates. A single false-positive rule with high weight ruins the whole pipeline. Default weight low; tune in chat config.
Idempotency. Rules firing AFTER dedup mean spam_messages is bypassed for re-processed retries. Gate the action on the moderation_actions uniqueness key (chat_id, target_user_id, action, message_id) before issuing any ban/kick.
CAS API can fail-open. Network down → never block a message. Log the timeout, continue.
Corpus tests are mandatory. Reviewer rejects PRs without YAML.
Telegram IDs i64 in any SQL the rule touches.
Verification
cargo test spam_pipeline.
cargo test --test spam_corpus_{rule} if you split the corpus into its own test file.