Skip to content

A Suppression Layer Must Mirror Its Detection Layer

Problem

When a soft gate reuses a base heuristic unchanged and adds a wrapper that suppresses the base's known false positives, the suppression predicates are easy to write too loosely. A looser suppressor silently converts false positives into false negatives — the worse failure class for a linter, because it stops warning without telling anyone. The over-fire everyone would have noticed becomes a silent miss nobody does.

Discovered in SPEC-155's spec-clarity-scan.py, which reuses Layer-1 (gray-area-heuristic.py) unchanged and owns three suppression rules to silence Layer-1's known false positives. Each suppressor matched more loosely than the detection it compensated for:

Suppression Detection contract Wrapper used (too loose) Silent miss
Given/When/Then is valid AC → drop user_story_no_ac structured AC markers substring \bgiven\b/\bwhen\b/\bthen\b anywhere in the block, no fence/quote skip a prose US ("…, given the backlog, … when demand is high … then …") and fenced Gherkin examples both suppress the warning
A marker meta-reference isn't a real TBD → drop tbd_marker a deferral marker named alongside a marker-type word "any two markers joined by / , +" (the marker list included TBD/TBA/TODO themselves) TBD, TODO: — two genuine deferrals — reads as meta and is dropped
A term defined in ## Begriffe is fine → drop terminology word-boundary match of the term in prose exact equality of the whole first cell **Upload (of a manifest)** defines "Upload" but the cell ≠ "upload", so it still fires

Two are silent false-negatives (the gate's own target cases); one over-fires in the safe direction but breaks the headline UX ("define it in the glossary and it goes away").

Solution

Make each suppression predicate symmetric with the detection it cancels:

  • structured, line-anchored markers (line start, optional bullet/bold) instead of a substring search — and skip code fences + blockquotes exactly as the detector does, so illustrative examples don't count;
  • a suppression partner set that excludes the tokens being suppressed (a meta- reference is a deferral marker named next to a non-deferral marker-type word);
  • word-boundary match of the term inside the full glossary cell text, so a phrase definition suppresses just like a bare one.

Then pin every suppression path with two regression guards:

  1. a must-still-suppress case (the legit input the suppression exists for), and
  2. a must-still-fire case (the genuine finding the suppression must not eat).

Positive-only suppression tests drift toward over-suppression, because widening a suppressor never reddens a "should suppress" test — only the paired "should fire" test catches it.

Why it matters

A base heuristic is calibrated against its own corpus (see heuristic-calibration-gate), but the wrapper's suppression layer is a second, independent surface with its own error budget. It is where false negatives hide, and it is invisible to the base heuristic's calibration. Review it adversarially and test it in both directions.

Provenance

SPEC-155 review Pass 1 (/vt-c-4-review) surfaced all three as MEDIUM; two independent reviewers converged on the Given/When/Then case. The findings were empirically confirmed by running the real predicates, then fixed TDD-style with the paired regression guards in tests/spec-155/test_suppression_precision.bats.