Kenneth, sixteen, Oshkosh, Wisconsin — 2026-08-30
I built a checker to stop my AI agent quoting things that were not in the source
document. It worked. Then it rejected a quotation that was perfectly correct,
properly sourced, and present in the document word for word.
The model had written 15‑minute demand. The document said 15-minute demand.
Those are different characters. The first is U+2011, a non-breaking hyphen. The
second is U+002D, the one on your keyboard. They render almost identically, they
mean exactly the same thing, and a substring comparison treats them as unrelated.
My verifier was not checking the claim. It was checking the font.
A verifier that produces false rejections does not merely annoy you. **It trains
you to ignore it.**
The first time it fires wrongly you investigate. The third time, you glance. By the
tenth you have learned that it cries wolf, and you dismiss the one that matters —
at which point you have kept all the confidence of having a verifier and lost the
verification.
A checker that flags the truth is more expensive than no checker at all, because
no checker at least leaves you appropriately nervous.
Every one of these appears in extracted PDFs and in model output. Several are
visually indistinguishable at normal reading size.
| Looks like | Actually is | Where it comes from |
|---|---|---|
- | U+002D hyphen-minus | Your keyboard |
‑ | U+2011 non-breaking hyphen | Typesetting, and models imitating it |
‐ | U+2010 hyphen | Unicode's "correct" hyphen, rarely typed |
– | U+2013 en dash | Ranges — 10–20 |
— | U+2014 em dash | Parenthetical asides |
− | U+2212 minus sign | Mathematics, and some PDF extractors |
' ' | U+2018/2019 | Smart quotes, applied automatically |
" " | U+201C/201D | Same |
| U+00AD soft hyphen | Invisible. Justified text, survives extraction |
| U+200B zero-width space | Invisible. Web copy-paste |
| U+00A0 non-breaking space | Looks exactly like a space |
fi fl | U+FB01/FB02 ligatures | PDF fonts, silently substituted |
Four of those are invisible or indistinguishable. You cannot debug this by
looking at the string.
Normalise typography. Never relax wording.
Those sound similar and they are opposites.
Normalising typography means every dash above becomes - before comparison,
every smart quote becomes ', the invisible characters are stripped, ligatures are
expanded. 15‑minute and 15-minute become the same string, because they were
always the same claim.
Relaxing wording means fuzzy matching, edit distance, stemming, embeddings —
anything that lets nearly the same text pass. And that destroys the whole
instrument, because in the documents I work with:
```
"the monthly demand charge of $8.540 per kW"
"the monthly demand charge of $9.540 per kW"
```
is an edit distance of one, a cosine similarity of about 0.999, and **a completely
different fact.** Any system loose enough to forgive a stray hyphen is loose enough
to forgive that digit, and the second failure is the one that reaches a client.
So: **$8.540 and $9.540 stay different forever. 15‑minute and 15-minute
become identical.** The line between those two is the entire design.
Filed documents contain characters that carry meaning and look like noise. A real
electricity tariff reads:
"the monthly demand charge of $8.540 per kW R will be reduced by $0.05266…"
That stray R is a revision marker flagging a changed line. It sits mid-sentence.
A quotation typed from memory omits it, and the checker refuses — correctly.
It is tempting to strip it. Do not. You cannot distinguish a revision marker from a
letter that belongs to the sentence without understanding the document, and a
checker that starts understanding documents is a checker that starts being wrong in
new ways.
**The correct answer is to work in the other direction: search the document, and
let it tell you its own wording.** Then quote that.
Last week my agent drafted a message to a prospect offering *"a cloud-based
development environment."* We do not have one. Everything runs on one laptop.
So I wrote a second checker — this one verifying claims about us rather than
about documents. I shipped it. It missed the cloud claim entirely, because the
agent had written cloud‑based with a non-breaking hyphen.
**Same character. Same failure. One hour after I had cited the original fix in a
commit message.**
I had a normaliser. I had written it, tested it, and published it. I simply did not
reach for it in the new place, because the new checker felt like a different kind of
problem and it was not.
Then I fixed that, and the checker blocked an honest sentence — a draft that said
the agent had "claimed a cloud environment, which it does not have." Every word
true. It flagged the truth, which is the failure at the top of this piece, arriving
from the opposite direction inside the same hour.
Three wrong versions of one checker in sixty minutes. They are all in the git
history with timestamps.
1 · Normalise before every comparison, in every place you compare. Not in the
library — at every call site, or better, make the comparison impossible to perform
without it. I knew the rule and still wrote a new comparison without it, because
knowing a rule is not the same as being unable to break it.
2 · Test both directions and weight them differently. A missed lie costs you a
client. A flagged truth costs you the checker itself, which costs you every
future lie. My tests now assert both, and the second set is longer.
3 · Never let a normaliser touch meaning. Dashes, quotes, invisible characters,
ligatures, whitespace. Nothing else. The moment it stems a word or drops a
punctuation mark that could be structural, you have built a fuzzy matcher with a
verifier's reputation.
The library is quoted. The normaliser is fifteen lines — the whole module is
sixty-six with the comments explaining why each substitution is there. It is the
least clever code in the project, which is why it is the part that keeps saving me.
(I first wrote "about forty lines" in this paragraph, then counted. In a piece
about unchecked comparisons that seemed worth mentioning rather than quietly
fixing.)
**If you have built one of these and normalise something I do not, I would like to
know what and why.** My list came from failures rather than from knowing the field,
and that is a poor way to arrive at a specification.
— Kenneth