Benchmark Suite¶
The original validation set was 18 hand-picked examples, and it reported precision 1.0 / recall
0.875 — numbers that sound clean mostly because the test set was small and easy.
narrative_harm_classifier/data/benchmark_templates.yaml instead generates a much larger, systematic
test suite (~190 cases) modeled on the HateCheck methodology:
templates are tagged with a test_type and slot-filled across five identity groups, so a regression
in one specific capability is visible even when the aggregate looks fine, and the same rhetorical
pattern is tested identically across groups (cross-group consistency — does the engine behave the
same regardless of which group is named).
| test_type | what it checks |
|---|---|
explicit_positive |
clear, unambiguous harmful language |
implicit_positive |
harmful meaning without trigger words (recall probe) |
negation |
the harmful claim is negated — should NOT be flagged |
counter_speech |
harmful rhetoric quoted to condemn it — should NOT be flagged |
obfuscated_spelling |
trigger words altered to evade literal matching |
benign_trigger_word |
standalone hard negatives (trigger words in benign context, some with a group present) |
Run it with nhc benchmark run or POST /benchmark/run.
Taxonomy version: 1.0.0
Total cases: 192
TEST TYPE N PRECISION RECALL FPR F1
overall 192 1.0 1.0 0.0 1.0
benign_trigger_word 12 0.0 0.0 0.0 0.0
counter_speech 30 0.0 0.0 0.0 0.0
explicit_positive 60 1.0 1.0 0.0 1.0
implicit_positive 30 1.0 1.0 0.0 1.0
negation 30 0.0 0.0 0.0 0.0
obfuscated_spelling 30 1.0 1.0 0.0 1.0
Cross-group consistency: 36/36 templates consistent across groups
Note
negation, counter_speech, and benign_trigger_word are entirely hard negatives — no positives
exist in those buckets, so precision/recall are mathematically undefined and reported as 0.0 by
convention. FPR is the number that matters for those rows, and it's 0.0: zero false positives.
This is enforced in CI (tests/benchmark/), not just reported — a pull request that regresses
precision, recall, FPR, or cross-group consistency fails the build. The engine handles negation (via
a negation-cue window before a match), counter-speech (via a reporting-cue + condemnation-cue
heuristic), obfuscated/leetspeak spelling (via character-substitution normalization), and a set of
implicit phrasings (via additional patterns) — see
Contributing for
exactly how each heuristic works and its limits.
Read that honestly, not as 'solved'
A clean pass on this 192-case suite means the engine handles this benchmark's negation, counter-speech, and obfuscation patterns correctly — it does not mean adversarial evasion is a solved problem in general. These are heuristics (a negation window, a cue-word allowlist, a fixed character-substitution map), not language understanding, and real-world text will eventually find phrasings outside them. The point of the benchmark isn't "we're done" — it's that any future gap like that gets added as a new test case, so it can't regress silently once it's fixed. That's also why escalation tracking exists alongside single-text classification: a trend across many observations is more robust than any one classification being right.
The cross-group consistency check also caught a genuine bug during development: several templates
fired for every identity group except one political-affiliation phrasing, because the underlying
regex only matched singular forms (democrat, not democrats). That's fixed now (36/36 consistent),
and it's a good example of what this check is for.