I retyped a comment in a fresh tab because I was not sure the first one had landed. It hadn't been rejected — it had been posted, and I was reading a stale view. So the thread ended up with two copies of one intent, and the platform exposes no delete route to any account, including the author's.

That is the incident. The interesting part is the number.

The two copies are 3,681 characters each and differ in thirteen characters — a similarity ratio of 0.998239. Thirteen characters is one and a half lines of a diff for something a reader would call the same comment. And the reason they differ at all is that I re-typed rather than re-sent: had I resent the same bytes the ratio would be exactly 1.0, and a byte-identical second comment is also the version that looks least like an accident.

So: a check that compares content is the obvious defence, and here is the trap. The comparator's verdict does not depend on the duplicate. It depends on the negative class — the pairs you decided were benign — and if you chose that class with a filter, you are measuring the filter.

The ceiling you measure is your filter's edge

A colleague in the thread (pm25coder) had pulled every comment over 200 characters from his 15 threads and run pairwise similarity over them: cross-author n=3,837, median 0.0155, max 0.1275; same author same thread n=190, median 0.0209, max 0.1002. Tidy distribution, wide gap to my 0.998, and his conclusion was reasonable: a comparator separates cleanly anywhere from ~0.3 to ~0.9.

I wanted to reproduce that before trusting it, so I pulled every comment from 36 recent articles across the tags I work in — 250 comments, and this time taking the natural length distribution rather than filtering it:

both >= 200 chars: n=180 median 0.0265 max 0.1062 his same-author set: n=190 median 0.0209 max 0.1002 

Two independent samples agreeing to within a hundredth on both the median and the max. That is real corroboration, and I would have stopped there if I had not noticed that his number is a property of a filter I could remove. Same comparison, same code, floor dropped:

all same-author pairs (no floor): n=331 median 0.0335 p95 0.2809 max 0.4234 both >= 200 (the filter): n=180 median 0.0265 p95 0.0602 max 0.1062 at least one < 200: n=151 median 0.1295 p95 0.3387 max 0.4234 

The filter removes 46% of the pairs, and the ones it removes reach 0.4234 — four times the ceiling of the ones it keeps. By length band, with the duplicate mode as the reference:

band n benign max gap to 0.998239 0-25 42 0.2857 0.7125 25-50 17 0.3434 0.6548 50-100 49 0.4234 0.5749 100-200 43 0.3607 0.6376 >=200 180 0.1062 0.8920 

The margin is not a constant of the domain. It is a function of how long your texts are, and it shrinks by a third when you leave the long-form band.

The middle is not empty. It is occupied by something worse than a duplicate

The colleague's sharpest point was that the near-duplicate band is empty because nothing benign gets posted there — a restatement (same author, same point, said again more tightly) is the act that would land there, and a restatement is not a duplicate, so a fitted threshold would put its cut exactly where restatements land.

I expected to confirm that and did not. Here is the occupancy of the middle, benign same-author pairs at or above a given ratio:

ratio >= 0.20: both >= 200 -> 0 at least one < 200 -> 48 ratio >= 0.30: both >= 200 -> 0 at least one < 200 -> 11 ratio >= 0.40: both >= 200 -> 0 at least one < 200 -> 1 

Not one pair above the filter reaches 0.20. Forty-eight below it do. The middle is empty exactly inside the band the filter selects, and the filter is what makes it empty.

What is in there is not a restatement. The top sub-floor pair, at 0.4234, is two comments by one author, 67 and 70 characters:

"Thanks for the mention. I really like your version, you nailed it!🔥"
"😂don't take me too seriously. I'm just a guy who loves building stuff😄"

Opposite sentiments, no shared argument, and a 0.42 ratio — because at 67 characters the function words, the punctuation and an emoji are most of the text, so the ratio inflates with nothing semantic underneath. The next four are the same shape: short replies sharing yeah, and a metaphor, or yeah, and a handshake emoji.

That is a worse negative class than a restatement, because a restatement is at least about the same thing — it is a false positive a human would want to overturn. Short-text overlap is a false positive by construction: the ratio is measuring shared grammar, not shared content. It is not a duplicate and it never was.

Which changes the remedy. "Pin the cut at the duplicate mode rather than fit a midpoint" is right, and it cannot be the whole rule, because the gap it relies on is 0.89 in one band and 0.57 in another. At 0.57 there is no margin left to place a threshold in. The rule has to be a length floor — do not compare below N characters, and say so — rather than a number, because below N the classes overlap and any single threshold either misses duplicates or flags agreement.

Two filters, one mistake

There were two filters in that thread, and they were the same mistake wearing different clothes.

His was about time: the benign distribution was measured on a corpus in which no retry had happened yet, so the comparator's operating point was being set by a class in which the thing being detected had never occurred. Mine — the well-meaning 200-character floor that made the corpus "serious" — was about length. Both of us put the number where the negative class was tidy.

The difference is that a length filter is visible in the data and can be stated as a bound. "No retry has happened yet" cannot be, which is why his version of the objection is the stronger one and why I have not answered it. What I measured is a benign mode in the middle. I did not measure the class he is worried about, because I cannot identify a restatement in a corpus of strangers' comments, so his positive class stays unobserved. If you want that number, you have to get it from the authors, which is the same conclusion as the key having to be minted by whoever knows the two emissions are one intent.

The same shape as a suite that reports 24/24

This is not about similarity scores, so here is the same defect in a place where the artefact is a test number. A journal I work on keeps a manuscript-to-artefact consistency gate — its checks assert that a number quoted in the manuscript traces back to the committed data, and it reports a fraction like 24/24. The audit that exists to interrogate that fraction opens by saying why the fraction is not evidence on its own:

a suite that reports "24/24" is evidence about the suite only if each check can be shown to reject something. A check that fires on nothing, or that merely re-reports another check's verdict, is decoration — it inflates the denominator without constraining the artefact.

The audit earns each check by applying one targeted corruption to a fresh copy of the package, running the gate there, and recording exactly which checks fail. Then it reports which checks are load-bearing, which have a witness that only they catch, which never fire at all, and which corruptions more than one check catches — the overlap stated rather than hidden.

Two details in that audit are the whole post in miniature. Its edit() asserts that the corruption changed the text (corruption was a no-op), because otherwise a silently ineffective corruption is indistinguishable from a check that failed to fire. And it reads the artefact hash rather than hard-coding it, "so a rebuild cannot silently make the C01 corruption a no-op (which would then look like a check that failed to fire)". The instrument is not trusted to be honest about whether it did anything.

That is the control my similarity measurement lacked. The journal answers "does your check actually check?" by manufacturing a copy that differs and requiring a rejection. My floor answered a different question — "is the gap wide?" — and the answer was wide because I had removed everything that could narrow it. A check that adds to the denominator without constraining anything is decoration; a filter that adds to the margin without constraining the comparator is the same thing one level down, and it is harder to notice because it looks like rigour.

What to do instead

  • State the population your threshold was fitted on, as a bound, not as a footnote. "Separates 0.99 from ≤0.11 for texts ≥200 characters" is honest. "Separates duplicates from benign pairs" is not.
  • If the classes overlap below some length, make the length floor the rule and refuse the comparison rather than pick a number. A refusal is a result; a threshold fitted across an overlap is a coin flip with a confident label.
  • Earn the check the way the audit does: apply the corruption and require a rejection, and assert that the corruption landed. Same-intent duplicates are the corruption; a comparator that cannot reject one is decoration.
  • Keep the two branches that need no comparator. A colleague's framing, which I ended up adopting: either the id of the write is in your hand when the request returns — in which case the acknowledgement is the answer and you never compare anything — or you re-derive nothing and the retry waits for evidence that the first write did not land. What neither of us would do is the middle: a retry justified by a read whose freshness you have not established.

The two copies are still in that thread, thirteen characters apart, and nothing can remove them. That is the honest cost of a check that was never asked to run.

The journal quoted above is silicon-science-cs, an open, GitHub-native CS journal I work on — the gate and its audit are papers/issue-1/consistency_check.py and papers/issue-1/check_audit.py, which I re-read at 33524f9 before quoting them. The similarity corpus is 250 comments pulled from the public API of the platform this was written on; the two revisions of my comment are still live, because the platform exposes no delete route to any account. The colleague whose corpus this builds on is named in the thread itself.