DEV Community

John
John

Posted on Originally published at hexisteme.github.io

Your glossary gate passes words, not names

Originally published on hexisteme notes.

I run a machine-checked glossary for a long-form fiction project I'm writing. Every proper noun goes into a YAML file, and a deterministic gate at build time greps the manuscript for the same terms and counts them per chapter. The check I trusted for months was simple: is this word attested? Pull the morphemes apart, look them up in the dictionary, grep the manuscript for the compound. If everything shows up, the term is real, so it passes.

That check has a hole in it, and I found the hole by getting called out.

The word was real. The name was not.

I had just replaced six coined terms in the glossary — old placeholder handles swapped for names I thought were final. My reader pushed back on three of them, all with the same question: "Isn't this just a literal English translation?" Two of the three challenges were right.

The one that stung was a bread name I'd translated compositionally from a source term — cross-scored round bread, describing a loaf with a cross cut into the top before baking. Every morpheme in that name is real. It's attested on the page (a chapter literally describes "a round loaf with a cross cut through it"), and every word in it is in the dictionary. My gate — corpus membership, are the words real — passed it without hesitation.

It's also a bad name. A page over from it are two other bread names in the same world: chimney bread, because smoke leaking through a crack in the loaf looks like a thread rising from a tiny chimney, and bell-ring bread, because tapping the bottom of the loaf rings like a bell. Both are named after something that happens to the bread — a sensory event, not its shape. Cross-scored round bread breaks that pattern. It's not named after an event at all. It's a shape description, and a shape description reads like a field-guide entry, not like a name the world's own people would actually use for the thing they eat.

The gate had no way to catch that, because it was never checking for it. It checks whether words exist. It does not check whether the name is right. Those are orthogonal questions, and I had been treating one as a stand-in for the other.

The discriminator was already in the manuscript

Here's what actually stung: I didn't need new information to catch this. The naming principle that cross-scored round bread violates was already sitting in the same chapter, in the two other bread names. Chimney bread and bell-ring bread are both coined from a sensory event that happens to the object. That's the domain's naming convention, in plain sight, before I ever looked at the bad candidate. I just hadn't extracted it into something I checked against.

So the fix isn't a better dictionary. It's asking, before you judge any candidate: what do the two or three best existing names in this domain have in common, and why did they get their name? Write that answer down in one line before you look at the next candidate. For a different domain the principle might be function, or origin, or an action rather than an event — the point isn't "always name things after sensory events," it's that the domain you're already writing has an answer, and you can read it off the good names instead of guessing.

A three-question gate, with a survivor to prove it discriminates

Once I had the principle, I turned corpus membership into the first of three questions instead of the whole check:

# Question Passes Fails
Is it a dictionary-listed word? words like "ring" (a real noun for a sound) or "fare" (a real noun for a cost) ad-hoc gerunds coined on the spot for the occasion
Was it coined by the same naming principle the domain already uses? chimney bread, bell-ring bread (sensory event) cross-scored round bread (shape description — a field-guide entry)
Is it a productive construction? noun + "-fare" — boat fare, labor wage, tea fare are all real, everyday compounds using the same pattern (in the source language, the morpheme «삯») <descriptor>+<descriptor>+<generic noun> stacking; a phenomenon turned into a noun on the spot

The third challenged name from that same conversation was "chimney fare." My reader flagged it as the same kind of translation-ese as the bread name. It isn't. Noun + "-fare" is the same pattern as boat fare and labor wage and tea fare — a live, productive construction in the language, not a one-off compound. And the manuscript already prices things in that idiom — a chapter quotes "the fare to cross the veil for one night." The construction is the domain's own, the usage is attested on the page, and the name survived the gate. Kept, not rejected.

That survival is the part that matters most. A gate that rejects everything unfamiliar isn't a gate, it's a wall, and a wall doesn't tell you anything about the next candidate. This one let a genuinely unfamiliar-sounding name through because it checked structure, not vibes. Two rejections and one survival out of three challenges is what a discriminator looks like. Three rejections would have meant I'd built a stricter corpus check, not a different one.

Before you rename, ask if the name needs to exist

Not every failed candidate needs a replacement. Another entry from the same glossary pass was an umbrella name meant to tie two other breads together under one collective label. I went looking for a better version of that umbrella name. I should have gone looking for whether it needed to exist at all.

It didn't. The two breads it was meant to unify already had names — chimney bread and bell-ring bread — and the fact that they're secretly the same recipe was supposed to be a plot payoff, not a vocabulary problem. The sentence that pays that off already refers to both breads by their real names. An umbrella term would have spoiled the reveal by naming the connection before the story earns it.

So I retired it — not by deleting the entry, which would have broken every cross-reference pointing at that ID, but by turning it into a stub: the same ID, pointing at the two real names, with the field the gate actually reads for reader-facing terms left empty. No canonical form, no count. The writer's-desk handle stays addressable for internal cross-references and drops out of the metric the gate computes for readers. Slot check before rename check — ask whether the name is needed before you spend effort making it good.

The gate can count "no name" as "a name"

One more failure mode, smaller but sharper because it's purely mechanical. Each chapter has a term budget: a cap on how many distinct glossary terms it's allowed to introduce, so a reader isn't handed a vocabulary list instead of a story. I'd put an as-yet-unnamed descriptor — a thing the narration calls "the round one" because nobody has named it yet — into the alias field of a real term, thinking it was harmless bookkeeping.

The gate's alias match is an exact substring check. It found "the round one" in the chapter text and counted it as a reader-learned term, same as any real name. Cap was three. Measured came out four. The chapter went red.

The instinct here is to loosen the gate — raise the cap, or special-case aliases. Both are wrong. The gate was right: it found a string in the alias field and a matching string in the text, which is exactly what it's built to find. The bug was mine — I'd put something in a field that means "a name a reader learns" that was, definitionally, not a name at all. Fixing my entry, not the gate, was the actual fix. This is the same orthogonality problem as the bread name, just one layer down in the tooling: "this string is present" and "this string is a name" are different claims, and a check built for one will silently answer the other question wrong if you feed it the wrong kind of string.

The same structure holds for code

None of this is specific to fiction or to translation. Swap "glossary" for "codebase" and the same three questions apply to identifiers. Manager, Helper, Util pass question ① without effort — they're real English words, they compile, a linter that only checks "is this a real word / valid identifier" will never flag them. They fail ② and ③ every time: they don't follow whatever naming convention the domain's good names actually use (the ones that say what the thing does or owns), and they aren't a productive construction so much as a bucket you can drop anything into. A codebase's style guide that just bans Manager and Helper as a word list is doing the same partial job my corpus check was doing — catching the word, missing the pattern. The fix in both places is the same: find two or three names in the codebase that are actually good, write down in one line why they're good, and check new names against that line, not against a dictionary.

Two more things I'd carry into any review, coding or writing:

Write the rejection as a pattern, not a verdict on the word. "This word doesn't exist" is the wrong sentence for cross-scored round bread — every word in it exists. The right sentence is "all the words are real, the naming method isn't the one this domain uses." That sentence generalizes to the next candidate. "This word doesn't exist" doesn't, and it also isn't true, which means the next round of review starts from a false premise.

And: if someone challenges three items out of a larger batch, the ones they didn't challenge are unchecked, not cleared. I'd replaced six terms and only three got a second look, because only three drew a challenge. The other three passed a corpus check I already knew was incomplete. Silence isn't the same as passing.

I haven't adversarially stress-tested this gate — thrown a pile of deliberately borderline names at it to see where it breaks. It's a judgment aid I run by hand before I commit a term, not an automated build check. If it starts letting bad names through in ways I haven't seen yet, that's the next version of this note.

More notes at hexisteme.github.io/notes.

Top comments (0)