Antispam: plugin docs, the learning acceptance test, and a bootstrap that refuses bad corpora

docs/plugins.md described an SDK that no longer exists. It now covers
messages.raw/sample, actions.move_to_special, the envelope's account_id /
mailbox_special_use / account_state, the message.changed transition payload, the
message.actions.menu slot, Slot.title and .wm-menu-item — with the reasons, not
just the signatures. M3's acceptance is that a plugin can be written from this
file alone, and it had drifted.

Acceptance 4 (§C.8): a test that teaches 20 spam + 20 ham to a real engine and
checks the classifier generalises to held-out mail, keeps ham under the banner
threshold, and does not leak between users. It skips when no engine is reachable.
It does NOT assert the literal move_threshold — a synthetic fixture cannot reach
12.0 once the SMTP-time symbols are zeroed, which is the whole point of the
tuning. Recorded as OQ-8 rather than quietly asserting something weaker.

Bayes ignores training until min_learns, stock 200. For a personal mailbox that
means every correction appears to be ignored for months, so it is now 20.

The bootstrap would have made this filter worse. Accounts do not sync their
provider's spam folder by default, so the real corpus here is 3 junk beside 1,294
aged inbox — teaching both as found trains "everything is ham". It now refuses
below 10 spam samples (leaving the account unmarked so a later run can do it
properly) and otherwise caps ham at 3x the spam count.

Run against the live account, it correctly declined:
  "only 3 spam samples (need 10); enable spam-folder sync to build a corpus"

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SqJYt7jD7oUEkoY68sBZo
This commit is contained in:
2026-09-17 20:34:50 +02:00
co-authored by Claude Opus 5
parent 76beb9d527
commit 5aa133e18c
6 changed files with 396 additions and 12 deletions
+32
View File
@@ -3,6 +3,38 @@
Per the setup rules, deviations and unresolved decisions are recorded here rather
than guessed silently.
## OQ-8 — Appendix C acceptance 4's threshold is not reachable by a fixture
**Status:** resolved by testing the property rather than the number; recorded
because the test deviates from the spec's literal wording.
**The criterion (§C.8.4):** after 20 spam + 20 ham teachings, "a held-out
obvious-spam fixture scores above `move_threshold` for that user and a ham
fixture below `banner_threshold`".
**What happens:** the ham half holds easily. The spam half does not — a held-out
spam fixture scores around 0.9 against a 12.0 threshold, even though `BAYES_SPAM`
fires correctly and the engine reports 160+ messages learned.
**Why, and why it is not a bug:** real spam crosses 12.0 by accumulating many
symbols, most of which describe its *delivery* — no DKIM, unknown sending host,
dates that do not line up. This deployment deliberately zeroes those
(`docker/rspamd/local.d/groups.conf`), because scoring already-delivered mail on
SMTP-time facts was firing on 11.3% of ordinary mail. A synthetic fixture has no
delivery history at all, so once those symbols contribute nothing, almost the
only thing left to score is Bayes itself — and one classifier symbol cannot carry
a message to 12.0 on its own, nor should it.
Asserting the literal number would therefore require either un-tuning the filter
or writing fixtures that fake plausible delivery headers — testing the fixture
generator rather than the learning loop.
**What the test asserts instead** (`tests/plugins/test_antispam_corpus.py`):
after training, Bayes recognises held-out spam it has never seen; it does not cry
spam at held-out ham; held-out ham stays below `banner_threshold`; spam outscores
ham; and an untaught user sees neither the symbol nor the score — the per-user
isolation the criterion also asks for.
## OQ-6 — Appendix C needs SDK surfaces the frozen §11 API does not have
**Status:** resolved by additive SDK extension; recorded here because C.8.1 says