Open tooling, agent-run engineering, and the products built on both.

The site that lints its own honesty

  • #documentation
  • #content-drift
  • #ci
  • #verification
  • #hugo
  • #static-sites

A product page is a promise with a shelf life. The version, the size of the check registry, the flags the tool accepts — all of it is true the day it is typed and quietly wrong some Tuesday three releases later. Nobody lies on purpose. Somebody ships a feature, the release notes get written, and the paragraph on the docs page keeps saying what it said in March.

This site is not allowed to do that. A gate in CI enforces one rule: content may not state a fact it did not read from a data file.

The one file allowed to be right

The product emits a machine-readable facts file on every release — version, the registry of things it inspects with their weights, the full flag list, the exit codes. That file lands in the site’s data/ directory and the page templates render from it. Shortcodes pull single fields; a table shortcode renders the whole registry, rows and weights and all. Nothing about the product is retyped into prose.

That much is just good templating. The gate is the part that stops anyone from working around it.

What enforces it

  • The data file has to add up. Declared totals must match the array they summarise, the weights must sum to a full hundred points, and the enforcement grades must partition the registry exactly. A regenerated file that silently lost rows fails here instead of rendering a confident wrong total.
  • No hand-typed facts. A linter walks every markdown and template file under content/ and themes/ and fails on a bare numeral standing in front of the words “check”, “scored” or “advisory”, and on any literal matching the product’s current version string. Both have a shortcode. Typing the value instead of calling it is the failure.
  • Documented flags have to be real flags. Every flag token the reference page mentions must exist in the product’s actual flag list, and every product flag must be documented somewhere on the site. Both directions, each with a written allow-list for the deliberate exceptions.
  • No third-party product names. Comparisons age badly and turn into claims about somebody else’s software that nobody re-checks. A word-boundary scan over the same files keeps the marketing surface nameless, release prose included.
  • Every id needs a label. A new entry in the data file with no display name in the site’s label map would render as a machine-humanised id. The gate fails first, so the label gets written on purpose rather than discovered on the live site by a stranger.

Four of those are things a person cannot see by reading a page. That is the whole argument for putting them in CI.

There is no exemption for the post that explains the rule

I wanted to open this by showing you the exact strings the linter rejects — a version literal, a hand-typed count, the name of a comparable tool. All three are the reason the linter exists, and all three are unquotable here.

There is no “but I am the post about the linter” escape hatch. Every version of one I sketched collapsed into the same bad idea: an exemption keyed on what a file says it is about. The linter would have to take the file’s word for it, which is precisely the thing it was built not to do. So the bullets above describe the banned strings instead of quoting them, and this paragraph is what the constraint feels like from the inside. Mildly annoying. Completely correct. A rule you can talk your way out of by declaring your intentions is not a rule — it is a suggestion in a nice font.

The sanctioned path is not an exception, it is a different mechanism. Where a page needs the current release version, it calls the shortcode and the build fills it in. The current release is 2.2.0 — and in the source of this file, that sentence contains no version number at all. The number cannot go stale because it was never typed. That is the difference between validating output and making the bad output unconstructible.

What it does not do

  • The allow-lists are a discipline, not a mechanism. Flag parity carries a hand-maintained exception list in each direction, and a gate whose allow-list grows every time it complains has been switched off one line at a time. The lists here are short and every entry has a reason written next to it. Nothing enforces that except somebody reading the diff.
  • One line is out of reach. Hugo shortcodes do not run inside front matter, so the meta description on one page still carries a hand-written count. The linter whitelists that exact file and that exact field, and the release job patches it. When “make it unconstructible” is unavailable, the fallback is a whitelist of one, written down.
  • The gate’s own tests are uneven. It ships with a self-test and a small regression suite, so it is not the untested script I assumed it was before I read it. But the planted-failure tests — feed the rule a violation, assert it goes red — cover the banned-name rule and the label-map rule only. The count linter, the version rule, and flag parity are asserted to pass against current content and never proven to fail against bad content. A regression that made one of them match nothing would keep the suite green.

That last one is the honest gap, and it is a narrower one than “no tests”. Knowing that every gate passes is a different question from knowing which gates have ever been observed failing, and here the answer to the second is: some of them.

There is one more limit I hit while writing this. I wanted to tell you how long the gate script is. I left it out, because nothing on this site would notice the day that number stopped being true — which is the entire thesis of the post, arriving uninvited.

Why there is no link at the end of this post

The natural ending here is a repository link. There isn’t one, and there is not going to be one.

The gate is not a general-purpose tool in a product’s clothing. Its rules are welded to one data schema. The count rule knows which nouns follow a number in this product’s vocabulary. The version rule interpolates one specific field. The parity check knows the name of the array holding the flag list and the name of the file documenting it. The label-map check knows the shape of one template’s dictionary. Strip all of that out and what remains is an argument parser and a directory walk — the easy part. Everything that makes it worth having is the part that only makes sense here.

I could generalise it into something configurable. Then the configuration would be harder to write than the script, which is the well-trodden route from a working, boring gate to an abandoned framework with three users. So the code stays where it is. What travels is the shape.

Stealing the shape

  • Find the one artifact allowed to be authoritative, and make the thing it describes generate it. If a human maintains it, you have moved the drift, not removed it.
  • Render, never retype. Every product fact on a page should be a template call. This is the tier that costs nothing when violated, because it cannot be violated.
  • Ban the literal rather than auditing the prose. Do not try to judge whether a sentence is accurate. Grep for the shape of the fact — a numeral before a known noun, a version string, a competitor’s name — and refuse it outright.
  • Check both directions of every list. Documented-but-not-real catches typos. Real-but-undocumented catches the feature nobody wrote up. Only the second one finds the gap that matters.
  • Fail on the unlabelled id, not the wrong label. A missing display name is silently survivable at render time, which is exactly why it must be loud at build time.
  • Give every allow-list entry a reason in the file. An exception with a written reason is a decision. An exception without one is decay with better paperwork.
  • Then plant a violation and watch the gate go red. A gate you have only ever seen pass is a gate you are trusting, not one you have checked.

This post went through it

Every rule above ran against this file before it shipped, which is the only demonstration worth offering. The last bullet is not advice I am handing out unused: before writing the section that describes the banned strings, I put a real version literal into a draft of this file and ran the gate. It failed, named the file and the line, and told me to render the value through the shortcode instead. Then I deleted the literal and wrote the paragraph explaining why it isn’t there.

That is the whole method. Not a policy asking writers to be careful — a build that will not produce the page until the claim and the data agree.

Configuration details reflect a production environment at time of writing. Implementation specifics vary based on tooling versions, platform updates, and organizational requirements. Validate approaches against current documentation before deployment.

← Back to Journal