The 31st check didn't exist: a failure-mode taxonomy of auditing LLM output in production Market Daily AI, a daily finance email service, disclosed that its LLM output audit system contains 30 checks, not the 31 previously claimed, after a phantom check name in the module docstring inflated the count. The audit layer, which validates personalized HTML digests before sending, includes checks for temporal errors, coverage gaps, and other failure modes, with severity levels determining recovery actions. The incident highlights that even counting checks has failure modes, as the false positive shipped in a published article. The 31st check didn't exist: a failure-mode taxonomy of auditing LLM output in production Subscribe free → https://marketdaily.ai/?lang=en&utm source=blog&utm medium=cta top&utm campaign=seo eng-audit-taxonomy-en-202608 email-step We run a daily finance email. Subscribers pick their holdings; an LLM pipeline generates a personalized HTML digest twice a day; a deterministic audit inspects the final HTML before anything gets sent. In the previous post I described the architecture multi-model council, judge, audit-retry-fallback . This post dissects the audit layer itself: every check, what incident created it, and — the interesting part — two cases where the defense itself became the outage. First, a confession The previous post claimed "31 independently named checks." The fact-check command behind that number was: grep -o '"check": " a-z 0-9 "' digest audit.py | sort -u | wc -l → 31 While building the table for this post, I listed the checks one by one and got 30. The discrepancy: the module docstring contains a usage example with a check name tldr has tw that doesn't exist in the code — the real check is named tldr missing tw . Counting fails.append call sites gives exactly 30. Our audit system's own audit method produced a false positive, off by one, and it shipped into a published article. I'm keeping "31" in the title because the phantom check is the best possible opening for a taxonomy of failure modes: even counting your checks has a failure mode. The honest number is 30. The design contract The file's docstring states the philosophy: every check corresponds to one scenario where a user got angry, or predictably will. Each check has a stable snake case name, a severity, and — this matters — a comment carrying the date of the incident that created it. The file reads like a fossil record. Severity is not decoration; it selects a recovery path: HIGH → sleep 60 seconds free-tier rate limits are per-minute token windows; retrying after 5s lands in the same window and hits the same 429 , regenerate with a stronger model forced, re-audit; still failing → deterministic fallback assembled by pure code, intentionally price-free. Never send a broken email; never fail to send an email. Two separate invariants. MED / LOW → logged as a quality deduction, email ships anyway. Of the 30: 20 fixed-HIGH, 6 MED, 2 LOW, and 2 with dynamic severity an empty TL;DR section escalates to HIGH; four same-direction cards escalate from LOW to MED if the verdict simultaneously preaches caution . The full table | | check | category | sev | one-line trigger | |---|---|---|---|---| | 1 | tw pre market tense | temporal | HIGH | 7 a.m. pre-market email says the TW market "already rose today" it opens at 9:00 | | 2 | tw pre market tense zaoshen | temporal | HIGH | "TW stocks up this morning" — unknowable before the open | | 3 | us holiday tense | temporal | HIGH | US market was closed last night, email says it "closed green/red" | | 4 | tw holiday open tense | temporal | HIGH | TW holiday, email says "opens at 9:00 this morning" | | 5 | us holiday tonight tense | temporal | HIGH | US closed tonight, email says "opens tonight" | | 6 | tw morning action missing | temporal | MED | TW trading day but no card mentions the morning-open action window | | 7 | us tonight action missing | temporal | MED | US trading night but no card gives a "tonight / after-hours" action | | 8 | tldr section missing | coverage | HIGH | the 30-second summary section is absent entirely | | 9 | tldr missing tw | coverage | HIGH | user holds TW stocks; TL;DR never mentions the TW market | | 10 | tldr missing us | coverage | HIGH | evening US edition; TL;DR never mentions US stocks | | 11 | tldr too short | coverage | HIGH/MED | 0 bullets empty section = HIGH; 1–2 bullets = MED | | 12 | holdings uncovered | coverage | HIGH | any single holding the user selected lacks an action card | | 13 | signal card missing battle | coverage | HIGH | an action card lacks the entry/target/stop triple | | 14 | market summary missing tw | coverage | MED | market-overview section ignores the TW index for a TW holder | | 15 | fake urls | fabrication | HIGH | example.com / placeholder.com style URLs in the output | | 16 | placeholder prices | fabrication | HIGH | unfilled placeholders: "amounts to XXX billion" | | 17 | earnings fabricated estimates | fabrication | HIGH | "expected EPS" appears when the data layer supplied no verified estimate | | 18 | confidence overclaim | fabrication | MED | confidence 75% historical calibration cap; above it = a defense was bypassed | | 19 | speculative causality | fabrication | LOW | sourceless attribution: "possibly related to…" | | 20 | prompt instruction leak | leakage | HIGH | instructions meant for the LLM copied verbatim into the product | | 21 | portfolio lens foreign ticker | leakage | HIGH | a "your portfolio" section contains a real listed security the user doesn't hold | | 22 | ai output truncated | structural | HIGH | HTML tail looks cut off token limit hit | | 23 | undefined css class | structural | HIGH | body uses a class