I swept the parameter instead of arguing about it
A threshold argument settled by measuring four values over the real corpus. The knee, the trade that stops paying, and why the numbers go in the comment.
A search description should be between 140 and 156 characters. Holding that as a hard floor forced a mid-clause cut on 74 of 101 posts — results ending "…is why rounded cards".
The question was what to lower the floor to. I could have argued about it. Instead I swept four values, which took about twenty minutes.
The case this post is about
One constant, and the twenty minutes that decided it is 120 and not 128:
floor severed mean length
128 42 148
120 34 146
115 32 145
110 31 145
Eight fewer severed descriptions for two characters of mean length, between the first two rows. Then almost nothing: 120 to 110 buys three more and makes every description shorter.
The knee is between the first two rows and it is not visible from either one alone. This post
follows those four rows, because the version of this that ships is a bare = 120 with nobody able
to say why — and the next person who thinks 128 looks better has to re-derive the whole table to
find out what it costs.
The symptom
Threshold arguments have a recognisable shape. Someone proposes a number, someone else proposes a different number, both are defensible, and the discussion runs on intuitions about which failure matters more.
They are also unusually cheap to settle, which is what makes them worth noticing. A threshold has exactly one dimension, the corpus already exists, and the metric you care about is usually a count you can produce in one pass.
The specific tension here: Medium's 140–156 guidance is about how much of a description Google renders. It is not guidance about where a sentence may be cut. Treating a rendering limit as a grammatical constraint is what produced 74 severed descriptions — each one technically in range and reading like a transmission failure.
Why the obvious fix failed
The obvious fix is to pick a lower floor that feels right. 120 sounds reasonable. So does 128.
The trouble is that both proposals are arguments about an imagined distribution. The real question — how many descriptions actually get severed at each floor, and how much length do you give up — has an answer, and neither of us knew it.
The second attempt was to handle it case by case: let a human write the short ones. That is not a fix, it is a queue. With 101 posts it is a day of work that regenerates every time the corpus grows.
The third was to make the checker cleverer — sentence-aware truncation that always finds a clean break. Worth doing eventually and orthogonal: it changes where the cut lands, not what the floor should be, and the floor question stays open underneath it.
The actual mechanism
Run the thing four times with four values and count.
floor severed mean length
128 42 148
120 34 146
115 32 145
110 31 145

The shape answers the question immediately. Between 128 and 120, severed descriptions drop by 8 and mean length falls by 2 characters — a good trade. Between 120 and 110, severed drops by 3 while every single description in the corpus gets shorter.
That is a knee, and it is at 120. Below it the trade stops paying: you are shortening a hundred descriptions to rescue three.
Nothing in that required judgement about what a good description looks like. It required running a loop four times over a corpus I already had.
The fix
The measurement is half of it. The other half is that the numbers go in the code, next to the constant:
120 is the knee, measured over this corpus — severed counts by floor:
128 → 42 120 → 34 115 → 32 110 → 31
with mean length 148 / 146 / 145 / 145. Below 120 the severed count
barely moves while every description gets shorter, so the trade stops paying.
const CLAUSE_MIN = 120;

That comment does three things a bare = 120 does not.
It pre-empts the argument. The next person to think 128 looks better can see what 128 costs without re-deriving it.
And the sweep that produced those four numbers is small enough to keep next to the constant, which is what makes re-measuring cheap when the corpus changes:
type Measurement = { floor: number; severed: number; meanLength: number };
function sweepFloor(descriptions: string[], floors: number[], max: number): Measurement[] {
return floors.map((floor) => {
let severed = 0, total = 0;
for (const full of descriptions) {
// Cut at the last clause boundary at or after the floor; if there is none
// before `max`, there is nowhere clean to cut and the sentence gets severed.
const window = full.slice(0, max);
const boundary = Math.max(
window.lastIndexOf(". "), window.lastIndexOf("? "), window.lastIndexOf("! "),
);
const cut = boundary >= floor ? boundary + 1 : max;
if (boundary < floor) severed++;
total += cut;
}
return { floor, severed, meanLength: Math.round(total / descriptions.length) };
});
}
Run over the corpus that existed when the constant was chosen:
const corpus = loadDescriptions(); // 161 posts
console.table(sweepFloor(corpus, [128, 120, 115, 110], 156));
floor severed meanLength
128 42 148
120 34 146
115 32 145
110 31 145
The knee is between 128 and 120: eight fewer severed descriptions for two characters of mean length.
Below that the trade stops paying — 120 to 110 buys three more at the cost of every description
getting shorter, which is the part a bare = 120 cannot tell you and the reason the comment records
all four rows rather than the winner. Re-running it after the corpus grew moved severed at 120 from
34 to 39 and left the knee exactly where it was, which is worth more than the original measurement:
the constant is justified by a shape, not by one number.
It says when it was true. A measurement over this corpus is a dated observation, so a reader with a corpus three times the size knows the sweep is worth re-running rather than trusting.
It records the reasoning, which is the part that rots slowest. Below the knee the trade stops paying remains the right frame even if every number changes.
The failure mode it prevents is the one I have caused most often: a constant tuned carefully once, then adjusted by someone reasonable who had no way to know it was not arbitrary.
How we proved it
The direct check is whether the chosen floor behaves as the sweep predicted, on posts written after it. Across the batch that produced this article:
descriptions in the 140-156 band 19 / 19
severed mid-clause 0
Every one landed cleanly, and several needed a second pass because the checker reported the cut — which is the mechanism working. The floor gave it room to accept a clause-complete 146 instead of forcing a severed 155.

The honest limitation, and it is not small: a sweep optimises the metric you chose to count. I counted severed descriptions and mean length. I did not measure whether a 146-character description performs differently in search from a 155 — which is the thing the whole exercise is nominally about. That would need traffic data over months, and I do not have it.
So what I have is a defensible answer to "how do we stop producing broken-looking descriptions without making them all short" — and an open question about whether any of it moves a click-through rate. The sweep settled the argument I could settle, and it is worth being clear about which one that was.
When not to bother
Three conditions have to hold for a sweep to be the cheap option, and when they do not, arguing is genuinely the better use of time.
The corpus has to exist already. Sweeping over data you must first construct is not a twenty-minute job, and the constructed data carries your assumptions anyway.
The metric has to be a count, not a judgement. How many were severed is countable. How many read well is a judgement, and sweeping it means grading four hundred items four times.
The parameter has to be genuinely one-dimensional. Two interacting thresholds is a grid, and a grid is an afternoon rather than twenty minutes — still often worth it, but no longer obviously cheaper than deciding.
The reason to state the conditions is that "just measure it" is easy advice to give and occasionally expensive to take. What makes this case a good one is that all three held, and I still nearly argued about it instead.
What this generalizes to
When a disagreement is about a number, check whether the corpus to settle it is already sitting there. Often it is, the sweep is minutes, and the argument was about intuitions neither party had data for.
Then write the sweep into the comment beside the constant — the values, the counts, and the sentence explaining why the trade stops paying where it does. A tuned constant with no record of its tuning is indistinguishable from an arbitrary one, and it will eventually be changed by someone reasonable who had no way to know.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".