Menu

The words our blog will not store, and why this post cannot name them

2026-10-09MCP Checkup

On 2026-10-08 our blog started keeping new posts in the database. A new post is now a row in a table, not a file in our code repository, and publishing it is a database write rather than a site build. The table has a trigger, and the trigger refuses to publish any post that contains a word from a short list.

This post went through that trigger like any other. So it cannot name the words, and it does not: not in quotes, not in a code block, not with dots or hyphens between the letters. It describes them by kind instead. That is less convenient for us, and it is the point.

What the list holds

The list has ten entries:

  • Four English adjectives of the kind printed on a certificate, a badge or a seal. One says an authority vouched for the thing, one that it is protected, one that it will do you no harm, and one that you may rely on it.
  • A letter grade: the top letter of a school report followed by the English word for a mark, with a hyphen, a space or nothing between them.
  • Four Chinese terms: one for a score, one for a pass rate, one for a health level, and a four-character phrase that means everything is fine.
  • One abbreviation: two capital letters with a slash between them, the one a spreadsheet puts in a cell that does not apply.

The English entries match as whole words in any letter case, so a longer word that merely contains one of them passes. The Chinese entries match anywhere, since Chinese puts no spaces between words. The abbreviation matches as a whole word, in capitals only.

The list was not made for the blog. It comes from the copy rules that govern every page of the site, and a script already checked our MDX posts against it at build time. What changed on 2026-10-08 is where the check runs.

Why the check moved into the database

A post stored in the database never passes through a build, so the check had to move to where the write happens. The trigger runs before every insert and update on the table. Drafts pass unchecked, so we can write one, read it back and fix it. When a row is written as published, or a draft is switched to published, its title, description and body are run through the list. On a hit, the trigger raises a check violation that names the rule, and nothing is written. The database roles our website uses can read published posts and nothing else; none of them can write to the table.

One list, two languages, one set of test strings

The list is defined once, in a JavaScript module. The MDX check imports it, the database holds a copy inside a SQL function, and a test compares the two copies entry by entry.

Equal lists are not enough, because the two languages do not match text the same way. Word boundaries are the clearest case. JavaScript's \b knows only ASCII, so a Chinese character next to an English word counts as a boundary. Postgres's \y treats a Chinese character as part of the word, so an English word written between two Chinese characters, with no spaces, would slip past a SQL check that used it. The SQL therefore never uses \y. It spells out JavaScript's boundary with explicit ASCII character classes.

To catch differences like that, we keep one set of test strings and run it through both checkers, under two database collations. The results have to match exactly. The set started with 36 strings and has 68 now, covering letter case, Chinese, word boundaries, longer words that contain a listed one, and line breaks.

Before each of the two migrations went live, we also ran the strings against production, in a transaction that was rolled back: 36 of 36 agreed for the first, 68 of 68 for the second. The first attempt at the second run showed one disagreement. Our own tooling had replaced one character of that test string on its way to the database. Sent intact, all 68 agreed.

A line break the browser removes

Browsers follow a CSS rule for East Asian text: a single line break between two wide characters, such as Chinese characters or Japanese kana, disappears on the page. So a two-character Chinese word split across two lines of the markdown source still reads as one word. The first version of the check matched the raw text and would have missed it. A code review caught that before any post was stored.

Since the second migration, both checkers first delete a run of whitespace that holds exactly one line break when the characters on both sides are wide. Two line breaks start a new paragraph and are left alone. Between Latin letters a line break shows as a space, so nothing is joined there.

A second look when the page renders

When the web page renders a post, the renderer checks again, this time on what a reader will actually see: entities decoded, emphasis joined, invisible characters dropped, full-width letters folded to ordinary ones. A hit there, and the page answers 404 instead of showing the post.

The renderer's code comments name one gap that neither check closes: a lookalike letter from another alphabet, placed inside a listed word. So the only thing keeping that spelling out of this post is that we do not use it, and the same goes for any other trick a check might miss. Dodging our own guard to make a point about the guard would defeat it.

No exemptions

The MDX check allows exemptions: one exact phrase, in one exact file, reviewed on its own. There is one today. Our post on why we don't score MCP servers contains a phrase that names the thing we do not give, in order to say that we do not give it.

The database has no exemption list, and the trigger's error message says so. When we translated that same post into Chinese and stored it as the first post in the database, it went through the trigger like everything else. This one did too.

Why we hold the blog to this

MCP Checkup does not give a score or a verdict. Each check ends in one of four states, Verified, Failed, Observed risk or Not checked, and each row carries the evidence behind it. Unsigned observations are shown without colour and never presented as signed conclusions. Conclusions cannot be bought, and the simplest way to keep that true is to have no overall conclusion to sell.

A verdict word on our own site would undo that. If a post here described a server with one of those adjectives, anyone could quote it as our conclusion, and no row on any report would stand behind it. The abbreviation is on the list for a related reason: a check we could not run is Not checked, with the reason we recorded, and two letters and a slash give no reason at all.

The blog is where we write most freely, which makes it the easiest place for a stray adjective to get in. So the rule that holds on the report pages holds here too, enforced by the database at the moment of publishing, on posts we write ourselves.

How we counted

The ten entries, the 36 and 68 test strings and the one exemption were counted in our repository on 2026-10-08: the shared word-list module, the test file that holds the strings, and the MDX check's exemption list. The 36 and 68 agreements are the results of the two dry runs against the production database on 2026-10-08, each made before its migration was applied.